2024 年第 5 题,四个选项我挨个判断,最后自信地选了 C。
题目问:下列数据结构中不适合直接使用折半查找的是哪些?
-
I 有序链表
-
II 无序数组
-
III 有序静态链表
-
IV 无序静态链表
我的判断是:I 不行(链表不能随机存取)、II 不行(无序)、IV 不行(两个毛病都占),但 III 有序静态链表我觉得可以------它物理上不就是用数组存的吗?数组能随机存取,那不就能折半了?
答案是 D:四个全都不行。
我漏掉的关键点是:静态链表中,下标的相邻不等于逻辑上的相邻 。它虽然躺在数组里,但元素的先后顺序是由 cur 游标串起来的,A[0] 后面接的可能是 A[7]、再接 A[3]。你去取 A[(low+high)/2] 这个"正中间的下标",取到的根本不是逻辑上排在中间的那个元素。
一句话:折半查找要的不是"存在数组里",而是"第 k 个元素能在 O(1) 时间内直接拿到"。
📍 查找系列共三篇 : 上篇(本篇)线性查找 ------顺序查找、折半查找、分块查找; 中篇 树形查找 ------二叉排序树、平衡二叉树; 下篇 多路与散列------B 树、B+ 树、散列表。
〇、先花两分钟:查找、ASL、判定树是什么
查找(Search):在一堆数据里找到关键字等于给定值的那个元素。
ASL (Average Search Length,读作"平均查找长度"):查找过程中平均需要比较多少次关键字。它是衡量查找算法效率的核心指标。
ASL = Σ (查到第 i 个元素所需的比较次数 × 查到第 i 个元素的概率)
等概率、每个元素都要考虑时,化简成:
ASL = (所有元素的比较次数之和) ÷ 元素个数
判定树 :把折半查找过程中"每次跟谁比"画成一棵二叉树。树里的每个结点是一次比较的对象,层数就是比较次数。
折半查找判定树、三种 ASL 公式推导、分块查找最优块长、跳跃查找步骤
一、ASL 的定义:先搞清"比较次数"是怎么数的
这是全章的地基,很多题做错不是因为公式不会,而是比较次数数错了。
一条约定:每拿待查关键字和表中一个元素比一次,就算 1 次比较。
举 5 个元素的有序表 10, 20, 30, 40, 50,等概率查每个元素:
| 元素 | 顺序查找需要的比较次数 |
|---|---|
| 10 | 1 |
| 20 | 2 |
| 30 | 3 |
| 40 | 4 |
| 50 | 5 |
查找成功时:
ASL成功 = (1 + 2 + 3 + 4 + 5) ÷ 5 = 3
⚠️ 注意这里数的是"从头到尾逐个比",找到 50 要比 5 次(比完前 4 个都不同,第 5 次才相等)。有人会想成"找到就停,最后那个不算",那就错了。
概率不等时要加权,不能除以 n
如果各个元素被查找的概率不一样,就必须按概率加权。举个 3 个元素的例子:
| 元素 | 位置 | 比较次数 | 被查找的概率 |
|---|---|---|---|
| a | 第 1 个 | 1 | 0.6 |
| b | 第 2 个 | 2 | 0.3 |
| c | 第 3 个 | 3 | 0.1 |
ASL = 1×0.6 + 2×0.3 + 3×0.1 = 0.6 + 0.6 + 0.3 = 1.5
(等概率时是 (1+2+3)/3 = 2。可见把查找频率高的元素往前放,能明显降低 ASL。)
二、顺序查找:ASL = (n+1)/2 是怎么来的
顺序查找就是从头到尾挨个比。n 个元素,查第 1 个比 1 次、查第 2 个比 2 次......查第 n 个比 n 次。
ASL成功 = (1 + 2 + … + n) ÷ n
= [n(n+1)/2] ÷ n
= (n + 1) / 2
这个公式怎么读:「平均要找一半左右的位置」。n = 5 时 (5+1)/2 = 3,和上面手算的一致。
查找失败时,要把整表扫完才知道没有,所以:
ASL失败 = n
顺序查找的时间复杂度是 O(n),优点是:
-
对存储没要求,顺序表和链表都行
-
不要求元素有序
哨兵(监视哨)
把待查关键字先放到下标 0(或 n),从后往前比,循环里就不用再判断下标有没有越界了。
推荐写法:
A[0] = x; // 哨兵
i = n;
while (A[i] != x) i--;
循环结束后若 i == 0 就是查找失败,否则 i 就是位置。
⚠️ 408 里有个细节容易考:加了哨兵之后,从后往前比时,查第 n 个元素比较 1 次、查第 1 个元素比较 n 次,ASL 仍然是 (n+1)/2,只是省掉了越界判断的开销。
有序表的顺序查找可以提前终止
如果表是有序的,顺序查找时一旦发现"当前元素已经比待查关键字大了",就能直接判定失败,不必扫完整个表。
n 个元素时,失败有 n+1 种情况(元素落在 n−1 个间隙里,或者比最小的小、比最大的大):
| 落入的位置 | 比较次数 |
|---|---|
| 第 1 个位置之前 | 1 |
| 第 1、2 个之间 | 2 |
| ... | ... |
| 第 n−1、n 个之间 | n |
| 第 n 个之后 | n |
ASL失败 = [1 + 2 + … + n + n] ÷ (n + 1)
= [n(n+1)/2 + n] ÷ (n + 1)
= n(n+3) / [2(n+1)]
n = 5 时 = 40/12 ≈ 3.33,明显小于无序表的 ASL失败 = n = 5。
这个点常出判断题:「对有序表做顺序查找,查找失败时平均比较次数约为 n/2」------说法是对的。
三、折半查找的两个前提:有序 + 随机存取
折半查找(二分查找):每次拿"中间那个元素"跟待查关键字比,比完就排除掉一半,再在剩下的一半里继续。
它成立必须同时满足两个条件:
| 前提 | 原因 |
|---|---|
| 元素有序(按关键字排序) | 否则"比中间值小就往左"这个判断不成立 |
| 支持随机存取(顺序存储) | 否则拿不到"正中间那个元素" |
2024(5) 逐个验(答案 D):
| 选项 | 有序? | 随机存取? | 结论 |
|---|---|---|---|
| I 有序链表 | ✓ | ✗ 链式 | 不适合 |
| II 无序数组 | ✗ | ✓ | 不适合 |
| III 有序静态链表 | ✓ | ✗ 逻辑次序由游标决定 | 不适合 |
| IV 无序静态链表 | ✗ | ✗ | 不适合 |
2024(5) 答案:D。
⚠️ 记牢 III 这条:静态链表的下标相邻 ≠ 逻辑相邻,这是我踩的那个坑。
折半查找的代码长什么样
非递归写法(标准写法,要会认、会填空):
int BinarySearch(int A[], int n, int x) {
int low = 0, high = n - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (A[mid] == x) return mid; // 找到了
else if (A[mid] > x) high = mid - 1; // 目标在左半边
else low = mid + 1; // 目标在右半边
}
return -1; // 查找失败
}
怎么读:
-
low和high圈定的是当前还可能包含目标的下标区间 ,闭区间[low, high] -
循环条件是
low <= high(不是<)------ 因为low == high时区间里还剩一个元素没比 -
取完
mid后若A[mid] > x,说明目标只可能在左边,于是把右边界收到mid − 1 -
一路没撞上就返回 −1
⚠️ 循环条件写成 low < high 是最常见的错,那样会把最后一个元素漏掉。
递归写法(了解即可):
int BinRec(int A[], int low, int high, int x) {
if (low > high) return -1;
int mid = (low + high) / 2;
if (A[mid] == x) return mid;
if (A[mid] > x) return BinRec(A, low, mid - 1, x);
return BinRec(A, mid + 1, high, x);
}
递归版每往下走一层区间就减半,递归深度 = 判定树高度 = ⌊log₂n⌋ + 1 ,所以空间复杂度是 O(log n)(递归栈)。
四、折半查找判定树:怎么画
画法规则(按 408 通用约定,向下取整):
mid = ⌊(low + high) / 2⌋
左子树 是所有比 Amid 小的元素(下标 low ~ mid−1),右子树是比它大的(下标 mid+1 ~ high)。递归下去。
拿 11 个元素 a₁ ~ a₁₁ 举例:
| 步骤 | low | high | mid = ⌊(low+high)/2⌋ | 根 |
|---|---|---|---|---|
| 1 | 1 | 11 | 6 | a₆ |
| 2(左) | 1 | 5 | 3 | a₃ |
| 3(右) | 7 | 11 | 9 | a₉ |
| 4 | 1 | 2 | 1 | a₁ |
| 5 | 4 | 5 | 4 | a₄ |
| 6 | 7 | 8 | 7 | a₇ |
| 7 | 10 | 11 | 10 | a₁₀ |
画出来:
a₆ ← 第 1 层(比较 1 次)
/ \
a₃ a₉ ← 第 2 层(比较 2 次)
/ \ / \
a₁ a₄ a₇ a₁₀ ← 第 3 层(比较 3 次)
\ \ \ \
a₂ a₅ a₈ a₁₁ ← 第 4 层(比较 4 次)
层号就是比较次数:查 a₆ 比 1 次,查 a₂ 比 4 次。
快速画法(考试时不用递归推):判定树每层元素个数按 1、2、4、8... 翻倍,直到凑满 n 个,最后一层不满就剩多少算多少。
| 元素总数 n | 各层元素个数 | 树高 |
|---|---|---|
| 7 | 1、2、4 | 3 |
| 11 | 1、2、4、4 | 4 |
| 16 | 1、2、4、8、1 | 5 |
(校验:每行加起来都等于对应的 n ✓)
这条能让你在十秒内判断出树高,进而直接得到"最多比较次数"。
⚠️ 关于取整:有的教材用 ⌈(low+high)/2⌉(向上取整),画出来的树形会左右镜像不同,但层数分布和 ASL 结论完全一样。408 真题里的标准答案按 ⌊⌋ 处理,考试时用哪个都不会影响"最多比较几次"这类问题的答案。
五、判定树的两条性质(2015(7) 就考这个)
性质 1:判定树是一棵二叉排序树。 任意结点,左子树所有元素 < 它 < 右子树所有元素。原因很直接:折半时"小的往左、大的往右"。
性质 2:判定树是一棵平衡二叉树(任意结点左右子树高度差不超过 1)。 因为每次都是从正中间切开,两边元素个数最多差 1。
这两条性质直接催生了 2015(7) 这种题:
下列选项中,不能构成折半查找中关键字比较序列的是( )。
做法:把序列当成"查找路径",检查每一步是否落在上一步划定的区间内。
以 A 选项 500, 200, 450, 180 为例:
| 步 | 当前值 | 上一步划定的区间 | 在区间内? |
|---|---|---|---|
| 1 | 500 | (−∞, +∞) | ✓ |
| 2 | 200 | 小于 500 → (−∞, 500) | ✓ |
| 3 | 450 | 大于 200 且小于 500 → (200, 500) | ✓ |
| 4 | 180 | 应为 (200, 500),但 180 < 200 | ✗ |
180 落在了区间外面,路径不合法。
其余三项验一遍(都能通过):
-
B
500, 450, 200, 180→ 区间依次收窄为 (−∞,500) → (−∞,450) → (−∞,200),180 在内 ✓ -
C
180, 500, 200, 450→ (180,+∞) → (180,500) → (200,500),450 在内 ✓ -
D
180, 200, 500, 450→ (180,+∞) → (200,+∞) → (200,+∞),450 在内 ✓
2015(7) 答案:A。
(等价的另一种思路:把四个选项直接画成树,看是不是二叉排序树------A 中 450 和 180 都挂在 200 下面,180 < 200 却在 200 的右子树方向,违反 BST 性质。)
六、查找成功的 ASL:用判定树一层层数
还是 11 个元素的例子,各层元素个数:第 1 层 1 个、第 2 层 2 个、第 3 层 4 个、第 4 层 4 个。
ASL成功 = (1×1 + 2×2 + 3×4 + 4×4) ÷ 11
= (1 + 4 + 12 + 16) ÷ 11
= 33 ÷ 11
= 3
有个更快的公式:n 个元素、判定树高度 h 时,
ASL成功 ≈ log₂(n + 1) − 1
这个怎么读:折半查找的平均比较次数约等于「log₂(n+1) 再减 1」。n=11 时 log₂12 ≈ 3.58,减 1 得 2.58------和实际值 3 有出入,因为判定树不是满树。
⚠️ 所以这个近似式只能用来估算数量级,考场上凡是要求精确值的题,一律老老实实用判定树分层数,别拿近似式硬算。
再拿 n = 16(2010(9) 那个长度)完整算一遍,熟悉一下流程:
按 mid = ⌊(low+high)/2⌋ 逐层切分,得到的判定树各层元素个数:
| 层数 | 该层元素 | 个数 |
|---|---|---|
| 1 | a₈ | 1 |
| 2 | a₄、a₁₂ | 2 |
| 3 | a₂、a₆、a₁₀、a₁₄ | 4 |
| 4 | a₁、a₃、a₅、a₇、a₉、a₁₁、a₁₃、a₁₅ | 8 |
| 5 | a₁₆ | 1 |
(校验:1+2+4+8+1 = 16 ✓,正好等于元素总数,说明没漏没重)
ASL成功 = (1×1 + 2×2 + 3×4 + 4×8 + 5×1) ÷ 16
= (1 + 4 + 12 + 32 + 5) ÷ 16
= 54 ÷ 16
≈ 3.38
顺带验证:树高 5 = ⌊log₂16⌋ + 1,跟 2010(9) 的答案完全一致。
七、最多比较次数:两个等价公式
这是折半查找考得最频繁的计算点。
最多比较次数 = ⌊log₂ n⌋ + 1 = ⌈log₂ (n + 1)⌉
两个式子等价,挑顺手的用。
2010(9) :长度 16 的顺序表,折半查找一个不存在的元素,最多比较几次?
⌊log₂ 16⌋ + 1 = 4 + 1 = 5
2010(9) 答案:B(5)。
2023(8):600 个元素的有序顺序表,折半查找关键字比较次数最多是多少?
log₂ 600 ≈ 9.23
⌊9.23⌋ + 1 = 9 + 1 = 10
(用另一个式子验:⌈log₂ 601⌉ = ⌈9.23⌉ = 10 ✓)
2023(8) 答案:B(10)。
n = 600 时,为什么是 10 次
不套公式,直接推一遍:每比较一次区间砍一半,从 600 个元素开始------
| 第 k 次比较 | 比较时区间内还有约几个元素 |
|---|---|
| 1 | 600 |
| 2 | 300 |
| 3 | 150 |
| 4 | 75 |
| 5 | 37 |
| 6 | 18 |
| 7 | 9 |
| 8 | 4 |
| 9 | 2 |
| 10 | 1 |
第 9 次比较时还剩 2 个元素,必须再比 1 次才能确认,所以是 10 次。
(跟 ⌈log₂601⌉ = 10 对得上------手算和公式互为印证,考场上任取一种都行。)
⚠️ 三个提醒:
-
"最多比较次数"等价于"判定树的高度",别把它和 ASL 混为一谈(ASL 是平均值,最多比较次数是最大值)。
-
题目说"查找不存在的元素"时,比较次数仍是判定树高度------因为不成功的查找也要一路比到叶子。
-
看到
⌈x⌉不认识?它叫上取整 (读作"ceiling"),就是"向上取到最近的整数";⌊x⌋是下取整(floor),向下取。
八、查找失败时,比较次数怎么数
失败的情况要引入外部结点(也叫"失败结点"):在判定树所有空指针的位置补上方形结点,每个方形结点代表一次失败的查找。
算法只有一句:
ASL失败 = (所有外部结点的层数之和) ÷ 外部结点个数 (外部结点个数 = n + 1)
拿第 4 节那棵 11 元素的判定树,把 12 个外部结点逐个标出来:
| 外部结点挂在谁的下面 | 层数 | 个数 |
|---|---|---|
| a₁、a₄、a₇、a₁₀ 的左侧空位 | 4 | 4 个 |
| a₂、a₅、a₈、a₁₁ 的左右两个空位 | 5 | 8 个 |
层数之和 = 4×4 + 5×8 = 16 + 40 = 56
ASL失败 = 56 ÷ 12 ≈ 4.67
对照一下成功的情况(第 6 节算过 ASL成功 = 3)------失败时要高不少,原因很直观:成功的查找可能半路就撞上了,失败的查找必须一路比到底才能确认"确实没有"。
⚠️ 两个提醒:
-
别拿近似式硬套 。
ASL失败 ≈ log₂(n+1)这类式子只在判定树接近满树(n = 2^h − 1)时才准,遇到 n = 11、n = 600 这种"不凑整"的数字会明显失真。考场上要求精确值时,一律画判定树数层数。 -
外部结点个数恒等于 n + 1,可以用它快速校验自己有没有数漏。
九、分块查找:两块 ASL 相加
分块查找(索引顺序查找):把 n 个元素分成若干块,满足
-
块间有序:第 i 块的最大关键字 < 第 i+1 块的最小关键字
-
块内无序:每块内部随便排
查找分两步:
-
先确定在哪个块(在索引表里查,可以顺序查也可以折半查)
-
再在块内查找(块内无序,只能顺序查)
于是:
ASL = ASL块间 + ASL块内
设总元素 n、每块 s 个元素,则块数 b = n / s。
都用顺序查找时(索引表无序、块内也无序):
ASL = (b + 1) / 2 + (s + 1) / 2
索引表用折半查找、块内顺序查找时:
ASL = ⌈log₂(b + 1)⌉ + (s + 1) / 2
举个完整的例子
n = 100 个元素,分成 10 块、每块 10 个。索引表和块内都用顺序查找:
ASL = (b + 1)/2 + (s + 1)/2 = 11/2 + 11/2 = 5.5 + 5.5 = 11
把索引表改成折半查找(索引表本来就是有序的):
ASL = ⌈log₂(10 + 1)⌉ + 11/2 = 4 + 5.5 = 9.5
省了 1.5 次。所以索引表能用折半就用折半------它没有"必须顺序查"的理由,放着 O(log n) 不用是浪费。
十、最优块长为什么是 √n(2025(7) 完整推导)
2025(7):400 个元素,等概率,均匀分块,块间和块内都用顺序查找,为效率最高每块应包含几个元素?
设每块 s 个,块数 b = n / s,代入上面的公式:
ASL(s) = (b + 1)/2 + (s + 1)/2
= (n/s + 1)/2 + (s + 1)/2
= (n/s + s)/2 + 1
要让 ASL 最小,就是让 n/s + s 最小。这里用均值不等式(高中数学):
对于正数 a、b:a + b ≥ 2√(ab),等号在 a = b 时成立
取 a = n/s、b = s:
n/s + s ≥ 2√(n/s · s) = 2√n
等号成立的条件:n/s = s,即
s² = n → s = √n
代入 n = 400:
s = √400 = 20
如果不太想得起来均值不等式,用求导也能得到同一个结论:
f(s) = n/s + s
f′(s) = −n/s² + 1
令 f′(s) = 0 → n/s² = 1 → s² = n → s = √n
两种方法结果一致,考场上写哪种都行。我更推荐均值不等式------不用写导数符号,两步就出结论。
2025(7) 答案:C(20)。
⚠️ 常见的两个错法:
-
把"块数"当成答案 → 400/20 = 20 块,这题凑巧也是 20,但换个 n 就露馅了(比如 n=100 时 s=10、块数=10,也凑巧;n=1000 时 s≈31.6、块数≈31.6,就不一样了)。记住求的是"每块元素数 s"。
-
忘了 ASL 里那两个
+1,直接对n/s + s求导------结论一样(s=√n),但考试要写的是 ASL 的完整表达式,别把 +1 漏掉。
十一、跳跃查找:步长不为 1 的顺序查找(2016(9))
2016(9) 给了一段伪代码,思路是"每次跳 3 个位置":
k = 0;
while (k < n 且 A[k] < x) k = k + 3;
if (k < n 且 A[k] == x) 查找成功;
else if (k - 1 < n 且 A[k - 1] == x) 查找成功;
else if (k - 2 < n 且 A[k - 2] == x) 查找成功;
else 查找失败;
读法:先每 3 步跨一次往前冲,等 A[k] >= x 时停住,然后往回退最多 2 个位置逐个检查。这是顺序查找的变种,步长从 1 变成 3。
题目问:与折半查找相比,什么情况下它可能比较次数更少?
-
x 在靠近开头处 :跳不了几步就停了,总比较次数很少 → 答案 B
-
x 在结尾处:要跳 n/3 次,比折半的 log₂n 次多得多 ✗
-
x 在中间:跳 n/6 次,同样吃亏 ✗
-
x 不在数组中:跳跃查找也要跳到尾部 ✗
2016(9) 答案:B(当 x 接近数组开头处)。
⚠️ 这道题不需要精确计算,定性判断就够:跳跃查找省时间的前提是"目标在前面"。
十二、三种线性查找横向对比
| 项目 | 顺序查找 | 折半查找 | 分块查找 |
|---|---|---|---|
| 存储要求 | 顺序 / 链式都行 | 必须顺序存储 | 顺序 / 链式都行 |
| 有序要求 | 不要求 | 必须有序 | 块间有序、块内无序 |
| ASL(成功) | (n+1)/2 | ≈ log₂(n+1) − 1 | (b+1)/2 + (s+1)/2 |
| 时间复杂度 | O(n) | O(log n) | O(√n)(取最优块长时) |
| 适用场景 | 无序、链表 | 有序顺序表 | 块间有序的大表 |
一句口诀:"顺序随便、折半挑剔、分块折中"。
怎么选?按这个顺序问自己:
-
数据是有序的吗?无序 → 只能顺序查找,折半和分块都用不了
-
是顺序存储还是链式存储?链式 → 折半直接出局(随机存取这条不满足)
-
数据量很大、又能在插入时维持块间有序 → 分块查找更划算
-
有序 + 顺序存储 + 很少插入 → 折半查找,O(log n) 最优
十三、易错点集中讲
1. 把"最多比较次数"当成 ASL。 最多比较次数 = 判定树高度(最大值),ASL = 平均值。二者差的不是一星半点。
2. 折半查找忘了"必须随机存取"。 链式存储的有序 链表照样不能折半------2024(5) 的 I 和 III 就是两个典型。
3. 分块查找求的是"每块元素数 s",不是"块数 b"。 2025(7) 里 n=400 时 s=20、b=20,数值恰好相同,极易掩盖概念错误。
4. 顺序查找的比较次数数到 n 就停。 查最后一个元素要比较 n 次(不是 n−1 次),ASL 才是 (n+1)/2。
5. 用近似式硬算非满树的 ASL。 n = 600 这类"不凑整"的数字,近似式误差很大,必须画判定树分层数。
十四、速查表(文末收藏版)
三个 ASL 公式
| 查找方式 | 查找成功 ASL | 备注 |
|---|---|---|
| 顺序查找 | (n + 1) / 2 | 查找失败 = n |
| 折半查找 | 用判定树分层数计算 | 近似式 ≈ log₂(n+1) − 1 |
| 分块查找 | (b + 1)/2 + (s + 1)/2 | b = n/s;最优 s = √n |
折半查找的两个关键量
| 项目 | 公式 |
|---|---|
| 最多比较次数(= 判定树高度) | ⌊log₂ n⌋ + 1 = ⌈log₂(n+1)⌉ |
| 外部结点个数 | n + 1 |
| 判定树性质 | 二叉排序树 + 平衡二叉树 |
折半查找的两个前提
① 元素有序(按关键字排列)
② 支持随机存取(顺序存储;链式、静态链表都不行)
真题考频(2010---2025,线性查找 6 道)
| 年份(题号) | 考点 |
|---|---|
| 2010(9) | n=16 折半查找不存在的元素,最多 5 次 |
| 2015(7) | 判定树比较序列合法性(答案 A) |
| 2016(9) | 跳跃查找,x 靠近开头时更快 |
| 2023(8) | n=600 折半查找最多比较 10 次 |
| 2024(5) | 不适合折半的四种结构(答案 D,全都不行) |
| 2025(7) | 分块查找最优块长 √400 = 20 |
一句话记忆
折半查找的前提是"有序 + 能一步跳到中间",缺一个都不行;最多比较次数 = ⌊log₂n⌋+1;分块查找最优块长 = √n。
