TIL103. Algorithm : Hash Tables
📌 이 포스팅에서는 Hash Tables에 대해 정리하였습니다.
🌈 Hash Tables
🔥 Hash Tables 이란?
🔥 Hash Tables vs Arrays
🔥 Hash Collisions
1. Hash Tables 이란?
🤔 사전이 알파벳 순으로 되어 있지 않다면 어떻게될까?
✔️ 사전이 알파벳 순으로 되어 있지 않다면, 내가 원하는 단어의 뜻을 알기 위해 처음부터 순차적으로 확인해야 한다. 하나하나 모두 확인해야하는 이 과정은 단순 탐색과 같고, 시간 복잡도는 O(N)이다.
✔️ 다행스럽게도 사전은 알파벳순으로 되어있기 때문에 이진 탐색이 가능하다. 이 경우에 시간 복잡도는 O(logN)이다.
✔️ 이번에는 사전이 아니라, 마트에서 계산원을 하고 있다고 생각해보자. 물품별로 가격이 적혀있는 장부가 있을 때, 단순 탐색은 O(N)이고, 이진탐색을하면 O(logN)의 시간복잡도가 필요하다.
✔️ 1초에 품목 10개를 읽을 수 있다고 가정하면, 판매중인 물건 1개의 값을 알아내는데 아래와 같은 시간이 필요하다.
✔️ 따라서 O(logN)의 시간 복잡도를 가지는 이진 탐색 또한 이러한 상황에서는 효율적인 방식이 아니다. 10000개의 품목을 판매 중이라면 계산원이 1개의 물품의 가격만을 확인하는데에도 최대 2초의 시간을 소요되기 때문이다.
✔️ 이에 더 효율적인 방식의 자료구조가 필요하다. 이를 가능하게하는 것이 해쉬다. 해쉬가 얼마나 빠른지는 뒤에서 살펴보겠다.
🤔 Hash Tables의 특징
✔️ 해쉬 테이블은 키(key)에 값을(value)를 저장하는 데이터 구조로 키(key) 값을 마치 index처럼 상용하여 값(value)을 읽을 수 있기 때문에 빠르다.
✔️ 이미 python에서는 dictionary라는 데이터 타입이 이를 지원하고 있다. 즉, python에서는 해쉬 테이블을 별도 구현할 필요 없이 딕셔너리를 사용할 수 있고, 딕셔너리 내부적으로 해쉬 테이블의 구조를 갖고 있다.
✔️ 해쉬의 대표적인 장점으로는 데이터를 Create/Read 하는 속도가 빠르고, 이미 Key가 존재하는지 중복을 처리하기가 쉽다.
✔️ 단점으로는 충분한 저장 공간을 확보해두어야하기 때문에 비교적 저장 공간이 많이 필요하고, 여러 Key에 해당하는 주소가 동일할 경우 충돌을 막기 위한 방편이 필요하다. 이는 추후 다뤄보겠다.
✔️ 이에 해쉬 테이블의 주 용도는 CRUD가 빈번한 경우, 검색이 자주 일어나는 경우, 캐쉬를 구현할 때 활용된다.
✔️ 예를 들어, 스마트폰의 주소록은 해쉬로 개념과 동일하다. 사람을 주소록에서 검색하면, 전화번호, 주소, email 등을 모두 빠르게 확인할 수 있다.
✔️ 뿐만아니라 앞에서 정리한 Domain과 IP Address의 관계도 이러한 해쉬로 이뤄져있다.
🤔 해쉬 테이블 관련 용어
✔️ hash : 임의에 값을 고정 길이로 변환하는 것
✔️ hash table : 키 값의 연산에 의해 직접 접근이 가능한 데이터 구조
✔️ hash function : Key에 대해 산술 연산을 적용해 해쉬 값으로 변환해주는 함수
✔️ hash value : Key를 해싱 함수에 대입하여 반환되는 것을 해쉬 값이라하고, 이를 기반으로 테이블에서 해당 Key에 대한 데이터 위치를 탐색
✔️ slot : 한개의 데이터를 저장할 수 있는 공간
2. Hash Tables vs Arrays
🤔 hash의 시간복잡도는 어떻게 될까?
✔️ 해쉬는 key값을 통해 value를 바로 읽어올 수 있기 때문에 장부의 품목이 무한대로 늘어난다해도 O(1)의 시간 복잡도를 가진다.
✔️ 사실, O(1)이라는 시간복잡도는 Key값을 해쉬 함수에 넣었을 때, 충돌이 발생되지 않을 때(반환되는 해쉬 값이 중복이 없을 때)에 해당된다. 이에 대해서는 아래서 다시 얘기해보도록 하자.
🤔 간단 해쉬 테이블 구현하기
✔️ 10자리의 간단한 해쉬 테이블은 배열을 사용하여 아래처럼 만들어 볼 수 있다.
# 빈 해쉬 테이블 만들기 hash_table = list([0 for i in range(10)]) print(hash_table) # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
✔️ 간단 해쉬 테이블 구현이기 때문에 division 방식으로 해쉬 함수가 아래처럼 존재한다고 가정해보자.
# 간단 해쉬 함수 # Division 방식 : 나누기를 통한 나머지 값을 사용하는 기법 def hash_func(key): return key % 5 # 👈 어떤 key값이 들어오면, 이를 5로 나눈 나머지를 반환해요!
✔️ 해쉬 테이블에 값을 저장하는 함수는 아래 처럼 작성할 수 있다. key값이 string이기 때문에 ord로 숫자로 변환 후, index값을 구하기 위해 해쉬 함수에 넣는다.
def storage_data(data, value): # 👈 키와 저장할 데이터를 받음 key = ord(data[0]) # 👈 키를 해쉬함수에 넣어 연산하기 위해 숫자로 변환 hash_address = hash_func(key) # 👈 해쉬 함수 작동하여 해쉬값(해쉬주소) 얻음 hash_table[hash_address] = value # 👈 해쉬 주소를 index로하여 데이터를 테이블에 저장 storage_data('Jaewon', '010-1234-5678') storage_data('Haezin', '010-4444-9999') storage_data('Ginsu', '010-8989-1212') # 해쉬 테이블 저장 후 해쉬 테이블 상태 확인 print(hash_table) # [0, '010-8989-1212', '010-4444-9999', 0, '010-1234-5678', 0, 0, 0, 0, 0]
✔️ 해쉬 테이블에 값을 저장했으니, 이를 읽어올 수 있는 함수도 아래 처럼 만들 수 있다.
# 해쉬 테이블에서 특정 주소의 데이터를 가져오는 함수 def get_data(data): key = ord(data[0]) # 👈 데이터에서 Key를 추출 hash_address = hash_func(key) # 👈 key를 해쉬 함수에 넣어 해쉬 주소를 얻음 return hash_table[hash_address] # 👈 해쉬 주소를 통해 배열에서 데이터를 반환 print(get_data('Jaewon')) # 010-1234-5678 print(get_data('Haezin')) # 010-4444-9999 print(get_data('Ginsu')) # 010-8989-1212
🤔 해쉬, 배열, 연결 리스트의 시간 복잡도는 어떻게 될까?
✔️ 사실 해쉬 테이블의 시간복잡도는 평균적으로 가지는 해쉬의 시간복잡도와 최악의 경우의 시간 복잡도가 존재한다.
✔️ 간단 해쉬 테이블의 예시를 통해 충돌이 어떻게 발생하는지 살펴보도록 하겠다.
✔️ 예를 들어, key값이 'Jeawon'과 'Jisu'가 있다면, 둘다 'J'를 ord 함수로 처리하기 때문에 74를 반환받는다. 이를 해쉬 함수에 넣어 5로 나눈 몫은 서로 동일하기 때문에 덮어씌어지는 충돌 문제가 발생한다.
✔️ 충돌을 해결하기 위한 방법으로 Open Hashing방식의 Chaining 기법 또는 Close Hashing 방식의 Linear Probing 기법을 사용하는데, 이럴 경우, 해당하는 key값을 찾기 위해 다시 반복이 필요하기 때문에 O(N)의 시간복잡도를 가진다.
✔️ 따라서 해쉬, 배열, 연결 리스트의 시간복잡도는 아래와 같다.
3. Hash Collisions
🤔 Close Hashing 기법
✔️ 해쉬 충돌을 해결하는 방법으로 Close Hashing 기법으로 대표적인 기술은 Chaining이다.
✔️ Chaining은 해쉬 테이블 공간 외의 새로운 공간을 마련하여 충돌이 발생 시 연결 리스트 자료 구조를 사용해 데이터를 저장하는 기법이다.
✔️ 또한 Chaining 기법은 데이터를 식별할 수 있는 key값을 함께 저장하여 이를 식별하는데, 단점으로는 공간이 많이 필요하다.
# 해쉬 테이블 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) # 👈 index_key라는 별도의 변수에 key값을 저장 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] == 0 👈 없을 때 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('Jaewon')%8) # 3 print(hash('Haezin')%8) # 2 print(hash('AaLa')%8) # 3 # 저장 save_data('Jaewon', '01011112222') save_data('Haezin', '01088889999') save_data('AaLa', '01033331234') print(hash_table) # [0, 0, [[-3231082452735185358, '01088889999'], [630994963842225842, '01033331234']], [[-6162502635708335149, '01011112222']], 0, 0, 0, 0] # 조회 print(read_data('Jaewon')) # 01011112222 print(read_data('Haezin')) # 01088889999
🤔 Open Hashing
✔️ Linear Probing 기법은 Open Hashing의 한 기술로 해쉬 테이블 저장 공간 안에서 충돌 문제를 해결하는 대표적인 방식이다.
✔️ 또한 Linear Probing 기법은 충돌 발생 시, hash adress의 다음 adress부터 맨 처음 나오는 빈 공간에 저장하는 방식이기 때문에 저장공간 활용도가 높다.
# 해쉬 테이블 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 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('Jaewon')%8) # 0 print(hash('Haezin')%8) # 0 print(hash('AaLa')%8) # 7 # 저장 save_data('Jaewon', '01011112222') save_data('Haezin', '01088889999') save_data('AaLa', '01033331234') print(hash_table) # [[3214169218863054112, '01011112222'], [-291548128366538008, '01088889999'], 0, 0, 0, 0, 0, [-143120377966334057, '01033331234']] # 조회 print(read_data('Jaewon')) # 01011112222 print(read_data('Haezin')) # 01088889999
Author And Source
이 문제에 관하여(TIL103. Algorithm : Hash Tables), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@jewon119/TIL103.-Algorithm-Hash-Tables저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)