Hashing
- 데이터를 빠르게 탐색하기 위한 방법
- 특정 키(key)를 해시 함수(hash function) 에 넣어
→ 해시 값(hash value) 또는 주소(address) 를 계산하고,
그 위치에 데이터를 저장하거나 찾아옴.
즉, “키 → 해시 함수 → 인덱스(주소)” 로 바로 접근 가능하기 때문에
평균 O(1) 의 접근 속도를 가짐.
- 기본 구조
key ─▶ hash function ─▶ hash value ─▶ hash table[index]
예시:
- key = “cat”
- hash("cat") = 3
- ⇒ 3번 인덱스에 “cat” 저장
- 해시 테이블(Hash Table)
- 배열(Array) 기반의 자료구조
- 각 원소는 (key, value) 쌍으로 저장됨
- 인덱스를 해시 함수로 계산해 접근
| 장점 | 단점 |
| 탐색, 삽입, 삭제 평균 시간복잡도: O(1) 대규모 데이터 검색에 매우 효율적 |
해시 충돌(Hash Collision) 발생 가능 해시 함수나 테이블 크기 설계가 중요 |
- 해시 충돌(Collision)
서로 다른 두 키가 같은 해시 값으로 계산되는 현상
예: hash("cat") = 3 // hash("dog") = 3
충돌 해결 방법
| 방식 | 설명 |
| Open Addressing | 빈 공간을 찾아 재배치 (예: Linear / Quadratic probing) |
| Chaining | 같은 인덱스에 연결 리스트로 여러 값 저장 |
| Rehashing | 새로운 해시 함수를 사용해 다시 해싱 |
Static Hashing (정적 해싱)
해시 테이블의 크기와 해시 함수가 고정된 방식
- 테이블이 꽉 차면 충돌이 증가하고, 비면 공간이 낭비됨.
즉, 한 번 구조가 만들어지면 크기나 함수가 변하지 않음.
작동 방식
- 해시 함수: h(K) = K mod M (M은 고정된 버킷 개수)
- 데이터가 증가하면 충돌이 많아지고,
삭제가 많으면 공간이 낭비됨.
| 장점 | 단점 |
| 구현이 간단하고 빠름 | 데이터 크기 변화에 비효율적, 충돌 처리 필요 |
: 정해진 데이터 크기(고정 크기 파일 등)
Dynamic Hashing (동적 해싱)
데이터 양의 증가/감소에 따라 해시 테이블 크기를 동적으로 조정하는 방식
- 주로 Extendible Hashing, Linear Hashing 기법이 사용됨
즉, 테이블이 커지면 새로운 해시 함수나 버킷을 추가해 자동으로 확장하는 방식
- Linear Hashing (선형 해싱)
- 디렉토리 없이 버킷을 순차적으로 확장
- 테이블 전체를 한 번에 확장하지 않고, 매 삽입 시 조금씩 확장 (점진적 확장 방식)
== “확장이 필요할 때마다 한 버킷씩 순차적으로 나누는 방식.”
메모리 효율적이고, 시스템 과부하를 방지할 수 있음.
- 버킷을 순서대로 하나씩 분할(split)
- 해시 함수가 단계별로 조금씩 바뀜
- Rehashing 전체 불필요, 기존 데이터 유지
작동원리
| Step 1) 초기상태 | 버킷 개수 = M (예: 4개) | 해시 함수: h(key) = key mod M | ||
| Step 2) 버킷 분할 발생 | 데이터가 많아져서 하나의 버킷이 꽉 차면, 그 버킷만 분할(split) 하고 다음 해시 함수를 사용하기 시작함. |
|||
| 처음 해시 함수: h0(key) = key mod 4 | 다음 단계 해시 함수: h1(key) = key mod 8 | |||
| 이때 분할 포인터(split pointer) 가 어느 버킷을 분할해야 하는지 가리킴. 한 번에 모든 버킷을 확장하지 않고, 매번 삽입될 때마다 포인터가 한 칸씩 이동함. |
||||
예시
초기: M = 4 (버킷 0~3)
데이터 삽입 중 → 버킷 0이 꽉 참 → 버킷 0 분할 → 새 버킷 4 추가 → 포인터(split pointer)가 1로 이동
다음에 버킷 1이 꽉 차면
→ 버킷 1 분할 → 새 버킷 5 추가
이런 식으로 포인터가 0→1→2→3 순으로 순환하면서
점진적으로 확장함.
| 장점 | 단점 |
| 데이터 크기 변화에 유연, 전체 재해싱 불필요 평균 O(1) 유지, 점진적 확장으로 부하 분산 |
구조 복잡, 구현 어려움 (여러 단계 해시 함수 관리 필요) |
- DB 인덱싱, 대규모 데이터 저장 시스템
- Extendible Hashing (확장형 해싱)
- 디렉토리(directory) 와 버킷(bucket) 개념 사용
- 해시 함수의 일부 비트(prefix) 를 사용하여 버킷 선택
- 해시값의 앞 2비트를 사용 (00, 01, 10, 11)
- 특정 버킷이 꽉 차면 → 그 버킷만 분할(split)
- 필요 시 디렉토리 전체를 2배로 확장
→ 전체 재해싱 없이 일부만 확장 가능
주요 구성 요소
| 디렉토리 (Directory) | 해시 값의 상위 몇 비트를 이용해 버킷을 가리키는 테이블 |
| 버킷 (Bucket) | 실제 데이터가 저장되는 공간 |
| 전역 깊이 (Global Depth) | 디렉토리가 사용하는 비트 수 |
| 지역 깊이 (Local Depth) | 버킷이 구분되는 비트 수 (몇 비트까지 구분되어 있는지) |
작동 원리
예를 들어 해시 함수 결과가 이진수로 나온다고 하자.
초기에는 1비트만 사용해 구분한다고 하면:
| 해시값(prefix) | 버킷 |
| 0 | B0 |
| 1 | B1 |
- 전역 깊이(Global depth) = 1
- B0, B1 각각 여러 데이터 저장 가능
*** Step 1: 버킷이 꽉 찼을 때
만약 B0이 꽉 찼다면,
→ 해당 버킷만 분할(split) 함.
→ local depth를 2로 늘리고
→ 2비트 구분이 필요하므로 디렉토리를 2배로 확장
| 해시 prefix | 버킷 |
| 00 | B0' |
| 01 | B0'' |
| 10 | B1 |
| 11 | B1 |
이렇게 되면 B0 관련 데이터만 재분배됨.
B1은 그대로 유지됨 → 전체 재해싱이 필요 X
예시
해시 함수: h(key) = key mod 8 → 3비트 사용
| Key | Binary Hash | bits(앞 2비트) |
| 5 | 101 | 10 |
| 6 | 110 | 11 |
| 2 | 010 | 01 |
- 처음에는 1비트만 사용 (0, 1 구분)
- 0번 버킷이 가득 차면 → 분할
- 전역 깊이 +1, 디렉토리 2배로
- 필요한 버킷만 다시 분배
| 장점 | 단점 |
| 전체 재해싱 불필요, 필요한 부분만 분할 동적 크기 조절 가능, 탐색 효율 O(1) 유지 |
디렉토리 메모리 사용 증가 구현 복잡 |
- Extendible vs Linear Hashing 비교
| 항목 | Extendible Hashing | Linear Hashing |
| 확장 방식 | 필요 시 디렉토리 2배 확장 | 버킷 하나씩 점진적 확장 |
| 관리 단위 | 디렉토리 기반 (prefix bits) | 버킷 기반 (순차적 split) |
| 공간 효율 | 디렉토리 메모리 사용 큼 | 메모리 효율적 |
| 구현 난이도 | 복잡하지만 구조적 | 구현 상대적으로 단순 |
| 사용 예 | DB 인덱스, 파일 시스템 | 해시 인덱싱, 캐싱 |
정리하자면,
| 항목 | Static Hashing | Dynamic Hashing |
| 해시 함수 | 고정 | 확장 가능 |
| 테이블 크기 | 고정 | 자동 확장 / 축소 |
| 충돌 처리 | 필수적 (chaining 등) | 분할로 완화 |
| 성능 | 일정하지만 크기 변화에 취약 | 안정적, 대용량에 적합 |
| 구현 난이도 | 쉬움 | 어려움 |
| 활용 예 | 간단한 캐시, 임시 해시맵 | DB, 파일 시스템 |
해시 충돌(Collision)
: 서로 다른 두 키가 같은 해시 값으로 계산되는 현상.
예를 들어 해시 함수가 hash(x) = x mod 5 일 때,
| Key | Hash |
| 7 | 2 |
| 12 | 2 |
두 키가 같은 인덱스(2)에 저장됨 → 충돌 발생
- 충돌 해결 방식
| Chaining | 같은 인덱스에 연결 리스트로 여러 데이터 저장 | 구조 단순 |
| Open Addressing | 충돌 시 빈 공간을 찾아 다른 위치에 저장 | 메모리 연속 |
| Rehashing | 새로운 해시 함수로 다시 해싱 | 확장성 높음 |
1. Chaining (체이닝)
같은 인덱스에 여러 개의 데이터를 연결 리스트(Linked List) 형태로 저장하는 방법.
Index 2 ─▶ [7] → [12] → [17]
| 장점 | 단점 |
| 충돌 발생해도 모든 데이터 저장 가능 구현 간단 데이터 삭제 용이 |
포인터(링크)로 인한 추가 메모리 사용 탐색 시 리스트 순회 필요 → 최악 O(n) |
예시 코드)
struct Node {
int key;
struct Node* next;
};
struct Node* hashTable[M];
void insert(int key) {
int idx = key % M;
struct Node* newNode = malloc(sizeof(struct Node));
newNode->key = key;
newNode->next = hashTable[idx];
hashTable[idx] = newNode;
}
2. Open Addressing (개방 주소법)
충돌이 나면 다른 빈 슬롯을 찾아서 저장하는 방식.
모든 데이터가 하나의 배열 안에 들어감.
| Linear Probing | h(k, i) = (h(k) + i) mod M — 한 칸씩 순차 탐색 |
| Quadratic Probing | h(k, i) = (h(k) + i²) mod M — 제곱 단위로 이동 |
| Double Hashing | h(k, i) = (h1(k) + i * h2(k)) mod M — 두 번째 해시 함수 사용 |
| 장점 | 단점 |
| 추가 메모리 구조 필요 없음 (배열 내 저장) 캐시 효율 좋음 |
삭제 처리 복잡 (lazy deletion 필요) 클러스터링(연속 충돌) 가능 |
예시코드 (Linear Probing)
int hashTable[M];
void insert(int key) {
int idx = key % M;
while (hashTable[idx] != EMPTY)
idx = (idx + 1) % M; // 다음 인덱스로 이동
hashTable[idx] = key;
}
3. Rehashing
해시 테이블이 너무 가득 차거나 충돌이 잦을 때, 더 큰 크기의 테이블로 전체를 다시 해싱하는 방법.
- 테이블 크기를 2배로 확장
- 기존 모든 데이터를 새 테이블에 재삽입
| 장점 | 단점 |
| 충돌 감소 검색 효율 유지 |
재해싱 시 시간 소모 큼 빈번한 확장은 비효율 |
'이론공부 > 자료구조, 알고리즘' 카테고리의 다른 글
| 9. Advanced.. (0) | 2025.11.13 |
|---|---|
| 7. Sorting (3) (0) | 2025.11.12 |
| 7. Sorting (2) (0) | 2025.11.12 |
| 7. Sorting (1) (0) | 2025.11.11 |
| 6. Graph (3) - Shortest Path (0) | 2025.11.11 |