(d3)有序向量:Fibonacci查找

官官方信息技术老师·17 页·深入(追求细节与边界)·0 次浏览·38 天前
有序向量斐波那契黄金分割复杂度

Fibonacci 查找

看完你能说清它为何比二分快、画出黄金分割切分点并推证明

按 空格/→ 演示下一步

问一句
1 / 17 页

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

有序向量斐波那契黄金分割复杂度

Fibonacci 查找

看完你能说清它为何比二分快、画出黄金分割切分点并推证明

1第 1 页 · Fibonacci 查找

二分查找的遗留问题

上一页我们看到,二分查找很好用,但有个细节:每次比较其实要做一次判断相等、再判断往左还是往右,失败时比了两次。而左右两个分支的宽度还不相等——这遗留问题正是 Fibonacci 查找的切入点。

比较的代价
成功时比较 1 次即可定位,失败时要先判不等再分方向,约 2 次
分支不等宽
中点把向量切成左右两半,但深入左右两侧的代价并不对称
不对称的启发
既然分支宽度不等,何不按不等的比例切分,让深层一侧更窄
天平称重找次品对应 →查找分支代价

轻的一侧对应比较更省的方向,切分点应向代价高的一侧倾斜

失败比较次数={1命中2未命中,再判方向\text{失败比较次数}=\begin{cases}1 & \text{命中}\\ 2 & \text{未命中,再判方向}\end{cases}
2第 2 页 · 二分查找的遗留问题

前后分支不平衡

上页看到二分查找有三次比较:小于、大于、等于。中点命中概率很小,多数时间都在往左或往右走。那这两个方向的代价一样吗?数一数就会发现,左右并不公平。

前后各半
失败前两个分支几乎等宽,各约一半元素
左转只花 1 次
mi < e 为假即判「向左」,单次比较即可
右转要 2 次
先证 mi<e 不成立,再证 mi>e,才落右侧
成本不对称
等宽却不同价,宽的一半反而更贵
分蛋糕切偏一刀对应 →前后分支不平衡

左右两块一样大,但拿右边那块要绕两次手

W(n)=1+W(⌊n/2⌋)  (左)W(n)=2+W(⌊n/2⌋)  (右)W(n)=1+W(\lfloor n/2\rfloor) \;\text{(左)} \qquad W(n)=2+W(\lfloor n/2\rfloor) \;\text{(右)}
3第 3 页 · 前后分支不平衡

让切分点偏后

上页结论:二分的两个分支深度不同,深的那条反而更常被走到。既然访问次数改不了,那就改变分法——把切分点往右挪,让常走的后段变短、变浅,代价自然降下来。

切分点后移
mi 不取中点,而落在偏右的位置上
前宽后窄
前段虽宽却只需一次比较,后段窄且更浅
Fib 定位
用 fib(k-1) 定切分点,加减即可,无需乘除
收费站排队对应 →分支代价

把更长的队排到通行更快的一侧,总耗时变短

mi=lo+fib(k−1)−1,其中fib(k−1)+fib(k−2)≥hi−lomi = lo + fib(k-1) - 1,其中 fib(k-1)+fib(k-2) \ge hi-lo
4第 4 页 · 让切分点偏后

黄金分割与 Fibonacci 数

上一页确认切分点要偏后,但偏到哪才恰到好处?把区间按黄金分割 0.618 切开:短段在前、长段在后。而这个比例,恰好能被 Fibonacci 数列越来越精确地逼近。

黄金分割点
λ≈0.618,长段与整段之比等于短段与长段之比
Fib 逼近
相邻项之比 F(k-1)/F(k) 越来越接近 λ
取切分点
mi 取在 lo + F(k-1) - 1,天然偏后
折纸找黄金比例对应 →Fib 切分

不断按比例折纸与不断用 Fib 相邻项作比,都收敛到 0.618

F(k−1)F(k)  →k→∞  5−12≈0.618\frac{F(k-1)}{F(k)}\;\xrightarrow{k\to\infty}\;\frac{\sqrt5-1}{2}\approx 0.618
5第 5 页 · 黄金分割与 Fibonacci 数

Fibonacci 数列与切分点

上一页我们看到,黄金分割能给出最合适的不平衡切分点。可是黄金分割比 0.618 是个无理数,比较时没法直接算——好在 Fibonacci 数列相邻两项之比,恰好会越来越逼近它。

比值逼近黄金分割
fib(k-1)/fib(k) 随 k 增大趋近 0.618
用 Fib 数定宽度
取区间长度为 fib(k),前段恰有 fib(k-1) 个元素
切分点公式
mi = lo + fib(k-1) - 1,偏后且只需加减法
圆周率的割圆术对应 →Fib 逼近黄金比

正多边形边数增加逼近圆周率,如同 Fib 相邻项之比逼近 0.618

mi=lo+fib(k−1)−1mi = lo + \mathit{fib}(k-1) - 1
6第 6 页 · Fibonacci 数列与切分点

算法流程

Fibonacci 查找的每一轮:按 fib 数定切分点,比较后向左或向右收缩区间。

1
定切分点
取 mi = fib(k-1)-1,让切分点偏后,与前段 fib(k-1) 对齐
2
比较
将目标 e 与 A[mi] 比一次,结果只有三种走向
3
e 小转左
目标在前段 [lo, mi),新宽度恰为 fib(k-1),继续递归
4
e 大转右
目标在后段 (mi, hi),新宽度为 fib(k-2),走更宽的那侧
5
收敛终止
区间宽度缩至 1 即命中或判定不存在,无需额外比较
7第 7 页 · 算法流程

代码实现

python

用 Fibonacci 数定位切分点 k,按三种比较结果收缩区间,直至命中或区间为空。

代码高亮加载中…

切分点取 lo+fib[k-1],使前段短、后段长;命中右侧时按 fib[k-2] 收缩,两分支代价趋于均衡。

8第 8 页 · 代码实现

Fib 数的快速推进

上一页的代码里,切分点靠 fib(k) 定位。但每次查找都从 fib(1) 算到 fib(k) 太浪费——其实整个查找过程中,只需要相邻两个 Fib 数随分支同步推进就够了。

只留两个变量
保存 fib(k-1) 与 fib(k-2),其余历史值全部丢弃
命中左分支
区间缩到左侧,Fib 数下移一档:k 减 1
命中右分支
区间缩到右侧,Fib 数下移两档:k 减 2
推进即减半
两个变量反复相减更新,O(log n) 步内收敛到答案
爬山只记当前和下一步对应 →两个变量的滚动推进

登山者的当前位置和剩余路程,对应 fib(k-1) 与 fib(k-2)

左分支:(a,c)→(a, fib(k−1))右分支:(fib(k−1), c)→(fib(k−2), c)\text{左分支:}(a,c)\to(a,\ fib(k-1))\qquad \text{右分支:}(fib(k-1),\ c)\to(fib(k-2),\ c)
9第 9 页 · Fib 数的快速推进

实例:完整查找过程

在长度为 12 的有序向量上查找目标,看 Fibonacci 查找如何逐步收缩区间。

1
初始化
取 fib(k)=13≥12,切分点 mi=fib(k-1)-1=7,前段长 7、后段长 4
2
比较 a[7]
目标 < a[7],走左分支——这正是偏后的切分点给左分支留出的宽裕深度
3
收缩左段
左段长 7 对应 fib(6)-1,切分点降为 mi=fib(5)-1=4
4
比较 a[4]
目标 > a[4],转向右分支,只剩短后段可查
5
收缩右段
右段长 4,对应 fib(4)-1,切分点降为 mi=2,区间已很小
6
命中 a[2]
目标 = a[2],查找结束;共 3 次比较,每层都在 Fibonacci 深度上
10第 10 页 · 实例:完整查找过程

实例的分段图示

从左往右看每个节点:方框内是当前向量宽度(Fib 数),箭头标注切分点 mi 位置,跟随箭头观察左右两支宽度如何递减。

图解渲染中…
a1:当前查找区间宽度恰为某个 Fibonacci 数b1:mi 偏后,左子向量宽 F12,占比更大b2:右子向量宽 F11,比左侧少一级h1:区间宽度逐轮沿 Fib 数列前移收窄至 1
11第 11 页 · 实例的分段图示

自测:下一步切在哪

点击作答

有序向量长 fib(k)-1,在 fib(k-1)-1 处切分后,若目标值大于切分元素,下一步在前段还是后段继续?宽度是多少?

12第 12 页 · 自测:下一步切在哪

平均查找长度的权衡

前面我们让切分点偏后,换来了更快的推进速度。但代价是什么?每一次查找走前后分支的概率和代价都不一样,真正要比较的是平均成本,而不是单步快慢。

成功查找
命中位置分布不均,落在后段的元素更少被访问
失败查找
必落到某个空区间,前后分支代价不同
切分比例
黄金分割 0.618:0.382,让两种代价取得平衡
平均结论
Fibonacci 查找平均略优,最坏情况反而更长
分蛋糕时偏心对应 →切分点偏后

切得偏一点,多数人拿大块,总量反而更省

ASLsucc≈O(log⁡ϕn), ϕ=1+52≈1.618ASL_{succ} \approx O(\log_\phi n),\ \phi=\frac{1+\sqrt5}{2}\approx1.618
13第 13 页 · 平均查找长度的权衡

二分 vs Fibonacci 查找

两者都是有序向量的减治查找,唯一差别在切分点位置——却直接决定比较次数的分布。

二分查找
  • 切分点固定取中点 mi = (lo+hi)/2
  • 前后两个分支等宽、完全对称
  • 每次比较成功概率相同,成本均摊
Fibonacci 查找
  • 切分点按 Fibonacci 数偏后,约 0.618 处
  • 前宽后窄,有意制造不平衡
  • 先比较后宽段:多数元素一次即命中
当元素更常落在前段(多数查找命中)时选 Fibonacci;追求实现简单、切分对称时选二分。
14第 14 页 · 二分 vs Fibonacci 查找

最优性论证

上一页比较了两者效率,Fibonacci 查找略胜,但胜在哪里、有没有极限?关键在于:切口偏后让后段更短,比较次数被压到 O(logφn),而这个比例在数学上恰恰是最优的。

切口偏后
前段长、后段短,多数路径提前进入小段快速收敛
O(logφn)
以 φ≈1.618 为底的对数,约 1.44 倍 log₂n
黄金分割最优
偏后比例中使最坏比较次数最小的取值正是 1/φ
搬两箱重物对应 →切分点选择

大箱走捷径(进入前段)、小箱抬着快跑,两边总耗时相等才最优

log⁡φn≈1.44 log⁡2n\log_{\varphi} n \approx 1.44\,\log_2 n
15第 15 页 · 最优性论证

要点收束

  • ✓深层成本在比较次数:转向成功查找,且转向更省,故切分点应偏后
  • ✓黄金分割确定比例,Fib 数给出可实现的整数切分点
  • ✓实现只用加减推进 fib,避免乘除,分界 case 需留意边界
  • ✓平均复杂度同为 O(logn),但常数更小,且证明其已达最优
延伸主题:深 入:插值查找与 O(loglogn)对比:跳转查找如何取舍推广:不平衡分支的其他场景
16第 16 页 · 要点收束

课后思考

三个问题没有唯一答案:先自己想几分钟,再对照参考答案。

1把二分的均分点改成黄金分割点,为什么反而可能更省时间?省在哪里?

参考答案思路:O(log n) 只数比较次数,而 Fibonacci 用加减代替除法取切点,且前后子问题规模按黄金比例递减,失败方向的代价更小。想想你的机器上除法与加减的真实开销。

2如果向前的分支代价远大于向后(比如要搬移数据),切分点该怎么调?

参考答案把代价写进权衡:总代价 ≈ 左分支代价×左规模 + 右分支代价×右规模。代价不等时,最优点会偏离黄金分割——代价高的分支应分到更小的规模,试着推出偏移方向。

3插值查找也是"按比例切",它为什么只对均匀分布的向量快?这说明比例切分的适用边界是什么?

参考答案插值按"值域比例"预估位置,分布均匀时一次到位,不均匀时退化甚至比二分差。启示:比例切分的前提是"比例假设"成立——先问数据满足什么分布,再谈哪把刀更快。

17第 17 页 · 课后思考