文章目录
- 第一章:什么是斐波那契查找?
-
- [1.1 核心思想](#1.1 核心思想)
- [1.2 数学原理(别怕,只要小学数学)](#1.2 数学原理(别怕,只要小学数学))
- 第二章:为什么要这么麻烦?
-
- [2.1 历史原因:避开除法](#2.1 历史原因:避开除法)
- [2.2 搜索效率](#2.2 搜索效率)
- 第三章:手把手实现 (C#))
-
- [3.1 代码全解析](#3.1 代码全解析)
- [第四章:深度逻辑拆解(为什么 k 要减 1 或减 2 ?)](#第四章:深度逻辑拆解(为什么 k 要减 1 或减 2 ?))
- [第五章:斐波那契 vs 二分查找 vs 插值查找](#第五章:斐波那契 vs 二分查找 vs 插值查找)
- 第六章:博主总结
博主寄语:哈喽,我是那个喜欢在代码里找数学美的博主------小狼君。
上一期我们聊了二分查找,每次都把数组对半劈开。很多同学问:"一定要对半劈吗?能不能劈在 1 / 3 1/3 1/3 处?或者劈在 0.618 0.618 0.618 处?"
问得好!这就引出了今天的主角------斐波那契查找。
它利用了斐波那契数列(1, 1, 2, 3, 5, 8...)的神奇特性,将"黄金分割"的自然法则应用到了算法里。
准备好,我们要开始一场数学与代码的华尔兹了。
第一章:什么是斐波那契查找?
1.1 核心思想
二分查找的核心是 mid = (low + high) / 2。
而斐波那契查找的核心是:利用斐波那契数来确定 mid 的位置,使分割点接近黄金分割比(0.618)。
1.2 数学原理(别怕,只要小学数学)
斐波那契数列: F ( k ) = F ( k − 1 ) + F ( k − 2 ) F(k) = F(k-1) + F(k-2) F(k)=F(k−1)+F(k−2)。
比如:1, 1, 2, 3, 5, 8, 13, 21, 34, 55...
这个公式告诉我们:一个大斐波那契数,可以完美切成两个小斐波那契数之和。
如果我们将数组的长度凑成 F ( k ) − 1 F(k) - 1 F(k)−1,那么整个数组就可以被分割为:
左半边长度: F ( k − 1 ) − 1 F(k-1) - 1 F(k−1)−1
右半边长度: F ( k − 2 ) − 1 F(k-2) - 1 F(k−2)−1
中间那个元素(Mid):1 个
加起来: ( F ( k − 1 ) − 1 ) + ( F ( k − 2 ) − 1 ) + 1 = F ( k ) − 1 (F(k-1) - 1) + (F(k-2) - 1) + 1 = F(k) - 1 (F(k−1)−1)+(F(k−2)−1)+1=F(k)−1。
完美闭环!
第二章:为什么要这么麻烦?
你可能会问:"二分查找代码那么简单,我为什么要为了个 0.618 0.618 0.618 搞这么复杂?"
2.1 历史原因:避开除法
在很久以前的计算机里,乘法和除法是非常慢的,而加法和减法飞快。
二分查找需要计算 / 2(或者右移位 >> 1)。
斐波那契查找全程只需要加减法(mid = low + F[k-1] - 1)。
在那个算力贫瘠的年代,这可是巨大的优化!
2.2 搜索效率
虽然平均复杂度也是 O ( log n ) O(\log n) O(logn),但在最坏情况下,斐波那契查找的路径可能比二分查找稍微短一丢丢(取决于数据分布)。
第三章:手把手实现 (C#)
斐波那契查找比二分查找多了一个步骤:补齐数组。
因为数组长度 n n n 不一定刚好等于 F ( k ) − 1 F(k)-1 F(k)−1,所以我们要把数组补长到最近的斐波那契数值。
3.1 代码全解析
csharp
using System;
using System.Linq;
public class FibonacciSearcher
{
// 最大斐波那契数组长度(int范围内够用了)
private const int MaxSize = 20;
// 1. 先生成一个斐波那契数列备用
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55...]
private int[] GetFibonacciArray()
{
int[] f = new int[MaxSize];
f[0] = 1;
f[1] = 1;
for (int i = 2; i < MaxSize; i++)
{
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
public int Search(int[] arr, int target)
{
int low = 0;
int high = arr.Length - 1;
int k = 0; // 当前使用的斐波那契下标
int mid = 0;
int[] f = GetFibonacciArray();
// 2. 计算 k,找到刚好大于或等于数组长度的斐波那契数值
// 也就是找到 F[k] 使得 F[k]-1 >= arr.Length
while (high > f[k] - 1)
{
k++;
}
// 3. 数组扩容(补齐)
// 因为 f[k] 值可能大于 arr.Length,我们需要构造一个新的临时数组
// 比如原数组 [1, 8, 10, 89],长度4。
// 最近的斐波那契数是 5 (F[k]-1 = 4),不需要补。
// 如果原数组长度是 5,最近的斐波那契数是 8 (F[k]-1 = 7),需要补 2 个。
// 补什么值?补最后一个元素的值(保持有序性)。
int[] temp = new int[f[k] - 1];
Array.Copy(arr, temp, arr.Length);
// 把多出来的部分填满原数组最后一个值
for (int i = arr.Length; i < temp.Length; i++)
{
temp[i] = arr[high];
}
// 4. 开始查找
while (low <= high)
{
// 核心公式:利用 F[k-1] 确定 mid
mid = low + f[k - 1] - 1;
if (target < temp[mid])
{
// 目标在左边
// 左边长度是 F[k-1]-1
high = mid - 1;
// 下一次,我们在 F[k-1] 这个范围内找
// 所以 k 减 1
k = k - 1;
}
else if (target > temp[mid])
{
// 目标在右边
// 右边长度是 F[k-2]-1
low = mid + 1;
// 下一次,我们在 F[k-2] 这个范围内找
// 所以 k 减 2
k = k - 2;
}
else
{
// 找到了!
// 如果 mid 落在原数组范围内,直接返回
if (mid < arr.Length)
{
return mid;
}
else
{
// 如果 mid 落在补齐的部分,说明找到的是最后一个元素(因为补的都是它)
return arr.Length - 1;
}
}
}
return -1; // 没找到
}
}
第四章:深度逻辑拆解(为什么 k 要减 1 或减 2 ?)
这是斐波那契查找最难理解的地方。
回顾公式: F ( k ) = F ( k − 1 ) + F ( k − 2 ) F(k) = F(k-1) + F(k-2) F(k)=F(k−1)+F(k−2)。
数组总长我们看作 F ( k ) − 1 F(k) - 1 F(k)−1。
mid 把数组分成了两部分:
左边部分:长度为 F ( k − 1 ) − 1 F(k-1) - 1 F(k−1)−1。
右边部分:长度为 F ( k − 2 ) − 1 F(k-2) - 1 F(k−2)−1。
场景 A:向左找 (target < temp[mid])
我要进入左边区域。
左边区域的总长度本来就是 F ( k − 1 ) − 1 F(k-1) - 1 F(k−1)−1。
这不就是我们要找的下一个"斐波那契完整状态"吗?
所以,只要让 k = k − 1 k = k - 1 k=k−1,数学逻辑就对上了。
场景 B:向右找 (target > temp[mid])
我要进入右边区域。
右边区域的总长度是 F ( k − 2 ) − 1 F(k-2) - 1 F(k−2)−1。
为了在下一轮循环中,把这块区域当做新的整体,我们需要把 k k k 调整为对应 F ( k − 2 ) F(k-2) F(k−2) 的状态。
所以,让 k = k − 2 k = k - 2 k=k−2。
博主骚话:
左边是大哥 ( k − 1 k-1 k−1),地盘大一点;
右边是二弟 ( k − 2 k-2 k−2),地盘小一点。
向左走一步,向右走两步(指 k k k 的减量)。
第五章:斐波那契 vs 二分查找 vs 插值查找
| 维度 | 顺序查找 (Sequential) | 二分查找 (Binary) |
|---|---|---|
| 前提条件 | 无 (啥都能查) | 必须有序 (数组) |
| 时间复杂度 | O(n) | O(log n) |
| 适用结构 | 数组、链表 | 仅限 数组 (支持随机访问) |
| 代码难度 | 有手就行 | 容易写出死循环或边界Bug |
| 场景 | 数据量小、乱序、一次性查找 | 数据量大、有序、频繁查找 |
第六章:博主总结
老实说,在今天的 PC 和手机上,二分查找(Binary Search) 依然是王道。CPU 的分支预测和位运算优化已经强到离谱,斐波那契查找那点"避免除法"的优势早就没了,反而因为要 new int[] 扩容数组和复杂的索引计算,可能比二分还慢。
那我们为什么还要学它?
面试装 X:当面试官问你二分查找时,你顺嘴提一句:"其实还有基于黄金分割的斐波那契查找...",这逼格瞬间拉满。
思维体操:它展示了分治算法不仅仅可以是"对半切",还可以按任何数学规律去切。
记住:算法不只是工具,它也是人类智慧的结晶。斐波那契查找,就是那颗闪着金光(黄金分割)的遗珠。
如果觉得这篇"黄金切割"大法让你脑洞大开,记得三连支持博主!下期见!