Menu

unordered_map w C++: szybka tablica mieszająca (hash map)

Poznaj std::unordered_map w C++, oparte na tablicy mieszającej rodzeństwo map, które daje średnio O(1) przy wstawianiu i wyszukiwaniu. Omawiamy podstawowe operacje, pułapkę automatycznego wstawiania przez [], count kontra find oraz to, kiedy wybrać je zamiast uporządkowanej mapy.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Gdy kolejność nie ma znaczenia

Poprzednia strona przedstawia std::map, które trzyma klucze posortowane w zrównoważonym drzewie i daje operacje w O(log n). Sortowanie ma jednak swoją cenę, a bardzo często nie obchodzi cię, w jakiej kolejności wychodzą klucze: chcesz tylko jak najszybciej zapytać "czy ten klucz tu jest i jaka jest jego wartość?".

Do tego służy std::unordered_map. To tablica mieszająca (hash table): przepuszcza każdy klucz przez funkcję mieszającą, żeby zdecydować, gdzie go zapisać, dzięki czemu wstawianie, wyszukiwanie i usuwanie mają średnio O(1) zamiast O(log n). Ceną jest nieokreślona kolejność iteracji: klucze wychodzą w takiej kolejności, w jakiej akurat leżą w kubełkach.

Interfejs jest celowo prawie identyczny jak w map: często możesz zamienić jedno na drugie, zmieniając tylko typ. Dołącz <unordered_map> (a nie <map>), a klucze będą wychodzić nieposortowane.

Wstawianie i aktualizacja

Jest kilka sposobów dodawania wpisów i nie wszystkie zachowują się tak samo. Najczęściej używane to operator[] i insert:

Kluczowa różnica: operator[] nadpisuje istniejącą wartość, a insert zostawia istniejący klucz w spokoju. W C++17 insert_or_assign(key, value) daje semantykę "ustaw bez względu na wszystko", a try_emplace(key, args...) tworzy wartość w miejscu tylko wtedy, gdy klucz jest nowy, co przydaje się przy wartościach kosztownych w budowie.

Pułapka operator[]: wstawia przy odczycie

To najczęstszy błąd związany z unordered_map, więc ma własną sekcję. m[key] nie jest czystym odczytem. Jeśli klucza brakuje, operator tworzy wartość domyślną i ją wstawia (int staje się 0, string staje się ""), a potem zwraca referencję. Kod, który wygląda na wyszukiwanie, po cichu powiększa więc mapę:

seen["y"] utworzyło wpis "y" -> 0 samym tym, że zostało użyte. Aby sprawdzić obecność bez zmieniania mapy, użyj count (zwraca 0 lub 1) albo find:

Praktyczna zasada: używaj [] tylko wtedy, gdy chcesz utworzyć lub zaktualizować wpis. Do czystych odczytów służą count, contains (C++20) albo find.

find i at: bezpieczne wyszukiwanie

find zwraca iterator do wpisu albo end(), gdy klucza nie ma. Nigdy niczego nie wstawia i pozwala w jednym wyszukiwaniu dostać się zarówno do klucza, jak i do wartości przez it->first oraz it->second:

Gdy wiesz, że klucz powinien istnieć, i chcesz twardego błędu, jeśli go nie ma, użyj at. W przeciwieństwie do [] metoda at niczego nie wstawia: dla brakującego klucza rzuca std::out_of_range:

int p = prices.at("pen");   // w porządku
int q = prices.at("hat");   // rzuca std::out_of_range: klucza nie ma

Masz więc trzy style wyszukiwania: [] (wstawia), at (rzuca wyjątek) oraz find/count (informują o obecności, nie ruszając mapy). Wybierz ten, którego zachowanie przy braku klucza pasuje do twojego zamiaru.

Iteracja i usuwanie

Pętla for po zakresie ze structured bindings to czysty sposób na przejście po wszystkich wpisach. Pamiętaj, że kolejność jest dowolna: nigdy na niej nie polegaj:

Aby usunąć wpis, erase przyjmuje bezpośrednio klucz i zwraca liczbę usuniętych elementów (0 lub 1). Ważna pułapka: usuwanie w trakcie iteracji unieważnia w unordered_map tylko iterator usuniętego elementu, więc do bezpiecznego przejścia dalej używaj wartości zwracanej przez erase(it):

Zapis w stylu wins.erase(it++) albo ++it po zwykłym erase(it) to klasyczna pułapka wiszącego iteratora: usunięty iterator jest martwy, więc zawsze bierz iterator zwrócony przez erase.

map czy unordered_map?

Oba kontenery przechowują pary klucz-wartość z podobnym API, więc wybór zależy od tego, czego potrzebujesz:

// std::map            -> klucze posortowane, O(log n), zapytania o zakresy (lower_bound)
// std::unordered_map  -> bez kolejności,     średnio O(1), najszybsze zwykłe wyszukiwanie

Sięgaj po unordered_map, gdy potrzebujesz tylko szybkiego wyszukiwania po kluczu, a kolejność jest bez znaczenia (liczenie częstości słów, cache, usuwanie duplikatów). Wybierz map, gdy potrzebujesz kluczy w kolejności posortowanej, chcesz iterować po kolei albo wykonujesz zapytania o zakresy. Dwa zastrzeżenia dotyczące unordered_map: jego O(1) to wartość średnia, którą słaba funkcja mieszająca może pogorszyć, a własny typ klucza wymaga specjalizacji hash albo funktora mieszającego, podczas gdy map potrzebuje tylko operator<.

Dalej: set

Znasz już oba rodzaje kontenerów klucz-wartość. Czasem jednak w ogóle nie potrzebujesz wartości: interesuje cię tylko, czy coś jest obecne, na przykład w kolekcji unikalnych tagów albo odwiedzonych identyfikatorów. Następnie przyjrzymy się std::set (i jego kuzynowi opartemu na haszowaniu, unordered_set), które przechowują same klucze i automatycznie dbają o ich unikalność.

Najczęściej zadawane pytania

Jaka jest różnica między map a unordered_map w C++?

std::map to zrównoważone drzewo binarne: klucze są zawsze posortowane, a operacje mają złożoność O(log n). std::unordered_map to tablica mieszająca (hash table): klucze nie mają określonej kolejności, ale wstawianie i wyszukiwanie działają średnio w O(1). Używaj unordered_map, gdy potrzebujesz tylko szybkiego wyszukiwania po kluczu i kolejność nie ma znaczenia, a map, gdy potrzebujesz iteracji w kolejności posortowanej lub zapytań o zakresy.

Czy unordered_map[] wstawia klucz, jeśli go nie ma?

Tak. m[key] tworzy wartość domyślną i wstawia klucz, jeśli go brakuje, a potem zwraca do niego referencję. Oznacza to, że nawet wyglądające na odczyt if (m[key] == ...) po cichu powiększa mapę. Aby sprawdzić obecność bez wstawiania, użyj m.count(key) albo m.find(key).

Czy unordered_map jest zawsze szybsze niż map w C++?

Nie. To średnio O(1), ale haszowanie ma swój koszt, a słaba funkcja mieszająca (albo złośliwie dobrane klucze) może pogorszyć wyszukiwanie do O(n). Przy małych mapach stałe czynniki i gorsze wykorzystanie pamięci podręcznej mogą sprawić, że posortowana map będzie równie szybka albo szybsza. Jeśli to ważne, zrób benchmark, ale domyślnie sięgaj po unordered_map, gdy potrzebujesz tylko wyszukiwania po kluczu.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ