Skip to content

408

数据结构

散列表(哈希表)


定位信息

项目内容
章节第六章 查找 · 6.7
知识单元编号DS-06-07
核心概念散列函数、冲突处理(开放定址法、链地址法)、装填因子、ASL
前置知识顺序查找、模运算
考试权重★★★★(高频重点,选择题和综合题常考)

知识点讲解

一、散列表的基本概念

散列表(Hash Table)是一种基于散列函数直接计算关键字存储位置的查找结构,理论上可以达到 O(1) 的查找效率。

核心概念:

  • 散列函数 H(key): 将关键字映射到散列表的地址。理想情况下,不同关键字映射到不同地址。
  • 冲突(Collision): 当 key₁ ≠ key₂ 但 H(key₁) = H(key₂) 时,称为冲突。冲突不可避免,但可以通过好的散列函数和冲突处理策略减少。
  • 同义词(Synonym): 发生冲突的不同关键字互为同义词。
  • 装填因子 α = 已存元素数 / 表长: α 越大,冲突越多,查找效率越低。

二、常用散列函数

散列函数公式特点
除留余数法H(key) = key mod p(p ≤ m)最常用,p 取素数效果好
直接定址法H(key) = a × key + b关键字分布连续时使用
数字分析法取关键字中某几位关键字位数多且分布已知
平方取中法取 key² 的中间几位关键字各位分布不均匀时

三、冲突处理方法(重点)

(一)开放定址法(Open Addressing)

所有元素都存在散列表内,冲突时按某种探测序列寻找下一个空位。

探测方法公式特点
线性探测Hᵢ = (H(key) + i) mod m, i=1,2,...简单但易产生聚集
二次探测Hᵢ = (H(key) ± i²) mod m减少聚集,但可能遗漏空位
伪随机探测Hᵢ = (H(key) + dᵢ) mod mdᵢ 为伪随机序列

线性探测的聚集问题: 冲突元素会连成一片(一次聚集),导致后续查找的探测长度增加。

(二)链地址法(拉链法)

将所有同义词存储在同一个链表中。散列表的每个槽(bucket)指向一个链表头。

散列表 H[m]:
  [0] → NULL
  [1] → 结点A → 结点D → NULL
  [2] → 结点B → NULL
  [3] → NULL
  [4] → 结点C → 结点E → 结点F → NULL
  ...

链地址法 vs 开放定址法对比表:

对比维度链地址法开放定址法
冲突处理链表存储同义词探测下一个空位
删除操作容易(链表删除)困难(需标记删除/重建)
装填因子限制α 可 > 1α 必须 < 1
空间利用动态分配,灵活预分配,可能浪费
聚集问题线性探测有聚集
ASL(查找)1 + α/2(成功)与探测方法有关
实际应用更常用(如 Java HashMap)特定场景

四、ASL 计算公式

链地址法(等概率):

  • 查找成功:ASL = 每个元素查找次数之和 / 元素个数
  • 查找失败:ASL = 每个槽的探测次数之和 / 槽的数量(遍历到链表末尾)

开放定址法(线性探测):

  • 查找成功:对每个元素,计算从其散列位置到该元素的探测次数,取平均
  • 查找失败:对每个地址 i,计算从 i 开始到第一个空位的探测次数,取平均

五、散列表查找伪代码(C 风格——线性探测法)

c
// 散列表结构(开放定址法-线性探测)
#define EMPTY   0   // 空位
#define DELETED -1  // 已删除标记
typedef struct {
    KeyType key;
    int status;     // 0=空, 1=占用, -1=已删除
} HashEntry;

// 散列函数:除留余数法
int Hash(KeyType key, int m) {
    return key % m;                  // m 为表长
}

// 线性探测法插入
void Hash_Insert(HashEntry HT[], int m, KeyType key) {
    int addr = Hash(key, m);         // 计算初始散列地址
    int i = 0;
    while (i < m) {
        int pos = (addr + i) % m;    // 线性探测
        if (HT[pos].status != 1) {   // 找到空位或已删除位
            HT[pos].key = key;
            HT[pos].status = 1;
            return;
        }
        if (HT[pos].key == key)
            return;                   // 关键字已存在,不插入
        i++;
    }
    // 表满,插入失败
}

// 线性探测法查找
int Hash_Search(HashEntry HT[], int m, KeyType key) {
    int addr = Hash(key, m);         // 计算初始散列地址
    int i = 0;
    while (i < m) {
        int pos = (addr + i) % m;
        if (HT[pos].status == 0)
            return -1;                // 遇到空位,查找失败
        if (HT[pos].status == 1 && HT[pos].key == key)
            return pos;               // 查找成功
        i++;                          // 继续探测
    }
    return -1;                        // 遍历全表未找到
}

六、散列表 ASL 计算示例对比

假设散列表长 m = 11,H(key) = key mod 11,插入 {25, 38, 16, 49, 22}:

线性探测法:

元素H(key)探测过程探测次数
253位置 3 空,直接插入1
385位置 5 空,直接插入1
165位置 5 占 → 位置 6 空2
495位置 5 占 → 6 占 → 7 空3
220位置 0 空,直接插入1

成功 ASL = (1+1+2+3+1)/5 = 1.6

链地址法:

链表内容查找次数累加
0221
3251
538 → 16 → 491+2+3=6

成功 ASL = (1+1+1+2+3)/5 = 1.6(巧合相同,通常链地址法更优)


记忆辅助

  1. "除留余数取素数,装填因子定效率" — 散列函数首选除留余数法,p 取素数;α 越小效率越高。
  2. "线性探测会聚集,链地址法最灵活" — 线性探测容易产生一次聚集,链地址法无此问题且支持删除。
  3. "查找失败看空位,查找成功看元素" — 查找失败 ASL 是对每个地址到空位的探测次数取平均;查找成功是对每个元素的探测次数取平均。
  4. "α = n/m,越小越好" — 装填因子直接影响 ASL,一般控制在 0.7 以下。

例题精解

例题 1:线性探测法构造散列表

设散列表长 m = 11,H(key) = key mod 11,用线性探测法依次插入 {47, 7, 29, 11, 16, 92, 22},画出散列表并计算成功 ASL。

解题四步法:

① 审题: 表长 11,除留余数法,线性探测。

② 分析: 计算每个关键字的散列地址,冲突时线性探测下一个空位。

③ 过程:

元素H(key)探测过程位置次数
4747%11=3位置 3 空31
77%11=7位置 7 空71
2929%11=77 占 → 8 空82
1111%11=0位置 0 空01
1616%11=5位置 5 空51
9292%11=4位置 4 空41
2222%11=00 占 → 1 空12

散列表:

下标: 0   1   2   3   4   5   6   7   8   9   10
     [11][22][  ][47][92][16][  ][ 7][29][  ][  ]

成功 ASL = (1+1+2+1+1+1+2)/7 = 9/7 ≈ 1.29

④ 结论: 线性探测法构造散列表,成功 ASL ≈ 1.29。注意 29 和 22 分别与 7 和 11 冲突,需要额外探测。


例题 2:链地址法 ASL 计算

设散列表长 m = 7,H(key) = key mod 7,用链地址法插入 {19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10, 79},计算成功和失败的 ASL。

解题四步法:

① 审题: 表长 7,链地址法,12 个关键字。

② 分析: 计算每个关键字的散列地址,统计链表长度。

③ 过程:

关键字(按插入顺序)链表
014, 21→实际无21, 但14%7=014 → NULL
101, 15→无15, 但01%7=1, 但01 实际为 11 → NULL

让我重新计算所有 key mod 7:

  • 19 % 7 = 5
  • 14 % 7 = 0
  • 23 % 7 = 2
  • 01 % 7 = 1
  • 68 % 7 = 5
  • 20 % 7 = 6
  • 84 % 7 = 0
  • 27 % 7 = 6
  • 55 % 7 = 6
  • 11 % 7 = 4
  • 10 % 7 = 3
  • 79 % 7 = 2
链表长度查找次数累加
014 → 8421+2=3
10111
223 → 7921+2=3
31011
41111
519 → 6821+2=3
620 → 27 → 5531+2+3=6

成功 ASL: 每个元素的查找次数 = 它在链表中的位置 = (1+2 + 1 + 1+2 + 1 + 1 + 1+2 + 1+2+3) / 12 = (3+1+3+1+1+3+6) / 12 = 18/12 = 1.5

失败 ASL: 每个槽查找失败时需遍历整个链表 = (2+1+2+1+1+2+3) / 7 = 12/7 ≈ 1.71

④ 结论: 链地址法下成功 ASL = 1.5,失败 ASL ≈ 1.71。装填因子 α = 12/7 ≈ 1.71,ASL 仍可控。


考情分析

维度说明
考频高频,每年必考
题型选择题(ASL 计算、冲突判断、散列函数)、综合题(完整构造散列表过程)
命题趋势线性探测法构造散列表 + ASL 计算是经典综合题;链地址法 ASL 计算
常见陷阱线性探测查找失败时的终止条件;链地址法成功/失败 ASL 计算方式不同

易错点

  1. 散列表的查找效率与关键字个数和表长的比值(装填因子 α)有关,与关键字个数 n 的绝对值无关。 α 相同则 ASL 相同。
  2. 线性探测法查找失败时,从散列地址开始探测到第一个空位就停止。 不是遍历整个表!
  3. 链地址法查找失败时,要遍历整个链表才能确认失败。 所以失败 ASL 用链表长度计算。
  4. 开放定址法中删除元素不能直接置空! 需要用特殊标记(如 DELETED),否则会截断后续元素的查找路径。
  5. 除留余数法中 p 选素数可以减少冲突。 如果 p 含有公因子,某些地址永远不会被映射到。

来源标注

来源说明
王道考研2026 版数据结构讲义 · 第六章 查找
天勤考研数据结构高分笔记 · 查找部分
严蔚敏《数据结构(C 语言版)》第 9 章
教材参考《数据结构》(第 5 版)李春葆 · 查找章节

考研全科复习资料 - 基于2026考研统考大纲