728x90
반응형
본 포스트는 필자가 학습하고 작성한 글이기 때문에 틀린내용이 있을 수 있습니다.
해시 (Hash)
해시는 데이터를 다른 데이터로 매핑하는 프로세스를 의미한다.
이를 위해 해시 함수를 사용하여 어떤 값을 고정된 크기의 값으로 변환한다.
해시 함수 (Hash Funtion)
- 입력 받은 값을 '일정한 길이의 해시 값'으로 변환
- 동일한 입력 값에 대해서는 항상 동일한 출력 값(해시 값 or 해시 코드)을 반환
- 작은 입력 값의 변화에도 출력 값이 크게 바뀐다(높은 민감도)
해시 테이블 (Hash Table)
해시 테이블은 키와 값의 쌍을 저장하는 데이터 구조다.
핵심 요소
- 키 (Key) : 유일한 값으로, 해시 함수를 통해 해시 값으로 변환
- 해시 함수 (Hash Funtion) : 키를 받아 해시 값을 반환하는 함수
- 해시 값 (Hash Value) or 해시 코드 (Hash Code) : 키를 해시 함수에 넣었을 때 나오는 결과값
- 버킷 (Bucket) or 슬롯 (Slot) : 해시 테이블 내의 위치나 주소를 의미하며, 해시 값에 따라 키-값 쌍이 저장
장점
- 일반적인 경우, O(1)의 시간 복잡도를 가지며 데이터의 검색이 매우 빠르다.
- 키-값 쌍의 삽입과 삭제도 빠르다.
단점
- 해시 충돌을 해결하기 위한 추가적인 메커니즘이 필요하다.
- 해시 함수와 메모리 사용량에 따라 공간 복잡도가 높을 수 있다.
- 해시 테이블의 크기를 조정해야 할 필요가 있을 때(리사이징) 비용이 발생한다.
리사이징을 하는 이유
- 해시 테이블의 로드 팩터 (Load Factor)가 특정 임계값을 초과 할 때 :
로드 팩터는 테이블 내에 저장된 항목의 수를 테이블의 전체 크기로 나눈 값이다.
로드 팩터가 높아지면 해시 충돌의 확률이 증가하므로, 성능이 저할될 수 있다.
이를 방지하기 위해 테이블 크기를 늘리는 것이 필요하다.
일반적으로 로드 팩터는 0.25 ~ 0.75 에서 효율이 좋다고 한다. - 공간 효율성을 위해 :
해시 테이블에서 많은 항목들이 삭제되어 로드 팩터가 매우 낮아진다면, 메모리를 비효율적으로 사용하게 된다.
이런경우, 테이블의 크기를 줄여 공간 효율성을 높일 수 있다.
동작 원리
- 키-값 쌍을 저장하거나 검색할 때 해시 함수에 넣어서 해시 값을 얻는다.
- 이 해시 값은 버킷의 인덱스로 사용되어 키-값 쌍이 해당 버킷에 저장되거나 검색된다.
- 동일한 해시 값을 갖는 다른 키(해시 충돌)가 있을 경우, 다양한 충돌 해결 전략을 사용한다.
해시 충돌 처리 방법
- 개방 주소법 (Open Addressing) : 충돌이 발생하면 다른 주소에 데이터를 저장
- 선형 탐사 : 일정한 간격으로 다음 버킷을 탐색
- 제곱 탐사 : 버킷의 인덱스에 제곱을 더해 탐색
- 이중 해싱 : 두번째 해시 함수를 사용하여 버킷을 탐색
- 체이닝 (Chaining) : 같은 주소의 데이터를 연결 리스트로 연결하는 방법
구현
해시 테이블의 구현은 해시 함수, 리사이징을 하는 로드 팩터, 해시 충돌 처리 방법을 고려해 구현해야 한다.
해시 함수의 경우 추가적인 사항 없이(실제론 요구사항에 맞춰 추가적인 로직을 갖는다) python의 hash()를 써서 size로 나눈 나머지값, 해시 충돌 처리 방법은 위에 소개한 4가지 모두, 리사이징을 처리할 로드 팩터는 0.5로 설정했다.
체이닝을 사용한 해시 테이블
class Node:
"""연결 리스트의 노드를 위한 클래스"""
def __init__(self, key, value):
self.key = key
self.value = value
self.next = None
class HashTable:
def __init__(self, size=10):
self.size = size
self.table = [None] * size
self.count = 0 # 저장된 요소의 수
def _hash(self, key):
"""해시 함수: 키를 받아 해시 값을 반환합니다."""
return hash(key) % self.size
def _resize(self, new_size):
"""해시 테이블의 크기를 조정합니다."""
old_table = self.table
self.size = new_size
self.table = [None] * new_size
self.count = 0 # 재삽입될 때 다시 계산
for head_node in old_table:
current = head_node
while current:
self.set(current.key, current.value)
current = current.next
def set(self, key, value):
"""키-값 쌍을 저장합니다."""
index = self._hash(key)
if self.table[index] is None:
self.table[index] = Node(key, value)
else:
current = self.table[index]
while current:
# 이미 키가 있으면 값을 갱신
if current.key == key:
current.value = value
return
if current.next is None: # 마지막 노드이면
current.next = Node(key, value)
break
current = current.next
self.count += 1
# 요소 수가 테이블 크기의 절반을 초과하면 리사이징
if self.count > self.size // 2:
self._resize(2 * self.size)
def get(self, key):
"""키에 해당하는 값을 반환합니다."""
index = self._hash(key)
current = self.table[index]
while current:
if current.key == key:
return current.value
current = current.next
return None
def __str__(self):
result = []
for head_node in self.table:
if head_node:
current = head_node
while current:
result.append(f"{current.key} : {current.value}")
current = current.next
return "{"+", ".join(result)+"}"
검증
# 해시 테이블 테스트
test = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lime", "mango"]
hash_table = HashTable()
for idx, value in enumerate(test):
hash_table.set(value, idx)
print(hash_table)
# 6, 11 번째 set에서 리사이징이 일어난다. 10(default) > 20 > 40
print(hash_table.size)
선형 탐사를 사용한 해시 테이블
class LinearProbingHashTable:
def __init__(self, size=10):
self.size = size
self.keys = [None] * size
self.values = [None] * size
self.count = 0 # 저장된 요소의 수
def _hash(self, key):
"""해시 함수: 키를 받아 해시 값을 반환합니다."""
return hash(key) % self.size
def _resize(self, new_size):
"""해시 테이블의 크기를 조정합니다."""
old_keys = self.keys
old_values = self.values
self.size = new_size
self.keys = [None] * new_size
self.values = [None] * new_size
self.count = 0 # 재삽입될 때 다시 계산
for key, value in zip(old_keys, old_values):
if key is not None:
self.set(key, value)
def set(self, key, value):
"""키-값 쌍을 저장합니다."""
index = self._hash(key)
original_index = index
# 선형 탐사
while self.keys[index] is not None and self.keys[index] != key:
index = (index + 1) % self.size
if index == original_index: # 전체 해시 테이블을 순회했으면 리사이징
self._resize(2 * self.size)
self.set(key, value)
return
# 새로운 키 삽입 시
if self.keys[index] is None:
self.count += 1
self.keys[index] = key
self.values[index] = value
# 요소 수가 테이블 크기의 절반을 초과하면 리사이징
if self.count > self.size // 2:
self._resize(2 * self.size)
def get(self, key):
"""키에 해당하는 값을 반환합니다."""
index = self._hash(key)
original_index = index
while self.keys[index] is not None:
if self.keys[index] == key:
return self.values[index]
index = (index + 1) % self.size
if index == original_index:
break
return None
def __str__(self):
return "{"+", ".join([f"{k} : {v}" for k, v in zip(self.keys, self.values) if k is not None])+"}"
검증
# 선형 탐사 해시 테이블 테스트
test = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lime", "mango"]
linear_hash_table = LinearProbingHashTable()
for idx, value in enumerate(test):
linear_hash_table.set(value, idx)
print(linear_hash_table)
# 6, 11 번째 set에서 리사이징이 일어난다. 10(default) > 20 > 40
print(linear_hash_table.size)
제곱 탐사를 사용한 해시 테이블
class QuadraticProbingHashTable:
def __init__(self, size=10):
self.size = size
self.keys = [None] * size
self.values = [None] * size
self.count = 0
def _hash(self, key):
return hash(key) % self.size
def _resize(self, new_size):
old_keys = self.keys
old_values = self.values
self.size = new_size
self.keys = [None] * new_size
self.values = [None] * new_size
self.count = 0
for key, value in zip(old_keys, old_values):
if key is not None:
self.set(key, value)
def set(self, key, value):
index = self._hash(key)
original_index = index
distance = 1
while self.keys[index] is not None and self.keys[index] != key:
index = (original_index + distance ** 2) % self.size
distance += 1
if distance > self.size: # 전체 해시 테이블을 순회했으면 리사이징
self._resize(2 * self.size)
self.set(key, value)
return
if self.keys[index] is None:
self.count += 1
self.keys[index] = key
self.values[index] = value
if self.count > self.size // 2:
self._resize(2 * self.size)
def get(self, key):
index = self._hash(key)
original_index = index
distance = 1
while self.keys[index] is not None:
if self.keys[index] == key:
return self.values[index]
index = (original_index + distance ** 2) % self.size
distance += 1
if distance > self.size:
break
return None
def __str__(self):
return "{"+", ".join([f"{k} : {v}" for k, v in zip(self.keys, self.values) if k is not None])+"}"
검증
# 제곱 탐사 해시 테이블 테스트
test = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lime", "mango"]
quadratic_hash_table = QuadraticProbingHashTable()
for idx, value in enumerate(test):
quadratic_hash_table.set(value, idx)
print(quadratic_hash_table)
# 6, 11 번째 set에서 리사이징이 일어난다. 10(default) > 20 > 40
print(quadratic_hash_table.size)
이중 해싱을 사용한 해시 테이블
class DoubleHashingHashTable:
def __init__(self, size=10):
self.size = size
self.keys = [None] * size
self.values = [None] * size
self.count = 0
def _hash(self, key):
return hash(key) % self.size
def _hash2(self, key):
return 1 + (len(key) % (self.size - 1))
def _resize(self, new_size):
old_keys = self.keys
old_values = self.values
self.size = new_size
self.keys = [None] * new_size
self.values = [None] * new_size
self.count = 0
for key, value in zip(old_keys, old_values):
if key is not None:
self.set(key, value)
def set(self, key, value):
index = self._hash(key)
original_index = index
hash2 = self._hash2(key)
while self.keys[index] is not None and self.keys[index] != key:
index = (index + hash2) % self.size
if index == original_index:
self._resize(2 * self.size)
self.set(key, value)
return
if self.keys[index] is None:
self.count += 1
self.keys[index] = key
self.values[index] = value
if self.count > self.size // 2:
self._resize(2 * self.size)
def get(self, key):
index = self._hash(key)
original_index = index
hash2 = self._hash2(key)
while self.keys[index] is not None:
if self.keys[index] == key:
return self.values[index]
index = (index + hash2) % self.size
if index == original_index:
break
return None
def __str__(self):
return "{"+", ".join([f"{k} : {v}" for k, v in zip(self.keys, self.values) if k is not None])+"}"
검증
# 이중 해싱 해시 테이블 테스트
test = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lime", "mango"]
double_hash_table = DoubleHashingHashTable()
for idx, value in enumerate(test):
double_hash_table.set(value, idx)
print(double_hash_table)
# 6, 11 번째 set에서 리사이징이 일어난다. 10(default) > 20 > 40
print(double_hash_table.size)
728x90
반응형