HashSet и BTreeMap в Rust
HashSet (множество) — это коллекция уникальных значений. Положишь одно и то же значение дважды — сохранится оно один раз. Представь список гостей у входа: охраннику важно только, есть ли твоё имя в списке, а не сколько раз его туда вписали.
Ты уже знаешь Vec и HashMap. Эта страница добавляет остальные коллекции, которые нужны чаще всего: HashSet, отсортированные BTreeMap и BTreeSet, а ещё VecDeque для очередей. В конце есть таблица, которая поможет выбрать подходящую.
Создаём HashSet
Заголовок раздела «Создаём HashSet»HashSet лежит в std::collections, поэтому нужна строка use. Метод insert возвращает bool: true, если значение новое, и false, если оно уже было.
use std::collections::HashSet;
fn main() { let mut tags = HashSet::new();
println!("{}", tags.insert("rust")); println!("{}", tags.insert("web")); println!("{}", tags.insert("rust")); // уже есть
println!("уникальных тегов: {}", tags.len());}Вывод:
truetruefalseуникальных тегов: 2Этот bool полезнее, чем кажется. Он отвечает на вопрос «видел ли я это раньше?» за тот же шаг, что и добавление, так что отдельная проверка не нужна.
Проверяем и удаляем значения
Заголовок раздела «Проверяем и удаляем значения»contains говорит, есть ли значение в множестве. remove убирает его и, как и insert, возвращает bool — было ли что-то на самом деле удалено.
use std::collections::HashSet;
fn main() { let mut online: HashSet<&str> = HashSet::from(["ana", "ben", "chen"]);
println!("ben в сети? {}", online.contains("ben"));
println!("удалили ben: {}", online.remove("ben")); println!("удалили ben ещё раз: {}", online.remove("ben"));
println!("ben в сети? {}", online.contains("ben")); println!("в сети: {}", online.len());}Вывод:
ben в сети? trueудалили ben: trueудалили ben ещё раз: falseben в сети? falseв сети: 2Почему здесь множество, а не Vec? contains у Vec просматривает все элементы по одному. А HashSet сразу переходит к ответу, каким бы большим он ни был. Когда пользователей миллион, разница огромная.
Убираем повторы из Vec
Заголовок раздела «Убираем повторы из Vec»Очень частая задача: в списке есть повторы, а нужно каждое значение по одному разу. Есть два хороших способа — в зависимости от того, важен ли исходный порядок.
use std::collections::HashSet;
fn main() { let visits = vec![3, 1, 3, 2, 1, 5, 2];
// Способ 1: собрать в множество, потом отсортировать для стабильного порядка let unique: HashSet<i32> = visits.iter().copied().collect(); let mut sorted: Vec<i32> = unique.into_iter().collect(); sorted.sort(); println!("уникальные по порядку: {:?}", sorted);
// Способ 2: оставить первое появление каждого значения, в исходном порядке let mut seen = HashSet::new(); let mut in_order = visits.clone(); in_order.retain(|x| seen.insert(*x)); println!("в исходном порядке: {:?}", in_order);}Вывод:
уникальные по порядку: [1, 2, 3, 5]в исходном порядке: [3, 1, 2, 5]Способ 2 использует bool из insert. retain оставляет элемент, только когда замыкание возвращает true, а insert возвращает true только при первой встрече значения.
Математика множеств: объединение, пересечение, разность
Заголовок раздела «Математика множеств: объединение, пересечение, разность»Множества можно комбинировать, как круги на диаграмме Венна. Каждый метод возвращает итератор, поэтому мы собираем результаты в Vec и сортируем их перед выводом, чтобы каждый раз получать одно и то же. Названия языков здесь — английские слова, поэтому сортировка по алфавиту работает предсказуемо.
use std::collections::HashSet;
fn main() { let alice: HashSet<&str> = HashSet::from(["rust", "python", "go"]); let bob: HashSet<&str> = HashSet::from(["go", "java", "rust"]);
let mut both: Vec<_> = alice.intersection(&bob).collect(); both.sort(); println!("знают оба: {:?}", both);
let mut either: Vec<_> = alice.union(&bob).collect(); either.sort(); println!("знает хоть один: {:?}", either);
let mut only_alice: Vec<_> = alice.difference(&bob).collect(); only_alice.sort(); println!("только Алиса: {:?}", only_alice);
let mut only_one: Vec<_> = alice.symmetric_difference(&bob).collect(); only_one.sort(); println!("знает только один: {:?}", only_one);}Вывод:
знают оба: ["go", "rust"]знает хоть один: ["go", "java", "python", "rust"]только Алиса: ["python"]знает только один: ["java", "python"]difference работает в одну сторону: alice.difference(&bob) — это «что знает Алиса и не знает Боб». Поменяй их местами — и получишь список Боба. symmetric_difference — обе стороны сразу: всё, что знает только кто-то один из них.
BTreeMap и BTreeSet: всегда отсортированы
Заголовок раздела «BTreeMap и BTreeSet: всегда отсортированы»BTreeMap работает так же, как HashMap, а BTreeSet — как HashSet. Разница в том, что они держат ключи отсортированными. При переборе ты каждый раз получаешь ключи по порядку.
use std::collections::{BTreeMap, BTreeSet};
fn main() { let text = "the cat sat on the mat the end";
let mut counts: BTreeMap<&str, u32> = BTreeMap::new(); for word in text.split_whitespace() { *counts.entry(word).or_insert(0) += 1; } for (word, count) in &counts { println!("{word}: {count}"); }
let scores: BTreeSet<u32> = BTreeSet::from([72, 95, 58, 88]); println!("наименьший: {:?}", scores.first()); println!("наибольший: {:?}", scores.last()); println!("все: {:?}", scores);}Вывод:
cat: 1end: 1mat: 1on: 1sat: 1the: 3наименьший: Some(58)наибольший: Some(95)все: {58, 72, 88, 95}Текст здесь оставлен на английском, потому что результат зависит от сортировки по буквам. API entry — тот же, что ты видел в уроке HashMap. Раз таблица — BTreeMap, слова выходят в алфавитном порядке без отдельной сортировки.
Что выбрать?
- По умолчанию бери
HashMap/HashSet. Поиск в них обычно быстрее. - Бери
BTreeMap/BTreeSet, когда данные нужны по порядку: вывести отсортированный отчёт, найти наименьший или наибольший ключ или получить диапазон ключей.
VecDeque: очередь
Заголовок раздела «VecDeque: очередь»VecDeque (произносится «век-дек») — это список, в который можно быстро добавлять и из которого можно быстро удалять с обоих концов. Поэтому он естественно подходит для очереди: первым пришёл — первым ушёл, как люди в очереди.
use std::collections::VecDeque;
fn main() { let mut line: VecDeque<&str> = VecDeque::new(); line.push_back("Аня"); line.push_back("Бен"); line.push_back("Чэнь");
while let Some(person) = line.pop_front() { println!("обслуживаем: {person}, ждут ещё: {}", line.len()); }}Вывод:
обслуживаем: Аня, ждут ещё: 2обслуживаем: Бен, ждут ещё: 1обслуживаем: Чэнь, ждут ещё: 0Можно было бы использовать для этого Vec::remove(0), но он сдвигает все остальные элементы на одну позицию. pop_front у VecDeque этого не делает, поэтому остаётся быстрым даже для длинной очереди.
Какую коллекцию выбрать?
Заголовок раздела «Какую коллекцию выбрать?»| Коллекция | Используй, когда нужно… | Пример |
|---|---|---|
Vec<T> |
Упорядоченный список с доступом по позиции | Строки файла, список результатов |
HashMap<K, V> |
Быстро находить значение по ключу | Логин → профиль пользователя |
HashSet<T> |
Уникальные значения и быстрая проверка «есть ли тут?» | Посещённые страницы, запрещённые слова |
BTreeMap<K, V> |
Ключ → значение, ключи отсортированы | Подсчёт слов с выводом по алфавиту |
VecDeque<T> |
Добавлять и удалять с обоих концов | Очередь задач или сообщений |
Если сомневаешься, начни с Vec. Меняй его, когда заметишь, что снова и снова ищешь в нём (бери множество или таблицу) или часто удаляешь с начала (бери VecDeque).
Частые ошибки
Заголовок раздела «Частые ошибки»Положить свою структуру в HashSet без нужных derive. HashSet должен хешировать значения и сравнивать их на равенство. Твоя структура сама этого не умеет:
use std::collections::HashSet;
struct Point { x: i32, y: i32,}
fn main() { let mut visited = HashSet::new(); visited.insert(Point { x: 0, y: 0 });}error[E0277]: the trait bound `Point: Eq` is not satisfied --> src/main.rs:10:13 |10 | visited.insert(Point { x: 0, y: 0 }); | ^^^^^^ the trait `Eq` is not implemented for `Point`
error[E0277]: the trait bound `Point: Hash` is not satisfied --> src/main.rs:10:13 |10 | visited.insert(Point { x: 0, y: 0 }); | ^^^^^^ the trait `Hash` is not implemented for `Point`Компилятор сообщает, что для Point не реализованы трейты Eq и Hash. Исправление — одна строка над структурой: #[derive(PartialEq, Eq, Hash)]. Для BTreeSet понадобятся ещё PartialOrd и Ord, чтобы он знал, как сортировать. Что значит каждый из них, рассказано в уроке Популярные трейты.
use std::collections::HashSet;
#[derive(PartialEq, Eq, Hash)]struct Point { x: i32, y: i32,}
fn main() { let mut visited = HashSet::new(); visited.insert(Point { x: 0, y: 0 }); visited.insert(Point { x: 1, y: 0 }); visited.insert(Point { x: 0, y: 0 }); // повтор, игнорируется println!("посещено клеток: {}", visited.len());}Вывод:
посещено клеток: 2Практика
Заголовок раздела «Практика»1. Первый повтор
Заголовок раздела «1. Первый повтор»Дан список слов ["красный", "синий", "зелёный", "синий", "красный"]. Выведи первое слово, которое встречается второй раз. Подсказка: insert возвращает false, когда значение уже есть в множестве, а break останавливает цикл.
Показать решение
use std::collections::HashSet;
fn main() { let words = ["красный", "синий", "зелёный", "синий", "красный"]; let mut seen = HashSet::new();
for word in words { if !seen.insert(word) { println!("Первый повтор: {word}"); break; } }}Вывод:
Первый повтор: синий!seen.insert(word) читается как «вставить не удалось», то есть «это я уже видел». Первым повторяется "синий". "красный" тоже повторяется, но позже.
2. Буквы по порядку
Заголовок раздела «2. Буквы по порядку»Посчитай, сколько раз каждая буква встречается в "mississippi", и выведи результат в алфавитном порядке. Подсказка: используй BTreeMap с тем же приёмом entry(...).or_insert(0) из раздела про BTreeMap и перебирай word.chars().
Показать решение
use std::collections::BTreeMap;
fn main() { let word = "mississippi"; let mut counts = BTreeMap::new();
for letter in word.chars() { *counts.entry(letter).or_insert(0) += 1; }
for (letter, count) in &counts { println!("{letter}: {count}"); }}Вывод:
i: 4m: 1p: 2s: 4С HashMap эти четыре строки могли бы выйти в любом порядке. BTreeMap держит ключи отсортированными, поэтому i, m, p, s каждый раз выводятся по алфавиту.
HashSetхранит уникальные значения.insertиremoveвозвращаютbool— изменилось ли что-то.containsу множества быстрый даже для огромных множеств. УVecон проверяет каждый элемент.union,intersection,differenceиsymmetric_differenceвыполняют операции над множествами. Перед выводом результаты сортируй.BTreeMapиBTreeSetдержат ключи отсортированными — бери их, когда важен порядок.VecDeque— основной выбор для очереди:push_back, чтобы встать в очередь,pop_front, чтобы выйти из неё.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что вернёт set.insert(x), если x уже есть в множестве?
2. Нужен подсчёт слов с выводом по алфавиту, без ручной сортировки. Какая таблица подойдёт лучше всего?
3. Почему вывод HashSet через {:?} может отличаться от запуска к запуску?
4. Какая коллекция лучше всего подходит для очереди «первым пришёл — первым ушёл»?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»std::collectionsoverview — документация стандартной библиотеки (на английском): подробное руководство по выбору коллекции со скоростью каждой операции.- HashSet — Rust на примерах: запускаемый пример с
union,intersectionиdifference(перевод на русский). HashSet— документация стандартной библиотеки (на английском): все методы множества, включаяis_subsetиretain.BTreeMap— документация стандартной библиотеки (на английском): отсортированные таблицы, в том числеrangeдля получения диапазона ключей.VecDeque— документация стандартной библиотеки (на английском): все методы очереди, напримерpush_front,pop_backиfront.