Skip to content

第15章 哈希表理论

学习目标

  • 掌握哈希表的形式化定义与数学基础
  • 理解哈希函数的设计原理与性质
  • 深入分析冲突解决策略的理论基础
  • 掌握通用哈希与完美哈希理论
  • 理解Python字典的CPython实现机制

15.1 哈希表的形式化定义

15.1.1 基本概念

定义 15.1(哈希函数) 设 $U$ 为全键域,$M = {0, 1, \ldots, m-1}$ 为地址空间。哈希函数 $h: U \rightarrow M$ 将任意键映射到有限地址空间。

定义 15.2(哈希表) 哈希表是一个三元组 $HT = (h, T, \sigma)$,其中:

  • $h: U \rightarrow M$ 是哈希函数
  • $T = (T_0, T_1, \ldots, T_{m-1})$ 是桶数组
  • $\sigma: M \rightarrow \text{Strategy}$ 是冲突解决策略

定义 15.3(冲突) 对于键 $k_1, k_2 \in U$,若 $k_1 \neq k_2$ 且 $h(k_1) = h(k_2)$,则称 $k_1$ 与 $k_2$ 发生冲突。

15.1.2 哈希函数的性质

定义 15.4(理想哈希函数) 哈希函数 $h$ 称为理想的,当且仅当:

$$P(h(k) = i) = \frac{1}{m}, \quad \forall k \in U, \forall i \in M$$

定义 15.5(简单均匀哈希假设) 简单均匀哈希假设(SUHA)断言:每个键等概率地独立映射到任意桶。

定理 15.1(冲突概率下界) 在SUHA下,向容量为 $m$ 的哈希表插入 $n$ 个键后,至少发生一次冲突的概率为:

$$P(\text{collision}) = 1 - \frac{m!}{m^n \cdot (m-n)!} \geq 1 - e^{-n(n-1)/(2m)}$$

证明:计算不发生冲突的概率。第一个键可放入任意桶,概率为 $1$。第二个键需放入剩余 $m-1$ 个桶,概率为 $(m-1)/m$。依此类推:

$$P(\text{no collision}) = \prod_{i=0}^{n-1} \frac{m-i}{m} = \frac{m!}{m^n(m-n)!}$$

利用不等式 $1-x \leq e^{-x}$:

$$P(\text{no collision}) = \prod_{i=0}^{n-1} \left(1 - \frac{i}{m}\right) \leq \prod_{i=0}^{n-1} e^{-i/m} = e^{-\sum_{i=0}^{n-1} i/m} = e^{-n(n-1)/(2m)}$$

因此 $P(\text{collision}) = 1 - P(\text{no collision}) \geq 1 - e^{-n(n-1)/(2m)}$。 ∎

推论 15.1(生日悖论) 当 $n \approx \sqrt{2m \ln 2} \approx 1.177\sqrt{m}$ 时,冲突概率超过 $50%$。对于 $m = 365$,$n \approx 23$。


15.2 哈希函数设计理论

15.2.1 哈希函数的评估准则

定义 15.6(雪崩效应) 哈希函数 $h$ 满足雪崩效应,若输入的微小变化导致输出的显著变化:

$$\forall k, k': \text{Ham}(k, k') = 1 \Rightarrow E[\text{Ham}(h(k), h(k'))] \approx \frac{m}{2}$$

其中 $\text{Ham}$ 表示汉明距离。

定义 15.7(扩散性) 哈希函数 $h$ 具有良好扩散性,若:

$$\forall i \in M: |h^{-1}(i)| \approx \frac{|U|}{m}$$

15.2.2 除法哈希法

定义 15.8(除法哈希) 除法哈希函数定义为:

$$h(k) = k \mod m$$

定理 15.2 当 $m$ 为质数且远离 $2^p$ 的幂时,除法哈希具有较好的分布特性。

证明:设 $m = 2^p$,则 $h(k)$ 仅取决于 $k$ 的低 $p$ 位。若键具有周期性模式,将导致聚集。选择质数可避免此问题。 ∎

15.2.3 乘法哈希法

定义 15.9(乘法哈希) 乘法哈希函数定义为:

$$h(k) = \lfloor m \cdot (k \cdot A \mod 1) \rfloor$$

其中 $A \in (0, 1)$ 为常数,Knuth建议 $A = (\sqrt{5}-1)/2 \approx 0.6180339887$。

定理 15.3(黄金分割最优性) 当 $A = \phi^{-1} = (\sqrt{5}-1)/2$ 时,乘法哈希对连续键序列具有最优扩散性。

证明:黄金分割比 $\phi = (1+\sqrt{5})/2$ 满足 $\phi^2 = \phi + 1$。对于连续键 $k, k+1, \ldots$,哈希值序列形成低差异序列,最大化空间填充效率。 ∎

15.2.4 多项式滚动哈希

定义 15.10(多项式哈希) 对于字符串 $s = s_0 s_1 \ldots s_{n-1}$:

$$h(s) = \sum_{i=0}^{n-1} s_i \cdot p^{n-1-i} \mod m$$

其中 $p$ 为基数(通常取质数如 $31, 37, 53$)。

python
def polynomial_hash(s: str, base: int = 31, mod: int = 2**61 - 1) -> int:
    h = 0
    for c in s:
        h = (h * base + ord(c)) % mod
    return h

15.3 冲突解决策略理论

15.3.1 链地址法

定义 15.11(链地址法) 每个桶存储一个链表(或其他容器),所有哈希到同一桶的元素存入该链表。

定理 15.4(链地址法期望复杂度) 在SUHA下,负载因子为 $\alpha = n/m$ 时:

  • 成功查找期望比较次数:$1 + \alpha/2$
  • 失败查找期望比较次数:$\alpha$
  • 插入期望比较次数:$\alpha$

证明:设 $n_j$ 为桶 $j$ 的元素数。成功查找键 $k$ 时,需遍历桶 $h(k)$ 中的元素。期望比较次数为:

$$E[C_{\text{success}}] = \frac{1}{n}\sum_{k \in HT} \text{position}(k) = \frac{1}{n}\sum_{j=0}^{m-1} \frac{n_j(n_j+1)}{2}$$

由SUHA,$E[n_j] = \alpha$,故:

$$E[C_{\text{success}}] = 1 + \frac{\alpha}{2}$$

失败查找需遍历整个桶,期望长度为 $\alpha$。 ∎

定义 15.12(负载因子) 负载因子定义为:

$$\alpha = \frac{n}{m}$$

其中 $n$ 为元素数,$m$ 为桶数。

15.3.2 开放寻址法

定义 15.13(开放寻址法) 所有元素直接存储在桶数组中。冲突时按探查序列寻找空桶。

定义 15.14(探查序列) 探查序列是函数 $p: U \times {0, 1, \ldots, m-1} \rightarrow M$,满足:

  • $p(k, 0), p(k, 1), \ldots, p(k, m-1)$ 是 $0, 1, \ldots, m-1$ 的排列
  • $p(k, i)$ 表示键 $k$ 的第 $i$ 次探查位置

线性探查

$$p(k, i) = (h(k) + i) \mod m$$

二次探查

$$p(k, i) = (h(k) + c_1 i + c_2 i^2) \mod m$$

双重哈希

$$p(k, i) = (h_1(k) + i \cdot h_2(k)) \mod m$$

定理 15.5(线性探查聚集效应) 线性探查导致主聚集,连续被占桶形成"块",期望块长度随负载因子指数增长。

证明:设 $q_i$ 为从位置 $i$ 开始的连续被占桶数。当插入新元素时,若哈希到块内任意位置,都将延长该块。块的"吸引力"与其长度成正比,形成正反馈。 ∎

定理 15.6(开放寻址期望探查次数) 在均匀哈希假设下,负载因子 $\alpha = n/m < 1$ 时:

  • 失败查找期望探查次数:$\frac{1}{1-\alpha}$
  • 成功查找期望探查次数:$\frac{1}{\alpha}\ln\frac{1}{1-\alpha}$

证明:失败查找时,每次探查成功的概率为 $(1-\alpha)$。期望探查次数服从几何分布:

$$E[\text{probes}] = \sum_{i=1}^{\infty} i \cdot \alpha^{i-1}(1-\alpha) = \frac{1}{1-\alpha}$$

成功查找时,需考虑元素插入时的表状态。设元素在第 $j$ 次插入,当时负载因子为 $j/m$:

$$E[\text{probes}] = \frac{1}{n}\sum_{j=0}^{n-1} \frac{1}{1-j/m} \approx \frac{1}{\alpha}\ln\frac{1}{1-\alpha}$$


15.4 通用哈希理论

15.4.1 通用哈希函数族

定义 15.15(通用哈希函数族) 哈希函数族 $\mathcal{H} \subseteq {h: U \rightarrow M}$ 称为通用的,若:

$$\forall k_1 \neq k_2 \in U: |{h \in \mathcal{H}: h(k_1) = h(k_2)}| \leq \frac{|\mathcal{H}|}{m}$$

定理 15.7(通用哈希冲突概率) 从通用哈希函数族中随机选取 $h$,对于任意 $k_1 \neq k_2$:

$$P(h(k_1) = h(k_2)) \leq \frac{1}{m}$$

证明:由定义15.15:

$$P(h(k_1) = h(k_2)) = \frac{|{h \in \mathcal{H}: h(k_1) = h(k_2)}|}{|\mathcal{H}|} \leq \frac{1}{m}$$

15.4.2 构造通用哈希函数族

定理 15.8(Carter-Wegman构造) 设 $p$ 为质数,$p \geq |U|$。定义:

$$\mathcal{H} = {h_{a,b}: h_{a,b}(k) = ((ak + b) \mod p) \mod m, ; a \in {1, \ldots, p-1}, b \in {0, \ldots, p-1}}$$

则 $\mathcal{H}$ 是通用哈希函数族。

证明:对于 $k_1 \neq k_2$,设 $r = (ak_1 + b) \mod p$,$s = (ak_2 + b) \mod p$。

由于 $a \neq 0$ 且 $k_1 \neq k_2$,有 $r \neq s$。

对于固定的 $k_1, k_2$,当 $a, b$ 均匀选取时,$(r, s)$ 在 ${(x, y) \in \mathbb{Z}_p^2 : x \neq y}$ 上均匀分布。

冲突条件为 $r \equiv s \pmod{m}$,即 $r - s \equiv 0 \pmod{m}$。

满足此条件的 $(r, s)$ 对数为 $p(p-1)/m$,故冲突概率为:

$$\frac{p(p-1)/m}{p(p-1)} = \frac{1}{m}$$

python
import random
from typing import Callable, TypeVar

K = TypeVar('K')

class UniversalHash:
    def __init__(self, m: int, p: int = 2**61 - 1):
        self.m = m
        self.p = p
        self.a = random.randint(1, p - 1)
        self.b = random.randint(0, p - 1)
    
    def __call__(self, k: K) -> int:
        return ((self.a * hash(k) + self.b) % self.p) % self.m
    
    @staticmethod
    def create_family(m: int, p: int = 2**61 - 1) -> Callable[[], 'UniversalHash']:
        return lambda: UniversalHash(m, p)

15.4.3 强通用哈希

定义 15.16(强通用哈希函数族) 哈希函数族 $\mathcal{H}$ 称为强通用(2-通用)的,若:

$$\forall k_1 \neq k_2, \forall i, j \in M: P(h(k_1) = i \land h(k_2) = j) = \frac{1}{m^2}$$

定理 15.9 强通用哈希函数族满足成对独立性:任意两个不同键的哈希值独立。


15.5 完美哈希

15.5.1 静态完美哈希

定义 15.17(完美哈希函数) 哈希函数 $h: S \rightarrow M$ 称为完美的,若 $|S| = |M|$ 且 $h$ 在 $S$ 上无冲突。

定理 15.10(Fredman-Komlós-Szemerédi定理) 对于任意 $n$ 元键集 $S$,存在 $O(n)$ 空间、$O(1)$ 最坏情况查询时间的完美哈希方案,构造时间为 $O(n)$ 期望。

FKS方案

  1. 第一层:使用通用哈希 $h_1: S \rightarrow {0, \ldots, m_1-1}$,$m_1 = O(n)$
  2. 第二层:对每个桶 $i$,若桶大小为 $n_i$,使用完美哈希 $h_{2,i}$ 映射到 $m_{2,i} = O(n_i^2)$ 空间

定理 15.11(FKS空间复杂度) FKS方案期望空间为 $O(n)$。

证明:第二层总空间为:

$$E\left[\sum_{i=0}^{m_1-1} m_{2,i}\right] = E\left[\sum_{i=0}^{m_1-1} c \cdot n_i^2\right] = c \cdot E\left[\sum_{i=0}^{m_1-1} n_i^2\right]$$

由通用哈希性质:

$$E\left[\sum_{i=0}^{m_1-1} n_i^2\right] = n + \sum_{k_1 \neq k_2} P(h_1(k_1) = h_1(k_2)) \leq n + \binom{n}{2} \cdot \frac{1}{m_1} = O(n)$$

当 $m_1 = n$ 时,期望空间为 $O(n)$。 ∎

15.5.2 最小完美哈希

定义 15.18(最小完美哈希) 完美哈希函数 $h: S \rightarrow {0, 1, \ldots, n-1}$ 称为最小的,若值域恰好为 $n$。

python
from typing import List, Dict, Optional
import random

class MinimalPerfectHash:
    def __init__(self, keys: List[str]):
        self.n = len(keys)
        self._build(keys)
    
    def _build(self, keys: List[str]) -> None:
        m1 = max(self.n, 1)
        while True:
            self.h1 = self._pick_hash(m1)
            buckets: Dict[int, List[str]] = {}
            for k in keys:
                b = self.h1(k)
                if b not in buckets:
                    buckets[b] = []
                buckets[b].append(k)
            
            self.h2 = {}
            self.g = {}
            used = set()
            success = True
            
            for b in sorted(buckets.keys(), key=lambda x: -len(buckets[x])):
                bucket_keys = buckets[b]
                m2 = len(bucket_keys) ** 2
                found = False
                for _ in range(100):
                    h2_func = self._pick_hash(m2)
                    slots = [h2_func(k) for k in bucket_keys]
                    if all((b * m2 + s) % self.n not in used for s in slots):
                        self.h2[b] = h2_func
                        for k, s in zip(bucket_keys, slots):
                            self.g[k] = (b, s)
                            used.add((b * m2 + s) % self.n)
                        found = True
                        break
                if not found:
                    success = False
                    break
            
            if success:
                break
    
    def _pick_hash(self, m: int) -> callable:
        a = random.randint(1, 2**31 - 1)
        b = random.randint(0, 2**31 - 1)
        return lambda k: ((a * hash(k) + b) % (2**31 - 1)) % m
    
    def __call__(self, key: str) -> int:
        b, s = self.g.get(key, (0, 0))
        h2_func = self.h2.get(b, lambda k: 0)
        return (b * len(self.h2.get(b, [key]))**2 + h2_func(key)) % self.n

15.6 Cuckoo哈希

15.6.1 基本原理

定义 15.19(Cuckoo哈希) 使用两个哈希函数 $h_1, h_2$,每个键可存储在 $h_1(k)$ 或 $h_2(k)$ 两个位置之一。查询最多检查两个位置。

定理 15.12(Cuckoo哈希查询复杂度) Cuckoo哈希的最坏情况查询时间为 $O(1)$,仅需两次探查。

定义 15.20(Cuckoo图) 对于键集 $S$,Cuckoo图 $G = (V, E)$ 定义为:

  • $V = {0, 1, \ldots, 2m-1}$(两个表的桶)
  • $E = {(h_1(k), h_2(k)) : k \in S}$

定理 15.13(Cuckoo哈希可行性) 当且仅当Cuckoo图无环时,存在有效放置方案。

证明:若Cuckoo图有环,则环上的键无法同时满足各自的位置约束。若Cuckoo图无环(森林),可通过拓扑排序确定放置顺序。 ∎

15.6.2 插入操作

python
from typing import Generic, TypeVar, Optional, List, Tuple
import random

K = TypeVar('K')
V = TypeVar('V')

class CuckooHashMap(Generic[K, V]):
    def __init__(self, capacity: int = 16, max_displacements: int = 500):
        self._capacity = capacity
        self._size = 0
        self._table1: List[Optional[Tuple[K, V]]] = [None] * capacity
        self._table2: List[Optional[Tuple[K, V]]] = [None] * capacity
        self._max_displacements = max_displacements
        self._seed1 = random.randint(1, 2**31 - 1)
        self._seed2 = random.randint(1, 2**31 - 1)
    
    def _h1(self, key: K) -> int:
        return hash((key, self._seed1)) % self._capacity
    
    def _h2(self, key: K) -> int:
        return hash((key, self._seed2)) % self._capacity
    
    def get(self, key: K) -> Optional[V]:
        i1 = self._h1(key)
        if self._table1[i1] and self._table1[i1][0] == key:
            return self._table1[i1][1]
        
        i2 = self._h2(key)
        if self._table2[i2] and self._table2[i2][0] == key:
            return self._table2[i2][1]
        
        return None
    
    def put(self, key: K, value: V) -> bool:
        if self.get(key) is not None:
            i1 = self._h1(key)
            if self._table1[i1] and self._table1[i1][0] == key:
                self._table1[i1] = (key, value)
            else:
                i2 = self._h2(key)
                self._table2[i2] = (key, value)
            return True
        
        return self._insert((key, value), 0)
    
    def _insert(self, pair: Tuple[K, V], table: int) -> bool:
        for _ in range(self._max_displacements):
            if table == 0:
                idx = self._h1(pair[0])
            else:
                idx = self._h2(pair[0])
            
            if table == 0:
                if self._table1[idx] is None:
                    self._table1[idx] = pair
                    self._size += 1
                    return True
                evicted = self._table1[idx]
                self._table1[idx] = pair
            else:
                if self._table2[idx] is None:
                    self._table2[idx] = pair
                    self._size += 1
                    return True
                evicted = self._table2[idx]
                self._table2[idx] = pair
            
            pair = evicted
            table = 1 - table
        
        self._rehash()
        return self._insert(pair, 0)
    
    def _rehash(self) -> None:
        old_table1 = self._table1
        old_table2 = self._table2
        self._capacity *= 2
        self._table1 = [None] * self._capacity
        self._table2 = [None] * self._capacity
        self._seed1 = random.randint(1, 2**31 - 1)
        self._seed2 = random.randint(1, 2**31 - 1)
        self._size = 0
        
        for pair in old_table1:
            if pair:
                self.put(pair[0], pair[1])
        for pair in old_table2:
            if pair:
                self.put(pair[0], pair[1])
    
    def remove(self, key: K) -> bool:
        i1 = self._h1(key)
        if self._table1[i1] and self._table1[i1][0] == key:
            self._table1[i1] = None
            self._size -= 1
            return True
        
        i2 = self._h2(key)
        if self._table2[i2] and self._table2[i2][0] == key:
            self._table2[i2] = None
            self._size -= 1
            return True
        
        return False

15.7 哈希表ADT规范

15.7.1 代数规范

ADT HashMap[K, V]
    imports Boolean, Integer, K, V, List[Pair[K, V]]
    
    sorts HashMap
    
    operations
        empty    :                        → HashMap
        put      : HashMap × K × V       → HashMap
        get      : HashMap × K           → V ∪ {None}
        remove   : HashMap × K           → HashMap
        contains : HashMap × K           → Boolean
        size     : HashMap               → Integer
        keys     : HashMap               → Set[K]
        values   : HashMap               → List[V]
    
    axioms
        ∀ m: HashMap, k, k': K, v, v': V
        
        [G1] get(empty, k) = None
        
        [G2] get(put(m, k, v), k) = v
        
        [G3] k ≠ k' ⇒ get(put(m, k, v), k') = get(m, k')
        
        [G4] put(put(m, k, v), k, v') = put(m, k, v')
        
        [R1] remove(empty, k) = empty
        
        [R2] remove(put(m, k, v), k) = remove(m, k)
        
        [R3] k ≠ k' ⇒ remove(put(m, k, v), k') = put(remove(m, k'), k, v)
        
        [C1] contains(empty, k) = false
        
        [C2] contains(put(m, k, v), k) = true
        
        [C3] k ≠ k' ⇒ contains(put(m, k, v), k') = contains(m, k')
        
        [S1] size(empty) = 0
        
        [S2] contains(m, k) ⇒ size(put(m, k, v)) = size(m)
        
        [S3] ¬contains(m, k) ⇒ size(put(m, k, v)) = size(m) + 1
end ADT

15.8 完整哈希表实现

15.8.1 基于链地址法的哈希表

python
from typing import TypeVar, Generic, Optional, List, Tuple, Iterator, Set
from collections import defaultdict
import math

K = TypeVar('K')
V = TypeVar('V')

class HashMap(Generic[K, V]):
    def __init__(self, capacity: int = 16, load_factor: float = 0.75):
        self._capacity = self._next_power_of_two(capacity)
        self._size = 0
        self._buckets: List[List[Tuple[K, V]]] = [[] for _ in range(self._capacity)]
        self._load_factor = load_factor
        self._mod_count = 0
    
    @staticmethod
    def _next_power_of_two(n: int) -> int:
        if n <= 0:
            return 1
        return 1 << (n - 1).bit_length()
    
    def _hash(self, key: K) -> int:
        h = hash(key)
        h ^= (h >> 16)
        return h & (self._capacity - 1)
    
    def _resize(self) -> None:
        old_buckets = self._buckets
        self._capacity *= 2
        self._buckets = [[] for _ in range(self._capacity)]
        self._size = 0
        
        for bucket in old_buckets:
            for key, value in bucket:
                self._put_internal(key, value)
        
        self._mod_count += 1
    
    def _put_internal(self, key: K, value: V) -> bool:
        index = self._hash(key)
        bucket = self._buckets[index]
        
        for i, (k, _) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)
                return False
        
        bucket.append((key, value))
        self._size += 1
        return True
    
    def put(self, key: K, value: V) -> Optional[V]:
        if self._size >= self._capacity * self._load_factor:
            self._resize()
        
        index = self._hash(key)
        bucket = self._buckets[index]
        
        for i, (k, v) in enumerate(bucket):
            if k == key:
                old_value = v
                bucket[i] = (key, value)
                return old_value
        
        bucket.append((key, value))
        self._size += 1
        self._mod_count += 1
        return None
    
    def get(self, key: K, default: Optional[V] = None) -> Optional[V]:
        index = self._hash(key)
        bucket = self._buckets[index]
        
        for k, v in bucket:
            if k == key:
                return v
        
        return default
    
    def remove(self, key: K) -> Optional[V]:
        index = self._hash(key)
        bucket = self._buckets[index]
        
        for i, (k, v) in enumerate(bucket):
            if k == key:
                del bucket[i]
                self._size -= 1
                self._mod_count += 1
                return v
        
        return None
    
    def contains(self, key: K) -> bool:
        return self.get(key) is not None
    
    def __getitem__(self, key: K) -> V:
        result = self.get(key)
        if result is None:
            raise KeyError(key)
        return result
    
    def __setitem__(self, key: K, value: V) -> None:
        self.put(key, value)
    
    def __delitem__(self, key: K) -> None:
        if self.remove(key) is None:
            raise KeyError(key)
    
    def __contains__(self, key: K) -> bool:
        return self.contains(key)
    
    def __len__(self) -> int:
        return self._size
    
    def __iter__(self) -> Iterator[K]:
        for bucket in self._buckets:
            for key, _ in bucket:
                yield key
    
    def items(self) -> Iterator[Tuple[K, V]]:
        for bucket in self._buckets:
            for pair in bucket:
                yield pair
    
    def keys(self) -> Set[K]:
        return set(self)
    
    def values(self) -> List[V]:
        return [v for _, v in self.items()]
    
    def clear(self) -> None:
        self._buckets = [[] for _ in range(self._capacity)]
        self._size = 0
        self._mod_count += 1
    
    @property
    def load_factor(self) -> float:
        return self._size / self._capacity
    
    def statistics(self) -> dict:
        lengths = [len(b) for b in self._buckets]
        non_empty = [l for l in lengths if l > 0]
        
        return {
            'capacity': self._capacity,
            'size': self._size,
            'load_factor': self.load_factor,
            'empty_buckets': lengths.count(0),
            'max_chain_length': max(lengths) if lengths else 0,
            'avg_chain_length': sum(non_empty) / len(non_empty) if non_empty else 0,
        }

15.8.2 基于开放寻址法的哈希表

python
from typing import TypeVar, Generic, Optional, List, Tuple, Iterator
from enum import Enum, auto

K = TypeVar('K')
V = TypeVar('V')

class EntryState(Enum):
    EMPTY = auto()
    OCCUPIED = auto()
    DELETED = auto()

class OpenAddressingHashMap(Generic[K, V]):
    def __init__(self, capacity: int = 16, load_factor: float = 0.5):
        self._capacity = self._next_power_of_two(capacity)
        self._size = 0
        self._deleted = 0
        self._entries: List[Tuple[Optional[K], Optional[V], EntryState]] = [
            (None, None, EntryState.EMPTY) for _ in range(self._capacity)
        ]
        self._load_factor = load_factor
    
    @staticmethod
    def _next_power_of_two(n: int) -> int:
        if n <= 0:
            return 1
        return 1 << (n - 1).bit_length()
    
    def _hash(self, key: K) -> int:
        h = hash(key)
        h ^= (h >> 16)
        return h & (self._capacity - 1)
    
    def _probe(self, key: K, i: int) -> int:
        return (self._hash(key) + i) & (self._capacity - 1)
    
    def _find_slot(self, key: K) -> Tuple[int, bool]:
        first_deleted = -1
        
        for i in range(self._capacity):
            index = self._probe(key, i)
            entry = self._entries[index]
            
            if entry[2] == EntryState.EMPTY:
                if first_deleted >= 0:
                    return first_deleted, False
                return index, False
            
            if entry[2] == EntryState.DELETED:
                if first_deleted < 0:
                    first_deleted = index
            elif entry[0] == key:
                return index, True
        
        if first_deleted >= 0:
            return first_deleted, False
        raise RuntimeError("Hash table is full")
    
    def _resize(self) -> None:
        old_entries = self._entries
        self._capacity *= 2
        self._entries = [(None, None, EntryState.EMPTY) for _ in range(self._capacity)]
        self._size = 0
        self._deleted = 0
        
        for key, value, state in old_entries:
            if state == EntryState.OCCUPIED:
                self.put(key, value)
    
    def put(self, key: K, value: V) -> Optional[V]:
        if (self._size + self._deleted) >= self._capacity * self._load_factor:
            self._resize()
        
        index, found = self._find_slot(key)
        
        if found:
            old_value = self._entries[index][1]
            self._entries[index] = (key, value, EntryState.OCCUPIED)
            return old_value
        
        if self._entries[index][2] == EntryState.DELETED:
            self._deleted -= 1
        
        self._entries[index] = (key, value, EntryState.OCCUPIED)
        self._size += 1
        return None
    
    def get(self, key: K, default: Optional[V] = None) -> Optional[V]:
        for i in range(self._capacity):
            index = self._probe(key, i)
            entry = self._entries[index]
            
            if entry[2] == EntryState.EMPTY:
                return default
            
            if entry[2] == EntryState.OCCUPIED and entry[0] == key:
                return entry[1]
        
        return default
    
    def remove(self, key: K) -> Optional[V]:
        for i in range(self._capacity):
            index = self._probe(key, i)
            entry = self._entries[index]
            
            if entry[2] == EntryState.EMPTY:
                return None
            
            if entry[2] == EntryState.OCCUPIED and entry[0] == key:
                value = entry[1]
                self._entries[index] = (None, None, EntryState.DELETED)
                self._size -= 1
                self._deleted += 1
                return value
        
        return None
    
    def __len__(self) -> int:
        return self._size
    
    def __contains__(self, key: K) -> bool:
        return self.get(key) is not None
    
    def __getitem__(self, key: K) -> V:
        result = self.get(key)
        if result is None:
            raise KeyError(key)
        return result
    
    def __setitem__(self, key: K, value: V) -> None:
        self.put(key, value)

15.9 Python字典内部实现

15.9.1 CPython字典结构

CPython使用开放寻址法与伪随机探查。字典条目结构:

c
typedef struct {
    Py_hash_t me_hash;
    PyObject *me_key;
    PyObject *me_value;
} PyDictKeyEntry;

探查序列

$$p(i) = p(0) + i \cdot \text{perturb} \mod 2^k$$

其中 $\text{perturb}$ 初始为哈希值,每次迭代右移5位:

python
def cpython_probe(hash_value: int, capacity: int) -> Iterator[int]:
    mask = capacity - 1
    perturb = hash_value
    i = hash_value & mask
    
    while True:
        yield i
        i = (i * 5 + perturb + 1) & mask
        perturb >>= 5

15.9.2 字典扩容策略

定理 15.14(CPython字典扩容) CPython字典在以下情况扩容:

  • 插入新键时,若使用率超过 $2/3$
  • 删除大量键后,若使用率低于 $1/6$

扩容时容量增长约2倍,选择最近的 $2^n$。


15.10 复杂度分析总结

操作平均情况最坏情况
插入$O(1)$$O(n)$
查找$O(1)$$O(n)$
删除$O(1)$$O(n)$
遍历$O(n)$$O(n)$

定理 15.15(哈希表摊还复杂度) 在SUHA下,哈希表操作的摊还时间复杂度为 $O(1)$。

证明:使用势能法。设势能函数 $\Phi = 2n - m$($n$ 元素数,$m$ 桶数)。

  • 插入:若不扩容,实际代价 $O(1)$,势能增加 $2$,摊还代价 $O(1)$
  • 扩容:实际代价 $O(n)$,但势能从正变负,摊还代价 $O(1)$

因此所有操作摊还代价 $O(1)$。 ∎


15.11 本章小结

本章系统介绍了哈希表理论:

  1. 形式化基础:哈希函数、冲突概率的数学分析
  2. 哈希函数设计:除法哈希、乘法哈希、多项式哈希
  3. 冲突解决策略:链地址法、开放寻址法的理论分析
  4. 高级技术:通用哈希、完美哈希、Cuckoo哈希
  5. 实现技术:ADT规范、完整实现、CPython内部机制

参考文献

  1. Knuth, D. E. (1998). The Art of Computer Programming, Volume 3: Sorting and Searching. Addison-Wesley.
  2. Cormen, T. H., et al. (2009). Introduction to Algorithms, 3rd ed. MIT Press.
  3. Carter, J. L., & Wegman, M. N. (1979). Universal classes of hash functions. Journal of Computer and System Sciences, 18(2), 143-154.
  4. Fredman, M. L., Komlós, J., & Szemerédi, E. (1984). Storing a sparse table with O(1) worst case access time. Journal of the ACM, 31(3), 538-544.
  5. Pagh, R., & Rodler, F. F. (2004). Cuckoo hashing. Journal of Algorithms, 51(2), 122-144.

下一章:第16章 图基础

Python技术丛书 - 江苏省宿城中等专业学校