자료구조와 알고리즘

자료구조 해쉬 테이블3

전설의개발자 2020. 10. 25. 17:37

1. 충돌(Collision) 해결 알고리즘 (좋은 해쉬 함수 사용하기)

해쉬 테이블의 가장 큰 문제는 충돌(Collision)의 경우입니다. 이 문제를 충돌(Collision) 또는 해쉬 충돌(Hash Collision)이라고 부릅니다.
*충돌(Collision) : 한개 이상의 데이터가 동일한 해쉬어드레스에 저장이 되어야 할 경우

6.1. Chaining 기법

  • 개방 해슁 또는 Open Hashing 기법 중 하나: 해쉬 테이블 저장공간 외의 공간을 활용하는 기법
  • 충돌이 일어나면, 링크드 리스트라는 자료 구조를 사용해서, 링크드 리스트로 데이터를 추가로 뒤에 연결시켜서 저장하는 기법

 

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

* hash(값) << 파이썬 내장함수 값을 숫자값으로 리턴 (매번 다름)

print (hash('Dave') % 8)
print (hash('Dbb') % 8)
print (hash('Data') % 8)

1

3

3

save_data('Dbb', '123')
save_data('Data', '456')
read_data('Data')

'456'

hash_table

 

6.2. Linear Probing 기법

  • 폐쇄 해슁 또는 Close Hashing 기법 중 하나: 해쉬 테이블 저장공간 안에서 충돌 문제를 해결하는 기법
  • 충돌이 일어나면, 해당 hash address의 다음 address부터 맨 처음 나오는 빈공간에 저장하는 기법
    • 저장공간 활용도를 높이기 위한 기법

링크드 리스트로 값을 저장하는 것이 아니라 다음 빈 공간에 값을 넣는다. 

 

연습3: 연습1의 해쉬 테이블 코드에 Linear Probling 기법으로 충돌해결 코드를 추가해보기
1. 해쉬 함수: key % 8
2. 해쉬 키 생성: 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):
    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

6.3. 빈번한 충돌을 개선하는 기법

최대한 충돌이 일어나지 않게 하는것이 해쉬코드를 쓰는 이유

  • 해쉬 함수을 재정의 및 해쉬 테이블 저장공간을 확대
  • 예:
hash_table = list([None for i in range(16)]) 

def hash_function(key): 
	return key % 16

공간을 늘리면 충돌이 줄어들어 탐색시간도 줄어드는 기법이다.

참고: 해쉬 함수와 키 생성 함수

  • 파이썬의 hash() 함수는 실행할 때마다, 값이 달라질 수 있음
  • 유명한 해쉬 함수들이 있음: SHA(Secure Hash Algorithm, 안전한 해시 알고리즘)
    • 어떤 데이터도 유일한 고정된 크기의 고정값을 리턴해주므로, 해쉬 함수로 유용하게 활용 가능
    • SHA-256은 실제로 블록체인에서 사용하는 기법

SHA-1 사용방법

* hashlib이 동작이 안된다면 pip install hashlib

import hashlib

#'test'라는 데이터를 String  sha1()알고리즘을 쓰고싶다
data = 'test'.encode()  
hash_object = hashlib.sha1()

#encode()는 값을 Byte로 바꾼다는 소리
#값을 바이트로 바꾼다음에 update를 해줘야 hash값이 추출이된다.
hash_object.update(data) #or update(b'test')

#hash_object를 몇진수로 추출하고 싶으냐hexdigest (여기선 16)
hex_dig = hash_object.hexdigest()
print (hex_dig)

a94a8fe5ccb19ba61c4c0873d391e987982fbbd3  << 'test'의 hash값을 16진수로 한것이 이값

 

SHA-256 사용방법

import hashlib

data = 'test'.encode()
hash_object = hashlib.sha256()
hash_object.update(data)
hex_dig = hash_object.hexdigest()
print (hex_dig)

9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

* 출력된 값으로 어떤 값을 입력한건지 추론은 불가능

 

어떤 데이터가 오던간에 고정된 길이로 일관된 값이 나온다.

 

연습4: 연습2의 Chaining 기법을 적용한 해쉬 테이블 코드에 키 생성 함수를 sha256 해쉬 알고리즘을 사용하도록 변경해보기
1. 해쉬 함수: key % 8
2. 해쉬 키 생성: 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()
        
        # hex_dig 이 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
        # 와 같이 16진수 문자열이기 때문에 key % 8을 할수가 없다.
        # 그래서 int 로 바꾸고 16진수의 문자열을 10진수 정수로 만든다는 뜻
        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

 

7. 시간 복잡도

  • 일반적인 경우(Collision이 없는 경우)는 O(1) 
  • 최악의 경우(Collision이 모두 발생하는 경우)는 O(n)

해쉬 테이블의 경우, 일반적인 경우를 기대하고 만들기 때문에, 시간 복잡도는 O(1) 이라고 말할 수 있음

검색에서 해쉬 테이블의 사용 예

  • 16개의 배열에 데이터를 저장하고, 검색할 때 O(n) << 배열의 사이즈에 따라 검색하고 순회를 해야한다.
  • 16개의 데이터 저장공간을 가진 위의 해쉬 테이블에 데이터를 저장하고, 검색할 때 O(1)

저장과 검색에 있어서 굉장히 효율적인 자료 구조이다.