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.