折半查找
在处理有序的顺序存储查找表时,顺序查找逐个比较的方式效率较低,而折半查找能通过"分治"思想大幅减少比较次数。折半查找的核心是"每次找中间元素对比,将查找范围缩小一半",就像我们在字典中查单词------先翻到中间页,根据单词首字母判断目标在左半本还是右半本,再在缩小的范围里重复这个过程,直到找到目标或确定没有目标。不过需要注意,折半查找有两个严格前提:一是查找表必须是有序的 (如升序或降序),二是查找表必须采用顺序存储结构(如数组),因为需要随机访问中间位置的元素。
1. 折半查找的基本过程
折半查找的实现依赖三个指针(或下标):low(查找范围的起始位置)、high(查找范围的结束位置)、mid(查找范围的中间位置)。以升序数组为例,具体步骤如下:
- 初始化:
low = 0(数组起始下标),high = n-1(数组末尾下标,n为元素个数); - 循环:计算
mid = low + (high - low) / 2(避免low+high溢出),将数组mid位置的元素与目标值target比较;
1)若arr[mid] == target:查找成功,返回mid(目标位置);
2)若arr[mid] > target:目标在左半范围,更新high = mid - 1;
3)若arr[mid] < target:目标在右半范围,更新low = mid + 1; - 终止:当
low > high时,查找范围为空,返回-1(查找失败)。
我们以升序数组arr = [11, 13, 17, 23, 31, 37, 43, 53, 61, 73, 83, 97](n=12)为例,分别演示"查找成功"(target=37)和"查找失败"(target=40)的过程,并用mermaid图展示步骤:
是 是 初始化:low=0, high=11, target=37 mid=0+(11-0)/2=5, arr[5]=37 arr[mid] == target? 返回mid=5,查找成功 初始化:low=0, high=11, target=40 mid=5, arr[5]=37 < 40 更新low=6, high=11 mid=6+(11-6)/2=8, arr[8]=61 > 40 更新low=6, high=7 mid=6+(7-6)/2=6, arr[6]=43 > 40 更新low=6, high=5 low > high? 返回-1,查找失败
从图中可清晰看到:
- 查找37时,仅1次比较就找到目标(mid=5);
- 查找40时,通过3次比较缩小范围至
low=6 > high=5,最终确定失败。这种"每次缩小一半范围"的逻辑,让比较次数远少于顺序查找。
2. 折半查找的判定树
折半查找的过程可以用"判定树"直观表示------树的每个节点对应数组中的一个元素,左子树对应左半查找范围,右子树对应右半查找范围。以之前的12个元素数组为例,其判定树如下(用mermaid绘制):
37(mid=5) 17(mid=2) 61(mid=8) 13(mid=1) 23(mid=3) 11(mid=0) 空 空 31(mid=4) 43(mid=6) 83(mid=10) 空 53(mid=7) 73(mid=9) 97(mid=11)
判定树有三个关键特性:
- 平衡二叉树 :除最后一层外,每一层的节点数都满,且最后一层节点靠左排列,树的高度最小(高度
h = ⌈log₂(n+1)⌉,n=12时h=4); - 查找次数=节点深度:根节点(37)深度为1,查找它需1次比较;第二层节点(17、61)深度为2,查找需2次比较,以此类推;
- 失败节点:判定树的叶子节点(如11、31、53等)的空孩子即为"失败节点",每个失败节点对应一种查找失败的情况(如目标在11左侧、13与17之间等)。
3. 折半查找的平均查找长度(ASL)
平均查找长度(ASL)是衡量折半查找效率的核心指标,需分别计算"查找成功"和"查找失败"两种情况,计算时需结合判定树的节点深度。
(1)查找成功的ASL
假设数组中12个元素被查找的概率相等(均为1/12),根据判定树的节点深度,各元素的查找次数如下表:
| 元素 | 11 | 13 | 17 | 23 | 31 | 37 | 43 | 53 | 61 | 73 | 83 | 97 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 位置(mid) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 查找次数 | 3 | 2 | 3 | 3 | 4 | 1 | 3 | 4 | 2 | 3 | 3 | 4 |
根据ASL公式,查找成功的平均查找长度为:
ASL成功=112×(1×1+2×2+3×4+4×5)=1+4+12+2012=3712≈3.08 ASL_{成功} = \frac{1}{12} \times (1 \times 1 + 2 \times 2 + 3 \times 4 + 4 \times 5) = \frac{1+4+12+20}{12} = \frac{37}{12} \approx 3.08 ASL成功=121×(1×1+2×2+3×4+4×5)=121+4+12+20=1237≈3.08
(2)查找失败的ASL
查找失败的情况对应判定树的13个失败节点(n个元素对应n+1个失败节点),每个失败节点的深度为"其父节点的深度+1",假设所有失败情况的概率相等(均为1/13),各失败节点的查找次数如下:
- 深度2的失败节点:2个(11左侧、13与17之间),查找次数2;
- 深度3的失败节点:4个(17与23之间、23与31之间、31与37之间、37与43之间),查找次数3;
- 深度4的失败节点:4个(43与53之间、53与61之间、61与73之间、73与83之间),查找次数4;
- 深度5的失败节点:3个(83与97之间、97右侧),查找次数5。
查找失败的平均查找长度为:
ASL失败=113×(2×2+3×4+4×4+5×3)=4+12+16+1513=4713≈3.62 ASL_{失败} = \frac{1}{13} \times (2 \times 2 + 3 \times 4 + 4 \times 4 + 5 \times 3) = \frac{4+12+16+15}{13} = \frac{47}{13} \approx 3.62 ASL失败=131×(2×2+3×4+4×4+5×3)=134+12+16+15=1347≈3.62
总体来看,折半查找的时间复杂度为O(log₂n),远优于顺序查找的O(n),尤其在n较大时(如n=1000),折半查找最多只需10次比较,而顺序查找平均需500次。
4. 折半查找的算法实现
折半查找的C语言实现需注意"避免下标溢出"(用low+(high-low)/2计算mid)和"循环终止条件"(low > high),代码如下(针对升序数组):
c
// 折半查找:arr为升序数组,n为元素个数,target为目标值
int BinarySearch(int arr[], int n, int target) {
int low = 0, high = n - 1; // 初始化查找范围
while (low <= high) { // 范围非空则继续
int mid = low + (high - low) / 2; // 计算中间位置(防溢出)
if (arr[mid] == target) {
return mid; // 找到目标,返回位置
} else if (arr[mid] > target) {
high = mid - 1; // 目标在左半,缩小范围
} else {
low = mid + 1; // 目标在右半,缩小范围
}
}
return -1; // 范围为空,查找失败
}
代码说明:
- 循环条件
low <= high:确保当low == high时(范围仅剩一个元素),仍会进行比较; mid = low + (high - low)/2:等价于(low+high)/2,但能避免low+high超过int最大值导致的溢出;- 查找成功返回元素下标,失败返回-1,逻辑清晰且高效。
5. 折半查找的特点与适用场景
折半查找的优势和局限性都很明显,需结合场景选择使用:
(1)核心特点
- 优点:
- 效率高,时间复杂度
O(log₂n),是有序顺序表中效率最高的查找算法之一; - 平均查找长度小,尤其适合数据量较大的查找表(如n>100)。
- 效率高,时间复杂度
- 缺点:
- 前提严格:必须是有序的顺序存储表,链式存储无法使用(无法随机访问中间元素);
- 动态性差:若查找表需频繁插入、删除元素,每次操作后需重新排序,成本极高;
- 不适用于小数组:当n较小时(如n<10),折半查找的优势不明显,甚至不如顺序查找(省去计算mid的开销)。
(2)适用场景
- 静态有序表:如系统配置表、固定的字典数据等,一旦创建后很少修改;
- 数据量较大的有序表:如学生成绩排名表(按分数升序)、商品价格表(按价格排序)等,需频繁查询但很少增删;
- 不适合动态表:如实时更新的新闻列表、频繁添加联系人的通讯录等,这类场景更适合后续的树型查找或散列表。
综上,折半查找通过"分治缩小范围"的逻辑,在有序顺序表中实现了高效查找,其判定树直观反映了查找过程,平均查找长度远低于顺序查找。但需注意其"有序、顺序存储"的前提,在选择时需结合查找表的静态/动态特性、数据量大小综合判断。理解折半查找的原理和适用场景,能帮助我们在后续学习中更好地对比树型查找、散列表等其他查找方式的优劣。