hashTable

DongJun·2022년 11월 9일

바닐라 코딩(Prep)

목록 보기
2/6
post-thumbnail

해시 테이블이란

해시 함수를 사용해서 변환한 값을 index로 삼아 key와 value를 저장하는 자료구조다.

해시 테이블은 어떤 특정 값을 받아서 해시 함수에 입력하고, 함수의 출력값을 인덱스로 삼아 데이터를 저장한다.

즉 해시 함수를 이용해서 key를 hash value로 매핑하고, 이 hash value를 index로 삼아 데이터의 value를 buckets(혹은 slots)에 저장한다.

파이썬의 dictionary, 루비의 hash, 자바의 map이 해시 테이블의 대표적인 예다.

특징

기본 연산으로는 search, insert, delete가 있다.

순차적으로 데이터를 저장하지 않는다.

key를 통해서 value를 얻을 수 있다. => 이진 탐색 트리나 배열에 비해서 속도가 획기적으로 빠름

커다란 데이터를 해시해서 짧은 길이로 축약할 수 있기 때문에 데이터를 비교할 때 효율적이다.

value는 중복 가능하지만 key는 유니크해야 한다.

수정 가능하다(mutable)

보통 배열로 미리 hash table 사이즈만큼 생성 후에 사용한다.

해시 테이블은 key-value가 1:1 매핑되어 있기 때문에 검색, 삽입, 삭제 과정에서 모두 평균적으로 O(1)의 시간복잡도를 갖는다. 공간 복잡도는 O(N)인데 여기서 N은 키의 개수다.

해시 함수

해시

해시란 단방향 암호화 기법으로 해시 함수를 이용하여 고정된 길이의 암호화된 문자열로 바꿔버리는 것을 의미한다. 이때 매핑 전 원래 데이터의 값을 key, 매핑 후 데이터 값을 hash value, 매핑하는 과정을 hashing이라고 한다.

해시 함수

임의의 길이를 갖는 메시지를 입력받아서 고정된 길이의 해시값을 출력하는 함수

어떤 입력 값에도 항상 고정된 길이(해시 함수에 따라 비슷한 길이까지 포함)의 해시값을 출력한다.

해시 함수를 통해 입력값은 완전히 새로운 모습의 데이터로 만들어지기 때문에 암호화 영역에서 아주 주요하게 사용되고 있다. SHA(Secure Hash Algorithm)알고리즘이 그 대표적인 예시다.

입력 값의 일부가 변경되면 전혀 다른 값을 출력하는데 이것을 가리켜 눈사태 효과라고 한다. 이 눈사태 효과로 결과값으로는 입력값을 유추할 수가 없다. 역추적이 안된다는 말은 단방향으로만 되어 있다는 의미를 내포한다.

#단점: 해시 충돌

하지만 이 해시 함수에도 단점이 존재하는데, 해시 함수는 입력값의 길이가 어떻든 고정된 길이의 값을 출력하기 때문에 입력값이 다르더라도 같은 결과값이 나오는 경우가 있다.

이것을 해시 충돌 hash collision이라고 표현하며, 충돌이 적은 해시 함수가 좋은 해시함수다

해시 충돌

해시 충돌을 설명하기 위해서는 적재율(load factor)이라는 개념이 필요하다. 적재율이란 해시 테이블의 크기에 대한 키의 개수의 비율을 뜻한다. 즉 키의 개수를 K, 해시 테이블의 크기를 N이라고 했을 때 적재율은 K/N이다. 만약 충돌이 발생하지 않을 경우 해시 테이블의 탐색, 삽입, 삭제 연산은 모두 O(1)에 실행되지만, 충돌이 발생할 경우에는 탐색과 삭제 연산이 O(K)만큼 걸리게 된다.

해시 충돌이 1도 없는 해시 함수를 만드는 것은 불가능하다. 따라서 해시 충돌에 대해 안전하다는 해시 함수는 '충돌을 찾는 게 거~의 희박하다'라는 뜻이다. 이 해시 충돌이 발생하는 근본적인 원인은 비둘기집 원리 (opens new window)다. 즉 해시 함수가 무한한 가짓수의 입력값을 받아 유한한 가짓수의 출력값을 생성하는 경우에는 비둘기집 원리에 의해 해시 충돌은 항상 존재한다. 띠라서 해시 테이블의 충돌을 완화하는 방향으로 문제를 보완해야 한다.

해시 충돌 완화

- 개방 주소법 open addressing으로 해시 테이블 크기는 고정하면서 저장할 위치를 잘 찾는다.

- 분리 연결법 seperate chaining으로 해시 테이블의 크기를 유연하게 만드는 방법이 대표적이다

1. 개방 주소법 open address

open addressing은 한 버킷 당 들어갈 수 있는 엔트리는 하나이지만 해시 함수로 얻은 주소가 아닌, 다른 주소에 데이터를 저장할 수 있도록 허용하는 방법이다. 하지만 이 방법은 부하율(load factor)이 높을 수록(= 테이블에 저장된 데이터의 밀도가 높을수록) 성능이 급격히 저하된다. 개방 주소법의 주요 목적은 저장할 엔트리를 위한 다음의 slot을 찾는 것인데 이 방법으로 2가지가 널리 쓰인다. - 이미지 출처(opens new window)

2. 분리 연결법 seperate chaining

분리 연결법은 개방 주소법과는 달리 한 버킷(슬롯) 당 들어갈 수 있는 엔트리의 수에 제한을 두지 않는다. 이때 버킷에는 링크드 리스트 linked list나 트리 tree를 사용한다.

해시 충돌이 일어나더라도 linked list로 노드가 연결되기 때문에 index가 변하지 않고 데이터 개수의 제약이 없다는 장점이 있다. 하지만 메모리 문제를 야기할 수 있으며, 테이블의 부하율에 따라 선형적으로 성능이 저하된다. 따라서 부하율이 작을 경우에는 open addressing 방식이 빠르다.

위의 2가지 방법 이외에도 해시 테이블의 적재율이 높아진 경우에는 크기가 더 큰 새로운 테이블을 만들어서 기존 데이터를 옮겨서 사용하는 방법이 있다. 혹은 분리 연결법을 사용했을 경우엔 재해싱을 통해서 너무 길어진 리스트의 길이를 나누어서 다시 저장할 수도 있다. 그리고 마지막으로 해시 함수 자체를 올바르게 쓰는 방법이다. 특정 값에 치우치지 않고 해시값을 고르게 만들어내는 좋은 해시 함수를 사용하는 것이다.

자바스크립트로 해시 테이블 구현하기

class HashTable {
  constructor(size) {
    this.data = new Array(size);
  }

  // Hash Function
  _hash(key) {
    let hash = 0;
    for (let i = 0; i < key.length; i++) {
      hash = (hash + key.charCodeAt(i) * i) % this.data.length;
    }

    return hash;
  }

  /**
   * Insert a key-pair value in object.
   * @param {key} key
   * @param {value} value
   * @return {object} this.data
   */
  set(key, value) {
    const address = this._hash(key);

    if (!this.data[address]) {
      this.data[address] = [];
    }

    // If there is already a data in the address, push it in the same array.
    // This will happen in case of hash collision (enough data, less memory space).
    this.data[address].push([key, value]); // Time complexity = O(1)
    return this.data;
  }

  /**
   * Look for a value at given key.
   * @param {key} key
   * @return {any} value
   */
  get(key) {
    const address = this._hash(key);
    const currentBucket = this.data[address];

    // In case of hash collision this will have length more than 1.
    if (currentBucket) {
      // Time Complexity = O(1) most often. In worst case (hash collision) it will be O(n)
      for (let i = 0; i < currentBucket.length; i++) {
        if (currentBucket[i][0] === key) {
          return currentBucket[i][1];
        }
      }
    }
  }

  /**
   * Find all keys in the object.
   * Time Complexity = O(N),
   * in case of hash collision it will be O(N^2) where N is keys.
   * @return {object} keysArray
   */
  keys() {
    let keysArray = [];

    for (let i = 0; i < this.data.length; i++) {
      if (this.data[i] && this.data[i].length) {
        if (this.data[i].length > 1) {
          for (let j = 0; j < this.data[i].length; j++) {
            if (this.data[i][j]) {
              keysArray.push(this.data[i][j][0]);
            }
          }
        } else {
          keysArray.push(this.data[i][0][0]);
        }
      }
    }

    return keysArray;
  }
}

const myHashTable = new HashTable(50);
myHashTable.set('has_garden', true);
console.log(myHashTable.get('has_garden'));

myHashTable.set('house_number', 54);
console.log(myHashTable.get('house_number'));

myHashTable.set('street_name', 'Main Street');
console.log(myHashTable.get('street_name'));

console.log(myHashTable.keys());
profile
성장하기위한 나만의 방법을 꾸준히 찾는중! 협동적 & 성실한 Frontend 개발자를 목표로…

0개의 댓글