Skip to content

哈希表:开放定址法

为什么需要哈希表:开放定址法

哈希表通过哈希函数将关键字映射到存储位置,理想情况下可以 O(1) 时间完成查找。但不同关键字可能映射到同一位置,产生冲突(碰撞)

开放定址法是解决冲突的一种方式:当发生冲突时,按照某种探测序列在哈希表中寻找下一个空闲位置,将元素直接存入表中。与拉链法不同,开放定址法不需要额外的链表空间,所有元素都存储在哈希表本身中。

408 考研中,开放定址法是散列表章节的核心考点,尤其是线性探测法的 ASL 计算几乎年年出现。

核心思想

开放定址法的通用公式:

H_i = (H(key) + d_i) % m,  i = 1, 2, 3, ...

其中 H(key) 是哈希函数,m 是表长,d_i 是第 i 次探测的增量,不同的 d_i 取法对应不同的探测方法:

  • 线性探测法:d_i = 1, 2, 3, ...
  • 二次探测法:d_i = 1², -1², 2², -2², ...
  • 双散列法:d_i = i × H₂(key)

核心数据结构定义:

c
#define EMPTY   -1   // 空位标记
#define DELETED -2   // 已删除标记(墓碑)

typedef struct {
    int *data;       // 存储数组
    int  size;       // 表长 m
    int  count;      // 当前元素个数
} HashTable;

交互可视化

通过下方的交互动画,你可以逐步观察哈希表:开放定址法的执行过程:

加载可视化中...

操作详解

线性探测法

当发生冲突时,依次探测下一个位置,增量序列为 d = 1, 2, 3, ...

c
// 线性探测法——插入
int linearInsert(HashTable *ht, int key) {
    int pos = key % ht->size;          // 初始哈希地址
    int i = 0;
    while (i < ht->size) {
        int cur = (pos + i) % ht->size;
        if (ht->data[cur] == EMPTY || ht->data[cur] == DELETED) {
            ht->data[cur] = key;
            ht->count++;
            return cur;                // 插入成功,返回位置
        }
        i++;
    }
    return -1;                         // 表满,插入失败
}

// 线性探测法——查找
int linearSearch(HashTable *ht, int key) {
    int pos = key % ht->size;
    int i = 0;
    while (i < ht->size) {
        int cur = (pos + i) % ht->size;
        if (ht->data[cur] == EMPTY)
            return -1;                 // 遇到空位,查找失败
        if (ht->data[cur] == key)
            return cur;                // 查找成功
        i++;                           // 遇到 DELETED 或其他值,继续探测
    }
    return -1;
}

举例:哈希表长 m = 11,H(key) = key % 11,依次插入 19, 1, 23, 14, 55:

关键字H(key)探测序列最终位置
19888
1111
2311→22
14333
55000

堆积问题(聚集/Clustering):线性探测法的最大缺陷。同义词(哈希值相同的关键字)和非同义词都可能争夺同一段连续区域,导致大量元素聚集在一起,探测次数急剧增加,查找效率下降。

二次探测法

增量序列为 d = 1², -1², 2², -2², 3², -3², ...,即交替探测哈希地址两侧的位置,可以有效缓解堆积问题

c
// 二次探测法——查找
int quadraticSearch(HashTable *ht, int key) {
    int pos = key % ht->size;
    int i = 0;
    while (i <= ht->size / 2) {
        // 正方向探测 +i²
        int cur = (pos + i * i) % ht->size;
        if (ht->data[cur] == EMPTY) return -1;
        if (ht->data[cur] == key)  return cur;
        // 负方向探测 -i²
        cur = ((pos - i * i) % ht->size + ht->size) % ht->size;
        if (ht->data[cur] == EMPTY) return -1;
        if (ht->data[cur] == key)  return cur;
        i++;
    }
    return -1;
}

注意:二次探测法要求表长 m 为4k + 3 形式的素数(如 7, 11, 19, 23, 43...),才能保证探测到所有位置。

双散列法:使用第二个哈希函数计算增量,d_i = i × H₂(key)。H₂(key) 的选取要求与 m 互素,常见做法是取 H₂(key) = p - (key % p),其中 p 是小于 m 的最大素数。双散列法能更均匀地分散探测位置,聚集问题最轻。

冲突处理对比

对比项线性探测二次探测双散列拉链法
堆积问题严重(一次聚集)较轻(二次聚集)最轻
删除操作需要墓碑标记需要墓碑标记需要墓碑标记直接删除
空间利用无额外指针无额外指针无额外指针需要指针域
表长要求无特殊要求m 为 4k+3 素数需互素条件无特殊要求
适用场景表规模小、装填因子低通用通用频繁插入删除

删除问题:开放定址法不能直接删除元素。如果直接将位置置空,会导致后续探测到此位置时误认为查找失败,中断探测链。解决方案是使用墓碑标记(Tombstone / 懒删除):将被删除的位置标记为 DELETED 状态,查找时跳过,插入时可复用。

ASL 分析

**ASL(平均查找长度)**是衡量哈希表性能的关键指标,分为查找成功和查找失败两种情况。

查找成功 ASL:对表中每个元素,统计找到它所需的比较次数,再求平均值。

查找失败 ASL:对每个可能的哈希地址(0 ~ m-1),统计从该地址出发探测到空位所需的比较次数,再对所有哈希地址求平均值。

举例:m = 11,H(key) = key % 11,依次插入 19, 1, 23, 14, 55,最终哈希表状态:

下标:  0   1   2   3   4   5   6   7   8   9   10
元素:  55  1   23  14  -   -   -   -   19  -   -

查找成功 ASL = (1 + 1 + 2 + 1 + 1) / 5 = 6/5 = 1.2

查找失败 ASL:从每个哈希地址出发探测到空位的次数:

地址012345678910
比较次数54321111211

查找失败 ASL = (5 + 4 + 3 + 2 + 1 + 1 + 1 + 1 + 2 + 1 + 1) / 11 = 22/11 = 2.0

装填因子 α = 已存元素数 / 表长,α 越大冲突越多,ASL 越大。一般建议 α ≤ 0.75。

复杂度分析

操作平均时间复杂度最坏时间复杂度说明
查找O(1/(1-α))O(n)取决于装填因子 α
插入O(1/(1-α))O(n)需先查找空位
删除O(1/(1-α))O(n)懒删除,标记墓碑

空间复杂度:O(m),m 为表长,不需要额外链表空间。

考研高频考点

  • ⭐ 给定哈希函数和关键字序列,画出哈希表并计算 ASL(选择/填空/大题必考)
  • ⭐ 线性探测法查找成功与查找失败的 ASL 计算(重中之重)
  • ⭐ 开放定址法不能直接删除元素的原因及墓碑标记方案(简答题高频)
  • ⭐ 堆积问题的含义:同义词和非同义词共同引起的聚集现象(概念辨析)
  • ⭐ 开放定址法 vs 拉链法的优缺点对比(简答题/选择题)
  • 装填因子 α 对查找效率的影响(概念题)
  • 二次探测法对表长的要求(4k + 3 形式的素数)