📢 해시테이블(Hash Table)
-
(키-주소) 매핑에 의해 구현된 사전 ADT
👉 예 : 컴파일러의 심볼 테이블, 환경변수들의 레지스토리 - 해시테이블 = 버켓 배열 + 해시함수
👉 항목들의 키를 주소(즉, 배열첨자)로 매핑함으로서 1차원 배열에 사전 항목들을 저장한다. - 성능
👉 탐색,삽입,삭제 : 최악의 경우 O(N), 그러나 기대시간 O(1)
📢 버켓배열(Bucket Array)
-
키가 유일한 정수며 [0...M-1] 범위에 잘 분포되어 있다면, 해시테이블에서의 탐색, 삽입, 삭제에 O(1) 소요
- 두 가지 중요한 결함
👉 O(N) 공간을 사용하므로, M이 N에 비해 매우 크면, 공간 낭비
👉 키들이 [0...M-1] 범위 내의 유일한 정수여야 하지만, 이는 종종 비현실적 - 설계 목표
👉 그러므로 해시테이블 데이터구조를 정의할 때는, 키를 [0...M-1] 범위 내의 정수로 매핑하는 좋은 방식과
버켓배열을 구성해야 한다.
📢 해시함수 및 해시테이블
- 해시함수(Hash Function), h : 주어진 형의 키를 고정범위 [0...M-1]로 매핑
👉 예 : h(x) = x % M ( h는 정수 키 x에 대한 해시함수 ) - 정수 h(x) : 키 x의 해시값(Hash value)
- 주어진 키 형의 해시테이블은 다음 두 가지로 구성
👉 해시함수 h
👉 크기 M의 배열(테이블이라 부른다.) - 사전을 해시테이블로 구현할 때, 목표는 항목(k,e)를 첨자 i = h(k)에 저장하는 것
- 해시함수(Hash Function)는 보통 두 함수의 복합체로 명세한다.
👉 해시코드맵(Hash code map) h1 : keys -> integers
👉 압축맵(Compression map) h2 : integers -> [0...M-1] - 먼저 해시코드맵을 적용하고, 그 결과에 압축맵을 적용한다. 즉,
👉 h(k) = h2(h1(k)) - 좋은 해시함수가 되기 위한 조건
👉 키들을 외견상 무작위하게(Random) 분산시켜야 한다.
👉 계산이 빠르고 쉬워야 한다 ( 가능하면 상수시간 )
📢 압축맵
- 나누기(Division) (보통사용 방식)
👉 h2(k) = |k| % M
👉 해시테이블의 크기 M은 일반적으로 소수(Prime)로 선택 - 승합제(MAD, Multiply Add and Divide)
👉 h2(k) = |ak + b| % M
👉 a와 b는 음이 아닌 정수로서, a % M != 0, 그렇지 않으면, 모든 정수가 동일한 값 b로 매핑됨
📢 해시코드맵
- 메모리 주소(Memory address)
👉 키 개체의 메모리 주소를 정수로 재해석( 모든 Java객체들의 기본 해시코드다. )
👉 일반적으로 만족스러우나 수치 또는 문자열 키에는 적용 곤란 - 정수 캐스트(Integer case)
👉 키의 비트값을 정수로 재해석
👉 정수형에 할당된 비트 수를 초과하지 않는 길이의 키에는 적당 ( Java의 byte, short, int, float ) - 요소합(Component sum)
👉 키의 비트들을 고정길이(16 or 32bit)의 요소들로 분할한 후 각 ㅊ 합한다(overflow는 무시한다.)
👉 정수형에 할당된 비트 수 이상의 고정길이의 수치 키에 적당 ( 예 : Java의 long, double )
👉 문자의 순서에 의미가 있는 문자열 키에는 부적당 ( 예: temp01-temp10, stop-tops-spot-pots ) - 다항 누적(Ploynomial accumulation)
👉 요소합과 마찬가지로, 키의 비트들을 고정길이(8,16,32bit)의 요소들로 분할 ( a(0),a(1),...,a(N-1) )
👉 고정값 z를 사용하여 각 요소의 위치에 따른 별도 계산을 부과한 다항식 p(z)를 계산 (overflow 무시)
👉 p(z) = a(0) + a(1) * z + a(2) * z^2 + .... + a(N-1) * z ^ (N-1)
👉 문자열에 특히 적당 ( 예 : 고정값 z = 33을 선택할 경우, 50,000개의 영단어에 대해 단지 6회 충돌만 발생
📢 충돌해제 (중요 !!)
- 충돌(Collision) : 두 개 이상의 원소들이 동일한 셀로 매핑되는 것
- 즉, 상이한 키, k1과 k2에 대해 h(k1) = h(k2)면 "충돌이 일어났다."고 말한다.
- 충돌 해결(Collision resolution)을 위한 일관된 전략이 필요하다.
👉 분리연쇄법(Separate Chaining)
👉 선형조사법(Linear Probing)
👉 2차 조사법(Quadratic Probing)
👉 이중해싱(Double Hashing)
📢 분리연쇄법
- 분리연쇄법(Separate Chaining), 연쇄법 : 각 버켓 A[i]는 리스트 L(i)에 대한 참조를 저장 ( L(i)는 리스트 )
👉 해시함수가 버켓 A[i]로 매핑한 모든 항목들을 저장
👉 무순리스트가 또는 기록파일 방식을 사용하여 구현된 미니 사전이라 볼 수 있다. - 장단점 : 단순하고 빠르다는 장점이 있으나 테이블 외부에 추가적인 저장공간을 요구
- 예시
👉 h(k) = k % M
👉 키 : 25,13,16,15,7,28,31,20,1 ( = 충돌 일어난 키값 )
👉 리스에 추가되는 항목들의 위치는, 리스트의 맨 앞에 삽입하는 것이 유리하다. (리스트의 tail 포인터 없는경우)
📢 개방주소법 ( 선형조사법 )
- 개방주소법(Open Addresssing) : 충돌 항목을 테이블의 다른 셀에 저장
- 장단점 : 분리연쇄법에 비해 공간 사용을 절약하지만, 삭제가 어렵다는 것과 사전 항목들이 연이어 군집화
- 선형조사법(Linear Probing) : 충돌 항목을 (원형으로)바로 다음의 비어있는 테이블 셀에 저장함으로써 충돌을 처리
- 즉, 다음 순서에 의해 버켓을 조사한다.
👉 A[ (h(k) + f(i)) % M ], f(i) = i, i = 0,1,2,... - 검사되는 각 테이블 셀은 조사(Probe)라 불린다.
- 충돌 항목들은 군집화하며, 이후의 충돌에 의해 더욱 긴 조사열(Probe sequence)로 군집됨 = "1차 군집화"
- 예시
👉 h(k) = k % M
👉 키 : 25,13,16,15,7,28,31,20,1,38 ( = 충돌 키 )
📢 개방 주소법( 2차 조사법 )
- 2차 조사법(Quadratic Probing) : 다음 순서에 의해 버켓을 조사
👉 A[ (h(k) + f(i) % M ], f(i) = i^2, i = 0,1,2,... - 해시 값이 동일한 키들은 동일한 조사를 수반한다.
- 1차 군집화를 피하지만, 나름대로의 군집을 형성 할 수 있다. = "2차 군집화"
- M이 소수가 아니거나 버켓 배열이 반 이상 차면, 비어 있는 버켓이 남아 있더라도 찾지 못할 수 있다. ( 단점 )
- 예시
👉 h(k) = k % M, f(i) = i^2
👉 키 : 25,13,16,15,7,28,31,20,1,38
📢 이중해싱
- 이중해싱(Double Hashing) : 두 번째의 해시함수 h`를 사용하여 다음 순서에 의해 버켓을 조사
👉 A[ (h(k) + f(i)) % M ], f(i) = i * h`(k), i = 0,1,2,.... - 동일한 해시값을 가지는 키들도 상이한 조사를 수반할 수 있기 때문에 군집화를 최소화한다.
- h`는 계산 결과가 0이 되면 안된다. 👉 ( 0이 되면, f(i)는 결과적으로 0만 되겠죠 ? 그럼 한자만 계속 해싱충돌 )
- 최선의 결과를 위해, h`(k)와 M은 서로소(Relative Prime)여야 한다.
👉 d(1)*M = d(2)*h`(k)이면, d(2) 개의 조사만 시도한다. 즉, 버켓들 중 d(2)/M만 검사
👉 h`(k) = q - (k % q) 또는 1 + (k % q)를 사용 ( 단, q < M은 소수며 M 역시 소수일 경우 )
👉 한 마디로 위에 두 조건을 만족시키는, 해시함수를 적용하면, 군집화를 최소화하면서, 최소화로 실행가능 - 예시
👉 h(k) = k % M, h`(k) = 11 - (k % 11), ( 이때 q = 11이고, 11은 서로소이다. )
👉 키 : 25,13,16,15,7,28,31,20,1,38 ( = 충돌 키 )
- 개방주소법( 선형조사법, 2차조사법 ), 이중해싱 알고리즘
Alg findElement(k)
input bucket array A[0...M-1], hash function h, key k
output element with key k
v <- h(k) // h(k) = 해시함수는 보통 주어진다.
i <- 0
while(i < M)
b <- getNextBucket(v,i)
if(isEmpty(A[b])
return NoSuchKey
elseif(k == key(A[b])
return element(A[b])
else
i <- i + 1
return NoSuchKey
=========================================================
Alg insertItem(k,e)
v <- h(k)
i <- 0
while(i < M)
b <- getNextBucket(v,i)
if(isEmpty(A[b]))
Set bucket A[b] to (k,e)
return
else
i <- i + 1
overflowException()
return
Alg getNextBucket(v,i) // 선형조사법
return (v + i) % M
==========================================
Alg getNextBucket(v,i) // 2차조사법
return (v + i^2) % M
==========================================
Alg getNextBucket(v,i) // 이중해싱
return (v + i * h`(k)) % M
===========================================
Alg initBucketArray() // 버킷초기화(예시)
input bucket array A[0...M-1]
output bucket array A[0...M-1] initialized with null buckets
for i <- 0 to M-1
A[i].empty <- 1 // Set empty
return
===========================================
Alg isEmpty(b)
input bucket b
output boolean
return b.empty
📢 적재율
- 해시테이블의 적재율(Load factor), a = N/M ( N은 키 수, M은 버켓배열 크기 )
👉 즉, 좋은 해시함수를 사용할 경우의 각 버켓의 기대크기 - 적재율은 낮게 유지되어야 한다 ( 가능하면 1 아래로, 즉 a = N/M < 1 되도록)
- 좋은 해시함수가 주어졌다면, 탐색,삽입,삭제 각 작업의 기대 실행시간 = O(a)
- 분리연쇄법
👉 a > 1 이면, 작동은 하지만 비효울적
👉 a < 1 이면, O(a) = O(1)의 기대실행시간 성취 가능 - 개방주소법 ( 선형조사법, 2차조사법 )
👉 항상 a < 1
👉 a > 0.5 이면, 선형 및 2차 조사인 경우 군집화 가능성 농후하다.
👉 a <= 0.5 이면, O(a) = O(1) 기대실행시간
📢 해싱의 성능
- 해시테이블에 대한 탐색,삽입,삭제
👉 (최악의 경우) O(N) 시간 소요 - 최악의 경우란 : 사전에 삽입된 모든 키가 충돌할 경우
- 적재율(Load factor) : a = N/M은 해시테이블의 성능을 좌우한다.
- 해시값들은 흔히, 난수(Random numbers)와 같다고 가정하면, 개방주소법(선형조사법 or 2차조사법)에 의한
삽입을 위한 기대 조사횟수는 1/(1 - a)라고 알려져 있다. - 해시테이블에서 모든 사전 ADT 작업들의 기대실행시간 : O(1)
- 실전에서, 적재율이 1(즉, 100%)에 가깝지만 않다면, 해싱은 매우 빠른 것이다.
- 응용분야
👉 소규모 데이테베이스 ( 인덱스 )
👉 컴파일러
👉 브라우저 캐시
728x90
반응형
'Algorithm Theory' 카테고리의 다른 글
그래프 순회(Graph Traversal) (0) | 2020.12.14 |
---|---|
그래프(Graph) (0) | 2020.12.13 |
이진탐색트리(Binary Search Tree) (0) | 2020.12.12 |
사전(Dictionary) (0) | 2020.12.12 |
힙과 힙정렬 ( Heap & Heap Sort ) (0) | 2020.09.26 |