해시 테이블은 키(key)를 값(value)에 연결하여, 각 키가 고유한 값을 갖도록 하는 데이터 구조다. 이는 키를 이용하여 데이터를 빠르게 검색하는 데 매우 유용하다. 해시 테이블은 효율적인 검색, 삽입, 삭제를 위해 해시 함수를 사용하여 키를 해시 코드로 변환하고, 이 해시 코드를 인덱스로 사용하여 데이터를 배열에 저장한다.
해시 테이블의 주요 특성은 다음과 같다.
해시 테이블은 효율적인 검색을 제공하는 강력한 데이터 구조이지만, 충돌 관리와 메모리 사용량 등을 고려해야 한다. 따라서, 해시 테이블을 사용할 때는 이러한 요소들을 잘 이해하고 적절한 해시 함수와 충돌 해결 방법을 선택하는 것이 중요하다.
해시 테이블은 '공간-시간 트레이드오프(Space-Time Tradeoff)'의 좋은 예시다. 이는 컴퓨터 과학에서 매우 중요한 개념으로, 공간(메모리)을 더 사용함으로써 시간(연산 시간)을 줄이거나, 반대로 시간을 더 사용함으로써 공간을 줄이는 기법을 말한다.
해시 테이블의 경우, 테이블의 크기를 미리 정해놓고(즉, 공간을 더 사용하고), 해시 함수를 통해 데이터의 키를 해당 테이블의 인덱스로 직접 변환한다. 이렇게 하면 특정 데이터를 찾기 위해 모든 데이터를 순회할 필요가 없어지므로(즉, 시간을 덜 사용하게 됨), 매우 빠른 검색 속도를 달성할 수 있다.
하지만, 해시 테이블의 크기를 미리 정하게 되면 해당 크기 이상의 데이터를 저장할 수 없으며, 해시 테이블의 공간이 미사용 상태로 남을 수 있다. 이는 메모리의 낭비를 초래할 수 있다. 따라서, 이러한 트레이드오프를 잘 고려하여 적절한 해시 테이블의 크기와 해시 함수를 선택하는 것이 중요하다.
또한, 여러 키가 동일한 인덱스에 매핑될 수 있는 해시 충돌 문제를 해결하기 위해 추가적인 자료구조(예: 연결 리스트)나 알고리즘(예: 개방 주소법, 폐쇄 주소법)이 필요하므로, 이 또한 공간과 시간의 트레이드오프에 영향을 미친다.
해시 구조는 키를 값에 매핑하여 데이터 저장 및 검색을 효율적으로 수행하는 데이터 구조를 말한다. 이 구조는 특히 데이터의 검색 및 저장에 있어서 빠른 속도를 제공한다.
해시 구조의 핵심 요소는 "해시 함수"다. 해시 함수는 임의의 크기의 데이터(키)를 받아 고정된 크기의 유일한 값(해시 코드)으로 변환하는 함수다. 이 해시 코드는 일반적으로 데이터를 저장하거나 찾을 때 배열의 인덱스로 사용된다.
해시 테이블, 해시 맵, 해시 셋 등 다양한 형태의 해시 구조가 있으며, 각각의 구조는 해시 함수를 통해 키를 고유한 해시 코드로 변환하고 이를 이용해 데이터를 저장하고 검색하는 메커니즘을 가지고 있다.
그러나 때때로 서로 다른 키가 같은 해시 코드를 생성하는 '충돌'이 발생할 수 있다. 이러한 충돌을 관리하기 위한 여러 전략이 있으며, 이는 각 해시 구조에 따라 다르게 구현됩니다. 일반적인 충돌 해결 전략에는 체이닝과 오픈 어드레싱 등이 있다.
요약하면, 해시 구조는 데이터의 효율적인 저장 및 검색을 가능하게 하는 데이터 구조로, 키를 해시 코드로 변환하고 이를 배열의 인덱스로 사용하여 데이터를 관리한다. 이로 인해 데이터의 검색, 삽입, 삭제 등의 연산을 대체로 O(1)의 시간 복잡도로 수행할 수 있다.
해시를 별도로 구현할 필요가 없다! - 딕셔너리 타입을 사용하면 된다.
hash_table = list([i for i in range(10)]) # [0,0,0,0,0,0,0,0,0]
# Division을 통한, 해쉬함수
def hash_func(key):
return key % 5
# 데이터 저장
data1 = 'Andy'
data2 = 'Dave'
data3 = 'Trump'
##ord(): 문자의 ASCⅡ(아스키)zhem flxjs
print(ord(data1[0]), ord(data2[0]), ord(data3[0])) #65 68 84
print(ord(data1[0]), hash_func(ord(data1[0]))) # 65 0
def storage_data(data, value):
key = ord(data[0])
hash_address = hash_func(key)
hash_table[hash_address] = value
## 헤쉬 테이블에서 특정 주소의 데이터를 가져오는 함수
storage_data('Andy','01055553333')
storage_data('Dave','01044443333')
storage_data('Trump','01022223333')
## 데이터 읽어오기
def get_data(data):
key = ord(data[0])
hash_address = hash_func(key)
return hash_table[hash_address]
이 코드는 해시 테이블을 구현하고 사용하는 예제다. 해시 테이블은 키-값 쌍을 저장하고 검색하는데 효율적인 자료구조다. 여기서는 문자열의 첫 글자를 키로 사용하고, 그에 해당하는 전화번호를 값으로 사용한다.
이 코드는 간단한 해시 테이블의 예제로, 실제 사용에서는 충돌 처리 등의 문제를 해결해야 한다. 예를 들어, 이 코드에서는 모든 키가 같은 해시 주소를 가질 경우에 대한 처리가 없다. 이 경우, 마지막에 저장된 데이터만이 해시 테이블에 남게 된다.
해시 테이블은 빠른 검색 속도와 효율적인 저장 공간으로 인해 많은 상황에서 활용된다. 특히, 빠른 검색이 요구되는 상황이나 데이터의 빠른 저장 및 수정이 필요한 경우에 주로 사용됩니다. 다음은 몇 가지 예시다.
이외에도 네트워크 서비스, 파일 시스템, 문자열 탐색 등 다양한 분야에서 해시 테이블이 활용되고 있다.
해쉬 함수: key % 8
해쉬 키 생성: hash(data)
hash_table = list([0 for i in range(8)])
def get_key(data):
return hash(data)
def hash_function(key):
return key % 8
def save_data(data, value):
hash_address = hash_function(get_key(data))
hash_table[hash_address] = value
def read_data(data):
hash_address = hash_function(get_key(data))
return hash_table[hash_address]
save_data('Dave', '0102030200')
save_data('Andy', '01033232200')
read_data('Dave') #0102030200
hash_table #['0102030200',0,0,0,0,0,0,'01033232200']
이 파이썬 코드는 간단한 해시 테이블을 구현하는 예제다. 여기에서 해시 테이블의 크기는 8로 설정되었으며, 해시 함수는 key를 8로 나눈 나머지를 해시 주소로 사용한다. 이제 코드의 각 부분을 자세히 살펴보자.
hash_table = list([0 for i in range(8)])
# 이 줄은 크기가 8인 해시 테이블을 생성합니다. 처음에는 모든 위치가 0으로 초기화된다.
def get_key(data):
return hash(data)
# 'get_key' 함수는 데이터를 입력으로 받아 파이썬 내장 함수인 hash()를 이용해 해당 데이터의 해시값(키)를 반환한다.
def hash_function(key):
return key % 8
# 'hash_function' 함수는 해시 키를 입력으로 받아, 이를 8로 나눈 나머지를 반환한다. 이 값이 최종적인 해시 주소가 된다.
def save_data(data, value):
hash_address = hash_function(get_key(data))
hash_table[hash_address] = value
# 'save_data' 함수는 데이터와 그에 해당하는 값을 받아 해시 테이블에 저장한다. 데이터로부터 키를 추출하고, 이 키를 해시 함수에 넣어 해시 주소를 얻은 뒤, 이 주소에 값을 저장한다.
def read_data(data):
hash_address = hash_function(get_key(data))
return hash_table[hash_address]
# 'read_data' 함수는 데이터를 입력받아 그에 해당하는 값을 해시 테이블에서 찾아 반환한다. 마찬가지로 데이터로부터 키를 추출하고, 이 키를 해시 함수에 넣어 해시 주소를 얻은 뒤, 이 주소의 값을 반환한다.
# 이후 코드에서는 'Dave'와 'Andy'라는 데이터와 그에 해당하는 전화번호를 해시 테이블에 저장하고, 'Dave'에 해당하는 전화번호를 읽어 출력한다. 마지막 줄은 전체 해시 테이블의 상태를 출력한다.
해시 테이블의 가장 큰 문제는 충돌(Collision)인 경우다. 이 문제를 충돌 또는 해쉬 충돌(Hash Collision)이라 부른다.
Open Hashing 또는 Separate Chaining이라 불리는 이 방법은 해시 테이블에서 충돌이 발생했을 때, 충돌 위치에 이미 값이 존재하면 그 위치에 연결 리스트를 만들어 충돌이 발생한 값을 추가하는 방법이다.
예를 들어, 해시 테이블의 i번째 슬롯에 대해 해시 충돌이 발생하면, 이 데이터를 i번째 슬롯에 연결된 연결 리스트에 추가한다. 이후에 이 키를 조회하거나 삭제하려면 i번째 슬롯의 연결 리스트를 탐색하면 된다.
즉, 체이닝 방법은 하나의 버킷(bucket)에 여러 개의 키를 가진 데이터를 저장하는 방식으로 해시 충돌을 해결한다. 이 방식은 구현이 간단하고 해시 테이블의 크기를 사전에 정하지 않아도 되는 장점이 있지만, 연결 리스트를 사용하므로 충돌이 자주 발생할 수록 성능이 저하될 수 있다.
# 예시
# 해시 테이블: [ [], [], [("key1", "value1"), ("key2", "value2")], [], [], ... ]
# 'key1'과 'key2'가 같은 해시 값을 가지므로 같은 연결 리스트에 저장됩니다.
hash_table = list([0 for i in range(8)])
def get_key(data):
return hash(data)
def hash_function(key):
return key % 8
def save_data(data, value):
index_key = get_key(data)
hash_address = hash_function(index_key)
if hash_table[hash_address] != 0:
for index in range(len(hash_table[hash_address])):
if hash_table[hash_address][index][0] == index_key:
hash_table[hash_address][index][1] = value
return
hash_table[hash_address].append([index_key.value])
else:
hash_table[hash_address] = [[index_key.value]]
def read_data(data):
index_key = get_key(data)
hash_address = hash_function(index_key)
if hash_table[hash_address] != 0:
for index in range(len(hash_table[hash_address])):
if hash_table[hash_address][index][0] == index_key:
return hash_table[hash_address][index][1]
return None
else:
return None
print(hash('Dd') % 8) # 2
print(hash('Data') % 8) # 2
save_data('Dd', '1201023010')
save_data('Data', '3301023010')
read_data('Dd') # '1201023010'
hash_table # [0,0,[[1341610532875195530, '1201023010'], [-9031202661634252870, '3301023010']], 0,0,0,0,0]
이 파이썬 코드는 해시 테이블에 체이닝 기법을 적용한 것이다. 각 함수에 대한 설명은 다음과 같다.
해시 테이블의 크기를 8로 설정하였고, 'Dd'와 'Data' 두 데이터가 동일한 해시 주소인 2를 가진다. 체이닝 기법을 사용해서 이 두 데이터를 동일한 주소에 저장했다. 이를 통해 해시 충돌 문제를 해결한다.
마지막 부분의 출력 결과를 보면, 'Dd'라는 데이터를 검색했을 때 올바른 값을 반환하고, 해시 테이블은 두 데이터가 동일한 인덱스(해시 주소)에 체이닝된 모습을 볼 수 있다.
Close Hashing기법 중 하나: 해쉬 테이블 저장공간 안에서 충돌 문제를 해결하는 기법
Linear Probing은 해시 테이블에서 해시 충돌이 발생하면, 해당 해시 주소의 다음 주소부터 차례대로 비어있는 주소를 찾아 데이터를 저장하는 방식이다. 이 방법은 해시 테이블이 배열로 구현되었다면 비교적 구현하기 쉽다.
예를 들어, 해시 함수가 데이터를 해시 테이블의 인덱스 3에 저장하려고 하지만 그곳에 이미 다른 데이터가 있다면, Linear Probing은 인덱스 4의 위치를 확인한다. 만약 인덱스 4도 이미 데이터로 차있다면, 인덱스 5를 확인하고, 이런 방식으로 비어있는 위치를 찾을 때까지 계속한다.
장점
단점
hash_table = list([0 for i in range(8)])
def get_key(data):
return hash(data)
def hash_function(key):
return key % 8
def save_data(data, value):
index_key = get_key(data)
hash_address = hash_function(index_key)
if hash_table[hash_address] != 0:
for index in range(hash_address, len(hash_table)):
if hash_table[index] == 0:
hash_table[index] = [index_key, value]
return
elif hash_table[index][0] == index_key: # 키가 동일한 것이 있을떄, 값만 업데이트 하는 경우
hash_table[index][1] = value
return
else:
hash_table[hash_address] = [index_key, value]
def read_data(data):
index_key = get_key(data)
hash_address = hash_function(index_key)
if hash_table[hash_address] != 0:
for index in range(hash_address, len(hash_table)):
if hash_table[index] == 0:
return None
elif hash_table[index][0] == index_key:
return hash_table[index][1]
else:
return None
print(hash('dk') % 8) # 4
print(hash('da') % 8) # 4
save_data('dk', '01200123123')
save_data('da', '33333333333')
read_data('dk') # '01200123123'
read_data('da') # '33333333333'
이 코드는 Linear Probing을 이용한 해시 충돌 해결 방법을 구현한 것이다.
이를 통해, Linear Probing 기법을 이용한 해시 테이블의 구현과 사용 예제를 볼 수 있다. 해시 키 'dk'와 'da'는 해시 함수 계산 결과가 같아서 해시 충돌이 발생한다. 그러나 Linear Probing 기법으로 인해 각 데이터가 다른 주소에 저장되고, 조회도 정확하게 이루어진다. 이로써 해시 충돌 문제를 해결하였다.
해시 함수를 재정의 및 해시 테이블 저장공간을 확대한다.
hash_table = list([None for i in range(16)])
def hash_function(key):
return key % 16
import hashlib
data = 'test'.encode()
hash_object = hashlib.sha1()
hash_object.update(data)
hex_dig = hash_object.hexdigest()
print (hex_dig) #a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
이 코드는 Python의 hashlib 모듈을 사용하여 문자열 "test"에 대한 SHA-1 해시 값을 계산하는 과정을 보여준다.
import hashlib
data = 'test'.encode()
hash_object = hashlib.sha256()
hash_object.update(data)
hex_dig = hash_object.hexdigest()
print (hex_dig) #9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
이 코드는 Python의 hashlib 모듈을 사용하여 문자열 "test"에 대한 SHA-256 해시 값을 계산하는 과정을 보여주고 있다.
해시 함수의 목표는 원래 데이터를 대표하는 고유한 값을 생성하는 것이며, SHA-256은 이 중 하나로 더 강력한 보안을 제공하지만 더 많은 계산을 필요로 한다.
해쉬함수: key % 8
해쉬 키 생성: hash(data)
import hashlib
hash_table = list([0 for i in range(8)])
def get_key(data):
hash_object = hashlib.sha256()
hash_object.update(data.encode())
hex_dig = hash_object.hexdigest()
return int(hex_dig, 16)
def hash_function(key):
return key % 8
def save_data(data, value):
index_key = get_key(data)
hash_address = hash_function(index_key)
if hash_table[hash_address] != 0:
for index in range(hash_address, len(hash_table)):
if hash_table[index] == 0:
hash_table[index] = [index_key, value]
return
elif hash_table[index][0] == index_key:
hash_table[index][1] = value
return
else:
hash_table[hash_address] = [index_key, value]
def read_data(data):
index_key = get_key(data)
hash_address = hash_function(index_key)
if hash_table[hash_address] != 0:
for index in range(hash_address, len(hash_table)):
if hash_table[index] == 0:
return None
elif hash_table[index][0] == index_key:
return hash_table[index][1]
else:
return None
print (get_key('db') % 8) # 1
print (get_key('da') % 8) # 2
print (get_key('dh') % 8) # 2
save_data('da', '01200123123')
save_data('dh', '3333333333')
read_data('dh') # 3333333333
이 코드는 연습2의 해쉬 테이블 구현을 개선하여, 키 생성 방식을 SHA-256 해시 알고리즘을 사용하도록 변경한 것이다. 이 변경을 통해, 동일한 입력에 대해 항상 동일한 출력을 얻는 등 더욱 견고한 해시 함수를 이용한다.
해쉬 테이블의 경우, 일반적인 경우를 기대하고 만들기 때문에, 시간 복잡도는 O(1)이라 말할 수 있다.
해시 테이블은 데이터를 저장하고 검색하는 데에 키를 사용하기 때문에 일반적인 경우에는 매우 빠른 성능을 제공한다. 이상적인 상황에서는 키를 통해 데이터를 즉시 찾을 수 있으므로, 시간 복잡도는 O(1)입니다. 이는 어떤 데이터를 찾든 간에 연산 시간이 일정하다는 것을 의미한다.
그러나 모든 키가 동일한 해시 주소에 매핑되는 해시 충돌이 발생하는 최악의 경우, 각 주소에 데이터가 몰리게 된다. 이럴 경우 해당 해시 주소에 있는 모든 데이터를 찾아야 하므로, 이 경우의 시간 복잡도는 O(n)이 된다. 이는 해시 테이블에 저장된 모든 데이터를 검색해야 한다는 것을 의미한다.
그러나 해시 충돌은 보통 해시 함수의 선택과 테이블 크기를 적절하게 설정함으로써 최소화할 수 있다. 충돌을 관리하는 기법, 예를 들어 체이닝(Chaining)이나 오픈 어드레싱(Open Addressing) 같은 방법을 사용하면 충돌이 발생했을 때도 효율적으로 데이터를 관리할 수 있다. 따라서 실제로는 해시 테이블의 시간 복잡도가 O(n)이 되는 경우는 거의 없다. 이러한 이유로, 일반적으로 해시 테이블의 시간 복잡도는 O(1)로 간주한다.