Когда порядок не важен
Вы только что видели std::map, который держит ключи отсортированными с помощью сбалансированного дерева и даёт операции за O(log n). Но сортировка чего-то стоит, и зачастую вам всё равно, в каком порядке выходят ключи — вы просто хотите как можно быстрее спросить: «есть ли здесь этот ключ и каково его значение?»
Именно для этого нужен std::unordered_map. Это хеш-таблица: она прогоняет каждый ключ через хеш-функцию, чтобы решить, где его хранить, благодаря чему вставка, поиск и удаление выполняются в среднем за O(1) вместо O(log n). Расплата в том, что порядок перебора не определён — ключи выходят в том порядке, в каком их хранят корзины.
Интерфейс намеренно почти идентичен map — зачастую вы можете заменить один на другой, просто изменив тип. Подключите <unordered_map> (а не <map>), и ключи будут выходить неотсортированными.
Вставка и обновление
Есть несколько способов добавить записи, и ведут они себя по-разному. Два, которые вы будете использовать чаще всего, — это operator[] и insert:
Ключевое различие: operator[] перезаписывает существующее значение, тогда как insert оставляет уже существующий ключ нетронутым. Если вы на C++17, insert_or_assign(key, value) даёт семантику «установить это в любом случае», а try_emplace(key, args...) создаёт значение на месте только тогда, когда ключ новый — удобно для значений, дорогих в построении.
Ловушка operator[]: он вставляет при чтении
Это самая частая ошибка с unordered_map, поэтому ей посвящён отдельный раздел. m[key] — это не чистое чтение. Если ключ отсутствует, он создаёт его по умолчанию и вставляет (int становится 0, string становится ""), а затем возвращает ссылку. Так что код, который выглядит как поиск, незаметно увеличивает map:
seen["y"] создал запись "y" -> 0 лишь от одного упоминания. Чтобы проверить наличие, не изменяя map, используйте count (возвращает 0 или 1) или find:
Правило большого пальца: используйте [] только тогда, когда намерены создать или обновить. Для чистого чтения используйте count, contains (C++20) или find.
find и at: безопасный поиск
find возвращает итератор на запись или end(), если ключ отсутствует. Он никогда не вставляет и позволяет добраться и до ключа, и до значения через it->first и it->second за один поиск:
Когда вы знаете, что ключ должен существовать, и хотите жёсткого отказа, если это не так, используйте at. В отличие от [], at не вставляет — он бросает std::out_of_range при отсутствующем ключе:
int p = prices.at("pen"); // нормально
int q = prices.at("hat"); // бросает std::out_of_range - ключа нет
Итак, у вас есть три стиля поиска: [] (вставляет), at (бросает) и find/count (сообщают о наличии, не трогая map). Выбирайте тот, чьё поведение при сбое соответствует вашему намерению.
Перебор и удаление
Цикл for по диапазону со структурными привязками — это аккуратный способ обойти каждую запись. Помните, что порядок произволен — никогда на него не полагайтесь:
Чтобы удалить запись, erase принимает ключ напрямую и возвращает, сколько было удалено (0 или 1). Важная ловушка: в unordered_map удаление во время перебора делает недействительным только итератор удалённого элемента, поэтому используйте возвращаемое значение erase(it), чтобы безопасно продвигаться:
Запись в стиле wins.erase(it++) или ++it после простого erase(it) — это классическая ловушка повисшего итератора: удалённый итератор мёртв, поэтому всегда берите итератор, возвращаемый erase.
map или unordered_map?
Оба хранят пары ключ-значение со схожим API, так что выбор сводится к тому, что вам нужно:
// std::map -> отсортированные ключи, O(log n), запросы по диапазону (lower_bound)
// std::unordered_map -> без порядка, O(1) в ср., самый быстрый простой поиск
Берите unordered_map, когда вам нужен только быстрый поиск по ключу и порядок не важен (подсчёт частоты слов, кеширование, устранение дубликатов). Выбирайте map, когда нужны ключи в отсортированном порядке, хотите перебирать по порядку или делать запросы по диапазону. Две оговорки для unordered_map: его O(1) — это среднее (плохой хеш может его ухудшить), а пользовательский тип ключа требует специализации хеша или хеш-функтора, тогда как map нужен только operator<.
Далее: Set
Теперь вы видели обе разновидности контейнеров ключ-значение. Однако иногда значение вам вовсе не нужно — вас интересует лишь то, присутствует ли что-то, например коллекция уникальных тегов или посещённых ID. Далее мы рассмотрим std::set (и его основанного на хешах родственника unordered_set), которые хранят только ключи и автоматически держат их уникальными.
Часто задаваемые вопросы
В чём разница между map и unordered_map в C++?
std::map — это сбалансированное двоичное дерево: ключи остаются отсортированными, а операции выполняются за O(log n). std::unordered_map — это хеш-таблица: ключи не имеют определённого порядка, но вставка и поиск выполняются в среднем за O(1). Используйте unordered_map, когда вам нужен только быстрый поиск по ключу и порядок не важен; используйте map, когда нужен упорядоченный перебор или запросы по диапазону.
Вставляет ли unordered_map[] ключ, если его нет?
Да. m[key] создаёт по умолчанию и вставляет ключ, если он отсутствует, а затем возвращает ссылку на него. Это значит, что даже выглядящий как чтение if (m[key] == ...) незаметно увеличивает map. Чтобы проверить наличие без вставки, используйте m.count(key) или m.find(key).
Всегда ли unordered_map быстрее map в C++?
Нет. Это O(1) в среднем, но у хеширования есть накладные расходы, и плохой хеш (или враждебно подобранные ключи) может ухудшить поиск до O(n). Для небольших map постоянные множители и худшее поведение кеша могут сделать отсортированный map таким же быстрым или даже быстрее. Замеряйте, если это важно, но по умолчанию выбирайте unordered_map, когда нужен только поиск по ключу.