"충돌은 막을 버그가 아니라 — 관리할 확실성이야. 가능한 키가 슬롯보다 많아서, 두 키가 반드시 같이 떨어져. 유일한 질문은 그럴 때 네가 뭘 하냐야."
충돌은 보장돼
테이블은 슬롯 수가 고정인데, 가능한 키의 우주는 사실상 무한해. 비둘기집 원리로, 다른 두 키가 결국 같은 슬롯으로 해시될 수밖에 없어. 그래서 해시맵 설계는 충돌을 피하는 게 아니라 (불가능) — 그걸 우아하게 해소하는 거야. 고전 전략이 둘 있고, 정반대 거래를 해.
체이닝: 각 슬롯에 작은 리스트
분리 체이닝 (separate chaining) 은 각 슬롯이 거기로 해시된 모든 키의 작은 리스트 ("체인") 를 들게 해. 삽입은 슬롯 리스트에 append; 조회는 슬롯으로 해시하고 그 짧은 리스트를 훑어. 리스트가 짧게 유지되는 한 (좋은 해시 + 낮은 부하), 조회가 O(1). 체이닝은 단순하고, 높은 부하에 우아하게 떨어지고 (리스트가 좀 길어질 뿐), 그저 그런 해시 함수에 관대해. 비용: 리스트/포인터의 추가 메모리, 그리고 흩어진 리스트 노드의 캐시 비친화성.
개방 주소법: 모두가 배열 안에 산다
개방 주소법 (open addressing) 은 모든 항목을 테이블 배열에 직접 저장해 — 곁다리 리스트 없음. 충돌 시, 고정 규칙으로 다음 빈 슬롯을 탐사해: 선형 탐사는 slot+1, slot+2, … 를 확인; 제곱 탐사는 커지는 간격으로 점프; 이중 해싱은 두 번째 해시로 보폭을 정해. 조회는 키나 빈 슬롯을 찾을 때까지 같은 탐사 순서를 따라. 개방 주소법은 캐시 친화적 (다 연속, 포인터 추적 없음) 이고 메모리를 덜 써 — 근데 테이블이 차면 가파르게 떨어지고 (탐사 순서가 길어짐, 클러스터링이라는 문제), 삭제가 까다로워: 슬롯을 그냥 비우면 탐사 사슬이 깨지니까, "tombstone" 으로 표시해.
Python 이 실제로 하는 것
CPython 의 dict 와 set 은 영리한 perturbation 기반 탐사 순서의 개방 주소법을 쓰고, 테이블을 많아야 약 2/3 차게 유지해 빠르게 (왜인지는 부하율 lesson 에서 봐). 그래서 Python dict 를 쓸 때 — 끊임없이 — 넌 밑에서 tombstone 삭제 있는 개방 주소법을 쓰는 거야. 구현할 필요는 없지만, 그걸 아는 게 dict 의 행동을 설명해: 왜 리사이즈하고, 왜 캐시-빠르고, 왜 삽입 순서가 보존되는지 (위에 얹은 별도 compact-array 트릭).