前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >斐波那契查找不再迷惑

斐波那契查找不再迷惑

作者头像
阮键
发布2020-07-13 09:57:52
8110
发布2020-07-13 09:57:52
举报

裴波那契查找的来源

裴波那契数列是一串按照F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)这一条件递增的一串数字:

代码语言:javascript
复制
1、1、2、3、5、8、13、21 ... ...

两个相邻项的比值会逐渐逼近0.618 —— 黄金分割比值。这个非常神奇的数列在物理,化学等各大领域上有相当的作用, 于是大家想: 能不能把它用在查找算法上嘞??

于是就有了裴波那契查找算法, 裴波那契数列最重要的一个性质是每个数都等于前两个数之和(从第三个数字开始)。 也就是一个长度为f(n)的数组,它能被分成f(n-1)和f(n-2)这两半, 而f(n-1)又能被分为f(n-2)和f(n-3)这两半。。。直到分到1和1为止(f(1)和f(2))。

(注意一个细节: 在分割时,可以选择将“大块”的f(n-1)放前面部分,也可以将“小块”的f(n-2)放前面,我下面的分割都是按照“大块”在前进行的)

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

三个数组以及它们之间的关系。

了解裴波那契查找的算法实现, 最重要的是理解“三个数组”之间的关系,它们分别是:

  • 待查找数组 (a)
  • 裴波那契数组(fiboArray)
  • 填充后数组(filledArray)

裴波那契数组

要按裴波那契数分割, 我们当然要创建一个容纳有裴波那契数的数组,那么,怎么确定这个数组的长度呢? 或者说, 怎么确定数组里裴波那契数的最大值呢?(最后一个值)

答:只要刚好能满足我们的需要就可以了,裴波那契数组的长度,取的是大于等于待查找数组长度的最小值。原数组长4则取5,长6则取8,长13取13(1、1、2、3、5、8、13、21 )

填充数组

其次我们要考虑的是: 我们的数组长度不可能总是满足裴波那契数的, 例如5、8、13、21等是裴波那契数, 但我们的数组长度可能是6,7,10这些非裴波那契数, 那这时候怎么办呢? 总不能对长度为10的待查找数组按照8和13进行第一次分割吧, 所以我们应该按照上面选定的裴波那契数组的最大值, 创建一个等于该长度的填充数组, 将待查找数组的元素依次拷贝到填充数组中, 剩下的部分用原待查找数组的最大值填满。

我们进行查找操作的并不是原待排序数组, 而是对应的填充数组!

查找到填充的部分元素如何处理?

当我们在填充数组中查找成功后,该元素可能来源于在原数组的基础上填充的部分元素(上图黄色9), 返回的下标(10,11,12)显然是不准确的,而应该返回原数组的最后一个元素的下标(9) 。

所以,解决方法就是: 在填充数组中查找成功后, 判断返回的元素下标和原数组长度的关系,如果:返回下标 > 原数组长度 - 1, 那么改为返回原数组最后一个元素下标就OK了。

查找过程

OK,有了上面的基础我们总结下查找的过程:

  1. 根据待查找数组长度确定裴波那契数组的长度(或最大元素值)
  2. 根据1中长度创建该长度的裴波那契数组,再通过F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)生成裴波那契数列为数组赋值
  3. 以2中的裴波那契数组的最大值为长度创建填充数组,将原待排序数组元素拷贝到填充数组中来, 如果有剩余的未赋值元素, 用原待排序数组的最后一个元素值填充
  4. 针对填充数组进行关键字查找, 查找成功后记得判断该元素是否来源于后来填充的那部分元素

具体代码

代码语言:javascript
复制
/**
 * @Author: HuWan Peng
 * @Date Created in 16:54 2017/12/7
 */ 
public class FibonacciSearch {
  /**
   * @description: 创建最大值刚好>=待查找数组长度的裴波纳契数组
   * @param a: 待查找的数组
   */ 
  private static int[] makeFiboArray(int[] a){
    int N = a.length;
    int first = 1,sec = 1,third=2,fbLength = 2;
    int high = a[N-1];
    while (third<N) { // 使得裴波那契数不断递增,直到值刚好大于等于原数组长度为止
      third = first + sec; // 根据f(n) = f(n-1)+ f(n-2)计算
      first = sec;
      sec = third;
      fbLength++; // 计算最后得到的裴波那契数组的长度     
    }
 
    int [] fb = new int[fbLength]; // 根据上面计算的长度创建一个空数组
    fb[0] = 1; // 第一和一二个数是迭代计算裴波那契数的基础
    fb[1] = 1;
    for(int i=2;i<fbLength;i++){
      fb[i] = fb[i-1] + fb[i-2]; // 将计算出的裴波那契数依次放入上面的空数组中     
    }
 
    return fb;
  }

  /**
   * @description: 裴波那契查找
   */ 
  public static int search (int [] a, int key) {
    int low, high;
    int lastA;
    int [] fiboArray = makeFiboArray(a); // 创建最大值刚好>=待查找数组长度的裴波纳契数组 
    int filledLength = fiboArray[fiboArray.length-1];
    int [] filledArray = new int[filledLength]; // 创建长度等于裴波那契数组最大值的填充数组  
    for(int i=0;i<a.length;i++) {
      filledArray[i] = a[i]; // 将原待排序数组的元素都放入填充数组中     
    }
 
    lastA = a[a.length-1]; // 原待排序数组的最后一个值 
    for (int i=a.length;i<filledLength;i++) {
      filledArray[i] = lastA; // 如果填充数组还有空的元素,用原数组最后一个元素值填满     
    }
 
    low = 0;
    high = a.length; // 取得原待排序数组的长度 (注意是原数组!) 
    int mid;
    int k = fiboArray.length -1;
    while(low<=high) {
      mid = low + fiboArray[k-1]-1;
      if(key<filledArray[mid]) {
        high = mid-1; // 排除右半边的元素
        k= k-1;       // f(k-1)是左半边的长度
      }else if(key>filledArray[mid]) {
        low = mid+1;  // 排除左半边的元素
        k = k-2;      // f(k-2)是右半边的长度
      }else {
        if(mid>high){ // 说明取到了填充数组末尾的重复元素了 
          return high;
        }else {
          return mid; // 说明没有取到填充数组末尾的重复元素         
        }
      }
    }
    return -1;
  }
}
 

点击这里在线运行代码

斐波那契查找的轨迹

不依赖数组的斐波那契查找

我百度“斐波那契查找”的时候, 一大部分基于数组实现的代码都是创建了一个长度固定为20的斐波那契数组。

而第20个斐波那契数是6765,所以这样的代码只能处理长度小于等于6765的数组。

于是就有了另一种编写斐波那契数组的方法: 不依赖数组的编码方法

请点这里:

不依赖数组的斐波那契查找

说一下这种方法和我上面介绍的方法的不同点

  • 我上面介绍的版本: 先把斐波那契数算出来,再全部用数组存起来, 要用的时候直接从数组里拿就可以了
  • 这个版本: 不用数组存, 只算出来需要的最大的斐波那契数, 要用的时候“临时”计算就可以了

二分,插值和裴波纳契查找的性能比较

二分查找:

二分查找的轨迹可以用一颗判定树来表示,例如:将下面的[20,30,40,50,80,90,100]表示为一颗判定树, 因为一开始查找的是位于整个数组1/2 位置的元素50, 所以将50置于根元素位置, 接下来查找的是30或90,所以放到50的子节点的位置。

结合一个结论:具有n个节点的判定树的深度为logn2 + 1, 所以二分查找时候比较次数最多为logn2 + 1,

插值查找

上面也说过了,插值查找只适用于关键字均匀分布的表,在这种情况下, 它的平均性能比二分查找好,在关键字不是均匀分布时, 它的性能表现就不足人意了。

斐波那契查找

斐波那契查找的平均性能比二分查找好, 但最坏情况下的性能(虽然仍然是O(logn))却比二分查找差,它还有一个优点就是分割时候只需进行加减运算(二分和插值都有乘/除)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-07-10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 裴波那契查找的来源
  • 三个数组以及它们之间的关系。
  • 查找到填充的部分元素如何处理?
    • 查找过程
      • 二分,插值和裴波纳契查找的性能比较
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档