본문 바로가기
Algorithm/자료구조와 함께 배우는 알고리즘(Python)

해시법

by k-mozzi 2021. 10. 12.
반응형
Preface

 

이번 장은 공부하는 데 생각보다 오랜 시간이 걸렸다.

 

개인적인 사정이 생겨 시간이 없었던 탓도 있지만, 내용 자체가 어렵고 길어 코드를 한 줄 한 줄 읽어가며 이해하는 것이 어려웠다.

 

무엇보다 이전엔 사용하지 않던 어노테이션을 사용하려 하니 손에 익지 않아서인지 꽤나 불편했다.

 

또, 다양한 클래스와 함수, 변수를 한 프로그램 내에서 모두 사용하자 정신이 없었다.

 

앞으로는 이보다 훨씬 길고 복잡한 코드를 자주 접하게 될텐데, 벌써부터 코드를 이해하는 데 어려움을 겪으니 걱정이 앞선다.

 

이번에 공부한 코드를 자주 읽어보며 구현력과 가독성을 높이자.


 

- 해시법 : '데이터를 저장할 위치 = 인덱스'를 간단한 연산으로 구하는 것

 

 

- 해시값 : 원소의 값 % 원소의 개수

 

 

- 해시 테이블 : 해시값을 인덱스로 하여 원소를 새로 저장한 배열

 

 

- 해시 함수 : 키를 해시값으로 변환하는 과정

 

 

- 버킷 : 해시 테이블에서 만들어진 원소

 

 

- 충돌 : 저장할 버킷이 중복되는 현상

→ 해시 테이블을 충분히 크게 만들면 충돌 발생을 억제할 수 있지만, 시간과 공간의 트레이드-오프(상충 관계) 문제가 발생한다.

 

 

- 해시법 구현 방법


1. 체인법 : 해시값이 같은 데이터를 체인 모양의 연결 리스트로 연결하는 방법

→ 오픈 해시법

 

 

- 체인법 코드

from __future__ import annotations
from typing import Any, Type
import hashlib


class Node:
    def __init__(self, key: Any, value: Any, next: Node) -> None:
        self.key = key
        self.value = value
        self.next = next


class ChainedHash:
    def __init__(self, capacity: int) -> None:
        self.capacity = capacity
        self.table = [None] * self.capacity

    def hash_value(self, key: Any) -> int:
        if isinstance(key, int):
            return key % self.capacity
        return (int(hashlib.sha256(str(key).encode()).hexdigest(), 16) % self.capacity)
        # key 가 int 형이 아닐 때

    def search(self, key: Any) -> Any:
        hash = self.hash_value(key)
        p = self.table[hash]

        while p is not None:
            if p.key == key:
                return p.value
            p = p.next

        return None

    def add(self, key: Any, value: Any) -> bool:
        hash = self.hash_value(key)
        p = self.table[hash]

        while p is not None:
            if p.key == key:
                return False
            p = p.next

        temp = Node(key, value, self.table[hash])
        self.table[hash] = temp
        return True

    def remove(self, key: Any) -> bool:
        hash = self.hash_value(key)
        p = self.table[hash]
        pp = None

        while p is not None:
            if p.key == key:
                if pp is None:
                    self.table[hash] = p.next
                else:
                    pp.next = p.next
                return True
            pp = p
            p = p.next
        return False

    def dump(self) -> None:
        for i in range(self.capacity):
            p = self.table[i]
            print(i, end='')
            while p is not None:
                print(f'  → {p.key} ({p.value})', end='')
                p = p.next
            print()

 

 

- 해시법의 key가 int 형이 아닌 경우

1) sha256 알고리즘 : 주어진 바이트 문자열의 해시값을 구하는 해시 알고리즘의 생성자

2) encode( ) 함수 : 바이트 문자열을 생성하는 함수

3) hexdigest( ) 함수 : sha256 알고리즘에서 해시값을 16진수 문자열로 꺼내는 함수

4) int( ) 함수 : hexdigest( ) 함수로 꺼낸 문자열을 16진수 문자열로 하는 int 형으로 변환

 

 

- 체인법 사용 코드

from enum import Enum
from chained import ChainedHash

Menu = Enum('Menu', ['추가', '삭제', '검색', '덤프', '종료'])


def select_menu() -> Menu:
    s = [f'({m.value}){m.name}' for m in Menu]
    while True:
        print(*s, sep='  ', end='')
        n = int(input(': '))
        if 1 <= n <= len(Menu):
            return Menu(n)


hash = ChainedHash(13)

while True:
    menu = select_menu()

    if menu == Menu.추가:
        key = int(input('추가할 키를 입력하세요.: '))
        val = input('추가할 값을 입력하세요.: ')
        if not hash.add(key, val):
            print('추가를 실패했습니다.')

    elif menu == Menu.삭제:
        key = int(input('삭제할 키를 입력하세요.: '))
        if not hash.remove(key):
            print('삭제를 실패했습니다!')

    elif menu == Menu.검색:
        key = int(input('검색할 키를 입력하세요.: '))
        t = hash.search(key)
        if t is not None:
            print(f'검색한 키를 갖는 값은 {t}입니다.')
        else:
            print('검색할 데이터가 없습니다.')

    elif menu == Menu.덤프:
        hash.dump()

    else:
        break

 

 

2. 오픈 주소법 : 충돌이 발생했을 때 재해시를 수행하여 빈 버킷을 찾는 방법

→ 닫힌 해시법, 선형 탐사법

 

 

- 오픈 주소법 코드

from __future__ import annotations
from typing import Any, Sequence
from enum import Enum
import hashlib


class Status(Enum):
    OCCUPIED = 0
    EMPTY = 1
    DELETED = 2


class Bucket:
    def __init__(self, key: Any = None, value: Any = None,
                 stat: Status = Status.EMPTY) -> None:
        self.key = key
        self.value = value
        self.stat = stat

    def set(self, key: Any, value: Any, stat: Status) -> None:
        self.key = key
        self.value = value
        self.stat = stat

    def set_status(self, stat: Status) -> None:
        self.stat = stat


class OpenHash:
    def __init__(self, capacity: int) -> None:
        self.capacity = capacity
        self.table = [Bucket()] * self.capacity

    def hash_value(self, key: Any) -> int:
        if isinstance(key, int):
            return key % self.capacity
        return (int(hashlib.md5(str(key).encode()).hexdigest(), 16)
                % self.capacity)

    def rehash_value(self, key: Any) -> int:
        return (self.hash_value(key) + 1) % self.capacity

    def search_node(self, key: Any) -> Any:
        hash = self.hash_value(key)
        p = self.table[hash]

        for i in range(self.capacity):
            if p.stat == Status.EMPTY:
                break
            elif p.stat == Status.OCCUPIED and p.key == key:
                return p
            hash = self.rehash_value(hash)
            p = self.table[hash]
        return None

    def add(self, key: Any, value: Any) -> bool:
        if self.search(key) is not None:
            return False

        hash = self.hash_value(key)
        p = self.table[hash]
        for i in range(self.capacity):
            if p.stat == Status.EMPTY or p.stat == Status.DELETED:
                self.table[hash] = Bucket(key, value, Status.OCCUPIED)
                return True
            hash = self.rehash_value(hash)
            p = self.table[hash]
        return False

    def remove(self, key: Any) -> int:
        p = self.search_node(key)
        if p is None:
            return False
        p.set_status(Status.DELETED)
        return True

    def dump(self) -> None:
        for i in range(self.capacity):
            print(f'{i: 2} ', end='')
            if self.table[i].stat == Status.OCCUPIED:
                print(f'{self.table[i].key} ({self.table[i].value})')
            elif self.table[i].stat == Status.EMPTY:
                print('-- 미등록 --')
            elif self.table[i].stat == Status.DELETED:
                print('-- 삭제 완료 --')

 

 

- 오픈 주소법 사용 코드

from enum import Enum
from open_hash import OpenHash

Menu = Enum('Menu', ['추가', '삭제', '검색', '덤프', '종료'])


def select_menu() -> Menu:
    s = [f'({m.value}){m.name}' for m in Menu]
    while True:
        print(*s, sep='  ', end='')
        n = int(input(': '))
        if 1 <= n <= len(Menu):
            return Menu(n)


hash = OpenHash(13)

while True:
    menu = select_menu()

    if menu == Menu.추가:
        key = int(input('추가할 키를 입력하세요.: '))
        val = input('추가할 값을 입력하세요.: ')
        if not hash.add(key, val):
            print('추가를 실패했습니다.')

    elif menu == Menu.삭제:
        key = int(input('삭제할 키를 입력하세요.: '))
        if not hash.remove(key):
            print('삭제를 실패했습니다.')

    elif menu == Menu.검색:
        key = int(input('검색할 키를 입력하세요.: '))
        t = hash.search(key)
        if t is not None:
            print(f'검색한 키를 갖는 값은 {t}입니다.')
        else:
            print('검색할 데이터가 없습니다.')

    elif menu == Menu.덤프:
        hash.dump()

    else:
        break

 

728x90
반응형

'Algorithm > 자료구조와 함께 배우는 알고리즘(Python)' 카테고리의 다른 글

큐란?  (3) 2021.10.24
스택이란?  (0) 2021.10.14
이진 검색  (0) 2021.10.07
검색 알고리즘과 선형 검색  (0) 2021.10.04
배열이란?  (0) 2021.10.02

댓글