오늘도 더 나은 코드를 작성하였습니까?

Hash Collision(해시 충돌) Chaining(체이닝) 및 kotlin 구현 본문

자료구조

Hash Collision(해시 충돌) Chaining(체이닝) 및 kotlin 구현

hik14 2023. 11. 14. 22:29

해시 충돌

hash function이 서로 다른 두 개의 입력값에 대해 동일한 출력값을 내는 상황

Chainning (체이닝)

hashTable의 각 슬롯이 연결 리스트의 형태로 데이터를 저장하여, 동일한 출력을 값을 가지는 데이터가 들어 오더라도 같은 hash table의Idx에 저장하여 충돌을 해결한다.

새로운 데이터를 넣을때는 각 해당 슬롯의 가장 앞에 삽입한다. 그러면 항상 O(1)의 시간이 걸린다.

하지만, 검색 및 삭제는 해당 슬롯의 충돌되어 삽입된 갯수에 따라 성능이 좌우된다. 

이때, hashFunction을 C-Universal Function을 사용하면, 각 슬롯의 평균적인 연결리스트의 길이는 O(1)이라 이미 증명되어 있다.

 

class ChainingHashTable<K, V>(private var initTableSize: Int = DEFAULT_TABLE_SIZE) {

    companion object {
        private const val DEFAULT_TABLE_SIZE = 20
    }

    private var size = 0
    private var table: Array<Node<K, V>?> = Array(initTableSize) { null }

    private class Node<K, V>(val key: K, var value: V, var next: Node<K, V>? = null) {
        val hashCode = key.hashCode()

        override fun toString(): String {
            return "(key: $key value: $value)"
        }
    }

    constructor(vararg args: Pair<K, V>) : this(if (args.size > DEFAULT_TABLE_SIZE) args.size else DEFAULT_TABLE_SIZE) {

        val tableSize = if (args.size > DEFAULT_TABLE_SIZE) args.size else DEFAULT_TABLE_SIZE
        table = Array(tableSize) { null }
        args.forEach {
            put(it.first, it.second)
        }
    }

    /*
        key를 입력받아 현재 hashTable 해당하는 key값을 가진 Node가 있는지 확인합니다.
        있다면 node를 반환하고 없으면 null을 반환 합니다.
     */
    private fun findNode(key: K): Node<K, V>? {
        val idx = hashValue(key)

        var p = table[idx]

        while (p != null) {
            if (p.key == key)
                return p
            p = p.next
        }
        return p
    }

    /*
       <key, Value> 데이터를 입력받아서,
       기존에 있던 Key면 데이터를 업데이트 합니다, 없다면 새로운 데이터를 해당 idx의 Chain 가장 앞에 삽입 합니다.
    */
    fun put(key: K, value: V): K {

        val node = findNode(key)
        if (node != null && node.key == key) {
            // 값을 업데이트
            node.value = value
        } else {
            // 새로운 노드 삽입.
            table[hashValue(key)] = Node(key, value, table[hashValue(key)])
            size += 1
        }
        return key
    }

    /*
        key 값에 대응 하는 value를 리턴한다.
     */
    fun search(key: K): V? {
        val idx = hashValue(key)

        var p = table[idx]

        while (p != null) {
            if (p.key == key)
                return p.value
            p = p.next
        }
        return null
    }

    /*
        해당 key가 table에 존재한다면, key 값에 대응 하는 value를 삭제하고 key 반환.
        key가 table에 없다면, 무시한다.
     */
    fun delete(key: K) {

        val node = findNode(key) ?: return

        // 삭제할 노드의 앞선 노드를 찾는다.
        var prev = table[hashValue(key)]!!
        // 삭제할 노드가 체인의 가장 앞쪽 이라면,
        if (prev.key == node.key){
            table[hashValue(key)] = null
        }else{
            while (prev.next?.key != node.key){
                val next = prev.next ?: return
                prev = next
            }
            prev.next = node.next
            size -= 1
        }
    }

    fun hashValue(key: K) = key.hashCode() % table.size

    override fun toString(): String {
        val sb = StringBuilder()
        table.forEachIndexed { idx, chain ->
            sb.append("idx: $idx ")
            var node = chain
            while (node != null) {
                sb.append(node.toString())
                node = node.next
            }
            sb.append("\n")
        }
        return sb.toString()
    }
}