[Python]HashMap(해시 관련)

Hashing(해싱)

  • 아래 HashMap은 정말 기초부터 만든 class니까 잘 참고.
  • key space » address space
    => 모든 경우의 key 값 공간의 크기에 맞추어 Record 저장공간을 준비하는 것이 아닌, 실제 저장이(예측) 되는 Record수(=주소공간)에 맞추어 저장하는 것이 저장 공간 활용 측면에서 효과적.
    이과정은 key값 공간에 존재하는 값을 주소 공간의 크기에 맞게 변형하는 매핑 함수가 있으면 가능.
    => 이것이 해시 함수(다양하게 있음)
  • Hashing(해싱) : 검색(search key)를 제한된 범위의 배열에 매핑(mapping)하는 과정
  • Hash Table : 키가 저장된 배열
  • 충돌과 오버플로 또한 해결 방법이 다양함.
    • 충돌 해결 기술 분류 체계
      • (Open Hashing) Separate Chaining, Closed Addressing 키 중복 O (연결리스트로 추가)
      • (Closed Hashing) Open Addressing, Linear, Quadratic, Double,, 키 중복 X
  • Clustering(군집화)
    • Primary clustering(1차 군집화) : 원래 가야하는 hash위치가 근처임으로 인해 발생하는 문제
    • Secondary clustering(2차 군집화) : 수식이 원래 hashslot에 기반해서 발생하는 문제
      => 1차 군집화를 해결하고자 Quadratic Priboing(이차 조사) 즉, slot = (home+i2)%M 이런식으로 +i에서 다르게 바꿨다고 해도 떨어진 곳에서 군집화가 또 발생한다는 이야기이다.


다양한 해시 함수

  • Hash function 1 : Divide and remainder Hashing : mod 이용 => 제일 쉬움
  • Hash function 2 : Mid-square(중간 제곱) Hashing : key 제곱해서 중간부분만 사용
  • Hash function 3 : Folding(중첩) Hashing : 접어서 합을 구함
  • 기타 해시 함수 : 숫자 추출, 숫자 이동, 진수 변환… 등


Collision and overflow(충돌과 오버플로) 해결 방법

  • 선형조사 : 말그대로 선형조사함.
  • 선형조사의 단점 개선으로 ‘2-패스 해시 파일’ 있음.

  • 독립 오버플로 구역 : 독립 오버플로 공간 새로 만들어서 이용.

  • 이중해싱 : 2차 해시 함수 사용

  • 동거자 체인 : 링크드 리스트로 따로 오버플로 영역 공간 만들어서 각 연결.

  • 버킷 체인



HashMap

  • 다양한 해시 방법들이 있는데 그중 제일 기초가 되는 방식


1. MapEntry

  • dict처럼 key, value 인스턴스 생성해서 사용하기 위함
class MapEntry: # key, value 인스턴스
    def __init__(self, key, value):
        self.key = key
        self.value = value

UNUSED = None
EMPTY = MapEntry(None, None)


2. HashMap Class

  • add, valueOf, findSlot, rehash, hash1, hash2, printHashAll
class HashMap: 
    def __init__(self):
        self.table = list(EMPTY for i in range(7)) # list로 수정! 0~6이니까 크기 7
        self.count = 0
        self.maxCount = len(self.table) - len(self.table) // 3
        self.rehashNumber = 0 # 총 리해시 넘버
        self.crashNumber = 0 # 총 충돌 넘버
        self.crashAfterPrint = [] # 마지막 리해시 후 충돌 출력(충돌 잘 되었는지 확인을 위해서!)
    
    def __len__(self): # 크기 반환
        return self.count
    
    def __contains__(self, key): # key를 포함하는지 체크
        slot = self.findSlot(key,False)
        return self.table[slot].key is not None # key가 None이 아니라면 True 반환

    def add(self, key, value): # 동일 키 False, 다른 키 True 반환
        # 동일한 키 유무 판단을 위해 for문 접근
        for slots in self.table: # 크기 7인 list를 각각 접근
            if slots.key == key: 
                slot = self.findSlot(key, False)
                self.table[slot].value = value
                return False
        # return이 아니라는 것은 동일한 키가 없다는 뜻
        slot = self.findSlot(key,True)
        self.table[slot] = MapEntry(key, value)
        self.count += 1
        if self.count == self.maxCount: # 2/3 수준정도에서 리해시 하는 부분
            self.rehash()
        return True

    def valueOf(self, key): # key에 연관된 값을 반환
        slot = self.findSlot(key,False)
        return self.table[slot].value

    def findSlot(self, key, forInsert): # forInsert가 True인 경우 추가, 아닌 경우 해당 값 update
        slot = self.hash1(key)
        step = self.hash2(key)
        M = len(self.table)
        while self.table is not UNUSED: # list가 아예 None이 아니라면 반복! False일시 반복문 탈출
            if forInsert and \
                (self.table[slot] is UNUSED or self.table[slot] is EMPTY): # True 이면서 slot이 None이라면 slot 반환
                return slot
            elif not forInsert and \
                (self.table[slot] is not EMPTY and self.table[slot].key == key): # False 이면서 slot이 None이 아니면서 slot의 key가 같은경우 slot 반환
                return slot
            elif not forInsert and \
                (self.table[slot] is UNUSED or self.table[slot] is EMPTY): # False 이면서 slot이 None이라면 slot 반환 => contains, valueOf를 위해 생성
                return slot
            else: # 충돌
                self.crashAfterPrint.append("충돌전 slot : " + str(slot))
                slot = (slot + step) % M
                self.crashAfterPrint.append("충돌후 slot : " + str(slot))
                self.crashNumber += 1

    def rehash(self):
        origTable = self.table
        newSize = len(self.table) * 2 + 1 # 크기 15
        self.table = list(EMPTY for i in range(newSize)) # list로 수정

        self.count = 0
        self.maxCount = newSize - newSize // 3
        
        for entry in origTable:
            if entry is not UNUSED and entry is not EMPTY:
                slot = self.findSlot(entry.key, True) # entry.key 넘겨서 slot 다시 반환
                self.table[slot] = entry
                self.count += 1
        self.crashAfterPrint = [] # 마지막 리해시 후 충돌 구하기위해 또다시 초기화
        self.rehashNumber += 1

    def hash1(self, key):
        return abs(hash(key)) % len(self.table) # hash()는 파이썬에서 제공하는 함수입니다!

    def hash2(self, key):
        return 1 + abs(hash(key)) % (len(self.table) - 2)

    def printHashAll(self): # 임의로 만든 총 리스트 출력 (충돌 잘 되었는지 확인을 위해서!)
        for i in range (len(self.table)):
            print("리스트 번호 : {0}, key : {1}, value : {2}".format(i, self.table[i].key, self.table[i].value))


3. 프로그램 테스트

# HashMap에 자료를 넣기
items = bms.get_items(100)

map = HashMap()
for item in items:
    key = item.title + item.link # 고유의 key
    value = item.title # 값
    map.add(key, value) # 추가하기
# 1. 총 리해시 횟수
print("총 리해시 횟수 : ", map.rehashNumber)
print("마지막 리해시 후 크기 : ", len(map.table))

# 2. 총 충돌 횟수
print("총 충돌 횟수 : ", map.crashNumber)
'''
총 리해시 횟수 :  5
마지막 리해시 후 크기 :  255
총 충돌 횟수 :  115
'''
# 리해시 할 때마다 slot의 위치또한 같이 변할 테니까 마지막 리해시 후 충돌된 slot들 확인

# 3. 마지막 리해시 후 충돌 출력!
print(map.crashAfterPrint)

# 4. 총 List 보여주는 함수
map.printHashAll()
'''
['충돌전 slot : 143', '충돌후 slot : 181', '충돌전 slot : 89', '충돌후 slot : 235', '충돌전 slot : 235', '충돌후 slot : 126', '충돌전 slot : 239', '충돌후 slot : 244', '충돌전 slot : 244', '충돌후 slot : 249', '충돌전 slot : 183', '충돌후 slot : 222', '충돌전 slot : 95', '충돌후 slot : 15', '충돌전 slot : 73', '충돌후 slot : 101', '충돌전 slot : 2', '충돌후 slot : 115', '충돌전 slot : 24', '충돌후 slot : 215']
리스트 번호 : 0, key : None, value : None
리스트 번호 : 1, key : Janicehttp://www.pkrucihdmk.com, value : Janice
리스트 번호 : 2, key : Charlenehttp://www.anzmvvjvrh.com, value : Charlene
리스트 번호 : 3, key : None, value : None
... 생략
'''


4. 복잡도

  • 주관적인 생각임으로 틀릴 수 있음
# HashMap 클래스 내 rehash, findSlot 메소드
"""
  T: O(N^2) - Quadratic time
  rehash : 재해시 하는 메소드
  => while문 안에 findSlot 써줌으로 써 N^2이 된다고 생각
"""  
"""
  T: O(N) - Linear time
  findSlot : findSlot 메소드
"""  

댓글남기기