Введение#
В языке 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)?
- Нужна сортировка ключей по умолчанию.
- Нужны выборки по диапазону через
.range(min..max). - Нужны предсказуемые гарантии времени без риска худшего сценария
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) | По приоритету |
Ниже — интерактивные слайды для каждой коллекции:
Тест для самопроверки#
Несколько вопросов, чтобы закрепить различия между коллекциями:


