Hash Map
마지막 업데이트
해시 맵은 키-값 쌍을 저장하고 키로 값을 평균 O(1)에 조회할 수 있게 해줍니다. 해시 테이블과 완전히 같은 방식으로 동작하지만, 각 버킷 항목이 키와 그에 연결된 값을 모두 담습니다. 쌍을 저장하거나 가져오려면 키를 버킷 인덱스로 해싱한 다음, 그 버킷의 체인을 순회하며 일치하는 키를 찾습니다. 위의 재생 버튼을 눌러 쌍이 해시된 버킷에 배치되고 값이 키로 조회되는 과정을 확인하세요.
이것은 Python의 dict, Java의 HashMap, JavaScript의 Map/객체 뒤에 있는 구조입니다. 충돌은 해시 테이블과 동일하게 처리되며, 여기서는 분리 연결법으로 다룹니다. 따라서 성능은 좋은 해시 함수와 낮은 적재율에 달려 있습니다.
시간 복잡도
| 연산 | 평균 | 최악의 경우 |
|---|---|---|
| 삽입/갱신 (put) | O(1) | O(n) |
| 조회 (get) | O(1) | O(n) |
| 삭제 | O(1) | O(n) |
| 공간 | O(n) | O(n) |
주요 언어의 해시 맵
| 언어 | 타입 |
|---|---|
| Python | dict |
| Java | HashMap |
| JavaScript | Map / 객체 |
| C++ | std::unordered_map |
| Go | map |
예제 풀이
hash(key) % 8을 사용하여 쌍 ("cat", 3), ("dog", 5), ("cat", 9), ("emu", 7)을 버킷 8개짜리 맵에 삽입합니다. hash("cat") % 8 = 2, hash("dog") % 8 = 5, hash("emu") % 8 = 2라고 가정합니다:
| 단계 | 구조 | 동작 |
|---|---|---|
Put ("cat", 3) | 버킷 2: [("cat", 3)] | 버킷 2로 해싱; 체인이 비어 있으므로 쌍을 추가. |
Put ("dog", 5) | 버킷 2: [("cat", 3)], 버킷 5: [("dog", 5)] | 버킷 5로 해싱; 체인이 비어 있으므로 쌍을 추가. |
Put ("cat", 9) | 버킷 2: [("cat", 9)], 버킷 5: [("dog", 5)] | 버킷 2로 해싱; 키 "cat"이 이미 체인에 있으므로 값을 9로 갱신. |
Put ("emu", 7) | 버킷 2: [("cat", 9), ("emu", 7)], 버킷 5: [("dog", 5)] | 버킷 2로 해싱; "cat"과 충돌하지만 키를 찾지 못하므로 쌍을 추가. |
Get "emu" | 버킷 2: [("cat", 9), ("emu", 7)] | 버킷 2로 해싱; 체인을 순회하며 "cat"을 건너뛰고 "emu"와 일치, 7을 반환. |
해시 맵을 사용해야 할 때
| 사용할 때 | 피해야 할 때 |
|---|---|
정확한 키로 평균 O(1) 조회, 삽입, 삭제가 필요할 때. | 키를 정렬 순서로 유지하거나 범위 질의가 필요할 때 — 균형 BST나 정렬 맵을 사용. |
| 키가 해시 가능하고 동등성이 잘 정의되어 있을 때(문자열, 정수, 튜플). | 키가 가변이고 삽입 후 바뀔 수 있을 때. 해당 버킷이 손상됨. |
| 식별자로 세기, 중복 제거, 캐싱, 인덱싱을 할 때. | 상각 평균 한계가 아니라 최악의 경우 O(log n) 보장이 필요할 때. |
| 순회 순서가 중요하지 않거나 삽입 순서 맵으로 충분할 때. | 메모리가 매우 빠듯할 때 — 버킷, 적재율 여유 공간, 포인터가 오버헤드를 더함. |
Hash Map 코드
Python, JavaScript, Java, C++, C로 작성된 깔끔하고 실행 가능한 Hash Map 구현입니다. 언어를 선택해 코드를 복사하거나 Coddy 플레이그라운드에서 바로 열어보세요.
Python로 구현한 Hash Map 코드
1# Python's built-in dict is a hash map: O(1) average2# insert, lookup, and delete.3inventory = {"apple": 3, "banana": 7}4
5# Insert and update6inventory["cherry"] = 57inventory["apple"] += 28
9# Lookup, with .get for a safe default on missing keys10print("apple: ", inventory["apple"])11print("mango: ", inventory.get("mango", 0))12
13# Membership test and delete14print("banana in stock:", "banana" in inventory)15del inventory["banana"]16print("banana in stock:", "banana" in inventory)17
18# Iterate over key-value pairs19for fruit, count in sorted(inventory.items()):20 print(f"{fruit}: {count}")21
22# Classic hash map use case: counting frequencies23words = "the quick brown fox jumps over the lazy dog the end".split()24freq = {}25for word in words:26 freq[word] = freq.get(word, 0) + 127
28print("Occurrences of the:", freq["the"])29print("Most common word: ", max(freq, key=freq.get))JavaScript로 구현한 Hash Map 코드
1// The built-in Map is the idiomatic hash map in JavaScript:2// any key type, insertion-ordered iteration, O(1) average lookups.3const ages = new Map();4
5ages.set("Alice", 30);6ages.set("Bob", 25);7ages.set("Carol", 35);8ages.set("Bob", 26); // set on an existing key updates the value9
10console.log("Bob:", ages.get("Bob"));11console.log("Has Dave?", ages.has("Dave"));12console.log("Size:", ages.size);13
14ages.delete("Carol");15console.log("Size after delete:", ages.size);16
17for (const [name, age] of ages) {18 console.log(`${name} is ${age}`);19}20
21console.log("Keys:", [...ages.keys()].join(", "));Java로 구현한 Hash Map 코드
1import java.util.HashMap;2import java.util.Map;3
4public class Main {5 public static void main(String[] args) {6 Map<String, Integer> ages = new HashMap<>();7 ages.put("Alice", 30);8 ages.put("Bob", 25);9 ages.put("Carol", 35);10
11 System.out.println("Bob -> " + ages.get("Bob"));12 System.out.println("contains Carol: " + ages.containsKey("Carol"));13 System.out.println("Dave or default: " + ages.getOrDefault("Dave", -1));14
15 // getOrDefault makes frequency counting a one-liner16 String[] words = {"apple", "banana", "apple", "cherry", "apple"};17 Map<String, Integer> counts = new HashMap<>();18 for (String w : words) {19 counts.put(w, counts.getOrDefault(w, 0) + 1);20 }21 System.out.println("apple count: " + counts.get("apple"));22
23 ages.remove("Bob");24 System.out.println("size after remove: " + ages.size());25
26 for (Map.Entry<String, Integer> entry : ages.entrySet()) {27 System.out.println(entry.getKey() + " is " + entry.getValue());28 }29 }30}C++로 구현한 Hash Map 코드
1#include <algorithm>2#include <iostream>3#include <string>4#include <unordered_map>5#include <vector>6
7int main() {8 std::unordered_map<std::string, int> ages;9
10 // Insert and update11 ages["alice"] = 30; // operator[] inserts or overwrites12 ages.insert({"bob", 25}); // insert keeps an existing value13 ages.emplace("carol", 41);14 ages["alice"] = 31;15
16 // Lookup17 auto it = ages.find("bob");18 if (it != ages.end()) {19 std::cout << "bob is " << it->second << "\n";20 }21 std::cout << "has dave: " << ages.count("dave") << "\n";22
23 // Remove24 ages.erase("carol");25
26 // Iterate (sort keys first: unordered_map has no defined order)27 std::vector<std::string> keys;28 for (const auto& entry : ages) keys.push_back(entry.first);29 std::sort(keys.begin(), keys.end());30 for (const std::string& name : keys) {31 std::cout << name << " = " << ages[name] << "\n";32 }33 std::cout << "size: " << ages.size() << "\n";34 return 0;35}C로 구현한 Hash Map 코드
1#include <stdbool.h>2#include <stdio.h>3#include <stdlib.h>4#include <string.h>5
6#define BUCKETS 167
8typedef struct Node {9 char key[24];10 int value;11 struct Node* next;12} Node;13
14// A small string -> int hash map with a reusable put/get/remove API15typedef struct {16 Node* buckets[BUCKETS];17 int size;18} HashMap;19
20unsigned int hashKey(const char* key) {21 unsigned int h = 5381;22 for (; *key; key++) h = h * 33 + (unsigned char)*key;23 return h % BUCKETS;24}25
26void mapPut(HashMap* map, const char* key, int value) {27 unsigned int i = hashKey(key);28 for (Node* n = map->buckets[i]; n != NULL; n = n->next) {29 if (strcmp(n->key, key) == 0) {30 n->value = value;31 return;32 }33 }34 Node* n = malloc(sizeof(Node));35 strcpy(n->key, key);36 n->value = value;37 n->next = map->buckets[i];38 map->buckets[i] = n;39 map->size++;40}41
42bool mapGet(const HashMap* map, const char* key, int* out) {43 for (Node* n = map->buckets[hashKey(key)]; n != NULL; n = n->next) {44 if (strcmp(n->key, key) == 0) {45 *out = n->value;46 return true;47 }48 }49 return false;50}51
52void mapRemove(HashMap* map, const char* key) {53 Node** link = &map->buckets[hashKey(key)];54 while (*link != NULL) {55 if (strcmp((*link)->key, key) == 0) {56 Node* old = *link;57 *link = old->next;58 free(old);59 map->size--;60 return;61 }62 link = &(*link)->next;63 }64}65
66int main(void) {67 HashMap map = {0};68 mapPut(&map, "alice", 30);69 mapPut(&map, "bob", 25);70 mapPut(&map, "carol", 41);71 mapPut(&map, "alice", 31); // updates the existing key72 int age;73 if (mapGet(&map, "bob", &age)) printf("bob is %d\n", age);74 printf("has dave: %s\n", mapGet(&map, "dave", &age) ? "yes" : "no");75 mapRemove(&map, "carol");76 printf("size: %d\n", map.size);77 return 0;78}해시 맵 자주 묻는 질문
해시 맵과 해시 테이블의 차이는 무엇인가요?
해시 맵의 시간 복잡도는 어떻게 되나요?
O(1)입니다. 모든 키가 하나의 버킷으로 충돌하는 병적인 경우에는 O(n)으로 나빠지므로, 크기 조정과 좋은 해싱이 중요합니다.해시 맵은 같은 버킷으로 해싱되는 두 키를 어떻게 처리하나요?
해시 맵과 이진 탐색 트리 중 무엇을 사용해야 하나요?
O(1) 연산을 원한다면 해시 맵을 사용하세요. 정렬된 키, 범위 질의, 선행자/후행자 조회가 필요하면 균형 이진 탐색 트리(TreeMap이나 std::map 등)를 사용하며, 이들은 O(log n)입니다. BST는 해시 맵이 제공할 수 없는 순서 보장을 위해 약간의 속도를 희생합니다.적재율이란 무엇이며 왜 크기 조정을 유발하나요?
0.75 같은 임계값을 넘으면 크기를 조정합니다(보통 버킷을 두 배로 늘리고 모두 재해싱). 크기 조정은 O(n)이지만 드물기 때문에 상각되어 평균 연산을 O(1)로 유지합니다.리스트 같은 가변 객체를 해시 맵의 키로 사용할 수 있나요?
list에 대해 TypeError: unhashable type을 발생시킵니다. 삽입 후 키를 변경하면 해시가 바뀌어 맵이 올바른 버킷에서 더 이상 찾지 못하고 조용히 항목을 잃습니다. 문자열, 숫자, 튜플 같은 불변 키를 사용하세요.