[DS] 해시 테이블(Hash Table)
자료 구조 중 해시 테이블(Hash Table)
해시 테이블(Hash Table)
- 고정된 크기의 배열을 만든다
- 해시 함수를 이용해서 key를 원하는 범위의 자연수로 바꾼다
- 해시 함수 결과 값 인덱스에 key - value 쌍을 저장
해시 함수?
- 특정 값을 원하는 범위의 자연수로 바꿔주는 함수
- 해시 함수는 결정론적이어야 한다 : 동일 input에 대해 항상 동일 output이 보장되어야 함
- 결과 해시 값이 치우치지 않고 고르게 나온다
- 빨리 계산할 수 있어야 한다.
해시 충돌(Collision)
- 서로 다른 input에 대해 해시 함수의 결과 값이 동일한 결과가 발생하는 경우
충돌
이 발생했다고 함 Chaining
방식을 통해 해결- 배열 인덱스에 링크드 리스트 저장해서 충돌 해결
Chaining 구현
class Node:
"""링크드 리스트의 노드 클래스"""
def __init__(self, key, value):
self.key = key
self.value = value
self.next = None # 다음 노드에 대한 레퍼런스
self.prev = None # 전 노드에 대한 레퍼런스
class LinkedList:
"""링크드 리스트 클래스"""
def __init__(self):
self.head = None # 링크드 리스트의 가장 앞 노드
self.tail = None # 링크드 리스트의 가장 뒤 노드
def find_node_with_key(self, key):
"""링크드 리스트에서 주어진 데이터를 갖고있는 노드를 리턴한다. 단, 해당 노드가 없으면 None을 리턴한다"""
iterator = self.head # 링크드 리스트를 돌기 위해 필요한 노드 변수
while iterator is not None:
if iterator.key == key:
return iterator
iterator = iterator.next
return None
def append(self, key, value):
"""링크드 리스트 추가 연산 메소드"""
new_node = Node(key, value)
# 빈 링크드 리스트라면 head와 tail을 새로 만든 노드로 지정
if self.head is None:
self.head = new_node
self.tail = new_node
# 이미 노드가 있으면
else:
self.tail.next = new_node # 마지막 노드의 다음 노드로 추가
new_node.prev = self.tail
self.tail = new_node # 마지막 노드 업데이
def delete(self, node_to_delete):
"""더블리 링크드 리스트 삭제 연산 메소드"""
# 링크드 리스트에서 마지막 남은 데이터를 삭제할 때
if node_to_delete is self.head and node_to_delete is self.tail:
self.tail = None
self.head = None
# 링크드 리스트 가장 앞 데이터 삭제할 때
elif node_to_delete is self.head:
self.head = self.head.next
self.head.prev = None
# 링크드 리스트 가장 뒤 데이터 삭제할 떄
elif node_to_delete is self.tail:
self.tail = self.tail.prev
self.tail.next = None
# 두 노드 사이에 있는 데이터 삭제할 때
else:
node_to_delete.prev.next = node_to_delete.next
node_to_delete.next.prev = node_to_delete.prev
return node_to_delete.value
def __str__(self):
"""링크드 리스트를 문자열로 표현해서 리턴하는 메소드"""
res_str = ""
# 링크드 리스트 안에 모든 노드를 돌기 위한 변수. 일단 가장 앞 노드로 정의한다.
iterator = self.head
# 링크드 리스트 끝까지 돈다
while iterator is not None:
# 각 노드의 데이터를 리턴하는 문자열에 더해준다
res_str += "{}: {}\n".format(iterator.key, iterator.value)
iterator = iterator.next # 다음 노드로 넘어간다
return res_str
class HashTable:
"""해시 테이블 클래스"""
def __init__(self, capacity):
self._capacity = capacity # 파이썬 리스트 수용 크기 저장
self._table = [LinkedList() for _ in range(self._capacity)] # 파이썬 리스트 인덱스에 반 링크드 리스트 저장
def _get_linked_list_for_key(self, key):
"""주어진 key에 대응하는 인덱스에 저장된 링크드 리스트를 리턴하는 메소드"""
hashed_index = self._hash_function(key)
return self._table[hashed_index]
def _look_up_node(self, key):
"""파라미터로 받은 key를 갖고 있는 노드를 리턴하는 메소드"""
linked_list = self._get_linked_list_for_key(key)
return linked_list.find_node_with_key(key)
def delete_by_key(self, key):
"""주어진 key에 해당하는 key - value 쌍을 삭제하는 메소드"""
node_to_delete = self._look_up_node(key) # 이미 저장된 key인지 확인한다
# 저장되어 있는 key면 삭제한다
if node_to_delete is not None:
linked_list = self._get_linked_list_for_key(key)
linked_list.delete(node_to_delete)
def _hash_function(self, key):
"""
주어진 key에 나누기 방법을 사용해서 해시된 값을 리턴하는 메소드
주의: key는 파이썬 불변 타입이여야 한다.
"""
return hash(key) % self._capacity
def look_up_value(self, key):
"""
주어진 key에 해당하는 데이터를 리턴하는 메소드
"""
return self._look_up_node(key).value
def insert(self, key, value):
"""
새로운 key - value 쌍을 삽입시켜주는 메소드
이미 해당 key에 저장된 데이터가 있으면 해당 key에 해당하는 데이터를 바꿔준다
"""
existing_node = self._look_up_node(key) # 이미 저장된 key인지 확인한다
if existing_node is not None:
existing_node.value = value # 이미 저장된 key면 데이터만 바꿔주고
else:
# 없는 key면 링크드 리스트에 새롭게 삽입시켜준다
linked_list = self._get_linked_list_for_key(key)
linked_list.append(key, value)
def __str__(self):
"""해시 테이블 문자열 메소드"""
res_str = ""
for linked_list in self._table:
res_str += str(linked_list)
return res_str[:-1]
Open Addressing 활용
선형 탐사 (Linear Probing)
: 충돌이 일어났을 때, 한 칸씩 다음 인덱스가 비었는지 확인- Open Addressing 방식이라고 해서 반드시 선형 탐사를 사용해야 하는 것은 아님
- 제곱 탐사(Quadratic Probing)도 가능