(CS/컴퓨터공학) 해시테이블 동작 원리와 충돌 해결 - 체이닝 vs 오픈 어드레싱

✨ 개요

해시테이블(Hash Table)은 키를 해시함수로 버킷 인덱스에 매핑해 평균 O(1)로 조회/삽입/삭제를 수행하는 자료구조입니다.
핵심은 충돌(collision) 관리부하율(load factor), 그리고 재해시(rehash) 전략입니다.


1. 한 장 요약

  1. 해시: h = H(key)
  2. 인덱싱: i = h mod capacity
  3. 충돌: 다른 키가 같은 인덱스에 매핑되면 해결 전략 적용
  4. 부하율: α = size / capacity가 임계치(예: 0.75) 넘으면 리사이즈 + 재해시

좋은 해시함수(균등분포) + 적정 부하율 유지가 성능의 90%를 좌우합니다.


2 충돌 해결 전략

2.1 체이닝(Chaining)

아이디어: 각 버킷이 리스트/트리를 가짐. 같은 인덱스의 항목을 연결해 저장.

index: 0 1 2 3
bucket: [ ] -> [K3] [K1]->[K8] [ ]
| |
val val

장점

단점

2.2 오픈 어드레싱(Open Addressing)

아이디어: 버킷 하나에 하나의 원소만 저장. 충돌 시 빈 슬롯탐사(probing) 로 찾아 테이블 내부에 저장.

table: [K1] [ ] [K8] [K3] [ ] [ ]
probe: i i+1 i+2 ...

장점

단점


3 체이닝 vs 오픈 어드레싱 비교표

항목 체이닝(Chaining) 오픈 어드레싱(Open Addressing)
저장 방식 버킷마다 리스트/트리 테이블 내부 빈 슬롯 탐사
메모리 포인터/노드 오버헤드↑ 테이블만 사용(효율↑)
캐시 지역성 낮음 높음(연속 메모리)
삭제 쉬움 tombstone 필요, 재해시로 청소 필요
부하율 민감도 낮음(α ↑ 가능) 매우 높음(α 낮게 유지)
최악 보장 리스트면 O(n), 트리화 시 O(log n) O(n) 가능
구현 복잡도 낮음(트리화 시 중간↑) 중간~높음(이중 해싱/삭제 관리)
실전 사용 Java/Kotlin HashMap 일부 언어 런타임/라이브러리, 고성능 집약 구조

4. 부하율(Load Factor)과 리사이즈/재해시(Rehash)


5. 해시함수와 인덱싱 팁


6. kotlin/Java 관점 실무 팁


7. 간단 예제: 체이닝 기반(싱글 버킷 리스트)

class ChainedHashMap<K, V>(initCap: Int = 16) {
    data class Node<K, V>(val k: K, var v: V, var next: Node<K, V>? = null)
    private var cap = Integer.highestOneBit(maxOf(2, initCap - 1) shl 1)
    private var size = 0
    private var table = arrayOfNulls<Node<K, V>>(cap)
    private val loadFactor = 0.75f

    private fun index(key: K) = (key.hashCode() * -0x7f4a7c15 /*mix*/).ushr(1) and (cap - 1)

    fun put(key: K, value: V): V? {
        if (size + 1 > cap * loadFactor) resize()
        val i = index(key)
        var n = table[i]
        while (n != null) {
            if (n.k == key) { val old = n.v; n.v = value; return old }
            n = n.next
        }
        table[i] = Node(key, value, table[i]); size++
        return null
    }

    fun get(key: K): V? {
        var n = table[index(key)]
        while (n != null) { if (n.k == key) return n.v; n = n.next }
        return null
    }

    private fun resize() {
        cap = cap shl 1
        val newTab = arrayOfNulls<Node<K, V>>(cap)
        for (head in table) {
            var n = head
            while (n != null) {
                val next = n.next
                val i = index(n.k)
                n.next = newTab[i]; newTab[i] = n
                n = next
            }
        }
        table = newTab
    }
}

8. 간단 예제: 오픈 어드레싱(선형 탐사 + tombstone)

class LinearProbingMap<K, V>(initCap: Int = 16) {
private data class Entry<K, V>(var k: K?, var v: V?, var tomb: Boolean = false)
private var cap = Integer.highestOneBit(maxOf(2, initCap - 1) shl 1)
private var size = 0
private var table = Array(cap) { Entry<K, V>(null, null, false) }
private val loadFactor = 0.6f

    private fun idx(key: K) = (key.hashCode() * 0x9e3779b9).ushr(1) and (cap - 1)

    fun put(key: K, value: V): V? {
        if ((size + 1).toFloat() / cap > loadFactor) resize()
        var i = idx(key)
        var firstTomb = -1
        while (true) {
            val e = table[i]
            when {
                e.k == null && !e.tomb -> {
                    val slot = if (firstTomb >= 0) firstTomb else i
                    table[slot].k = key; table[slot].v = value; table[slot].tomb = false; size++
                    return null
                }
                e.k == null && e.tomb && firstTomb < 0 -> firstTomb = i
                e.k == key -> { val old = e.v; e.v = value; return old }
            }
            i = (i + 1) and (cap - 1)
        }
    }

    fun get(key: K): V? {
        var i = idx(key)
        while (true) {
            val e = table[i]
            if (e.k == null && !e.tomb) return null
            if (e.k == key) return e.v
            i = (i + 1) and (cap - 1)
        }
    }

    fun remove(key: K): V? {
        var i = idx(key)
        while (true) {
            val e = table[i]
            if (e.k == null && !e.tomb) return null
            if (e.k == key) {
                val old = e.v
                e.k = null; e.v = null; e.tomb = true
                size--; return old
            }
            i = (i + 1) and (cap - 1)
        }
    }

    private fun resize() {
        val old = table
        cap = cap shl 1
        table = Array(cap) { Entry<K, V>(null, null, false) }
        size = 0
        for (e in old) if (e.k != null && !e.tomb) put(e.k as K, e.v as V)
    }
}

결론



Related Posts