Алгоритмы и структуры данных на Rust: практическое руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в алгоритмы и структуры данных на Rust

Язык программирования Rust завоевал признание разработчиков благодаря своей высокой производительности и строгим гарантиям безопасности памяти. Однако переход на Rust с других языков, таких как Python или JavaScript, может быть непростым, особенно при реализации классических алгоритмов и структур данных. В этой статье мы рассмотрим, как эффективно реализовать базовые структуры данных и алгоритмы на Rust, учитывая особенности системы владения (ownership) и заимствования (borrowing).

Статья предназначена для разработчиков, уже знакомых с основами Rust. Мы сосредоточимся на практических примерах кода, которые можно сразу использовать в реальных проектах. Все примеры будут сопровождаться подробными комментариями.

Реализация связного списка на Rust

Связный список — одна из фундаментальных структур данных. В Rust его реализация требует особого внимания из-за правил владения. Рассмотрим односвязный список с использованием Box для управления памятью.

Базовый односвязный список

Начнём с определения узла и самого списка:

use std::fmt::Display;

#[derive(Debug)]
struct Node<T> {
    value: T,
    next: Option<Box<Node<T>>>,
}

#[derive(Debug)]
struct LinkedList<T> {
    head: Option<Box<Node<T>>>,
    length: usize,
}

impl<T> LinkedList<T> {
    fn new() -> Self {
        LinkedList {
            head: None,
            length: 0,
        }
    }

    fn push_front(&mut self, value: T) {
        let new_node = Box::new(Node {
            value,
            next: self.head.take(),
        });
        self.head = Some(new_node);
        self.length += 1;
    }

    fn pop_front(&mut self) -> Option<T> {
        self.head.take().map(|node| {
            self.head = node.next;
            self.length -= 1;
            node.value
        })
    }

    fn is_empty(&self) -> bool {
        self.head.is_none()
    }

    fn len(&self) -> usize {
        self.length
    }
}

Обратите внимание на использование Option<Box<Node<T>>>. Это стандартный способ создания рекурсивных структур в Rust, так как компилятор требует заранее известный размер. Метод take() позволяет безопасно извлечь значение из Option, заменяя его на None.

Итерация по списку

Реализуем итератор для списка:

impl<T: Clone> LinkedList<T> {
    fn iter(&self) -> LinkedListIterator<T> {
        LinkedListIterator {
            current: self.head.as_deref(),
        }
    }
}

struct LinkedListIterator<T> {
    current: Option<&Node<T>>,
}

impl<T> Iterator for LinkedListIterator<T> {
    type Item = &T;

    fn next(&mut self) -> Option<Self::Item> {
        self.current.take().map(|node| {
            self.current = node.next.as_deref();
            &node.value
        })
    }
}

Бинарное дерево поиска (BST) на Rust

Деревья — ещё одна важная структура. Реализуем бинарное дерево поиска с поддержкой вставки, поиска и обхода.

Структура и базовая вставка

#[derive(Debug)]
struct TreeNode<T> {
    value: T,
    left: Option<Box<TreeNode<T>>>,
    right: Option<Box<TreeNode<T>>>,
}

#[derive(Debug)]
struct BinarySearchTree<T> {
    root: Option<Box<TreeNode<T>>>,
}

impl<T: Ord> BinarySearchTree<T> {
    fn new() -> Self {
        BinarySearchTree { root: None }
    }

    fn insert(&mut self, value: T) {
        let mut current = &mut self.root;
        while let Some(node) = current {
            if value < node.value {
                current = &mut node.left;
            } else if value > node.value {
                current = &mut node.right;
            } else {
                // Значение уже существует, ничего не делаем
                return;
            }
        }
        *current = Some(Box::new(TreeNode {
            value,
            left: None,
            right: None,
        }));
    }
}

Рекурсивная реализация может быть элегантнее, но итеративная версия с while let безопаснее с точки зрения заимствования. Ключевой момент — мы используем изменяемую ссылку на Option, чтобы найти место для вставки.

Поиск и обход дерева

impl<T: Ord> BinarySearchTree<T> {
    fn contains(&self, value: &T) -> bool {
        let mut current = &self.root;
        while let Some(node) = current {
            if *value < node.value {
                current = &node.left;
            } else if *value > node.value {
                current = &node.right;
            } else {
                return true;
            }
        }
        false
    }

    fn inorder_traversal(&self) -> Vec<&T> {
        let mut result = Vec::new();
        Self::inorder_helper(&self.root, &mut result);
        result
    }

    fn inorder_helper(node: &Option<Box<TreeNode<T>>>, result: &mut Vec<&T>) {
        if let Some(ref n) = node {
            Self::inorder_helper(&n.left, result);
            result.push(&n.value);
            Self::inorder_helper(&n.right, result);
        }
    }
}

Рекурсивный обход здесь допустим, так как глубина дерева обычно невелика. Для больших деревьев стоит рассмотреть итеративный подход с использованием стека.

Алгоритмы сортировки на Rust

Рассмотрим три популярных алгоритма сортировки: быструю сортировку (quicksort), сортировку слиянием (merge sort) и встроенную сортировку Rust.

Быстрая сортировка (Quicksort)

fn quicksort<T: Ord>(arr: &mut [T]) {
    if arr.len() <= 1 {
        return;
    }

    let pivot_index = partition(arr);
    quicksort(&mut arr[..pivot_index]);
    quicksort(&mut arr[pivot_index + 1..]);
}

fn partition<T: Ord>(arr: &mut [T]) -> usize {
    let pivot_index = arr.len() - 1;
    let mut i = 0;

    for j in 0..pivot_index                

Блоги

Рекомендаций по книгам