算法:斐波那契(黄金分割法)查找算法

斐波那契数列

斐波那契数列,该数列公式为F(K) = F(k-1) + F(k-2),即 1、1、2、3、5、8、13、21……。F(k-1)/f(K)随着K的递增,该数越来越接近黄金分割比例,所以该方法也叫黄金分割法。

斐波那契查找算法也叫做黄金分割查找

原理

对于一个数组来说,如果数组长度为契波纳切数列中的某一个数字,那么我们就可以用黄金分割比例来分割该数组。当然,如果数组长度没有达到要求,那么我们可以尝试它扩大来满足要求,

其实,该算法的本质也还是二分法,只不过跟插入排序法一样,也是将目标的mid值改变成其它的,以至于分割的结果不一样,查找的效果也不一样。【也就是说,真正需要我们做的是找到这个mid】

那么具体是怎样分割的呢?这里的mid不再是折半或者插值得到,而是位于黄金分割点附近,即mid = low + F(k-1) -1

这里用图片直观理解一下:
在这里插入图片描述

对F(k-1)-1的理解:

  • 由斐波那契数列F(k) = F(k-1) + F(k - 2)的性质,可以得到(F[k] - 1) = (F[k - 1] - 1) + (F[k - 2] - 1) + 1 。该式说明:只要顺序表的长度为F[k]-1,则可以将该表分成长度为F[k-1]-1和F[k-2]-1的两段,即如上图所示。从而中间位置为mid=low+F(k-1)-1

即:斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F[ n ],将原查找表扩展为长度为F[n] (如果要补充元素,则补充重复最后一个元素,直到满足F[n]个元素),完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。

ps:二分查找, 插值查找和裴波那契查找的基础其实都是:对数组进行分割, 只是各自的标准不同: 二分是从数组的一半分, 插值是按预测的位置分, 而裴波那契是按它数列的数值分。

例子

一个例子

对于斐波那契数列:1、1、2、3、5、8、13、21、34、55、89……(也可以从0开始),前后两个数字的比值随着数列的增加,越来越接近黄金比值0.618。

比如这里的89,把它想象成整个有序表的元素个数,而89是由前面的两个斐波那契数34和55相加之后的和,也就是说把元素个数为89的有序表分成由前55个数据元素组成的前半段和由后34个数据元素组成的后半段,那么前半段元素个数和整个有序表长度的比值就接近黄金比值0.618,假如要查找的元素在前半段,那么继续按照斐波那契数列来看,55 = 34 + 21,所以继续把前半段分成前34个数据元素的前半段和后21个元素的后半段,继续查找,如此反复,直到查找成功或失败,这样就把斐波那契数列应用到查找算法中了。
在这里插入图片描述

斐波那契查找算法(黄金分割查找算法)

例子二

1、目前由一个有序递增数组,要在这个数组中找元素99
在这里插入图片描述

2、创建一个斐波那契数列,根据:
在这里插入图片描述
也即是:斐波那契数列要求原始表中记录的个数为某个斐波那契数列 -1 ,也就是数组长度应该是 arr.length = Fabonacci(k) - 1.
又因为数组的长度不一定刚好是Fabonacci(k) - 1。

  • 如果大于等于,直接返回k
  • 如果斐波那契数列 - 1 小于 arr.length, 那么k++ : 也就是要查找的区间应该比当前k要大
int k = 0;
while(arr.length > Fabonacci(k) - 1){
	k++;
}

// 当Fab(k) - 1刚好等于数组长度或者略大于数组长度时,当前的k就是我们期待的数组长度,因此跳出循环

在这里插入图片描述
3、如果原始表的长度就是我们期望的数组长度就什么也不干,如果原始数组中的长度小于我们期望的长度F(k), 则将原始数组的长度扩展到F(n):(如果要补充元素,则补充重复最后一个元素,直到满足F[n]个元素)。

又因为java中数组的长度是固定的,因此我们创建一个临时数组,将原始数组复制到临时数组,并补充元素
在这里插入图片描述

int[] temp = Arrays.copy(arr, Fabonacci(k));
for(int i = arr.length; i < temp.length; i++){
	temp[i] = arr[arr.length - 1];
}

3、完成后进行契波纳切数分割·,即F(k)的元素分割为前半部分F(k - 1)个元素,后半部分F(k-2)个元素

  • 没有开始之前:low = 0, high = arr.length - 1;临时数组长度为fab(k)
  • 求出mid = low + fab(k-1) -1
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

当目标值大于中间值时,k = k - 2;
为什么是k -= 2:

  • 全部元素 = 前面的元素 + 后面的元素
  • F[k] = F[k - 1] + F[k - 2] 【应该向右边找】
  • 因为后面我们又F[k-1] = F[k-3] + F[k - 4]
  • 即在f[k-2] 的前面进行查找 k -=2
  • 即下次循环 mid = f[k - 1 - 2] - 1
  1. 找出要查找的元素在那一部分并递归,直到找到。

代码实现

目标数组必须是有序数组。

public class TreeNode {

    private static int maxSize = 20;
    public static int[] fib() {
        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;
    }



    // 数组中没有重复的元素
    /**
     *
     * @param a  数组
     * @param key 我们需要查找的关键码(值)
     * @return 返回对应的下标,如果没有-1
     */
    public static int fibSearch(int[] a, int key) {
        int low = 0;
        int high = a.length - 1;
        int k = 0; //表示斐波那契分割数值的下标
        int mid = 0; //存放mid值
        int f[] = fib(); //获取到斐波那契数列
        //获取到斐波那契分割数值的下标
        while(high > f[k] - 1) {
            k++;
        }
        //因为 f[k] 值 可能大于 a 的 长度,因此我们需要使用Arrays类,构造一个新的数组,并指向temp[]
        //不足的部分会使用0填充
        int[] temp = Arrays.copyOf(a, f[k]);
        //实际上需求使用a数组最后的数填充 temp
        //举例:
        //temp = {1,8, 10, 89, 1000, 1234, 0, 0}  => {1,8, 10, 89, 1000, 1234, 1234, 1234,}
        for(int i = high + 1; i < temp.length; i++) {
            temp[i] = a[high];
        }

        // 使用while来循环处理,找到我们的数 key
        while (low <= high) { // 只要这个条件满足,就可以找
            mid = low + f[k - 1] - 1;

            if(key < temp[mid]) { //我们应该继续向数组的前面查找(左边)
                high = mid - 1;
                //为甚是 k--
                //说明
                //1. 全部元素 = 前面的元素 + 后边元素
                //2. f[k] = f[k-1] + f[k-2]
                //因为 前面有 f[k-1]个元素,所以可以继续拆分 f[k-1] = f[k-2] + f[k-3]
                //即 在 f[k-1] 的前面继续查找 k--
                //即下次循环 mid = f[k-1-1]-1
                k--;
            } else if ( key > temp[mid]) { // 我们应该继续向数组的后面查找(右边)
                low = mid + 1;
                //为什么是k -=2
                //说明
                //1. 全部元素 = 前面的元素 + 后边元素
                //2. f[k] = f[k-1] + f[k-2]
                //3. 因为后面我们有f[k-2] 所以可以继续拆分 f[k-1] = f[k-3] + f[k-4]
                //4. 即在f[k-2] 的前面进行查找 k -=2
                //5. 即下次循环 mid = f[k - 1 - 2] - 1
                k -= 2;
            } else { //找到
                //需要确定,返回的是哪个下标
                if(mid <= high) {
                    return mid;
                } else {
                    return high;
                }
            }
        }
        return -1;
    }



    public static void main(String[] args) {
        int arr[] ={1,2, 3, 4, 5, 99 , 100};

        int i =  fibSearch(arr, 99);
        if (i == -1){
            System.out.println("找不到");
        }else{
            System.out.println("索引" +i+ "值" + arr[i]);
        }
    }
}

为什么不直接用(hi-lo)*0.618来寻找分割点

为什么不直接用(hi-lo) * 0.618来寻找分割点?这个问题我个人的看法是乘法的开销较大,而且对于查找效果的提高有限。斐波那契数前后项之比 fib(n) / fib(n - 1) 也只是在n比较大的时候才接近黄金比例1.618…,而且区间的长度不一定为某个斐波那契数减一(也有可能查找的目标是最后一个数嘛),所以斐波那契查找的意义应该是在效果与开销之间找到一个平衡,使效率尽可能的最大化

斐波那契查找的时间复杂度还是O(log2n):斐波那契查找,就平均性能而言,要优于二分查找,但是如果是最坏的情况,比如key=0,那么始终在左侧长半区在查找,查找的效率要低于折半查找。

算法与数据结构学习(31)-斐波那契(黄金分割法)查找算法

斐波那契查找算法
参考

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值