3 분 소요

해싱

  • 순차 탐색이나 이진 탐색은 탐색 키와 각 레코드를 비교하여 원하는 레코드를 찾음
  • 해싱은 탐색 키에 어떤 함수를 돌려 레코드가 저장되어야 할 위치를 바로 계산

해싱의 구조

  • 해시 테이블: 레코드를 저장한 테이블
  • 테이블은 M개의 버킷으로 이루어짐, 각 버킷은 보통 여러개의 슬롯으로 나누어짐
  • 각 슬롯에 하나의 레코드가 저장된다

이상적인 해싱과 실제의 해싱

  • 충돌(collision): 두개의 서로 다른 키가 해시 함수에 의해 같은 주소로 계산되는 상황
  • 버킷에 여러 개의 슬롯이 있다면 비어 있는 다른 슬롯에 저장
  • 오버플로(overflow): 충돌이 슬롯 수보다 더 많이 발생
  • 이상적인 해싱: 충돌이 절대 일어나지 않는 경우
  • 실제의 해싱: 충돌과 오버플로가 빈번하게 발생할 수 있음

해시 함수

  • 임의의 길이를 갖는 데이터를 고정된 길이의 데이터로 변환 시켜주는 함수

(좋은 해시 함수의 조건)

  • 충돌이 적게 발생해야 함
  • 해시 결과가 테이블의 주소 영역 내에서 고르게 분포되어야 함
  • 계산이 빨라야 함

제산 함수(나머지 연산 함수)

  • $h(k) = kmodM$ (테이블의 크기: M, 탐색 키: k)
  • 해싱에서 가장 흔히 사용, 항상 성능이 좋지만은 않음

탐색 키가 문자열인 경우

(문자열 탐색키에 대한 해시 함수 예)

def hashFnStr(key) :
    sum = 0
    for c in key : # 문자열의 모든 문자에 대해
        sum = sum + (ord(c))
    return sum % M

오버플로 처리하기

  • 개방 주소법(open addressing): 오버플로가 일어나면 그 항목을 해시 테이블의 다른 위치(주소)에 저장 ex. 선형 조사법, 이차 조사법, 이중 해싱법 등
  • 체이닝(chaining): 해시 테이블의 하나의 위치에 여러 개의 항목을 저장할 수 있도록 테이블의 구조를 변경

선형 조사법

  • 개방 주소법 전략을 사용
  • 조사(probing): 비어 있는 공간을 찾는 것

삽입 연산

  • 군집화: 한번 충돌이 발생한 위치 부근에서 연속적으로 충돌이 발생하는 경향ㄷ
M = 13
table = [None]*M

def hashFn(key) :   # 해시 함수
    return key % M

def lp_insert(key) :
    id = hashFn(key)
    count = M
    while count>0 and (table[id] != None and table[id] != -1) :
#    while count>0 and (table[id] != None) :
        id = (id + 1 + M) % M   # 다음 위치로 이동
        count -= 1              # 검사할 남은 위치의 수

    if count > 0 :
        table[id] = key      # 해당 슬롯에 항목 저장
    return

탐색 연산

  • 해당 키의 레코드를 찾거나, 레코드가 없는 버킷을 만나거나 모든 버킷을 다 검사할 때까지 진행
    def lp_search(key) :
      id = hashFn(key)
      count = M
      while count>0 :
          if table[id] == None :  # 찾는 항목이 테이블에 없음
             return None
          if table[id] != -1 and table[id] == key : 
    #        if table[id] == key : 
              return table[id]    # 찾는 항목이 테이블에 있음
          id = (id + 1 + M) % M   # 없으면 다음 위치 검사
          count -= 1
      return None
    

삭제 연산

  • 빈 버킷을 두 가지로 분류해야 함
  • 한번도 사용하지 않은 것, 삭제되어 현재 비어 있는 것
  • 탐색 과정은 한 번도 사용이 안 된 버킷을 만나야만 중단 됨
def lp_delete(key) :
    id = hashFn(key)
    count = M
    while count>0 :
        if table[id] == None : return None
        if table[id] != -1 and table[id] == key : 
            table[id] = -1
            return
        id = (id + 1 + M) % M
        count -= 1

선형 조사법의 테스트 프로그램

print("   최초:", table )
lp_insert(45);  print( "45 삽입:", table )
lp_insert(27);  print( "27 삽입:", table )
lp_insert(88);  print( "88 삽입:", table )
lp_insert(9);   print( " 9 삽입:", table )

lp_insert(71);  print("71 삽입:", table )
lp_insert(60);  print("60 삽입:", table )
lp_insert(46);  print("46 삽입:", table )
lp_insert(38);  print("38 삽입:", table )
lp_insert(24);  print("24 삽입:", table )
lp_delete(60);  print("60 삭제:", table )
print("46 탐색:", lp_search(46) )

실행 결과

최초: [None, None, None, None, None, None, None, None, None, None, None, None, None]
45 삽입: [None, None, None, None, None, None, 45, None, None, None, None, None, None]
27 삽입: [None, 27, None, None, None, None, 45, None, None, None, None, None, None]  
88 삽입: [None, 27, None, None, None, None, 45, None, None, None, 88, None, None]
 9 삽입: [None, 27, None, None, None, None, 45, None, None, 9, 88, None, None]
71 삽입: [None, 27, None, None, None, None, 45, 71, None, 9, 88, None, None]
60 삽입: [None, 27, None, None, None, None, 45, 71, 60, 9, 88, None, None]
46 삽입: [None, 27, None, None, None, None, 45, 71, 60, 9, 88, 46, None]
38 삽입: [None, 27, None, None, None, None, 45, 71, 60, 9, 88, 46, 38]
24 삽입: [24, 27, None, None, None, None, 45, 71, 60, 9, 88, 46, 38]
60 삭제: [24, 27, None, None, None, None, 45, 71, -1, 9, 88, 46, 38]
46 탐색: 46

군집화 문제 완화 방법

  • 해싱은 공간을 이용해 시간 복잡도를 $O(1)$ 까지 낮출 수 있는 최강의 탐색 기법
  • 이차 조사법: 주소를 1씩 증가시키는 것이 아니라 $h(k), h(k)+1, h(k)+4, h(k)+9와 같이 제곱의 순으로 움직이는 방법
  • 이중 해싱법 또는 재해싱: 충돌이 발생해 다음 위치를 정할 때, 원래 해시 함수와 다른 별개의 함수를 이용 탐색 키가 다르면 1차 해시 주소가 같아도 2차 함수에서 다른 주소가 나오고, 따라서 집중을 완화할 수 있는 아이디어

댓글남기기