具体内容:
该文章的源代码仓库为: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,…}这种的数据表,效率确实不太高。但每种分割方式均有适用场景,不能单纯地说折半查找比插值查找差。