本文对应你当前看的这一部分:
7.1 查找基本概念 → 7.2 顺序查找、折半查找、分块查找 。
重点不是背代码,而是弄清:适用条件、查找过程、ASL、判定树、复杂度和三种方法的区别。
一、先建立整体框架
这部分实际上只有三种思路:
顺序查找:一个一个找
折半查找:每次砍掉一半
分块查找:先找哪一块,再到块内找
对应效率大致是:
顺序查找:O(n)
折半查找:O(log n)
分块查找:介于二者之间
但它们的适用条件不同,不能只看复杂度。
二、查找的几个基本概念
1. 查找表
用于查找的数据集合称为:
查找表
查找结果通常分成:
查找成功
查找失败
2. 关键字
能够标识数据元素的数据项称为:
关键字 key
如果一个关键字可以唯一标识一个元素,就叫:
主关键字
3. 静态查找表和动态查找表
静态查找表只进行:
查询
典型:
顺序查找
折半查找
动态查找表还会进行:
插入
删除
后面会涉及:
二叉排序树
AVL树
B树
散列表
你当前这一部分主要还是:
静态查找
三、平均查找长度 ASL
ASL = Average Search Length,平均查找长度。
定义:
ASL = Σ(Pi × Ci)
其中:
Pi:查找第i个元素的概率
Ci:找到第i个元素所需的关键字比较次数
如果所有元素等概率:
Pi = 1/n
则:
ASL = 所有元素比较次数之和 / n
做ASL题时不要先背公式,先问:
每个元素到底比较几次?
四、顺序查找
核心思想:
从一端开始,一个一个比较。
若相等:
查找成功
扫完整张表仍找不到:
查找失败
五、为什么顺序查找常设置"哨兵"
典型写法:
ST.elem[0] = key
然后从末尾往前找:
for(i = n; ST.elem[i] != key; --i);
这样不用在循环中反复判断:
i是否越界
作用:
减少边界判断
注意:
哨兵不是正常数据元素。
六、一般线性表顺序查找的ASL
假设:
n个元素
从后往前查
各元素等概率
成功比较次数可能是:
1,2,3,...,n
所以:
ASL成功
= (1+2+...+n)/n
= (n+1)/2
结论:
顺序查找成功ASL = (n+1)/2
如果采用你教材中的哨兵写法,查找失败时会比较:
n个真实元素 + 1次哨兵
所以:
ASL失败 = n+1
注意:不同教材是否把哨兵比较算进去可能不同;按你当前教材体系记 n+1。
七、有序表的顺序查找
如果表按关键字升序排列,例如:
10 20 30 40 50
查找:
25
扫描到:
30 > 25
即可判断:
25不可能在后面
所以可以提前失败。
关键区别:
无序顺序查找:
失败通常要扫完整表
有序顺序查找:
遇到第一个大于key的元素即可失败
八、折半查找:这部分的绝对重点
折半查找又叫:
二分查找
核心思想:
每次拿当前查找区间的中间元素与key比较
升序表中:
key == mid
-> 成功
key < mid
-> 去左半区
key > mid
-> 去右半区
每比较一次:
查找范围大约减半
所以:
时间复杂度 O(log n)
九、折半查找的两个硬条件
必须同时满足:
① 元素按关键字有序
② 能随机访问中间位置
因此典型适用:
有序数组 / 有序顺序表
不适合:
链表
即使链表有序,也无法:
O(1)直接访问第mid个元素
所以口诀:
折半查找 = 有序 + 随机访问
十、折半查找标准过程
low = 0
high = n-1
while(low <= high){
mid = (low + high) / 2
if(A[mid] == key)
return mid
else if(A[mid] > key)
high = mid - 1
else
low = mid + 1
}
查找失败:
low > high
十一、mid向上还是向下取整
常见代码:
mid = (low+high)/2
整数除法通常向下取整。
理论上也可以规定:
向上取整
但要求:
整棵折半判定树必须使用同一种取整规则
不能某个子区间向上,另一个子区间又向下。
十二、折半查找判定树
折半查找过程可以用:
判定树
表示。
树中真实结点对应:
有序表中的关键字
而:
结点所在层数 = 查找到该元素所需比较次数
例如:
根结点 -> 比较1次
第二层 -> 比较2次
第三层 -> 比较3次
十三、判定树的性质
1. 是二叉排序树
满足:
左 < 根 < 右
因此中序遍历:
得到原来的有序序列
2. 基本平衡
因为每次都是对半分。
对n个元素:
树高 h = ceil(log2(n+1))
等价地,最大比较次数:
ceil(log2(n+1))
也常写:
floor(log2 n)+1
3. n个元素有n+1个失败位置
例如:
10 20 30
查找失败可能落在:
(-∞,10)
(10,20)
(20,30)
(30,+∞)
因此:
n个关键字
-> n+1个失败区间
十四、折半查找成功ASL
最稳的方法:
按判定树层数统计
例如7个结点构成满三层:
第1层:1个
第2层:2个
第3层:4个
则:
ASL成功
= (1×1 + 2×2 + 3×4) / 7
= 17/7
通用:
ASL成功
= Σ(层数 × 该层真实结点数) / n
十五、折半查找失败ASL
查找失败会落到:
失败结点 / 外部结点
若失败结点位于第k层,那么实际完成的是:
k-1次真实关键字比较
因此失败ASL本质上是:
各失败位置所需真实关键字比较次数的平均值
强化阶段更常考:
最多比较几次
而不是复杂的失败ASL手算。
十六、最大比较次数
直接记:
最大比较次数
=
判定树高度
=
ceil(log2(n+1))
例如:
n = 16
则:
ceil(log2 17) = 5
最多比较:
5次
十七、折半查找"比较序列"怎么判断
例如比较过程:
500 -> 200 -> 450
从500到200说明:
key < 500
从200到450说明:
key > 200
所以此时key必须在:
200 < key < 500
以后新的比较值必须仍符合当前区间。
所以这类题的本质是:
不断缩小key的合法取值区间
十八、分块查找
分块查找又叫:
索引顺序查找
它把数据分成若干块。
要求:
块内可以无序
块间必须有序
然后建立索引表。
十九、索引表记录什么
通常记录:
每块最大关键字
+
该块起始地址
查找分两步:
第1步:查索引表,确定目标在哪一块
第2步:到块内做顺序查找
所以:
分块查找 = 索引查找 + 块内查找
二十、分块查找ASL
假设:
n个元素
分成b块
每块s个
n = b×s
若:
索引表顺序查找
块内也顺序查找
则:
索引查找ASL = (b+1)/2
块内查找ASL = (s+1)/2
所以:
ASL
= (b+1)/2 + (s+1)/2
因为:
b = n/s
得到:
ASL
= (n/s + s + 2)/2
二十一、最优块大小
要让:
n/s + s
最小,最优时:
s = √n
同时:
b = √n
因此:
每块元素数 ≈ √n
此时:
ASL最小 ≈ √n + 1
二十二、三种查找方法对比
| 方法 | 是否要求有序 | 是否要求随机访问 | 复杂度 | 特点 |
|---|---|---|---|---|
| 顺序查找 | 否 | 否 | O(n) | 最通用 |
| 折半查找 | 是 | 是 | O(log n) | 快,但条件严格 |
| 分块查找 | 块间有序 | 依赖索引 | 约O(√n) | 折中 |
二十三、最容易错的地方
1. 有序就一定能折半
错。
还必须:
能随机访问
所以:
有序链表也不能直接折半
2. 分块查找要求块内有序
错。
正确:
块内可以无序
块间必须有序
3. ASL就是时间复杂度
不是。
ASL:平均比较次数
时间复杂度:增长数量级
4. 折半判定树就是任意平衡BST
不完全对。
它还必须符合:
每次mid的选取规则
二十四、最后只记三句话
顺序:
一个一个找。
折半:
有序 + 随机访问,每次砍一半。
分块:
先找块,再进块内找。
再配三条公式:
顺序成功ASL = (n+1)/2
折半最大比较次数 = ceil(log2(n+1))
分块顺序+顺序的最优块大小 = √n