自己动手写数据结构(10)——有序表查找(详解斐波那契查找)

自己动手写数据结构总目录

具体内容:

该文章的源代码仓库为:https://github.com/MeteorCh/DataStructure/blob/master/Java/DataStructure/src/Searching/OrderListSearching.java
这一节开始学习查找方面的知识。首先来说最简单的查找——有序表查找。其前提是给定的数据表是排序好的。其中最常见的有序表的查找有三种:折半查找、插值查找、斐波那契查找。他们的思想都是二分查找,所不用的是每个二分点的位置不同。

一、折半查找

1.原理

二分查找的原理很简单,对于一个有序表,每次查找的时候,先去看中间记录,如果查找值和中间记录的关键字相等,则查找成功;如果查找值小于中间记录,则在中间记录的左半区继续查找;如果查找值大于中间记录,则在中间记录的右半区继续查找,不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。

2.代码实现

二分查找原理简单,这里就不再画图了,直接贴代码。下面是用Java实现的二分查找的一个静态函数,用于查找一个整形数组中的元素。如果查找成功则返回该元素的下标,查找不到则输出-1。

/**
     * 二分查找
     * @param data 有序表
     * @param key 待查找的键值
     * @return 查找到的下标,找不到返回-1
     */
    public static int binarySearch(int[] data,int key){
        int low=0,high=data.length-1;
        while (low<=high){
            int mid=(low+high)/2;
            if (data[mid]>key)//前半部分
                high=mid-1;
            else if (data[mid]<key)
                low=mid+1;
            else
                return mid;
        }
        return -1;
    }

如要在{0,1,16,24,35,47,59,62,73,88,99}查找16,我们调用下面的代码:

		int[] data={0,1,16,24,35,47,59,62,73,88,99};
        System.out.println(OrderListSearching.binarySearch(data,16));

其中,OrderListSearching为二分查找函数所在的类。输出结果为2。查找成功。

二、插值查找

1. 原理

在上面的代码中,我们计算mid时,公式为mid=(low+high)/2,即每次让mid位于low和high的中间。那为什么每次都要位于中间呢,假如我们的数据是{0,1,2,300,432,498,500,528,578,654,678},我们要查找1,那这样折半的效率就不会很高,我们应该让尽量让mid偏右一点,这样就能尽快查找到查找值。要决定mid偏右还是偏右,应该拿查找值和每次查找范围中的最大最小值比较,确定一个合适的权重。 我们将mid=(low+high)/2变形,得到:
在这里插入图片描述
我们现在就是要让这个1/2变成和查找值还有查找表相关的一个权重,这个计算公式很经典了,大家应该见过很多。计算公式为:
在这里插入图片描述
通过这样计算,当key较小的时候,mid就会靠近low,key较大的时候,mid就会靠近high,这样理论上来说查找的效率会比较高。mid的计算公式修改后的查找方式称为插值查找。

2.代码实现

插值查找和折半查找就差mid计算的一行代码

/**
     * 插值查找
     * @param data 有序表
     * @param key 待查找的键值
     * @return 查找到的下标,找不到返回-1
     */
    public static int interpolationSearch(int[] data,int key){
        int low=0,high=data.length-1;
        while (low<=high){
            int mid=low+(key-data[low])/(data[high]-data[low])*(high-low);
            if (data[mid]>key)//前半部分
                high=mid-1;
            else if (data[mid]<key)
                low=mid+1;
            else
                return mid;
        }
        return -1;
    }

三、斐波拉契查找

1.原理

既然分割的时候能用1/2分割,也能用上面的权重计算公式,那我也可以用斐波那契数列来分割。要用斐波那契数列来分割,输入的数据表格个数必须为f[k]-1(其中f[k-1]<nf[k]-1,n为数据表中元素的个数),不足的用数据表中的最后一个元素值补齐。比如我们的数据表为:
在这里插入图片描述
斐波拉契数列f为
在这里插入图片描述
根据上面的公式,我们得到数据表中元素的个数必须为f[7]-1=12,而实际上,数据表中的元素个数只有11个,就要用数据表中的最后一个元素99来补齐,补齐后的数据表为:
在这里插入图片描述
关于为什么数据表的个数必须要为f[k]-1,在讨论部分解释。
有了补全的数据表以后,我们就要用斐波那契数列来分割数据表了。根据斐波那契数列的性质,有f[k-1]+f[k-2]=f[k],那我们正好就把f[k]-1长度的数据表分割为f[k-1]-1,mid(长度为1),f[k-2]-1三个部分。如下图所示,其中mid=low+(f[k-1]-1)。分割以后,每次就按照二分查找的思想,去判断mid位置的数据和查找元素的大小关系去继续二分查找即可。
在这里插入图片描述

2.实现代码

怎么分割了解清楚后,我们来看怎么实现。用Java实现如下,此处只贴函数:

/**
     * 斐波拉契查找
     * @param data 有序表
     * @param key 待查找的键值
     * @return 查找到的下标,找不到返回-1
     */
    public static int fbSearch(int[] data,int key){
        int dataSize=data.length;
        //构造斐波那契数列
        LinkedList<Integer> fb=new LinkedList<>();//存储斐波拉契数列
        fb.add(0);
        fb.add(1);
        while (fb.getLast()-1<dataSize)
            fb.add(fb.getLast()+fb.get(fb.size()-2));
        //将data中的元素补足
        int newDataSize=fb.getLast()-1;
        int[] newData=new int[newDataSize];
        System.arraycopy(data,0,newData,0,dataSize);//将data中数据赋值到newData中
        for (int i=dataSize;i<newDataSize;i++)
            newData[i]=data[dataSize-1];
        //开始斐波那契查找
        int low=0,high=dataSize;
        int k=fb.size()-1;
        while (low<=high){
            int mid=low+fb.get(k-1)-1;
            if (key<newData[mid]){//左分割
                high=mid-1;
                k=k-1;
            }else if (key>newData[mid]){//右分割
                low=mid+1;
                k=k-2;
            }else {
                if (mid<=dataSize)//查找位置不在补齐位置
                    return mid;
                else//查找位置在补齐位置
                    return dataSize-1;
            }
        }
        return -1;
    }

这里,其他部分都比较好理解。有两点需要解释一下:

  • 1.k=k-1和k=k-2怎么理解?
    在上面我们已经画过斐波那契分割的原理图了,那在计算得到mid后,如果data[mid]>key,那就是对左边的f[k-1]-1部分再次进行斐波那契分割,分解后,左半部分的长度为f[(k-1)-1]-1=f[k-2]-1,右半部分的长度为f[(k-1)-2]-1=f[k-3]-1,如下图所示:
    在这里插入图片描述
    从上面看,不就是另k=k-1,然后再次分割么。当data[mid]<key,即对有半部分分割时,道理一样,不过此时应该是另k=k-2,然后再次分割。这就是k=k-1,k=k-2的解释。
  • 2.查找过程中如果计算得到的索引超过了dataSize,怎么得到查找结果
    我们说过,利用斐波那契查找,需要对数据表补全,所以有了能计算得到的索引会大于原数据表的个数,此时就需要去判断。具体的判断过程就是上面代码中的
            else {
                if (mid<=dataSize)//查找位置不在补齐位置
                    return mid;
                else//查找位置在补齐位置
                    return dataSize-1;
            }

3.讨论

  • 1.为什么规定数据表的长度必须是f[k]-1?
    为了分割方便,确实,只是为了方便分割。假设我们的数据表的长度是f[k],那根据f[k]=f[k-1]+f[k-2],我们可以把数据表分为f[k-1]-1、mid(长度为1)、f[k-2]三个部分(或者也可以分为f[k-1]、mid、f[k-2]-1),但是这样,有没有发现,左右两边的式子不统一了。我们规定数据表的长度为f[k]-1,再次分割的时候,可以非常方便地另k=k-1,或者k=k-2,然后用统一的公式再去分割。但如果数据表的长度为f[k],那要怎么再次分割?其实分割是可以分割的,但是就需要判断是左分割还是右分割了,需要区别对待,这样就会很麻烦。
  • 2.斐波那契查找到底有没有意义?
    这个是我最大的疑问,这种查找到底有没有意义?使用斐波那契查找的原因,就是根据它的性质,斐波那契数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618)。那我在计算mid的时候,直接让mid=low+0.618*(high-low)他不香吗? 为什么要费这么大的周折,去先计算得到斐波那契数列,然后再把原数据表补齐成f[k]-1,再去费劲地去分。我个人感觉,这个斐波那契查找就只是一个噱头,就只是故意把黄金分割率神话后的产物,他的效率、意义都思考和商榷。 如果有小伙伴知道他有什么神奇的作用和意义,希望能在讨论区讨论。

四、总结

自己对上面的三种有序表查找算法进行一个总结,如下表:
在这里插入图片描述
其实,我感觉,插值查找的分割方式是三种分割方式中最科学的,就是当遇到例如{0,1,2,2000,20001,…}这种的数据表,效率确实不太高。但每种分割方式均有适用场景,不能单纯地说折半查找比插值查找差。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MeteorChenBo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值