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

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

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

Начать курс

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

Rust — это современный системный язык программирования, который сочетает производительность C++ с безопасностью памяти, обеспечиваемой системой владения (ownership). Изучение алгоритмов и структур данных на Rust позволяет не только освоить классические концепции Computer Science, но и глубже понять уникальные особенности языка: заимствование, времена жизни и нулевые затраты на абстракции.

В этой статье мы разберём три фундаментальные структуры данных (стек, очередь и бинарное дерево поиска) и один классический алгоритм (быстрая сортировка), реализованные на Rust. Каждый пример сопровождается кодом и пояснениями, которые помогут вам перейти от теории к практике.

1. Стек на Rust: реализация через Vec

Стек — это структура данных, работающая по принципу LIFO (Last In, First Out). В Rust проще всего реализовать стек на основе стандартного Vec, который предоставляет все необходимые методы: push для добавления и pop для извлечения.

Однако, чтобы продемонстрировать безопасность Rust, мы создадим обёртку с явной обработкой ошибок:

pub struct Stack<T> {
    elements: Vec<T>,
}

impl<T> Stack<T> {
    pub fn new() -> Self {
        Stack { elements: Vec::new() }
    }

    pub fn push(&mut self, item: T) {
        self.elements.push(item);
    }

    pub fn pop(&mut self) -> Option<T> {
        self.elements.pop()
    }

    pub fn peek(&self) -> Option<&T> {
        self.elements.last()
    }

    pub fn is_empty(&self) -> bool {
        self.elements.is_empty()
    }
}

fn main() {
    let mut stack = Stack::new();
    stack.push(10);
    stack.push(20);
    stack.push(30);

    println!("Верхний элемент: {:?}", stack.peek()); // Some(30)
    println!("Извлекаем: {:?}", stack.pop());         // Some(30)
    println!("Извлекаем: {:?}", stack.pop());         // Some(20)
    println!("Стек пуст? {}", stack.is_empty());      // false
}

Ключевое преимущество Rust здесь — система типов. Если вы попытаетесь извлечь элемент из пустого стека, компилятор заставит вас обработать Option, что исключает ошибки разыменования нулевого указателя, характерные для C/C++.

2. Очередь: ручная реализация с кольцевым буфером

Очередь (FIFO) сложнее стека, потому что требует эффективного удаления из начала. Rust не предоставляет готовой двухсторонней очереди с гарантированной производительностью, поэтому мы реализуем кольцевой буфер (circular buffer) фиксированного размера.

Это отличный пример для демонстрации unsafe-кода и сырых указателей, но мы останемся в безопасной зоне, используя Vec и индексы:

pub struct Queue<T> {
    buffer: Vec<Option<T>>,
    head: usize,
    tail: usize,
    capacity: usize,
}

impl<T> Queue<T> {
    pub fn new(capacity: usize) -> Self {
        let mut buffer = Vec::with_capacity(capacity);
        buffer.resize_with(capacity, || None);
        Queue {
            buffer,
            head: 0,
            tail: 0,
            capacity,
        }
    }

    pub fn enqueue(&mut self, item: T) -> Result<(), &str> {
        if (self.tail + 1) % self.capacity == self.head {
            return Err("Очередь переполнена");
        }
        self.buffer[self.tail] = Some(item);
        self.tail = (self.tail + 1) % self.capacity;
        Ok(())
    }

    pub fn dequeue(&mut self) -> Option<T> {
        if self.head == self.tail {
            return None;
        }
        let item = self.buffer[self.head].take();
        self.head = (self.head + 1) % self.capacity;
        item
    }

    pub fn is_empty(&self) -> bool {
        self.head == self.tail
    }
}

fn main() {
    let mut queue = Queue::new(5);
    queue.enqueue(1).unwrap();
    queue.enqueue(2).unwrap();
    queue.enqueue(3).unwrap();

    println!("Извлекаем: {:?}", queue.dequeue()); // Some(1)
    println!("Извлекаем: {:?}", queue.dequeue()); // Some(2)
    println!("Очередь пуста? {}", queue.is_empty()); // false
}

Обратите внимание на использование Option<T> в буфере. Это позволяет безопасно "забирать" элемент из ячейки через .take(), не нарушая владение. Rust гарантирует, что мы не получим висячую ссылку.

3. Бинарное дерево поиска: рекурсия и владение

Бинарное дерево — классический пример для демонстрации рекурсивных структур. В Rust мы сталкиваемся с вызовом: как смоделировать рекурсивный тип данных, не нарушая правила владения?

Решение — использовать Box (умный указатель на кучу) и Option для представления отсутствующих узлов:

use std::cmp::Ordering;

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

impl<T: Ord> TreeNode<T> {
    pub fn new(value: T) -> Self {
        TreeNode {
            value,
            left: None,
            right: None,
        }
    }

    pub fn insert(&mut self, new_value: T) {
        match new_value.cmp(&self.value) {
            Ordering::Less => {
                match self.left {
                    Some(ref mut left) => left.insert(new_value),
                    None => self.left = Some(Box::new(TreeNode::new(new_value))),
                }
            }
            Ordering::Greater => {
                match self.right {
                    Some(ref mut right) => right.insert(new_value),
                    None => self.right = Some(Box::new(TreeNode::new(new_value))),
                }
            }
            Ordering::Equal => { /* дубликаты игнорируем */ }
        }
    }

    pub fn search(&self, target: &T) -> bool {
        match target.cmp(&self.value) {
            Ordering::Equal => true,
            Ordering::Less => self.left.as_ref().map_or(false, |l| l.search(target)),
            Ordering::Greater => self.right.as_ref().map_or(false, |r| r.search(target)),
        }
    }
}

fn main() {
    let mut root = TreeNode::new(10);
    root.insert(5);
    root.insert(15);
    root.insert(3);
    root.insert(7);

    println!("По                

Блоги

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