Skip to content

哈希表:拉链法

拉链法:链式解决哈希冲突

哈希冲突了怎么办?最直觉的做法是:冲突的元素用一条链表串起来。这就是拉链法——每个哈希槽位挂一条链表,所有映射到该位置的元素都存在这条链上。插入、删除都是链表操作,不存在"堆积"问题。

408 考研中,拉链法的 ASL 计算(成功和失败)几乎每年必考,且计算方式与开放定址法不同,容易混淆。

核心思想

拉链法的核心特点:

  • 哈希函数定位:通过 H(key) 计算出数组下标,O(1) 时间定位到对应槽位
  • 链表解决冲突:每个槽位维护一个链表,所有哈希值相同的元素挂在同一链表上
  • 动态扩展:链表长度不受限制,无需担心表满溢出

哈希表的基本结构:

下标:  0       1       2       ...     p-1
槽位: [头指针] [头指针] [头指针] ...    [头指针]
       ↓       ↓       ↓              ↓
      链表    链表    链表            链表

每个槽位存储一个链表头指针,冲突元素依次挂在对应链表上。

交互可视化

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

加载可视化中...

操作详解

哈希函数

408 考研中最常考的哈希函数是除留余数法

H(key) = key % p

其中 p 通常取不大于表长的最大素数,这样可以使关键字的分布更均匀,减少冲突。

例如,表长 m = 13,则取 p = 13;表长 m = 15,则取 p = 13。

冲突的定义:若 key₁ ≠ key₂,但 H(key₁) = H(key₂),则称 key₁ 和 key₂ 发生了冲突,它们互为同义词

拉链法原理

拉链法的数据结构定义:

c
#define MAX_SIZE 13  // 哈希表长度

typedef struct Node {
    int key;
    struct Node *next;
} Node;

typedef struct {
    Node *head;      // 每个槽位的链表头指针
} HashTable[MAX_SIZE];

构造过程示例:设哈希函数 H(key) = key % 13,依次插入关键字 {16, 74, 60, 43, 54, 90, 46, 31, 29, 88, 77}:

下标  链表
0    → NULL
1    → [16] → [29] → NULL
2    → [54] → NULL
3    → [16]  (注: 16%13=3)
...

实际映射结果:

关键字H(key) = key % 13
163
749
608
434
542
9012
467
315
293
8810
7712

H(16) = H(29) = 3,H(90) = H(77) = 12,它们分别产生冲突,挂在同一链表上。

查找与插入

查找操作

  1. 计算 H(key),定位到对应槽位
  2. 在该槽位的链表中顺序查找目标关键字
  3. 找到则返回节点指针;遍历完链表未找到则查找失败
c
Node* search(HashTable ht, int key) {
    int idx = key % MAX_SIZE;
    Node *p = ht[idx].head;
    while (p != NULL) {
        if (p->key == key)
            return p;       // 查找成功
        p = p->next;
    }
    return NULL;            // 查找失败
}

插入操作

  1. 先查找,若已存在则不插入(或更新)
  2. 若不存在,创建新节点,采用头插法插入到对应链表的表头
c
void insert(HashTable ht, int key) {
    if (search(ht, key) != NULL)
        return;                     // 已存在,不重复插入
    int idx = key % MAX_SIZE;
    Node *newNode = (Node*)malloc(sizeof(Node));
    newNode->key = key;
    newNode->next = ht[idx].head;   // 头插法
    ht[idx].head = newNode;
}

删除操作

  1. 计算 H(key),定位到对应槽位
  2. 在链表中找到目标节点并删除

拉链法的删除操作非常方便,直接在链表中摘除节点即可,这是相比开放定址法的一大优势。开放定址法删除时只能做"逻辑删除"(打标记),不能直接物理删除,否则会中断查找链。

ASL 分析

设哈希表长为 m,存储了 n 个元素,装填因子 α = n / m。

查找成功的 ASL:对每个已存入的元素,计算查找它需要的比较次数,然后取平均。

$$ASL_{成功} = \frac{1}{n} \sum_{i=1}^{n} C_i$$

其中 Cᵢ 是查找第 i 个元素所需的比较次数(即该元素在链表中的位置,第 1 个节点比较 1 次,第 2 个比较 2 次...)。

查找不成功的 ASL:对每个槽位,计算查找失败需要的比较次数(即遍历该链表的全部节点数),然后对所有槽位取平均。

$$ASL_{失败} = \frac{1}{m} \sum_{j=0}^{m-1} L_j$$

其中 Lⱼ 是第 j 个槽位链表的长度。

理论结论:在等概率条件下,拉链法的平均查找长度为:

指标公式
ASL 成功≈ 1 + α/2
ASL 失败≈ α

α 越小(表越空),查找效率越高。通常建议 α <= 1。

易错:拉链法的查找失败 ASL 计算方式与开放定址法不同:拉链法是对每个哈希地址,数该地址链表上的结点数(比较次数)。如果链表为空,比较 0 次(不需要比较就知道失败)。而开放定址法遇到空位也要算 1 次比较。

易错:拉链法的装填因子 α 可以大于 1(链表长度不受限制),而开放定址法的 α 必须小于 1。α > 0.75 时拉链法的性能仍然可接受,但开放定址法会严重退化。

计算示例:上述 11 个关键字存入表长 m = 13 的哈希表,α = 11/13 ≈ 0.85。

查找成功 ASL:各元素比较次数求和后除以 n。对于链表中第 1 个插入的节点(在链尾),比较次数等于该链表长度;最后插入的节点(在链头),比较次数为 1。具体值需要根据插入顺序计算每个节点在链表中的位置。

查找失败 ASL:各槽位链表长度之和除以 m。

复杂度分析

操作平均时间复杂度最坏时间复杂度说明
查找O(1+α)O(n)平均取决于装填因子 α
插入O(1+α)O(n)先查找后头插
删除O(1+α)O(n)链表删除,无需逻辑删除

空间复杂度:O(m + n),m 为槽位数组空间,n 为链表节点空间。

与开放定址法的对比

比较维度拉链法开放定址法
冲突处理链表存储同义词探测下一个空位
装填因子可 > 1必须 < 1
删除操作直接删除节点只能逻辑删除(打标记)
堆积现象不会产生会产生聚集(堆积)
空间利用需要额外指针空间无额外指针开销
适用场景表长不确定、频繁增删表长已知、较少删除

考研高频考点

  • ⭐ 给定关键字序列和哈希函数,画出拉链法的哈希表(选择/填空/综合题高频)
  • ⭐ ASL 成功和 ASL 失败的计算(必考计算题)
  • ⭐ 装填因子 α 的定义及对查找效率的影响(概念题)
  • ⭐ 拉链法 vs 开放定址法的优缺点对比(简答题高频)
  • 冲突、同义词的概念辨析(选择题)
  • 除留余数法中 p 的选取原则(选择题)
  • 拉链法中删除操作的优势(对比开放定址法的逻辑删除)

相关知识

  • 哈希表(开放定址法)是另一种冲突处理策略,两者的 ASL 计算方式和适用场景不同,408 常考对比
  • 查找算法对比中横向比较了所有查找算法的 ASL 和适用条件
  • 拉链法中每个槽位挂的链表就是单链表,链表的插入/删除操作是拉链法的基础
  • 哈希表不支持范围查询,如果需要有序遍历应选择B 树B+ 树

真题练习

相关真题(3题)

2025Q9选择题2分

散列表冲突处理对比:线性探查只要表不满一定能找到空位,二次探查则不一定

2022Q9选择题2分

散列表性能:装填因子、散列函数、冲突解决策略均影响平均查找长度

2015Q41综合题15分

链表去重:删除单链表中绝对值重复的结点(辅助哈希数组标记法)