数据结构 · 2027 考研计算机 408

第 7 章 查找(B 树、散列)

目标院校:四川大学 / 电子科技大学 | 建议用时:概念 3 小时 + 例题练习 5 小时

本章地位:查找是 408 数据结构「选择题 + 大题」的双料高产户:散列表的 ASL 计算几乎隔年就考一次大题,B 树的插入删除、折半查找的判定树与比较次数是常青选择题。本章主线是「查找效率的阶梯」——顺序查找 \(O(n)\) → 折半 / 树型查找 \(O(\log_2 n)\) → 散列查找 \(O(1)\)。所有 ASL 结论都配「逐关键字列表格、逐项打 ✓」的完整计算,照着推一遍,大题分就稳了。

7.1 查找基本概念

定义查找表(Search Table)是由同一类型的数据元素组成的集合,其中每个元素至少有一个能唯一标识它的关键字。查找:在查找表中确定一个关键字等于给定值的数据元素(或记录)的操作。查找成功则返回其位置(或记录),否则(表中无此关键字)称查找不成功,返回空指针 / 0 等标识。

7.1.1 查找表:静态与动态

两类查找表
  1. 静态查找表:只做查询 / 检索操作(查找「有没有」),不改动表内容——如顺序表上的顺序查找、折半查找;
  2. 动态查找表:查找的同时可能插入 / 删除元素(查找「没有就插入」)——如二叉排序树、B 树、散列表上的操作。
适合静态表的存储结构不要求便于增删(有序顺序表即可);动态表则要选便于插入删除的结构(链式 / 树形 / 开散列)。
一句话记忆静态 = 只查不动,动态 = 边查边改。折半查找只能建于有序顺序表,所以它天生是静态查找表的方法;二叉排序树查找失败就顺手插入新结点,天生是动态的。

7.1.2 平均查找长度 ASL

定义衡量查找算法效率的最主要指标是平均查找长度(Average Search Length):查找过程中关键字比较次数的期望值 \[ \mathrm{ASL}=\sum_{i=1}^{n}P_i\,C_i \] 其中 \(P_i\) 为查找第 \(i\) 个元素的概率(通常设等概率 \(P_i=1/n\)),\(C_i\) 为找到第 \(i\) 个元素所需的比较次数。
易错ASL 分成功与不成功两种,大题常常两个都要算:
① 成功 ASL:对表中 n 个已有关键字各查找一次,总比较次数 ÷ \(n\);
② 失败 ASL:对「不在表中」的关键字各查找一次,总比较次数 ÷ 失败情况的个数(散列表中即散列函数能映到的地址个数,如 \(H(\mathrm{key})=\mathrm{key}\bmod 13\) 就是 13 种失败起点,除以 13,不是除以表长)。分母取错是散列大题最冤的失分点。
练习 1

判断正误:(1) 折半查找可以在有序单链表上进行;(2) 二叉排序树是一种动态查找表;(3) 查找不成功时的比较次数也应计入失败 ASL。

查看答案

(1) 错:折半查找需要随机存取中间元素,链表不支持随机存取,只能用于有序顺序表。

(2) 对:二叉排序树查找失败时可继续插入新结点,表结构在查找过程中可变,属动态查找表。

(3) 对:失败 ASL 正是由「确定不存在」所经过的比较次数平均出来的。

7.2 顺序、折半与分块查找

7.2.1 顺序查找与哨兵技巧

基本思想从表的一端逐个比较关键字,直到成功或扫完整个表。哨兵技巧:把待查值预先存放在表头(或表尾)的「哨兵位」,循环内就不必每次判断下标越界,循环结束后按下标即可区分成功 / 失败——省掉一次边界比较,代码更紧凑。

// 顺序查找(a[0] 作哨兵,关键字存于 a[1..n])

int seq_search(int a[], int n, int key) {
    int i;
    a[0] = key;                        // 哨兵:保证循环必然终止
    for (i = n; a[i] != key; i--)      // 无需判断 i >= 1
        ;
    return i;                          // 返回 0 即失败(只碰过哨兵)
}
ASL 结论设每个元素等概率被查(\(P_i=1/n\)):第 \(i\) 个元素(从后往前找)比较 \(n-i+1\) 次, \[ \mathrm{ASL}_{成功}=\sum_{i=1}^{n}\frac{1}{n}(n-i+1)=\frac{n+1}{2} \] 查找失败时全表扫一遍加哨兵共比较 \(n+1\) 次,\(\mathrm{ASL}_{失败}=n+1\)。时间复杂度 \(O(n)\)。
例 1 顺序查找 ASL 数值验证

n=5 的表 {10, 20, 30, 40, 50},从后往前顺序查找,求成功 ASL。

查看解答

逐项数:查 50 比较 1 次、查 40 比较 2 次、查 30 比较 3 次、查 20 比较 4 次、查 10 比较 5 次。

\[ \mathrm{ASL}=\frac{1+2+3+4+5}{5}=\frac{15}{5}=3=\frac{5+1}{2}\ ✓ \]

失败:扫完 5 个元素再碰哨兵,共 \(5+1=6=n+1\) 次 ✓。

7.2.2 折半查找与判定树 高频考点

要求与思想折半查找(二分查找)仅适用于有序的顺序表(要随机存取中间元素,链表不行)。每轮与当前区间中点比较:相等则成功;给定值更小则抛弃右半,更大则抛弃左半,区间每次折半。

// 折半查找(升序表 a[0..n-1],返回下标,失败返回 -1)

int bin_search(int a[], int n, int key) {
    int low = 0, high = n - 1, mid;
    while (low <= high) {
        mid = (low + high) / 2;        // 即 mid = ⌊(low+high)/2⌋
        if (a[mid] == key)
            return mid;                // 成功
        else if (a[mid] > key)
            high = mid - 1;            // 抛弃右半
        else
            low = mid + 1;             // 抛弃左半
    }
    return -1;                         // low > high:失败
}
易错① 循环条件是 low ≤ high(写 low < high 会漏掉「区间只剩一个元素」的命中);② mid 必须取下取整\(\lfloor(\text{low}+\text{high})/2\rfloor\),题目若取上取整,判定树形状随之改变;③ 表必须有序且顺序存储——「有序链表上折半」是错误说法。
例 2 高频考点 折半查找全过程(1 基下标)

有序表 {7, 14, 18, 21, 23, 29, 31, 35, 38, 42, 46, 49, 52}(n=13,存于 a[1..13]),分别查找 7 与 39,写出每轮的 low、high、mid 与比较结果。

查看解答

查 7:

轮次lowhighmid=⌊(low+high)/2⌋a[mid]比较与动作
11137297 < 29 → high=6
2163187 < 18 → high=2
31217相等,成功(3 次比较)✓

查 39:

轮次lowhighmida[mid]比较与动作
111372939 > 29 → low=8
2813104639 < 46 → high=9
38983539 > 35 → low=9
49993839 > 38 → low=10 > high=9,失败(4 次比较)✓

失败时 low 越过 high,区间为空即终止。注意 13 个元素的表,任何查找最多 \(\lceil\log_2 14\rceil=4\) 次比较——本题 4 次已到上限。

判定树把每轮「与 mid 比较」画成一棵二叉树:当前区间的 mid 作根,左右半区的 mid 作左右子树的根,如此递归,得到折半查找判定树。它是一棵平衡二叉树(任意结点左右子树高度差 ≤ 1),且只与元素个数 n 有关,与具体取值无关。比较次数 = 该元素所在层数;失败对应走到外部(失败)结点,失败结点共 n+1 个(n+1 个「空隙」),失败比较次数 = 其父结点所在层数。
639 14710 25811 失败结点 n+1=12 个: 第1层 1个第2层 2个第3层 4个第4层 4个
图 7-1 n=11 的折半查找判定树(结点内为元素下标):第 1~4 层分别 1、2、4、4 个结点,失败结点共 n+1=12 个(红色方格)
例 3 方法 判定树逐层计数求成功 ASL

11 个元素的有序表,按图 7-1 的判定树求折半查找的成功 ASL,并验证最大比较次数。

查看解答

逐层数结点个数与比较次数:

第 1 层:1 个结点 × 1 次 = 1;第 2 层:2 个 × 2 次 = 4;第 3 层:4 个 × 3 次 = 12;第 4 层:4 个 × 4 次 = 16。

\[ \mathrm{ASL}_{成功}=\frac{1\times1+2\times2+4\times3+4\times4}{11}=\frac{1+4+12+16}{11}=\frac{33}{11}=3\ ✓ \]

结点总数 \(1+2+4+4=11\) ✓(不重不漏,每个元素恰在一个结点上)。最大比较次数 = 树高 = 4,而 \(\lceil\log_2(n+1)\rceil=\lceil\log_2 12\rceil=4\) ✓。

套路总结:判定树层计数法——「第 k 层贡献(第 k 层结点数 × k)」,分母永远是 n;先数每层结点数,再核对总数 = n。

复杂度结论折半查找成功 / 失败的比较次数均不超过判定树高度 \(\lceil\log_2(n+1)\rceil\),故 \(T(n)=O(\log_2 n)\)。失败结点(外部结点)恰有 n+1 个。

7.2.3 分块查找(索引顺序查找)

思想「块间有序、块内无序」:把表均分或按区间分成 b 块,前一块的最大关键字 < 后一块的最小关键字;另建索引表存每块最大关键字与块起始位置。先查索引(块间可顺序可折半),再在块内顺序查找。
ASL 公式设表长 n、分 b 块、每块 s 个元素(\(n=b\times s\)): \[ \mathrm{ASL}=L_{索引}+L_{块内},\quad L_{块内}\approx\frac{s+1}{2} \] 索引表用顺序查找时 \(L_{索引}=\dfrac{b+1}{2}\);用折半查找时 \(L_{索引}\approx\log_2(b+1)\)(近似,视索引判定树而定)。当 \(b=s\)(即 \(s=\sqrt n\))时顺序索引版取最优,\(\mathrm{ASL}\approx\sqrt n+1\)。
例 4 真题风格 分块查找 ASL 计算

含 10000 个元素的表分 100 块、每块 100 个元素,分别求索引表用顺序查找与折半查找时的 ASL。

查看解答

顺序查索引:\(\mathrm{ASL}=\dfrac{b+1}{2}+\dfrac{s+1}{2}=\dfrac{101}{2}+\dfrac{101}{2}=50.5+50.5=101=\sqrt{10000}+1\) ✓(b=s=100 恰是最优分块)。

折半查索引:\(\mathrm{ASL}\approx\log_2(b+1)+\dfrac{s+1}{2}=\log_2 101+50.5\approx6.66+50.5\approx57.2\)。

块内永远是顺序查找(块内无序,不能折半)。两者都远小于整表顺序查找的 \(\frac{10000+1}{2}\approx5000\),但都大于整表折半的 \(\log_2 10001\approx13.3\)——分块是「无序表用不上折半」时的折中方案。

练习 2 易错

n=100 的有序表折半查找:失败结点有多少个?任何一次查找最多比较多少次?

查看答案

失败结点 \(n+1=101\) 个。最多比较次数 = 判定树高 \(\lceil\log_2(n+1)\rceil=\lceil\log_2 101\rceil=7\)(\(2^6=64\lt101\le128=2^7\))✓。

易错点:失败结点数是 n+1 不是 n;比较次数上界写 \(\lfloor\log_2 n\rfloor+1=7\) 数值相同,但标准表述是 \(\lceil\log_2(n+1)\rceil\)。

练习 3

表长 2500 的表分块查找,索引表顺序查找,要使 ASL 最小应每块多少个元素?此时 ASL 为多少?

查看答案

取 \(s=\sqrt n=50\)(此时 \(b=50=s\)),\(\mathrm{ASL}=\sqrt{2500}+1=51\)。

核对:\(\dfrac{b+1}{2}+\dfrac{s+1}{2}=\dfrac{51}{2}+\dfrac{51}{2}=51\) ✓。若取 s=25(b=100):\(\dfrac{101}{2}+\dfrac{26}{2}=63.5\gt51\),确实更差 ✓。

7.3 B 树与 B+ 树 高频考点

7.3.1 m 阶 B 树的定义与高度

定义一棵 m 阶 B 树(多路平衡查找树)或为空树,或为满足下列性质的 m 叉树:
  1. 每个结点至多有 m 棵子树(即至多 m−1 个关键字);
  2. 若根结点不是终端结点,则至少 2 棵子树(至少 1 个关键字);
  3. 除根外的所有非终端(非叶)结点至少 \(\lceil m/2\rceil\) 棵子树,即至少 \(\lceil m/2\rceil-1\) 个关键字;
  4. 结点内关键字自左向右有序,第 \(i\) 棵子树的所有关键字严格介于第 \(i\) 个与第 \(i+1\) 个关键字之间;
  5. 所有终端结点(叶结点)位于同一层,故内部结点平衡(这是「分块 + 多路折半」的结构保障)。
5 阶 B 树数值速记m=5 时:非根结点关键字数 \(\lceil5/2\rceil-1=2\) ~ \(m-1=4\) 个,即 2 ≤ 关键字数 ≤ 4;子树数 3 ~ 5(根 2 ~ 5)✓。分裂时 5 个关键字取中位 \(\lceil5/2\rceil=3\) 个(即第 3 个)上移。
高度公式含 \(n\) 个关键字的 m 阶 B 树,高度 \(h\) 满足 \[ \log_m(n+1)\le h\le\log_{\lceil m/2\rceil}\!\left(\frac{n+1}{2}\right)+1 \] 左边:所有结点都装满(\(m-1\) 个关键字)时最矮,\(h\) 层最多 \(m^{h}-1\) 个关键字;右边:除根外都取最少 \(\lceil m/2\rceil-1\) 个关键字时最高,最少 \(2\lceil m/2\rceil^{h-1}-1\) 个关键字。
例 5 高频考点 B 树参数与高度范围

(1) 5 阶 B 树中非根结点最少、最多各有多少个关键字?(2) 含 100 个关键字的 5 阶 B 树,高度 \(h\) 的取值范围是多少?

查看解答

(1) 最少 \(\lceil5/2\rceil-1=3-1=2\) 个,最多 \(m-1=4\) 个 ✓(2 ≤ 关键字数 ≤ 4)。

(2) 下界:\(h\ge\log_5(100+1)=\log_5 101\)。验证:\(5^2=25\lt101\le125=5^3\),故 \(\log_5 101\approx2.87\),\(h\ge3\)。

上界:\(h\le\log_3\!\left(\dfrac{101}{2}\right)+1=\log_3 50.5+1\approx3.57+1=4.57\),故 \(h\le4\)。

核对(数结点):\(h=2\) 至多装 \(5^2-1=24\) 个 < 100,装不下 ✓;\(h=3\) 至多装 \(5^3-1=124\ge100\) ✓;\(h=4\) 最少装 \(2\times3^{3}-1=53\le100\),\(h=5\) 最少装 \(2\times3^{4}-1=161\gt100\) 装不下 ✓。综上 3 ≤ h ≤ 4。

练习 4

3 阶 B 树(2-3 树)含 7 个关键字,用高度公式求 \(h\) 的范围,并各画一棵达到上下界的树(只需说明关键字数分布)。

查看答案

下界 \(h\ge\log_3 8\approx1.89\Rightarrow h\ge2\);上界 \(h\le\log_2 4+1=3\)。故 \(2\le h\le3\)。

达到下界 h=2:根 2 个关键字 + 3 个孩子各 2 个关键字,共 \(2+6=8\ge7\) ✓(7 个可按 2+2+2+1 分布,其中一叶 1 个也合法——叶最少 1 个?不,非根最少 \(\lceil3/2\rceil-1=1\) 个,合法 ✓)。

达到上界 h=3:每个结点 1 个关键字(非根最少 1 个),共 \(1+2+4=7\) 个关键字,恰是满二叉树形状 ✓。

7.3.2 插入:溢出分裂、中位上移

插入流程① 从根出发按关键字大小定位到最底层某个终端结点(新关键字总是插在终端结点,不会插到中间层);② 直接插入后若该结点关键字数 超过 m−1(上溢,此时结点有 m 个关键字):取中位——第 \(\lceil m/2\rceil\) 个关键字上移插入父结点,左、右两半分裂为两个结点;③ 上移可能使父结点再溢出,连锁分裂向上传播;若根分裂,中位关键字成为新根,树高加 1——B 树长高是「向上」长,这是与二叉排序树最大的不同。
① 依次插入 4、9、14、19:结点 [4 | 9 | 14 | 19],已达上限 4 个 4 9 14 19 ② 插入 24:上溢(5 > m−1 = 4) 4 9 14 19 24 ← 第 3 个(中位)14 上移 14 4 9 19 24 ③ 分裂成两个 2 关键字结点,14 成为新根,树高 +1 ④ 继续插入 29、34 → 右结点 [19|24|29|34],再插 39 又上溢 → 中位 29 上移;再插 44、49、54,右结点第三次上溢 → 中位 44 上移,最终: 14 29 44 4 9 19 24 34 39 49 54 根 3 个、各叶 2 个关键字:均在 2~4 范围内,所有终端结点同层
图 7-2 5 阶 B 树插入 {4, 9, 14, 19, 24, 29, 34, 39, 44, 49, 54}:每次上溢都取中位关键字上移,本例共分裂三次(14、29、44 依次升为根内关键字)
例 6 方法 B 树插入全过程

将关键字序列 {4, 9, 14, 19, 24, 29, 34, 39, 44, 49, 54} 依次插入初始为空的 5 阶 B 树,写出每次分裂的细节并画出最终形态。

查看解答

按图 7-2 逐步执行(m=5,结点关键字上限 4,溢出取第 \(\lceil5/2\rceil=3\) 个上移):

① 4、9、14、19 依次进入根(终端)结点,共 4 个,未超限;

② 插 24 → [4,9,14,19,24] 上溢 → 中位 14 上移建根,左右分裂为 [4,9] 与 [19,24](各 2 个 ✓);

③ 插 29、34 → 落入右结点成 [19,24,29,34];插 39 → 上溢 → 中位 29 上移,右结点分裂为 [19,24] 与 [34,39],根变 [14,29];

④ 插 44、49 → [34,39,44,49];插 54 → 上溢 → 中位 44 上移,分裂为 [34,39] 与 [49,54],根变 [14,29,44]。

最终:根 3 个关键字(≤4 ✓),四个终端结点各 2 个(≥2 ✓),全部同层 ✓。11 个关键字 = 3+2×4 ✓。

套路总结:B 树插入题「一路向下找叶 → 直接插入 → 超限就取中位上移」;检查答案的三个动作——根关键字数、每叶关键字数 ≥ ⌈m/2⌉−1、所有叶同层。

7.3.3 删除:借兄弟与合并

终端(叶)结点上删除设 m 阶,非根终端结点关键字下限 \(t=\lceil m/2\rceil-1\):
  1. 够删:删除后仍 ≥ t 个 → 直接删;
  2. 兄弟够借:删除后不足 t 个,且相邻兄弟 > t 个 → 从兄弟借一个,双亲中相应关键字下来中转(再补一个兄弟关键字上移),保持大小关系;
  3. 兄弟也最少:删除后不足,相邻兄弟也恰为 t 个 → 本结点 + 双亲中分隔关键字 + 兄弟三者合并为一个结点(t+t+1 ≤ m−1 个,必不超限)。双亲少一个关键字可能连锁下溢,合并继续向上;若根的两个孩子合并,根被架空,树高减 1。
非终端结点上删除用被删关键字的直接前驱(左子树中最右下终端结点的最大关键字)或直接后继(右子树中最左下终端结点的最小关键字)替换它,问题转化为「在终端结点中删除那个前驱 / 后继」——于是非终端删除最终都归结为终端删除的三种情景。
例 7 真题风格 B 树删除三种情景

在图 7-2 最终得到的 5 阶 B 树(根 [14, 29, 44],终端结点 [4,9]、[19,24]、[34,39]、[49,54])上依次删除 9、19、29,说明每步用了哪种处理并给出结果。

查看解答

删 9(终端结点 [4,9]):删后只剩 1 个 < 下限 2,右兄弟 [19,24] 恰好 2 个也最少,借不动 → 情景③合并:[4] + 双亲 14 + [19,24] → [4,14,19,24](4 个 ≤4 ✓),根变 [29,44]。此时四个终端结点变为三个:[4,14,19,24]、[34,39]、[49,54],仍同层 ✓。

删 19(终端结点 [4,14,19,24]):删后剩 3 个 ≥ 2 → 情景①直接删,得 [4,14,24] ✓。

删 29(非终端,在根 [29,44] 中):取 29 的直接前驱 = 左子树 [4,14,24] 的最大关键字 24 → 根变 [24,44],再在终端结点中删 24 → [4,14](2 个 ✓,够删)。最终:根 [24,44],终端结点 [4,14]、[34,39]、[49,54]。

核对:各结点关键字数 2~4 ✓,全部终端结点同层 ✓,总数 \(2+3\times2=8=11-3\) ✓。

7.3.4 B+ 树与 B 树的区别

B+ 树一棵 m 阶 B+ 树满足:每个分支结点至多 m 棵子树;非根分支结点至少 \(\lceil m/2\rceil\) 棵子树(关键字数=子树数);结点中 n 个关键字恰对应 n 棵子树;全部关键字及其记录都出现在终端结点,并按大小链成有序单链表;非终端结点只放索引(通常是各子树中最大关键字的副本),查找任何关键字都必须查到终端结点才算数。
对比项B 树B+ 树
关键字与子树n 个关键字对应 n+1 棵子树n 个关键字对应 n 棵子树
非根结点关键字数\(\lceil m/2\rceil-1\) ~ \(m-1\)\(\lceil m/2\rceil\) ~ \(m\)
终端 / 叶结点含部分关键字,其下还有空失败结点含全部关键字与记录,叶间有链指针
非终端结点作用关键字本身即是查找目标(命中即返回)仅作索引,不存记录,命中不停
查找终点可在任意层命中结束必须走到终端结点
典型应用文件系统(如索引结点)数据库索引(MySQL InnoDB 等)
一句话记忆B+ 树 = 「索引在上、数据在叶、叶子串链」。范围查询(如 WHERE age BETWEEN 18 AND 25)只需定位到起点后沿叶链顺序扫,这正是数据库选 B+ 而不选 B 的根本原因。
例 8 易错 B+ 树性质辨析

下列关于 m 阶 B+ 树的叙述中,错误的是( )
A. 结点中 n 个关键字对应 n 棵子树
B. 非终端结点中存放的关键字是其各子树中最大关键字的副本
C. 查找某个关键字时可能在非终端结点处就成功终止
D. 终端结点包含全部关键字,且相互链接成有序链表

查看解答

C。B+ 树的非终端结点只起「路标」作用,即使中途结点里出现相同值也不停,必须查到终端结点才能确认成败——这与 B 树「任意结点命中即返回」相反。A、B、D 均为 B+ 树定义 ✓。

例 9 真题风格 B 树与 B+ 树对比

下列叙述中正确的是( )
A. B+ 树的非终端结点中包含了全部关键字
B. B 树中 n 个关键字对应 n 棵子树
C. B+ 树中查找任何关键字都必须查到终端结点
D. B 树的终端结点之间用指针链接以便顺序遍历

查看解答

C。逐项:

A 错:包含全部关键字的是 B+ 树的终端结点,非终端结点只作索引;
B 错:B 树是 n 个关键字对应 n+1 棵子树,n 对 n 是 B+ 树;
C 对:B+ 树非终端结点仅是副本索引,成败须在终端结点判定;
D 错:终端结点链接是 B+ 树的特征,B 树没有。

套路总结:B/B+ 对比选择题就抓三处——「n 对 n 还是 n+1」「数据在叶还是各层都有」「叶链有没有」,逐一对照排除。

7.4 散列表 高频考点

7.4.1 散列函数的构造

定义散列(哈希)存储:按关键字直接计算存储地址,\(addr=H(\mathrm{key})\),\(H\) 称散列函数。关键字不同而散列值相同,称这两个关键字互为同义词,该现象叫冲突——冲突只能缓解不能彻底避免(地址数有限、关键字无穷)。理想目标:地址分布均匀、计算简单。
常用构造法
  1. 直接定址法:\(H(\mathrm{key})=a\times\mathrm{key}+b\),地址与关键字线性对应——不会冲突,但关键字稀疏时浪费空间(适合连续编号,如年龄);
  2. 除留余数法(最常用、408 主考):\(H(\mathrm{key})=\mathrm{key}\bmod p\),\(p\) 取不大于表长 m 的素数;
  3. 数字分析法、平方取中法、折叠法等:了解思想即可,考频低。
p 为何取素数若 \(p\) 含质因子 \(q\),则所有被 \(q\) 整除的关键字都映到 \(q\) 的倍数地址上,冲突扎堆。取素数能让余数在 \(0\sim p-1\) 上分布更均匀、冲突更少。例:\(p=12\) 时关键字 {4,16,28,40}(都是 4 的倍数)全部落地址 4;\(p=13\) 时分别落 4、3、2、1,散开了 ✓。

7.4.2 冲突处理:开放定址与链地址

开放定址法发生冲突后按探测序列在表内另找空位: \[ H_i=(H(\mathrm{key})+d_i)\bmod m,\quad i=1,2,3,\dots \] (注意模的是表长 m;\(H(\mathrm{key})\) 本身算第 0 次探测。)按 \(d_i\) 取法分三类:
  1. 线性探测法:\(d_i=1,2,\dots,m-1\)。一找邻位就顺手,但会把非同义词也堆到一起,产生堆积(聚集);
  2. 平方探测法:\(d_i=1^2,-1^2,2^2,-2^2,\dots,k^2,-k^2\)(\(k\le\lfloor m/2\rfloor\))。跳跃分散、不易堆积;只要表中有空位,在前 \(\lfloor m/2\rfloor\) 次探测内必能探到;当表长 \(m\) 是形如 \(4k+3\) 的素数时可探测到全表每个位置;
  3. 双散列法:\(d_i=i\times H_2(\mathrm{key})\)(\(H_2\) 是另一个散列函数)。探测序列依赖关键字,聚集最小。
链地址法(开散列)把散列值相同的所有同义词挂到同一链表:表长 m 的散列表是 m 条链的数组。插入删陈权在链上做,无「堆积」,删除无需标记;代价是额外指针空间。开放定址法则把冲突留在表内「就地安置」,存储紧凑但删除受限(见 7.4.4)。
练习 5 方法

表长 m=11,\(H(\mathrm{key})=\mathrm{key}\bmod 11\)。插入 key=22 时发现 H(22)=0 的单元已被占用,分别写出线性探测法与平方探测法接下来依次探测的 4 个地址。

查看答案

线性探测(\(d_i=1,2,3,4\)):\(0{\to}1{\to}2{\to}3{\to}4\)(含已占用的 0 共探测 5 个地址)。

平方探测(\(d_i=1,-1,4,-4\)):\(0{\to}0{+}1=1{\to}0{-}1=10{\to}0{+}4=4{\to}0{-}4=7\)。注意负向探测要 mod m:\(0-1=-1\bmod11=10\) ✓。

若 \(H_2(\mathrm{key})=7\)(双散列):\(0{\to}7{\to}14\bmod11=3{\to}21\bmod11=10{\to}28\bmod11=6\)。

7.4.3 ASL 计算大题(核心)高频考点

大题模板① 按给定散列函数逐个插入关键字(冲突按指定探测法走),记下每个关键字的探测(比较)次数;② 成功 ASL = 各关键字探测次数之和 ÷ n(已有关键字个数);③ 失败 ASL = 从每个散列地址出发、沿探测法数到空单元的探测次数之和 ÷ p(散列地址个数,即 mod 的那个 p)。
例 10 真题风格高频考点 线性探测法建表并求 ASL

关键字序列 {19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79},散列函数 \(H(\mathrm{key})=\mathrm{key}\bmod 13\),表长 m=16,用线性探测法处理冲突。画出散列表,求成功与失败的 ASL。

查看解答

第一步:逐个插入(探测次数 = 比较过的单元数,含最终落位那次):

关键字H(key)探测过程落位次数
1966 空61
1411 空11
231010 空101
111 占 → 2 空22
6833 空31
2077 空71
8466、7 占 → 8 空83
2711、2、3 占 → 4 空44
5533、4 占 → 5 空53
111111 空111
101010、11 占 → 12 空123
7911~8 占 → 9 空99

得散列表(下标 0~15):
0:空 1:14 2:1 3:68 4:27 5:55 6:19 7:20 8:84 9:79 10:23 11:11 12:10 13~15:空

第二步:成功 ASL(分母 = n = 12):

\[ \mathrm{ASL}_{成功}=\frac{1+1+1+2+1+1+3+4+3+1+3+9}{12}=\frac{30}{12}=2.5\ ✓ \]

第三步:失败 ASL(分母 = p = 13,即从地址 0~12 各失败查一次,数到首个空单元为止):

起始地址0123456789101112
探测次数11312111098765432

(例:从 1 出发要一路数过 1~12 共 12 个占用单元,到 13 空,共 13 次;从 12 出发数 12 后到 13 空,2 次。)

\[ \mathrm{ASL}_{失败}=\frac{1+13+12+11+10+9+8+7+6+5+4+3+2}{13}=\frac{91}{13}=7\ ✓ \]

易错:失败 ASL 分母是 13(mod 13 的地址个数)不是表长 16;地址 13~15 不是合法散列起点,绝不参与失败统计。

同一组数据 {19,14,23,1,68,20,84,27,55,11,10,79},H(key)=key mod 13,链地址法: 1 14 1 27 79 ∧(4 个结点) 3 68 55 ∧(2 个结点) 6 19 84 ∧(2 个结点) 7 20 ∧(1 个结点) 10 23 10 ∧(2 个结点) 11 11 ∧(1 个结点) 地址 0、2、4、5、8、9、12 的链为空(失败 0 次比较)。 链上结点按插入先后排列, 查找沿链逐个比较。
图 7-3 链地址法处理冲突:只有 6 个地址有非空链,同义词按到达先后挂在链上,插入 / 删除都只动链表
例 11 方法 同数据链地址法 ASL 对照

对例 10 的同一组关键字与散列函数,改用链地址法处理冲突,求成功与失败的 ASL,并与线性探测法对比。

查看解答

成功 ASL:每个关键字的比较次数 = 它在链上的位置(按图 7-3):

链 1:14 第 1、1 第 2、27 第 3、79 第 4 → \(1+2+3+4=10\);链 3:68 第 1、55 第 2 → 3;链 6:19 第 1、84 第 2 → 3;链 7:20 → 1;链 10:23 第 1、10 第 2 → 3;链 11:11 → 1。

\[ \mathrm{ASL}_{成功}=\frac{10+3+3+1+3+1}{12}=\frac{21}{12}=1.75\ ✓ \]

失败 ASL(分母 = 13):从地址 0~12 出发,失败的比较次数 = 该链结点数(空链为 0):

\[ 0,4,0,2,0,0,2,1,0,0,2,1,0\ \Rightarrow\ \mathrm{ASL}_{失败}=\frac{4+2+2+1+2+1}{13}=\frac{12}{13}\approx0.92\ ✓ \]

(若按「与空指针的比较也计一次」的口径,则为 \(\frac{12+13}{13}=\frac{25}{13}\);408 王道口径取结点数本身,答题以参考答案口径为准。)

对比:线性探测 2.5 / 7,链地址 1.75 / 0.92——链地址法不产生堆积,且删除方便;代价是指针空间与 Cache 局部性差。

7.4.4 装填因子与删除标记

装填因子表中已存元素个数 \(n\) 与表长 \(m\) 之比 \[ \alpha=\frac{n}{m} \] 散列表的 ASL 是 α 的函数:α 越大表越满、冲突越密、ASL 越大。只要 α 不变,n 与 m 同比例放大,ASL 基本不变——这正是散列表「与规模无关」的精髓。
两个必考易错① 「ASL 与表长 m 有关」——错,直接相关的是 α=n/m,不是 m 本身(表长翻倍、元素数也翻倍,ASL 不变);
② 开放定址法的散列表删除只能打删除标记(墓碑),不能物理清空——清空会截断后续同义词的探测链,使本可查到的元素「查不到」。例:例 10 中若物理删除 27(槽 4),再查 55(H=3→4 空)会误判「不存在」✗。链地址法无此限制,直接摘链即可。
练习 6 易错

判断:(1) 表长 16、已存 8 个元素,装填因子为 0.5;(2) 散列表的 ASL 只与装填因子有关,与表长无直接关系;(3) 链地址法删除结点需要打删除标记;(4) 线性探测法比平方探测法更容易产生堆积。

查看答案

(1) 对:\(\alpha=8/16=0.5\) ✓。

(2) 对:在散列函数与冲突处理给定后,ASL 由 α 决定(这是散列区别于顺序 / 折半查找的根本点)。

(3) 错:链地址法直接从链上摘除结点即可,无需标记;要打标记的是开放定址法。

(4) 对:线性探测「挨个往后挪」使非同义词连成一片(堆积);平方探测正负跳跃,分散得多。

7.5 章末自测 真题风格

限时 50 分钟,先做后看答案。难度:★★ 基础 / ★★★ 强化 / ★★★★ 冲刺。散列 ASL 大题务必动笔列表格,别跳步。

自测 1(选择 · ★★)

下列存储结构中,适合进行折半查找的是( )
A. 有序单链表 B. 有序顺序表 C. 分块有序的单链表 D. 二叉链表存储的二叉排序树

查看答案

B。折半查找要求有序 + 顺序存储(可随机存取);链表无法 O(1) 定位中点,A、C 错;D 是树形查找,不叫折半查找。

自测 2(选择 · ★★★)

长度为 12 的有序表,等概率下折半查找成功的最多比较次数与失败结点数分别为( )
A. 4、12 B. 4、13 C. 3、12 D. 3、13

查看答案

B。最多比较次数 = 判定树高 = \(\lceil\log_2(12+1)\rceil=\lceil\log_2 13\rceil=4\)(\(2^3=8\lt13\le16\))✓;失败结点 \(n+1=13\) 个 ✓。

自测 3(选择 · ★★★ 高频考点)

含 20 个关键字的 5 阶 B 树,其高度 \(h\) 的可能范围是( )
A. \(2\le h\le3\) B. \(3\le h\le4\) C. \(2\le h\le4\) D. \(3\le h\le5\)

查看答案

A。下界 \(h\ge\log_5 21\approx1.89\Rightarrow h\ge2\);上界 \(h\le\log_3\!\big(\frac{21}{2}\big)+1=\log_3 10.5+1\approx2.18+1=3.18\Rightarrow h\le3\)。核对:\(h=2\) 最多装 \(5^2-1=24\ge20\) ✓,\(h=3\) 最少装 \(2\times3^2-1=17\le20\) ✓,\(h=4\) 最少装 \(2\times3^3-1=53\gt20\) 不可能 ✓。

自测 4(选择 · ★★★)

下列关于 m 阶 B+ 树的叙述中,正确的是( )
A. 结点中 n 个关键字对应 n+1 棵子树
B. 查找任一关键字都可能在非终端结点处成功终止
C. 终端结点包含全部关键字,且相互链接成有序链表
D. 非终端结点中存放数据记录本身

查看答案

C。A 错:B+ 树是 n 对 n(n+1 是 B 树);B 错:必须查到终端结点;D 错:非终端结点只作索引不存记录。数据库索引正是用 B+ 树,范围查询沿叶链顺序扫描。

自测 5(选择 · ★★)

除留余数法 \(H(\mathrm{key})=\mathrm{key}\bmod p\) 中,p 取不大于表长的最大素数,主要目的是( )
A. 节省存储空间 B. 使散列地址分布更均匀、减少冲突 C. 加快取余计算 D. 防止同义词产生

查看答案

B。素数模能把「含公共质因子」的一批关键字打散(见 7.4.1 例)。D 错:同义词(同余)永远可能产生,散列函数只能减少冲突,不能消灭冲突。

自测 6(填空 · ★★)

表长 100 的顺序表,等概率下顺序查找的成功 ASL 为 \(\underline{\hspace{1cm}}\),失败时(含哨兵)需比较 \(\underline{\hspace{1cm}}\) 次。

查看答案

成功 \(\frac{100+1}{2}=50.5\);失败 \(n+1=101\) 次(扫完 100 个元素再比较一次哨兵)。

自测 7(填空 · ★★★ 易错)

表长 13 的散列表已存入 6 个元素,装填因子 \(\alpha=\)\(\underline{\hspace{1cm}}\);若把表长与元素个数同时加倍,ASL 将 \(\underline{\hspace{1cm}}\)(变大 / 变小 / 基本不变)。

查看答案

\(\alpha=6/13\approx0.46\);基本不变——散列表的 ASL 取决于装填因子 α,而不是表长或元素个数本身。

自测 8(选择 · ★★★ 易错)

用线性探测法解决冲突的散列表中删除一个元素,正确的做法是( )
A. 将其单元直接置空 B. 仅做逻辑删除标记 C. 把其后所有元素前移一格 D. 重建整张散列表

查看答案

B。物理置空会截断探测链:设 \(H(a)=H(b)\) 且 b 存在 a 后面,删 a 置空后再查 b,探测到 a 的空位即误判失败。删除标记让探测「路过但不停」,插入时可复用。链地址法则可直接摘链删除。

自测 9(解答 · ★★★★ 真题风格高频考点)

关键字序列 {10, 22, 31, 4, 15, 28, 17, 88, 59},\(H(\mathrm{key})=\mathrm{key}\bmod 11\),表长 11,线性探测法处理冲突。画出散列表,计算成功与失败的 ASL。

查看解答

建表(逐项验证):10→10 空(1 次);22→0 空(1);31→9 空(1);4→4 空(1);15:4 占→5(2);28→6 空(1);17:6 占→7(2);88:0 占→1(2);59:4、5、6、7 占→8(5)。

散列表(0~10):0:22 1:88 2:空 3:空 4:4 5:15 6:28 7:17 8:59 9:31 10:10

\[ \mathrm{ASL}_{成功}=\frac{1+1+1+1+2+1+2+2+5}{9}=\frac{16}{9}\approx1.78\ ✓ \]

失败(分母 11,从 0~10 数到首个空单元):起点 0 → 3 次;1 → 2;2 → 1;3 → 1;4 → 4~10、0、1 占到 2 空 → 10;5 → 9;6 → 8;7 → 7;8 → 6;9 → 5;10 → 4。

\[ \mathrm{ASL}_{失败}=\frac{3+2+1+1+10+9+8+7+6+5+4}{11}=\frac{56}{11}\approx5.09\ ✓ \]

易错:失败从地址 4 出发要越过 4~10 共 7 个占用位再绕回 0、1,最后在 2 停下——「绕回」别忘 mod 表长。

自测 10(解答 · ★★★★ 冲刺)

将关键字 {30, 60, 90, 20, 15} 依次插入初始为空的 3 阶 B 树,写出每次分裂过程并画出最终树,最后检查合法性。

查看解答

m=3:结点关键字上限 2,下限(非根)\(\lceil3/2\rceil-1=1\);上溢取第 \(\lceil3/2\rceil=2\) 个(中位)上移。

① 30、60 → 根 [30,60];② 插 90 → [30,60,90] 上溢 → 中位 60 上移建根,分裂为 [30] 与 [90](树高 +1);③ 插 20 → 落左结点 [20,30];④ 插 15 → [15,20,30] 上溢 → 中位 20 上移,左结点分裂为 [15] 与 [30],根变 [20,60]。

最终:根 [20,60](2 个 ≤2 ✓),终端结点 [15]、[30]、[90](各 1 个 ≥1 ✓,同层 ✓)。关键字总数 \(2+3=5\) ✓。

套路总结:3 阶 B 树每个结点最多 2 个关键字,「第三个必分裂」;分裂时中间的上移、两边各留一个。

7.6 本章考点总结

考点常考题型热度核心方法
折半查找过程与判定树选择 / 解答★★★★★ 每年必考mid=⌊(low+high)/2⌋ 逐步列表;判定树逐层计数;比较次数 ≤ ⌈log₂(n+1)⌉,失败结点 n+1 个;只适用于有序顺序表
顺序 / 分块查找 ASL选择 / 填空★★★顺序成功 (n+1)/2、失败 n+1;分块 ASL=L索引+L块内,最优 s=√n 时 ≈ √n+1
m 阶 B 树定义与高度选择★★★★关键字数 ⌈m/2⌉−1 ~ m−1、根至少 1、叶同层;高度 log_m(n+1) ≤ h ≤ log_⌈m/2⌉((n+1)/2)+1,代小值核对
B 树插入 / 删除选择 / 解答★★★★ 高频插入:到终端结点,上溢取中位上移可连锁分裂(树高向上长);删除:够删直接删、缺则借兄弟、都缺则与双亲中分隔关键字合并;非终端删用前驱 / 后继替换
B+ 树辨析选择★★★n 关键字 n 子树;叶含全部关键字且串链;非叶仅索引;查找必到叶;数据库索引用 B+ 树
散列函数与冲突处理选择 / 大题第一步★★★★除留余数 p 取素数;线性探测易堆积、平方探测 ⌊m/2⌋ 内必中、双散列最散;链地址无堆积、删除方便
散列表 ASL 计算综合大题★★★★★ 高频大题逐关键字列探测次数表;成功 ÷ n,失败 ÷ p(mod 的模数,不是表长);绕回 mod 表长别漏
装填因子与删除标记选择 / 判断★★★α=n/m 决定 ASL,与表长无直接关系;开放定址删除只能打标记(保探测链),链地址直接摘链
下一步本章过关标准:例题全部独立重做;自测 10 题至少 8 题正确;能徒手画 n=11 的折半判定树并算 ASL;5 阶 B 树插删操作三分钟内完成;散列 ASL 大题「成功 ÷ n、失败 ÷ p」两个分母永不混。然后进入 第 8 章 排序——数据结构最后一座大山,也是与本章 ASL 同级的必考大题来源。