topic난이도 · 약 25

해시맵 · 해시셋 — O(1) 조회의 비밀

키를 해시함수로 주소로 바꿔서 즉시 접근. Map/Set/dict/Object의 내부.

#해시맵#Map#Set#dict#O(1)#해시충돌
왜 배우는가

이중 for문을 해시맵 하나로 바꾸면 O(n²) → O(n)이 된다. Claude가 `for ... for` 패턴을 만들었을 때 '여기 Map 쓰면 더 빠르지 않아?'를 볼 수 있는 눈이 필요하다. 실무 성능 이슈의 근원.

해시맵은 '주소 계산기'가 달린 사물함이다. 키(예: `"짓친"`)를 해시함수에 넣으면 숫자가 나오고, 그 숫자가 사물함 번호가 된다. 찾을 때도 같은 계산을 반복하므로 데이터가 100만 개든 1개든 걸리는 시간은 같다 → O(1).

해시맵 구조 — 키 → 해시함수 → 버킷 인덱스. 배열 순회 없이 바로 주소 계산
작업배열/리스트해시맵
키로 값 조회O(n) — 처음부터 훑음O(1) — 바로 계산
값 추가 (끝)O(1)O(1)
값 추가 (앞/중간)O(n)O(1)
특정 키 존재 확인O(n)O(1)
순서 유지⚠️ (JS Map은 삽입순, 객체는 보장 X)

언어마다 이름이 다르지만 실체는 같다. Claude가 만든 코드에서 아래 이름들이 보이면 전부 해시맵이다.

언어해시맵해시셋특징
JavaScript/TS`Map`, 일반 `{}``Set`Map은 키 타입 자유, 객체는 문자열 키만
Python`dict``set`dict comprehension `{k:v for ...}`
Java`HashMap`, `LinkedHashMap``HashSet`초기 용량·로드팩터 튜닝
Go`map[K]V``map[K]struct{}`셋 타입 없어서 이디엄
Rust`HashMap`, `BTreeMap``HashSet`BTreeMap은 정렬 유지
javascript
// ━━━ JavaScript Map vs 객체 ━━━

// 1) Map — 권장 (키 타입 자유, 순회 쉬움, 크기 O(1))
const users = new Map();
users.set("짓친", { age: 25 });
users.set(42, "숫자 키도 OK");   // 객체 `{}`는 불가능
users.get("짓친");                // → { age: 25 }  O(1)
users.has("홍길동");              // → false          O(1)
users.size;                       // → 2
users.delete("짓친");

for (const [key, value] of users) { /* 순회 순서 = 삽입 순서 */ }

// 2) 일반 객체 — 간단할 때만
const cache = {};
cache["key1"] = "value1";
"key1" in cache;                  // → true

// 3) Set — 중복 제거 + 포함 여부 확인
const tags = new Set(["js", "ts", "js"]); // → {"js", "ts"}  자동 중복 제거
tags.has("ts");                   // → true  O(1)

2023년 이후 현대 JS에서는 "키가 자유롭고 대량인 경우 Map, 설정값처럼 고정 키면 객체"가 기본. Claude가 `Object.keys(obj).length`를 자주 쓴다면 Map으로 바꾸면 O(1)이다.

Python `in` 연산자가 dict/set에 대해서는 O(1), list에 대해서는 O(n). Claude가 큰 리스트에 `in`을 쓴다면 set으로 바꾸자는 제안이 맞다.

이 패턴을 보면 해시맵을 떠올려라. 두 리스트를 비교하는 이중 for문 → 한 쪽을 Set/Map으로 만들면 O(n)으로 떨어진다.

javascript
// ❌ O(n²) — 1만 건이면 1억 번 비교 → 느림
const found = [];
for (const a of listA) {
  for (const b of listB) {
    if (a.id === b.id) found.push(a);
  }
}

// ✅ O(n) — Set으로 O(1) 조회
const idsInB = new Set(listB.map(b => b.id));
const found = listA.filter(a => idsInB.has(a.id));

// 실전: 두 배열의 교집합·차집합
const aSet = new Set(a);
const intersection = b.filter(x => aSet.has(x));     // 교집합
const difference   = a.filter(x => !new Set(b).has(x)); // 차집합

이 패턴 하나 외우는 것으로 실무 성능 이슈의 절반을 풀 수 있다. "배열끼리 중첩 검색"을 보면 Set으로 전처리.

다른 키에서 같은 해시값이 나오면? 이걸 충돌(collision)이라 한다. 해결책 두 가지. ① 체이닝 — 같은 버킷에 리스트를 매달아 둠. ② 개방주소법 — 비어있는 다음 칸으로 이동. JS Map, Python dict 모두 내부에서 자동 처리한다.

충돌이 많아지면 O(1) 보장이 깨진다. 최악 O(n)까지. 악의적 입력으로 일부러 충돌을 만들어 서버를 느리게 만드는 해시충돌 DoS 공격이 실존했다. 현대 언어 런타임은 무작위 해시 시드로 이를 완화한다.

해시셋은 값의 존재 여부만 빠르게 확인하는 자료구조다. 실전 용도 3가지:

javascript
// ① 중복 제거
const unique = [...new Set(nums)];

// ② 빠른 포함 확인 — 게시판 차단 단어 리스트
const bannedWords = new Set(["spam", "abc", ...]); // 1만 개
const isBanned = (text) =>
  text.split(/\s+/).some(word => bannedWords.has(word));
// 리스트였다면 매 단어마다 O(1만), Set이므로 O(1)

// ③ 이미 처리한 항목 추적 — 무한루프 방지
const visited = new Set();
function walk(node) {
  if (visited.has(node.id)) return;
  visited.add(node.id);
  node.children.forEach(walk);
}

그래프 순회·BFS·DFS에서 `visited` 집합은 거의 항상 Set을 쓴다.

상황해시맵?이유
키로 조회가 잦다본래 용도
정렬 순서가 필요TreeMap/SortedDict 또는 정렬 후 사용
범위 검색 (A~M)Tree(B-Tree) 계열이 맞음
데이터가 매우 작음 (n<20)배열이 오히려 빠른 구간 있음
키 순서 = 삽입 순서 필요✅ (JS Map)Map은 보장, 객체는 X

Claude에게 이렇게 말하면 자동 리팩터링된다. "listA에서 listB의 id에 포함되는 항목만 뽑는 이중 for문이 있어. Set으로 전처리해서 O(n)으로 바꿔줘." — 'Set', 'O(n)' 같은 단어가 들어가면 Claude가 정확히 의도한 방식으로 고친다.

한 줄 요약: 해시맵은 '키 → 해시함수 → 주소'를 한 번에 계산해서 O(1). 배열 이중 반복은 Set/Map 전처리로 O(n)으로 뚝 떨어진다. 중복 제거·포함 확인·방문 추적은 거의 항상 Set.

실기 드릴 4문항
edit실기 드릴 · 단답형

JavaScript에서 `{}` 객체 대신 `Map`을 써야 하는 대표적 이유 두 가지는?

check_circle실기 드릴 · OX

해시맵의 조회는 언제나 O(1)이다.

code실기 드릴 · 코드 추적

다음 코드의 출력은?

python
nums = [1, 2, 3, 2, 1]
print(len(set(nums)))
check_circle실기 드릴 · OX

정렬된 순서로 범위 검색(A~M)이 필요할 때 해시맵이 적합하다.