Menu

C++ unordered_map: 빠른 해시 맵 조회 완벽 설명

C++의 std::unordered_map을 배워 보세요. 평균 O(1)의 삽입과 조회를 제공하는 map의 해시 테이블 형제입니다. 기본 연산, [] 자동 삽입의 함정, count와 find의 차이, 정렬된 map 대신 언제 선택할지 다룹니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

순서가 중요하지 않을 때

방금 std::map을 봤습니다. 균형 트리를 사용해 키를 정렬된 상태로 유지하고 O(log n) 연산을 제공하죠. 하지만 정렬에는 비용이 들고, 많은 경우 키가 어떤 순서로 나오는지는 신경 쓰지 않습니다. 그저 "이 키가 여기 있는가, 그 값은 무엇인가?"를 최대한 빠르게 묻고 싶을 뿐입니다.

바로 그것을 위한 것이 std::unordered_map입니다. 이것은 해시 테이블입니다. 각 키를 해시 함수에 통과시켜 저장 위치를 정하므로, 삽입·조회·삭제가 O(log n)이 아니라 **평균 O(1)**이 됩니다. 그 대가로 순회 순서는 명시되지 않습니다. 키는 버킷이 보관하고 있는 순서대로 나옵니다.

인터페이스는 의도적으로 map과 거의 동일합니다. 흔히 타입만 바꿔 한쪽을 다른 쪽으로 교체할 수 있습니다. <map>이 아니라 <unordered_map>을 포함하면 키가 정렬되지 않은 상태로 나옵니다.

삽입과 갱신

항목을 넣는 방법은 여러 가지이고, 모두 같은 방식으로 동작하지는 않습니다. 가장 많이 쓰게 될 두 가지는 operator[]insert입니다.

핵심 차이는 이렇습니다. operator[]는 기존 값을 덮어쓰지만, insert는 이미 있는 키를 건드리지 않습니다. C++17이라면 insert_or_assign(key, value)는 "무조건 이것으로 설정한다"는 의미를 주고, try_emplace(key, args...)는 키가 새로울 때만 값을 제자리에서 생성합니다. 생성 비용이 큰 값에 유용합니다.

operator[]의 함정: 읽을 때 삽입한다

이것은 가장 흔한 unordered_map 버그라서 별도의 절을 둡니다. m[key]는 순수한 읽기가 아닙니다. 키가 없으면 기본 생성하여 삽입하고(int0이 되고, string""이 됩니다) 참조를 반환합니다. 그래서 조회처럼 보이는 코드가 맵을 슬그머니 키웁니다.

seen["y"]는 언급된 것만으로 "y" -> 0 항목을 만들었습니다. 맵을 변경하지 않고 존재를 검사하려면 count(0 또는 1을 반환)나 find를 사용하세요.

경험칙: []는 생성하거나 갱신할 의도가 있을 때만 사용하세요. 순수 읽기에는 count, contains(C++20), 또는 find를 사용합니다.

find와 at: 안전한 조회

find는 항목에 대한 반복자를 반환하고, 키가 없으면 end()를 반환합니다. 절대 삽입하지 않으며, 한 번의 조회로 it->firstit->second를 통해 키와 값 모두에 접근하게 해 줍니다.

키가 존재해야 한다고 확신하고 없을 때 확실히 실패하길 원한다면 at을 사용하세요. []와 달리 at은 삽입하지 않습니다. 없는 키에 대해 std::out_of_range던집니다.

int p = prices.at("pen");   // 정상
int q = prices.at("hat");   // std::out_of_range를 던짐 - 키가 없음

그래서 조회 방식이 세 가지 있습니다. [](삽입함), at(던짐), 그리고 find/count(맵을 건드리지 않고 존재를 알림)입니다. 실패 방식이 의도에 맞는 것을 선택하세요.

순회와 삭제

구조적 바인딩을 사용한 범위 기반 for 루프는 모든 항목을 깔끔하게 도는 방법입니다. 순서는 임의라는 점을 기억하고, 절대 의존하지 마세요.

항목을 제거하려면 erase가 키를 직접 받아 제거된 개수(0 또는 1)를 반환합니다. 중요한 함정: unordered_map에서 순회 중 삭제는 삭제된 원소의 반복자만 무효화하므로, 안전하게 진행하려면 erase(it)의 반환값을 사용하세요.

wins.erase(it++) 식으로 쓰거나 평범한 erase(it) 뒤에 ++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)은 평균이며(나쁜 해시가 악화시킬 수 있음), 사용자 정의 키 타입에는 해시 특수화나 해시 함수 객체가 필요한 반면 mapoperator<만 있으면 됩니다.

다음: Set

이제 키-값 컨테이너의 두 가지 형태를 모두 봤습니다. 하지만 때로는 값이 전혀 필요 없을 때가 있습니다. 고유한 태그 모음이나 방문한 ID처럼, 무언가가 존재하는지만 중요한 경우입니다. 다음으로 std::set(그리고 해시 기반 사촌인 unordered_set)을 살펴보겠습니다. 이들은 키만 저장하고 자동으로 고유하게 유지합니다.

자주 묻는 질문

C++에서 map과 unordered_map의 차이는 무엇인가요?

std::map은 균형 이진 트리입니다. 키가 정렬된 상태를 유지하며 연산은 O(log n)입니다. std::unordered_map해시 테이블입니다. 키에 특정한 순서는 없지만 삽입과 조회가 평균 **O(1)**입니다. 키로 빠르게 조회만 하면 되고 순서가 상관없을 때는 unordered_map을, 정렬된 순회나 범위 질의가 필요할 때는 map을 사용하세요.

unordered_map[]은 키가 없으면 삽입하나요?

네. m[key]는 키가 없으면 기본 생성하여 삽입한 뒤 그에 대한 참조를 반환합니다. 즉, 읽기처럼 보이는 if (m[key] == ...)조차 맵을 조용히 키웁니다. 삽입 없이 존재 여부를 확인하려면 m.count(key)m.find(key)를 사용하세요.

C++에서 unordered_map이 항상 map보다 빠른가요?

아니요. 평균적으로 O(1)이지만 해싱에는 오버헤드가 있고, 나쁜 해시(또는 악의적인 키)는 조회를 O(n)까지 악화시킬 수 있습니다. 작은 맵에서는 상수 인자와 더 나쁜 캐시 동작 때문에 정렬된 map이 같거나 더 빠를 수 있습니다. 중요하다면 벤치마크하되, 키 조회만 필요하다면 기본적으로 unordered_map을 선택하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기