Appearance
折半查找(二分查找)
2026 大纲 六(四)折半查找法。
两个前提,各封杀了一类结构
折半查找每比较一次就断言"目标只可能在左半边"或"只可能在右半边",一刀砍掉一半候选。能这么断言,靠的是两个前提,缺一不可:
前提一:表必须有序。 唯一依据是"左半边全部小于
前提二:必须顺序存储。 折半要在
比直接顺序扫一遍的
🔴 静态链表也不行,这一条最容易漏。 静态链表是用数组模拟的链表,结点确实住在数组里,但邻接关系靠
next字段维护,物理下标连续 ≠ 逻辑顺序连续。有序静态链表里逻辑上的第 1 个元素可能在下标 7、第 2 个在下标 2,要取"逻辑上的中间元素"必须从首结点沿next走步,不能直接 arr[mid]。"数组实现的"不等于"能随机存取的"。
由这两条还顺带得到一个结论:折半查找不适用于动态查找表。顺序有序表插一个元素平均要移动一半元素,
先动手看一眼
代码与三处边界
c
// a[0..n-1] 为升序有序数组,返回下标;返回 -1 表示查找失败
int BinarySearch(int a[], int n, int key) {
int low = 0, high = n - 1, mid;
while (low <= high) { // <= :low == high 时区间里还剩一个元素,必须查
mid = low + (high - low) / 2; // 等价于 (low+high)/2,但避免 low+high 溢出
if (a[mid] == key)
return mid;
else if (a[mid] > key)
high = mid - 1; // a[mid] 已被排除,所以是 mid-1 而不是 mid
else
low = mid + 1; // 同理,保证区间严格缩小,循环必然终止
}
return -1; // 区间为空,查找失败
}| 写法 | 写错会怎样 |
|---|---|
while (low <= high) | 写成 < 时区间收缩到只剩一个元素(low == high)就退出,最后一个候选没被检查。a = {5}、key = 5:0 < 0 为假直接返回 -1,漏判 |
high = mid - 1 / low = mid + 1 | 写成 high = mid 或 low = mid,区间只剩两个元素时 mid 恒等于 low,区间不再缩小,死循环 |
mid = low + (high-low)/2 | 直接写 (low+high)/2 在两端都接近 INT_MAX 时加法溢出成负数,a[mid] 越界。408 的表长不会这么大,写 (low+high)/2 不算错 |
空表:n = 0 时 low = 0、high = -1,循环条件为假直接返回 -1,正确。
判定树与最坏比较次数
构造方法就是把算法递归展开:对区间
mid 向下取整时得到的树:
[29] ← 第 1 层
/ \
[13] [37] ← 第 2 层
/ \ / \
[7] [16] [32] [41] ← 第 3 层
\ \ \ \
[10] [19] [33] [43] ← 第 4 层第 3 层的四个结点都来自只剩 2 个元素的区间,所以都只有右孩子——这不是画错,是向下取整的必然结果。
逐层统计得
树高就是最坏比较次数,而这个数被真题直接问过两次:
两式对一切
上手就用:
⚠️ 问"查找一个不存在的元素最多比较几次",答案同样是
判一个序列是不是合法的比较序列
题目给四个数列,问哪个不能构成折半查找的比较序列。最快的方法不是去画树,而是维护一个可行区间:
初始区间
。比较过 之后:若下一个 (往左走),区间收成 ;若 (往右走),区间收成 。 任何时刻,下一个被比较的数都必须落在当前区间内,否则这个序列非法。
道理很直白:比较序列就是判定树里从根往下走的一条路径,而判定树是一棵二叉排序树——一旦你往某个结点的右子树走了,后面遇到的每一个数都必须比它大,反之亦然。
拿 500, 200, 450, 180 走一遍:
| 步 | 比较 | 走向 | 新区间 |
|---|---|---|---|
| 1 | 500 | — | |
| 2 | 200 < 500 | 左 | |
| 3 | 450 > 200 | 右 | |
| 4 | 180 < 450 | 左 → 须落在 | ❌ |
第 3 步走了 200 的右子树,此后所有数都必须大于 200,可第 4 步冒出个 180——非法。
对照一个合法的 180, 500, 200, 450:区间依次是
判一棵树是不是合法的判定树
另一种问法给四棵二叉树,问哪棵可能是折半查找的判定树。判断条件有四条,前三条好想,第四条最容易漏:
- 中序遍历有序(它是一棵 BST)。题目画空结点时这条自动满足。
- 每个内部结点,左右子树的结点数之差不超过 1。
- 每个内部结点,左右子树的高度差不超过 1(它是平衡的)。
- 🔴 整棵树的
mid取法必须前后一致。 向下取整就全程向下取整,向上取整就全程向上取整——不能这棵子树用一种、那棵子树用另一种。
第 4 条落到图上就是一句可直接目测的话:最底层那些"只有一个孩子"的结点,它们的孩子要么全挂在左边,要么全挂在右边。出现"左半边的叶子挂左孩、右半边的叶子挂右孩"这种镜像混搭,就是混用了两种取法,不合法。
真题里被排除的三个选项,全都栽在第 4 条上——前三条它们都满足。
取整方向:改比较次数,不改 ASL
| 向下取整 | 向上取整 | |
|---|---|---|
| 每层的结点个数 | 1, 2, 4, 4 | 1, 2, 4, 4(相同) |
| 3 | 3(相同) | |
| 查 10 需要几次 | 4 次(29→13→7→10) | 3 次(29→13→10) |
| 查 7 需要几次 | 3 次 | 4 次 |
为什么必然相同(不是这个例子的巧合):设当前区间有
⚠️ 所以取整方向的真正影响是改变了某个具体元素的比较次数。题目问"查找
需要比较几次"时必须先确认取整方式;问"ASL 是多少"时两种取整同一个答案。
还有一条常被忽略的性质:判定树的形态只由元素个数
逐区间展开画树、逐层与逐外部结点求和的全过程(第一次学、或想手动模拟时展开)
以有序表 mid 向下取整。
| 区间 | 取到的元素 | 左子区间 | 右子区间 | |
|---|---|---|---|---|
| 5 | 29 | |||
| 2 | 13 | |||
| 8 | 37 | |||
| 0 | 7 | 空 | ||
| 3 | 16 | 空 | ||
| 6 | 32 | 空 | ||
| 9 | 41 | 空 |
以
成功 ASL——逐层统计:
| 层数 | 结点 | 结点数 |
|---|---|---|
| 1 | 29 | 1 |
| 2 | 13, 37 | 2 |
| 3 | 7, 16, 32, 41 | 4 |
| 4 | 10, 19, 33, 43 | 4 |
失败 ASL——逐个外部结点,比较次数等于父结点层数:
| 失败区间 | 探测路径 | 比较次数 |
|---|---|---|
| 29→13→7→左空 | 3 | |
| 29→13→7→10→左空 | 4 | |
| 29→13→7→10→右空 | 4 | |
| 29→13→16→左空 | 3 | |
| 29→13→16→19→左空 | 4 | |
| 29→13→16→19→右空 | 4 | |
| 29→37→32→左空 | 3 | |
| 29→37→32→33→左空 | 4 | |
| 29→37→32→33→右空 | 4 | |
| 29→37→41→左空 | 3 | |
| 29→37→41→43→左空 | 4 | |
| 29→37→41→43→右空 | 4 |
第 3 层的 7、16、32、41 各只有右孩子,各贡献 1 个空左子树 → 4 个"比较 3 次";其余 8 个来自第 4 层四个叶结点各自的两个空子树。4 + 8 = 12 = n+1 ✓
树高与两个 ASL 公式的完整推导(不想只背结论就展开)
树高:判定树是平衡的,除最后一层外每层都填满。高度为
由右半边得
成功 ASL:取最整齐的情形
利用恒等式
代入
失败 ASL:满二叉树情形下所有
⚠️ 失败 ASL 的分母是
判定树的形态规律与复杂度逐项来历(手画判定树、或要核对量级时展开)
| 性质 | 说明 | 怎么用 |
|---|---|---|
| 判定树是一棵平衡二叉排序树 | 中序遍历得到原有序表;任意结点左右子树高度差不超过 1 | 可以直接套用平衡二叉树的高度分析 |
| 向下取整时,若区间元素数为偶数,左子树结点数比右子树少 1 | 区间大小 | 手画时先算左右子树各几个结点,比一格一格数快 |
| 成功查找的比较次数 = 该元素所在的层数 | 从根走到它经过几个内部结点 | 逐层统计即可求成功 ASL |
| 失败查找的比较次数 = 该外部结点父结点的层数 | 外部结点不含关键字,不参与比较 | 逐个外部结点统计即可求失败 ASL |
| 指标 | 复杂度 | 怎么来的 |
|---|---|---|
| 最好时间 | 第一次取的 mid 恰好命中 | |
| 最坏时间 | 比较次数等于判定树高度 | |
| 平均时间 | ||
| 空间 | 只用 low、high、mid 三个变量;递归写法是 |
⚠️ 空间那一行的括号是常被忽略的一处:非递归实现
变体:在两个等长升序序列里找中位数(大题走法)
真题考过一道设计题:两个等长的升序序列
设
:它就是答案,返回。 :中位数不可能落在 的左半段,也不可能落在 的右半段——丢掉 的左半、 的右半。 :对称地,丢掉 的右半、 的左半。
每轮把两段同时减半,直到各剩 1 个元素,返回两者中较小的那个。
⚠️ 两处细节:① 丢弃时要保证两段仍然等长,段长为偶数和奇数时舍去的边界不一样,写代码时要分开处理;② 题面对"中位数"的定义是合并后第
时间
易混淆知识点
| 易混对象 | 区别 | 判别依据 |
|---|---|---|
| 判定树 vs 二叉排序树 | 判定树由 | 问"树形唯一吗":判定树唯一,BST 不唯一 |
| 折半查找 vs 折半插入排序 | 后者是用折半定位插入位置的排序方法,仍要移动元素 | 见折半插入排序:比较降到 |
考点速记
三条结论:
- 最坏比较次数 = 判定树高度
,查不存在的元素也是这个数。 - 判定树的形态只由
与取整方式决定,与表里装什么数无关;取整方向只改变个别元素的比较次数,不改变 ASL。 - 两个前提分别封杀了无序数组与一切链式结构(含静态链表)。
这一节在真题里被考过的形式(下方「真题练习」逐题对应):
- 求最多比较次数:给表长(16、600 这类数)问关键字比较次数最多是几。直接算
——16 个元素是 5,600 个元素是 10。题面说"查找一个不存在的元素"也不改变答案。 - 判某序列不能构成比较序列:四个数列选一个非法的。维护可行区间
往下走,出现落在区间外的数即非法。 - 判某二叉树可能是判定树:四棵树选一个合法的。前三条条件(BST、左右子树结点数差 ≤1、高度差 ≤1)多数选项都满足,真正起作用的是第四条——整棵树的
mid取法必须一致,看底层叶子是不是全挂同一侧。 - 哪些结构不适合直接折半查找:有序链表 ✗、无序数组 ✗、有序静态链表 ✗、无序静态链表 ✗——四个全不行。静态链表那一项最容易误判成"可以"。
- 与其他查找策略比较次数的对比:给一段"每次跳 3 格往后扫"的伪代码,问它在什么情形下可能比折半更少比较——答案是目标接近数组开头时(跳查从头走,前几步就能命中;折半永远先比中间)。
- 双数组折半求中位数(大题):两个等长升序序列求合并后的中位数,
时间、 空间。
易错:有序静态链表不能折半查找。 它虽然存在数组里,但逻辑顺序由
next串起,物理下标不是逻辑序号,取不到"中间那个"。
易错:失败 ASL 的分母是
。 用 去除会把 算成 。
易错:判定树合法性的关键在"取整方式全树一致"。 只查结点数差和高度差,三个错误选项都会被放过。
易错:递归实现的空间是
,不是 。 问"空间复杂度"时要先看题目给的是迭代还是递归。
教材出处
- 严蔚敏《数据结构(C 语言版)》(第 2 版)7.2.2 节「折半查找」,p193: "折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列"。
- 同书 p195:折半查找判定树,并给出与本篇完全相同的算例——长度为 11 的有序表, "比较 1 次的只有一个根结点,比较 2 次的有两个结点,比较 3 次和 4 次的各有四个结点",
。 - 同书 p196:判定树深度为
;外部结点与内部结点的定义; 满二叉树情形下的求和推导得 , 较大时近似为 ;以及"折半查找不适用于数据元素经常变动的线性表"。
相关知识
查找基本概念(判定树与"失败情况共