[algorithms] Hashing

해싱 개요

  • 해싱은 데이터를 단시간에 삽입하거나 저장된 데이터를 가져올 때 주로 사용하는 기법이다.
  • 해싱은 해시 테이블이라는 자료 구조를 이용한다.
  • 해싱을 이용하면 데이터를 빠르게 삽입하고, 삭제하고, 가져올 수 있지만, 최솟값이나 최댓값 찾기 등 검색 동작은 효율이 떨어진다.
  • 검색이 필요한 상황이라면 이진 탐색 트리 같은 자료구조를 사용하는 것이 좋다.
  • 해시 테이블 자료구조는 배열을 이용한다.
  • 키(key)라 불리는 데이터 요소로 배열에 저장된 데이터 요소를 참조할 수 있다.
  • 해시 함수는 각 키를 자체 배열 요소로 저장한다. 되도록 키가 한 곳에 집중되지 않도록 저장하는 것이 좋다.
  • 해시 함수에서는 두 키의 해시 결과(해시 함수 수행 결과)가 같은 값일 때도 있다. 이를 충돌(collision) 이라 한다.
  • 해시 테이블에 사용할 배열의 크기는 소수(prime number) 여야 한다.

해시 테이블 클래스

심플 해싱

  • 정수키를 가진 해시 테이블이라면 배열의 크기로 나눈 나머지를 반환(모듈로:modulo 연산)하는 해시 함수를 이용할 수 있다. 이때, 키가 모두 0으로 끝나며 배열의 크기가 10인 상황(320 % 10)에서는 이런 간단한 해시 함수를 사용할 수 없다. 따라서 배열의 길이는 소수로 만들어 주는 것이 유리하다.
  • 해싱 결과 값이 항상 테이블 범위 안에 있게 하기 위해선 다음과 같이 모듈로 연산을 해야 한다. (n % table.length)
class HashTable {
  table: string[];

  constructor() {
    this.table = new Array(137);
  }

  simpleHash(data: string) {
    // 각 문자의 아스키 값의 합을 얻어 해시 값을 계산한다.
    const total = data.split("").reduce((acc, char) => {
      return acc + char.charCodeAt(0);
    }, 0);

    console.log(`Hash value: ${data} -> ${total}`);
    // 계산 결과가 항상 해당 테이블 범위 안에 있게 하기 위해 모듈러 연산을 사용한다.
    return total % this.table.length;
  }

  // 배열에 실제로 저장된 이름을 출력한다.
  showDistro() {
    this.table
      .filter(data => {
        return !!data;
      })
      .forEach((data, index) => console.log(`${index}: ${data}`));
  }

  // 실제 해시테이블에 데이터를 저장할 수 있도록 한다.
  put(key: string, data: string) {
    // const pos = this.simpleHash(data);
    const pos = this.betterHash(key);
    this.table[pos] = data;
  }

  get(key: string) {
    return this.table[this.betterHash(key)];
  }
}

호너의 메소드 해시 함수

호너의 메소드를 이용하려면 결과에 소수를 곱하는 과정을 추가해야 한다.

// 호너의 메서드 알고리즘을 사용해서 충돌이 안나게끔 해싱 함수를 만들자.
  betterHash(data: string) {
    const H = 37;
    const total = data.split("").reduce((acc, char) => {
      return H * acc + char.charCodeAt(0);
    }, 0);
    console.log(`Hash value: ${data} -> ${total}`);

    const hashKey = total % this.table.length;
    return hashKey;
  }

충돌 처리

모든 키가 해시 테이블에 저장될 수 있게 충돌을 처리하는 분리된 체인, 선형 조사 라는 두가지 충돌 해결 방법을 설명합니다.

분리된 체인

해시된 키를 저장할 배열을 만든 다음 해시 테이블의 각 배열 요소에 빈 배열을 할당하는 방식이다. 즉, 2차 배열을 만드는 것이다.

const tables = [
  60: [David], // key: David, data: David
  68: [Jennifer],
  69: [Mike],
  70: [Donnie, Jonathan]
  78: [Cynthia, Danny]
  88: [Raymond, Clayton]
]
class HashTable {
  table: string[][];

  constructor() {
    this.table = new Array(137);
    // 충돌처리 - 분리된 체인
    for (let i = 0; i < this.table.length; i++) {
      this.table[i] = new Array();
    }
  }

  simpleHash(data: string) {
    // 각 문자의 아스키 값의 합을 얻어 해시 값을 계산한다.
    const total = data.split("").reduce((acc, char) => {
      return acc + char.charCodeAt(0);
    }, 0);

    console.log(`Hash value: ${data} -> ${total}`);
    // 계산 결과가 항상 해당 테이블 범위 안에 있게 하기 위해 모듈러 연산을 사용한다.
    return total % this.table.length;
  }

  // 호너의 메서드 알고리즘을 사용해서 충돌이 안나게끔 해싱 함수를 만들자.
  betterHash(data: string) {
    const H = 37;
    const total = data.split("").reduce((acc, char) => {
      return H * acc + char.charCodeAt(0);
    }, 0);
    console.log(`Hash value: ${data} -> ${total}`);

    const hashKey = total % this.table.length;
    return hashKey;
  }

  // 배열에 실제로 저장된 이름을 출력한다.
  showDistro() {
    this.table
      .filter((dataList) => {
        return !!dataList[0];
      })
      .forEach((data, index) => console.log(`${index}: ${data}`));
  }

  put(key: string, data: string) {
    // const pos = this.simpleHash(data);
    const pos = this.betterHash(key);
    // this.table[pos] = data;
    const tableKeyPoint = this.table[pos];
    const tableKeyPointLength = tableKeyPoint.length;

    if (tableKeyPointLength === 0) {
      tableKeyPoint[0] = data;
    } else {
      tableKeyPoint[tableKeyPointLength - 1] = data;
    }
  }

  get(key: string) {
    return this.table[this.betterHash(key)];
  }
}

선형 조사

선형 조사는 오픈 주소법 해싱(open-addressing hashing)이라 불리는 일반적인 해싱 기법이다. 오픈 주소법에는 충돌이 발생하면 해시 테이블의 다음 요소가 비어 있는지 확인한 후 다음 요소가 비어 있으면 비어 있는 요소에 키를 저장합니다.

대부분의 해시 테이블에는 비어 있는 공간이 많이 있으므로 비어있는 공간을 이용해 키를 저장한다는 것이 선형 조사 기법의 핵심입니다.

// key 저장
const tables = [
  60: David, // key: David, data: David
  68: Jennifer,
  69: Mike,
  70: Donnie, //Jonathan
  71: Jonathan,
  78: Cynthia, //Danny]
  79: Danny,
  88: Raymond, Clayton
  89: Clayton
]
// Data 저장
const values = [
  60: David, // key: David, data: David
  68: Jennifer,
  69: Mike,
  70: Donnie, //Jonathan
  71: Jonathan,
  78: Cynthia, //Danny]
  79: Danny,
  88: Raymond, Clayton
  89: Clayton
]
class HashTable {
  table: string[][];

  constructor() {
    this.table = new Array(137); // 해싱된 키를 저장하고
    // 선형 조사
    this.values = []; // values에 키에 해당하는 데이터를 저장한다.
  }
  put(key: string, data: string) {
      // const pos = this.simpleHash(data);
      const pos = this.betterHash(key);

      if(this.table[pos] === undefined) {
        this.table[pos] = key;
        this.values[pos] = data;
      } else {
        while(this.table[pos] !== undefined) {
          pos++;
        }
        this.table[pos] = key;
        this.values[pos] = data;
      }
    }

  get(key: string) {
    const pos = this.betterHash(key);
    if(pos > -1) {
      for(let i = pos; this.table[pos] !== undefined; i++) {
        if(this.table[pos] === key) {
          return this.values[pos];
        }
      }
    }
    return undefined;
  }
}
© 2021 Merlin.ho, Built with Gatsby