【查找篇章之三:斐波那契查找】斐波那契查找:用黄金分割去“切”数组

文章目录

  • 第一章:什么是斐波那契查找?
    • [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:当面试官问你二分查找时,你顺嘴提一句:"其实还有基于黄金分割的斐波那契查找...",这逼格瞬间拉满。

思维体操:它展示了分治算法不仅仅可以是"对半切",还可以按任何数学规律去切。

记住:算法不只是工具,它也是人类智慧的结晶。斐波那契查找,就是那颗闪着金光(黄金分割)的遗珠。

如果觉得这篇"黄金切割"大法让你脑洞大开,记得三连支持博主!下期见!

相关推荐
橘颂TA1 小时前
【剑斩OFFER】算法的暴力美学——二进制求和
算法·leetcode·哈希算法·散列表·结构与算法
地平线开发者3 小时前
征程 6 | cgroup sample
算法·自动驾驶
姓蔡小朋友3 小时前
算法-滑动窗口
算法
君义_noip4 小时前
信息学奥赛一本通 2134:【25CSPS提高组】道路修复 | 洛谷 P14362 [CSP-S 2025] 道路修复
c++·算法·图论·信息学奥赛·csp-s
kaikaile19954 小时前
基于拥挤距离的多目标粒子群优化算法(MO-PSO-CD)详解
数据结构·算法
不忘不弃4 小时前
求两组数的平均值
数据结构·算法
leaves falling4 小时前
迭代实现 斐波那契数列
数据结构·算法
珂朵莉MM4 小时前
全球校园人工智能算法精英大赛-产业命题赛-算法巅峰赛 2025年度画像
java·人工智能·算法·机器人
Morwit4 小时前
*【力扣hot100】 647. 回文子串
c++·算法·leetcode