(d4)有序向量:二分查找(改进)

官官方信息技术老师·17 页·深入(追求细节与边界)·0 次浏览·38 天前
有序向量二分查找版本演进边界细节

二分查找的改进

看完你能说清版本B、C的边界处理差异及为何更优

按 空格/→ 演示下一步

问一句
1 / 17 页

全部页面点击任意一页,跳回舞台从这页播放

有序向量二分查找版本演进边界细节

二分查找的改进

看完你能说清版本B、C的边界处理差异及为何更优

1第 1 页 · 二分查找的改进

回顾:基础二分查找

上一页我们说要改进二分查找,那先回到原版长什么样:每次取区间中点比一下,根据结果三分岔——相等就中、太小往右、太大往左。看清它的比较方式和区间收缩,才知道哪里值得动手。

三分支比较
每次与中点比对,产生相等、小于、大于三种走向
区间收敛
无论走哪个分支,查找区间每轮至少缩短一半
终止条件
区间缩到空仍找不到,才判定元素不存在
深版的突破口
三个分支中「小于」分支常被浪费,这是改进的入口
翻字典查单词对应 →二分查找

翻到中间页对比:目标词靠前就扔掉后半本,每次折半

2第 2 页 · 回顾:基础二分查找

版本 A 的代价

把基础版二分查找的每步比较摊开算账,暴露转向次数是隐藏的代价。

1
每次取中点
lo+hi 除以 2,取 A[mid] 与目标比较一次,判定大小关系
2
三个分支
相等返回;小于则转向左半段;大于转向右半段——方向不止两种
3
转向成本
左右两个方向各有转向,转向次数可达 log n 次,最坏情况次数多
4
总结瓶颈
每次迭代 1~2 次比较,成功查找平均比较次数约为 log₂n − 1,仍有优化空间
3第 3 页 · 版本 A 的代价

改进的构思

前面算过:版本 A 每深入一层要比两次——先问大小,再确认相等。可相等是极小概率事件,第二次比较岂不是白做了?

两次比较的浪费
每层先比小于、再比大于,绝大多数情况都走向「不相等」分支
砍到一次比较
每层只问 mi 是否小于 x,是则进右侧,否则进左侧
相等的代价
左侧命中后还需回看一次判断相等,只对极少数相等情形多花一次
海关检查对应 →单次比较改造

绝大多数货物走快速通道,少数可疑的才回头细查

版本A:每层 2 次比较⇒改进后:每层 1 次比较\text{版本A:每层 }2\text{ 次比较} \quad\Rightarrow\quad \text{改进后:每层 }1\text{ 次比较}
4第 4 页 · 改进的构思

版本 B 的策略

顺着箭头看:每个节点是一次判断,两条边只做一次比较,区间随之向左或向右收缩。

图解渲染中…
b1:mid 只做一次 x 与 A[mid] 的比较,不做第二次。c2:“其余”包含相等:相等也归入右半,避免二次判断。d1:hi=mid:轴点留在区间内,可能就是答案。d2:lo=mid+1:轴点已排除,区间严格变小。
5第 5 页 · 版本 B 的策略

版本 B 的实现

cpp

C++ 代码展示版本 B:三分支合并为两分支,全靠循环不变式。

代码高亮加载中…

循环条件 1 < hi-lo 保证区间宽度递减仍可继续;第4行把相等并入右分支,用 hi=mi / lo=mi 保持不变式 A[lo]<=e<A[hi]。

6第 6 页 · 版本 B 的实现

版本 B 的语义

版本 A 失败时只答「没找到」,白跑一趟。版本 B 约定:不管中不中,都返回位置 mi。那失败时这个 mi 是什么?得把语义说清楚,调用方才能放心用。

命中时
mi 即目标元素所在下标,直接返回
失败时
mi 指向最后一个小于 e 的元素——e 若插入应在 mi 右侧
语义统一
两种结果合并为一条:返回不大于 e 的最后一个位置
约定收益
查找失败的同时给出插入点,查找与插入一次完成
停车场管理员对应 →失败时的 mi

没找到空位时,管理员指给你「最后一个被占的车位」

7第 7 页 · 版本 B 的语义

版本 A vs 版本 B

两个版本形似神异:比较次数从 1+x 降到 1,但语义和后继成本都变了,容易混。

版本 A(三分支)
  • 每次迭代 1 次相等比较 + 1 次大小比较
  • 命中即返回,平均查找更早结束
  • 语义:MI/hi 处可直接命中返回
版本 B(两分支)
  • 每次迭代只做 1 次大小比较,无相等判断
  • 迭代次数固定为 ⌈log₂n⌉,与命中位置无关
  • 语义:仅收敛到 MI,命中须额外验证
看单次命中速度选 A;看稳定最坏性能与有序向量通用性(含 insert/remove)选 B。
8第 8 页 · 版本 A vs 版本 B

版本 B 遗留的问题

前页对比表已经显示:版本 B 平均省了比较次数,但仔细看两栏——命中左侧时每次都要深度比较,右侧却常一眼即中。省下的次数不等于省下的时间,这份「偏科」值得深挖。

左侧分支:深比较
向左收缩需比较到最深层,vector 元素规模大时代价陡增
右侧分支:浅比较
向右收缩仅比较一次就返回,多数情况浅尝辄止
成本不对称
两种比较的耗时可能差一个量级,比较次数少未必更快
何时才真快
仅当元素比较本身便宜(如基本类型)时,B 才稳定占优
改进方向
让左右分支的比较成本对称,才谈得上彻底均衡
搬箱子上两条坡道对应 →左右分支成本

一条陡一条缓,走的次数少不等于先到山顶

9第 9 页 · 版本 B 遗留的问题

fib 式划分的构思

版本 B 按几何中点切,每次比较却代价不同——往深了猜一次很便宜,往浅了猜一次很贵。既然比较代价不均,为何不按代价均分区间?这就是 fib 式划分的出发点。

代价不均
BST 中向左深入代价小,向右回退代价大
按深度切分
让两侧子树深度相近,而非长度相近
黄金分割点
切分点取在黄金比例处,深度两边接近
Fib 数刻画
fib(k)-1 恰好是深度为 k-1 的最大区间长度
按重量切金条对应 →fib 式划分

不看切在哪、看两边花费是否对等

fib 区间:  n=fib(k)−1⇒深度恰为 k−1\text{fib 区间}:\; n=\text{fib}(k)-1 \Rightarrow \text{深度恰为 } k-1
10第 10 页 · fib 式划分的构思

版本 C 的流程

从入口区间出发,每轮按 fib 比例取 mi、只做一次比较,再深入左半或右半,循环至区间宽度为 1。

图解渲染中…
b1:与 A[mi-1] 比较是每轮唯一一次比较c1:向左深入含 mi-1,故新上界为 mi-1c2:向右深入从 mi 起,mi-1 已被排除e1:返回不大于 e 的最后一个元素下标
11第 11 页 · 版本 C 的流程

版本 C 的实现

cpp

版本 C 的代码:fib 式划分点如何计算与分支。

代码高亮加载中…

划分点不再取中点,而按 fib(k-1) 偏移;向左走只需多算 fib(k-2),右侧 fib 更短——用加法换掉除法。

12第 12 页 · 版本 C 的实现

正确性论证

前面三个版本的代码都写出来了,但「能跑」不等于「算得对」。这一页我们换个视角:不逐个试例子,而是找到一个贯穿全程的不变量,一次性证明版本 C 必然正确终止。

不变性
每次迭代开始前都成立:若 e 存在,必在区间 [lo, hi) 内
初始成立
开始时 [lo, hi) 即整个向量,e 存在显然落在其中
保持成立
按与 e 的大小关系排除无希望的子区间,e 若在仍留区间内
终止正确
区间宽度按斐波那契数递减至有限,出口时区间已定,结论必然可靠
缩小包围圈搜山对应 →循环不变性

每一轮搜完,目标若在山里就必在剩余包围圈内

13第 13 页 · 正确性论证

三个版本全景对比

A、B 都靠元素比较决定走向,C 却靠减法划界——两者语义与代价结构截然不同,值得系统对照。

版本 A / B:比较式
  • 每步做一次元素比较,按结果二分支
  • 命中即返回,语义是『找到与否』
  • 最坏约 log₂n 次比较,B 稳定不退化
版本 C:减治式
  • 每步只做一次减法,单分支确定一侧
  • 到深度即停,语义是『最佳插入位置』
  • 最坏约 log_φn 次减法,分摊更省
当需要查找命中语义时选 A/B;当只关心不-success时的插入位置时选 C,因其语义本来就是后继定位。
14第 14 页 · 三个版本全景对比

自测:版本选择

点击作答

在有序向量上查找目标 e,若有多个命中位置,要求返回最靠后的那个,应选用哪个版本及理由?

15第 15 页 · 自测:版本选择

要点收束

  • ✓查找区间的语义决定了边界写法:往右缩进才安全
  • ✓中点后移一位,最坏情况从 log n+1 降一档
  • ✓fib 划分只用加减比较,代价换常数收益
  • ✓正确性靠不变式:目标始终落在 (lo, hi) 之内
  • ✓对比版本是收敛起点:三分支减到两分支是主线
延伸主题:向有序向量插入元素二分查找的批量去重散列表:另一条查找路线
16第 16 页 · 要点收束

课后思考

先自己想一阵,再看参考答案——答案只是起点。

1版本 B 返回的「不大于 x 的最后一个元素」,这个语义为什么比「找到才返回」更有用?

参考答案回顾核心:语义统一后无需区分命中与失败,秩总在 [0, n],可直接作为插入点、前驱查询等接口,调用方少写分支。

2如果待查区间里元素全是重复的,三个版本的行为各有什么不同?

参考答案应用层思考:版本 A 命中哪个不确定;版本 B 稳定返回最右侧不大于者;fib 版本只改变切分点,不改变语义,结果与 B 一致。

31.44 倍的常数差距很小,什么时候它才真正值得在意?

参考答案迁移到成本模型:比较代价高(字符串、磁盘 IO、缓存不友好)时每次比较都贵,常数才放大为可感知差距;否则 B 已够用。

17第 17 页 · 课后思考
(d4)有序向量:二分查找(改进) · 知识图解