↓ Перейти к основному содержимому
  1. Rust/

Использование и применение std::collections в Rust: BTreeMap, HashSet, BinaryHeap и VecDeque

980 слов·5 минут· loading · loading · · ·Rust-middle
О Rust - Эта статья часть цикла.
Статей прочитано 0/41
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
32 Использование и применение std::collections в Rust: BTreeMap, HashSet, BinaryHeap и VecDeque (текущая)
Не прочитана

Введение
#

В языке Rust обычно используеются стандартные Vec<T> и HashMap<K, V>. Но когда дело доходит до очередей сообщений, каких-то выборок или планировщиков задач - дефолтных контейнеров уже становится маловато.

Стандартный модуль std::collections закрывает большинство прикладных задач без внешних крейтов. Но при этом важно замечать не только на оценку скорости выполнения O(1) или O(log N), но и на то, как данные размещаются в памяти. Современные процессоры любят работать с непрерывными кусками памяти: удачное размещение данных и попадание в L1/L2 кэш процессора на практике это часто перекрывают теоретическую сложность алгоритмов.

Разберем четыре ключевые структуры из стандартной библиотеки: VecDeque, BTreeMap, BTreeSet и BinaryHeap.


1. Коллекции: VecDeque vs Vec vs LinkedList
#

Динамический массив (Vec<T>)
#

Vec<T> хранит элементы в виде непрерывного массива в куче. За счет этого итерация по вектору работает очень быстро, а чтение по индексу оценивается как O(1).

Слабое место вектора — вставка и удаление в начале или середине: для этого приходится сдвигать весь хвост массива за O(N).

Двусторонняя очередь (VecDeque<T>)
#

Если вам нужна очередь (FIFO) или кольцевой буфер последних событий, берите VecDeque<T>.

Под капотом - динамический кольцевой буфер:

  • Вставка и удаление с конца (push_back / pop_back) — O(1).
  • Вставка и удаление с начала (push_front / pop_front) — O(1) без сдвига всего массива.
Совет

Почти в любом сценарии, где кажется нужным использовать связный список, VecDeque выигрывает и по скорости и по расходу памяти.

Связный список (LinkedList<T>)
#

Двунаправленный LinkedList<T> в реальном коде на Rust почти не встречается. Каждый узел живет в куче в собственной аллокации. При обходе списка процессор вынужден постоянно прыгать по памяти и ловить промахи по кешу.


2. Словари: BTreeMap vs HashMap
#

стандартная библиотека предлагает два инструмента для работы с парами ключ-значение.

Хеш-таблицы (HashMap<K, V> и HashSet<T>)
#

  • Под капотом: алгоритм SwissTable.
  • Сложность: поиск, вставка и удаление в среднем занимают O(1).
  • Особенности: порядок элементов случаен. Для ключей обязательны типажи Hash + Eq.

B-деревья (BTreeMap<K, V> и BTreeSet<T>)
#

  • Под капотом: B-дерево, где в каждом узле хранится компактный массив элементов. Это отлично укладывается в кэш-линии процессора.
  • Сложность: поиск, вставка и удаление гарантированно занимают O(log N).
  • Особенности: ключи всегда отсортированы. Для ключей требуется типаж Ord.
use std::collections::BTreeMap;

let mut map = BTreeMap::new();
map.insert(3, "Третий");
map.insert(1, "Первый");

// Итерация гарантированно вернет элементы по возрастанию: 1, затем 3
for (key, val) in &map {
    println!("{key}: {val}");
}
Примечание

Зачем брать BTreeMap, если у HashMap заявлен O(1)?

  1. Нужна сортировка ключей по умолчанию.
  2. Нужны выборки по диапазону через .range(min..max).
  3. Нужны предсказуемые гарантии времени без риска худшего сценария O(N) при коллизиях хешей.

3. Очередь с приоритетом: BinaryHeap
#

BinaryHeap<T> — это двоичная Max-куча реализованная поверх вектора. Коллекция создана для планировщиков задач и очередей событий, где первым нужно обрабатывать самый приоритетный элемент.

  • Добавление элемента .push(item) занимает O(log N).
  • Извлечение максимального элемента .pop() занимает O(log N).
  • Подсмотреть максимальный элемент через .peek() можно за O(1).
Примечание

Чтобы превратить BinaryHeap в Min-кучу (где первым выходит наименьший элемент), оберните значения в std::cmp::Reverse(x).


4. Шпаргалка по сложности
#

КоллекцияДоступВставка в началоВставка в конецПорядок элементов
Vec<T>O(1) по индексуO(N)O(1) аморт.Не сортирован
VecDeque<T>O(1) по индексуO(1) аморт.O(1) аморт.Не сортирован
HashMap<K, V>O(1) среднее——Случайный
BTreeMap<K, V>O(log N)——Отсортирован
BinaryHeap<T>O(1) для Max—O(log N)По приоритету

Ниже — интерактивные слайды для каждой коллекции:

Работа с VecDeque, BTreeMap и BinaryHeap
Шаг 1/3
 1use std::collections::VecDeque;
 2
 3fn main() {
 4    // Двусторонняя очередь (Ring Buffer)
 5    let mut queue = VecDeque::with_capacity(5);
 6
 7    // Добавление в конец очереди:
 8    queue.push_back("Задача 1");
 9    queue.push_back("Задача 2");
10
11    // Срочное добавление в начало очереди:
12    queue.push_front("Приоритетная задача!");
13
14    println!("Элементы очереди: {:?}", queue);
15
16    // Извлечение с противоположных концов за O(1):
17    if let Some(first) = queue.pop_front() {
18        println!("Обработана из начала: {first}");
19    }
20
21    if let Some(last) = queue.pop_back() {
22        println!("Обработана с конца: {last}");
23    }
24}
25

Двусторонняя очередь: VecDeque

  • VecDeque<T> реализован как кольцевой буфер (Ring Buffer).
  • Позволяет выполнять вставку и удаление за O(1) как с начала (push_front/pop_front), так и с конца (push_back/pop_back).
  • Идеальный выбор для реализаций FIFO-очередей задач или буферов последних сообщений.

Тест для самопроверки
#

Несколько вопросов, чтобы закрепить различия между коллекциями:

Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
▶ Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/41
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
32 Использование и применение std::collections в Rust: BTreeMap, HashSet, BinaryHeap и VecDeque (текущая)
Не прочитана

Связанные статьи