자료구조 & 알고리즘

[자료구조] 해시함수(Hash Function) 충돌(Collision) - 선형조사법(1)

진기명기 2023. 3. 28. 21:03
❗️ 해시함수를 사용하면 빠른 속도로 특정 값을 신속하게 찾아 성능을 높일 수 있지만, 충돌이 일어날 수 있어 이를 항상 대비하며 사용해야 한다. 충돌이란?! 아래에서 다뤄보고자 한다.

지난 포스팅을 통해 해시함수 메서드를 확인해 보자 ⬇️⬇️⬇️

https://cyjcyj.tistory.com/122

 

[자료구조] 해시함수(Hash Function) 메서드 구현

🙃 지난 포스팅에서 해시함수에 대해 간단한 특징과 생성자 함수를 구현을 해보고, 직접적으로 key 값을 charCodeAt()을 통해 해당 index로 변경하여 지정한 공간에 누적시키는 코드를 실습했다. 이

cyjcyj.tistory.com

 

 

❗️ 충돌(Collision)이 생긴 경우

ex) YJ, SH를 해시함수로 변경한 경우, 둘 다 2로 변경되었다고 가정

--> 해당 2에 YJ가 있어 SH가 공간에 들어오지 못하는 상황이 되거나, 이미 2가 채워져 SH가 있다고 착각하는 등의 경우가 발생

 

👉🏻 저번 포스팅에서 구현한 메서드를 사용하여, 충돌의 예시를 아래와 같이 작성

// hashTable 생성
let ht4 = new HashTable()

ht4.put('Ana', 172)
ht4.put('Donnie', 183)
ht4.put('Sue', 155)
ht4.put('Jamie', 168)
ht4.put('Paul', 180)

console.log('')
/*
HashTable {
  table: [
    <5 empty items>,
    Element { key: 'Sue', value: 155 },
    <7 empty items>,
    Element { key: 'Ana', value: 172 },
    <18 empty items>,
    Element { key: 'Paul', value: 180 },
    <4 empty items>
  ],
  length: 3
}
*/
✅ 실제로 Donnie와 Jamie는 hash 충돌이 일어났으며, put()에서 공간이 undefined가 아닌 경우, false를 return 하기 때문에 데이터가 추가되지 않은 것을 확인할 수 있다.

 

 

 


💡 충돌 해결 방법

① 자료구조 확장 > 선형 조사법 해시테이블(Linear probing Hash Table)

- Hash 충돌이 발생했을 때, 그 다음 주소를 확인하고 비어있다면 그 자리에 대신 저장하는 해시테이블 기반 자료구조

 

🫠 선형 조사법 해시테이블의 다양한 메서드 구현

// Element(): key, value 저장을 위한 생성자
function Element(key, value){
  this.key = key
  this.value = value
}


// LinearHashTable() : 생성자
function LinearHashTable(){
  this.table = new Array(HASH_SIZE)
  this.length = 0
}


// hashCode() : 해시 함수
LinearHashTable.prototype.hashCode = function(key){
  let hash = 0
  for(let i = 0; i < key.length; i++){
    hash += key.charCodeAt(i)
  }
  return hash % HASH_SIZE
}


// clear() : 초기화
LinearHashTable.prototype.clear = function(){
  this.table = new Array(HASH_SIZE)
  this.length = 0
}


// size() : 크기 반환
LinearHashTable.prototype.size = function(){
  return this.length
}


// getBuffer() : 데이터 셋 반환
LinearHashTable.prototype.getBuffer = function(){
  let array = []

  for(let i = 0; i < this.table.length; i++){
    if(this.table[i]){
      array.push(this.table[i])
    }
  }
  return array
}


// print() : 데이터 셋 출력
LinearHashTable.prototype.print = function(){
  for(let i = 0; i < this.table.length; i++){
    if(this.table[i]){
      console.log(i + ' -> ' + this.table[i].key + ' : ' + this.table[i].value)
    }
  }
}
👉🏻 어디서 많이 본 코드 아닌가? 
바로 지난 시간 포스팅했던 해시함수 메서드 구현에서 작성한 메서드와 똑같다. 
HashTable 생성자 함수에서 LinearHashTable 생성자 함수로 변경된 것 외에 크게 달라진 점은 없기 때문에
따로 설명은 하지 않겠다. (지난 시간에 관련 코드에 대해 설명하였으니 확인 ✅)

 

 

 


🫠 선형 조사법 해시테이블의 메서드(put(), get(), remove())

> 이번에 다룰 메서드는 변경된 코드 구현이다.

① put() : 데이터 추가

// put() : 데이터 추가
LinearHashTable.prototype.put = function(key, value){
  let index = this.hashCode(key)
  let startIndex = index // 배열의 공간을 한 바퀴 돌았는지 여부 체크

  console.log(`key : ${key} -> index : ${index}`) // key가 어떻게 변형됐는지 확인

  do{
    if(this.table[index] === undefined){ // 공간이 비어있다는 뜻
      this.table[index] = new Element(key, value)
      this.length++
      return true
    }

    index = (index + 1) % HASH_SIZE // 해당 index가 비어있지 않은 경우, index에 +1 증가
  } while(index !== startIndex) // 처음 위치로 돌아오는 경우에는 false (공간이 없다는 뜻)

  return false
}
👉🏻 코드 설명은 주석 처리 확인
✅ do-while문을 통해 코드를 작성하였으며, while(index !== startIndex)의 조건을 넣어 비어있는 Index의 공간을 찾을 때까지 한 바퀴 돌고, 결국 처음 넣었던 startIndex와 같아진다면 while문을 빠져나오도록 구현하였다.
(즉, 해당 key의 index 값이 이미 담겨져있는 공간이라면 index + 1을 하여 while문을 돌아 빈 공간을 찾는 코드)

 

 

❗️출력해보자

// LinearHashTable 생성
let lht2 = new LinearHashTable()

lht2.put('Ana', 172)
lht2.put('John', 179)
lht2.put('Donnie', 183)
lht2.put('Mindy', 190)
/*
key : Ana -> index : 2
key : John -> index : 4
key : Donnie -> index : 0
key : Mindy -> index : 3
*/

console.log(lht2)
/*

LinearHashTable {
  table: [
    Element { key: 'Donnie', value: 183 },
    <1 empty item>,
    Element { key: 'Ana', value: 172 },
    Element { key: 'Mindy', value: 190 },
    Element { key: 'John', value: 179 }
  ],
  length: 4
}
*/
👉🏻 현재 1번째 자리만 비워진 상태로 총 4개의 데이터가 추가된 것을 확인할 수 있다.
만약 여기서 충돌되는 데이터를 추가한다면?

 

 

❗️Paul 데이터 추가

console.log(lht2.put('Paul', 168))
/*
key : Paul -> index : 2 // Ana와 index가 겹침
true // 비어있는 index를 찾아 아래 주석과 같이 index : 1에 들어간 것을 확인
*/


console.log(lht2)
/*
LinearHashTable {
  table: [
    Element { key: 'Donnie', value: 183 },
    Element { key: 'Paul', value: 168 },
    Element { key: 'Ana', value: 172 },
    Element { key: 'Mindy', value: 190 },
    Element { key: 'John', value: 179 }
  ],
  length: 5
}
*/


console.log(lht2.put('Sue', 163))
/*
key : Sue -> index : 1
false // index가 1이 나왔지만, 이미 공간이 차있기 때문에 추가되지 못하고 false
*/
👉🏻 주석 처리 확인
✅ 새로 추가한 Paul 데이터가 Ana와 충돌되었지만, put() 메서드의 do-while문을 통해 비어있는 index를 찾을 때까지 index + 1을 하여, 현재 비어있던 첫 번째 자리에 데이터가 추가된 것을 확인할 수 있다.
❗️But 이후 추가한 Sue의 index 값이 1이지만, 이미 공간이 꽉 찼기 때문에 false를 리턴하고 데이터가 추가되지 못한 것을 확인할 수 있다.

 

 

🥹 포스팅이 길어져, 다음 포스팅에서 데이터를 조회하는 get() 메서드와 데이터를 삭제하는 remove() 메서드를 구현하고, 출력해 보도록 하겠다. (해시.. 해쉬.. 해시.. 감자..)