Ch.3 해시맵과 해시셋

해시 충돌과 해결 — Chaining vs Open Addressing

해시 충돌이 발생하는 이유와 해결 방법 두 가지를 비교한다Load Factor가 성능에 미치는 영향을 이해한다

두 사람의 이름이 같은 사물함 번호를 받았다면?

해시 함수는 완벽하지 않습니다. 서로 다른 키가 같은 인덱스를 가리킬 수 있죠. 이 '충돌'을 어떻게 해결할까요?

충돌이 많아지면 O(1)이 깨지지 않나?

Chaining(연결 리스트)과 Open Addressing(빈자리 찾기), 두 전략으로 충돌을 관리합니다.

lightbulb

핵심 개념

해시 충돌

서로 다른 키가 같은 해시 인덱스를 가리키는 현상

Load Factor

저장된 데이터 수 / 배열 크기. 충돌 확률의 지표


article

핵심 내용

비둘기집 원리를 아시나요? 11마리 비둘기, 10개 집 → 최소 1개 집에 2마리!

# 충돌 예시
def simple_hash(key, size):
    return sum(ord(c) for c in key) % size

# 배열 크기 = 10
print(simple_hash("abc", 10))  # (97+98+99) % 10 = 4
print(simple_hash("bac", 10))  # (98+97+99) % 10 = 4  ← 충돌!

"abc"와 "bac"는 다른 문자열이지만 같은 인덱스 4를 받았습니다!

해결법 ①: 같은 인덱스에 연결 리스트를 매달아 여러 값을 저장!

class ChainingHashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [[] for _ in range(size)]  # 각 슬롯 = 리스트

    def _hash(self, key):
        return hash(key) % self.size

    def put(self, key, value):
        idx = self._hash(key)
        # 이미 같은 키가 있으면 덮어쓰기
        for i, (k, v) in enumerate(self.table[idx]):
            if k == key:
                self.table[idx][i] = (key, value)
                return
        self.table[idx].append((key, value))

    def get(self, key):
        idx = self._hash(key)
        for k, v in self.table[idx]:
            if k == key:
                return v
        return None

Chaining 장점: 구현이 간단, 삭제가 쉬움 Chaining 단점: 리스트가 길어지면 O(n)으로 퇴화

해결법 ②: 충돌하면 빈 슬롯을 찾아 이동합니다

class LinearProbingHashTable:
    def __init__(self, size=10):
        self.size = size
        self.keys = [None] * size
        self.values = [None] * size

    def _hash(self, key):
        return hash(key) % self.size

    def put(self, key, value):
        idx = self._hash(key)
        while self.keys[idx] is not None:
            if self.keys[idx] == key:  # 같은 키면 덮어쓰기
                self.values[idx] = value
                return
            idx = (idx + 1) % self.size  # 다음 슬롯으로!
        self.keys[idx] = key
        self.values[idx] = value

    def get(self, key):
        idx = self._hash(key)
        while self.keys[idx] is not None:
            if self.keys[idx] == key:
                return self.values[idx]
            idx = (idx + 1) % self.size
        return None

두 전략의 장단점을 한눈에 비교해봅시다

Load Factor = n / m 데이터가 많아지면 충돌이 급증합니다

Load Factor = 저장된 항목 수(n) / 배열 크기(m) • LF < 0.7 → 성능 양호 • LF > 0.7 → 충돌 급증 → 리사이즈 필요! • Python dict는 LF > 2/3일 때 자동 리사이즈

리사이즈 = 배열을 2배로 키우고 모든 키를 다시 해싱합니다 (O(n) 비용)

리사이즈는 가끔 일어나므로 amortized O(1) — 평균적으로는 여전히 O(1)!

Chaining 방식에서 인덱스 3에 이미 "apple"이 있는데 "banana"도 인덱스 3이 나왔습니다. 어떻게 처리하나요?

Load Factor가 1.0을 넘으면 해시 테이블은 더 이상 사용할 수 없다

Python dict가 리사이즈를 시작하는 Load Factor 기준은?

충돌 해결의 두 전략

key

핵심 용어

💥

해시 충돌

서로 다른 키 → 같은 인덱스를 가리키는 현상

📊

Load Factor

n / m (저장 데이터 수 / 배열 크기)

🔗

Chaining

리스트로 연결 — 삭제 쉬움, 메모리 추가 필요

➡️

Open Addressing

빈 슬롯 탐색 — 캐시 효율적, 삭제 복잡

🐍

Python dict

Open Addressing 변형 사용 (최적화된 프로빙)

edit_note

정리 노트

해시 충돌 — Chaining vs Open Addressing

충돌 해결 전략

Chaining
같은 인덱스에 연결 리스트 저장 — 구현 간단, 삭제 쉬움
Open Addressing
충돌 시 빈 슬롯 탐색 — 캐시 효율적, 삭제 복잡
Linear Probing
다음 칸으로 이동 (idx+1), 클러스터링 문제 있음

Load Factor

공식
n / m (데이터 수 / 배열 크기)
임계값
Python 2/3, Java 0.75 — 초과 시 리사이즈
리사이즈
배열 2배 확장 + 전체 재해싱 → amortized O(1)

면접 포인트: 'Chaining vs Open Addressing 차이'는 단골 질문!

image

시각 자료

다이어그램: ds-d19
다이어그램: ds-d21
check_circle

핵심 정리

  • 1해시 충돌 = 서로 다른 키가 같은 인덱스를 가리킴
  • 2Chaining = 리스트 연결, Open Addressing = 빈 슬롯 탐색
  • 3Load Factor 관리가 성능의 핵심

퀴즈와 인터랙션으로 더 깊이 학습하세요

play_circle인터랙티브 레슨 시작