斐波那契查找
斐波那契查找也是对折半查找的一种优化。在折半查找中,无论要查找的数据是偏大还是偏小,一组数据总是从中间一分为二,这样未必合理。插值查找对它做了优化,但只适用于数据分布均匀的查找表,限制性较强。本节再来学习一种针对有序表的查找算法—斐波那契查找,该算法利用了黄金分割的原理。
在C语言中我们学习过斐波那契数列,在斐波那契数列中,有一个特性,对于任一角标k(k>=2),F[k]=F[k-1]+F[k-2],即后边每一个数都是前两个数的和。而且随着数列的递增,前后两个数的比值会越来越接近黄金分割数—0.618,利用这个特性,就可以将黄金比例运用到查找技术中。现在有一组斐波那契数列F,如图1所示。
图1 斐波那契数列
但是首先要明确一点:如果一个有序表的元素个数为n,并且n正好是某个斐波那契数减1,即满足n=F[k]-1时,才能使用斐波那契查找。如果元素个数n不满足这个关系,那么需要将查找表扩展,直到n满足这个关系 。假如现在有一组数据组成的查找表arr,如图2所示。
图2 查找表arr
数组arr有10个元素,即n=10,在斐波那契数列F中,没有数据满足n=F[k] -1,最近的是数据13-1=12,那么就把数据arr由10个元素扩展到12个,用最后一个元素来扩展另外两个空间。扩展后的数组arr如图3所示。
图3 扩展后的查找表arr
扩展元素的代码实现如下所示:
for (int i = n; i < F[k] - 1; i++)
arr[i] = arr[n-1];
在这个案例中,n的值为10,k的值应为7,F[k]=F[7]=13。查找表arr被扩充为F[7]-1=12个元素。
查找表arr的长度其实也很好估算,假如定义了n=7个元素的数组,那么在斐波那契数列中,8-1=7,8对应的角标为6,即7=F[6]-1,满足n=F[k]-1的关系,不必扩展;如果定义的查找中有n=25个元素,那么F[9]-1=34-1=33,那么要把arr从25个元素扩展到33个。这就是刚开始说的n与F[k]-1的关系。
在二分查找中,分割点mid=(begin+last)/2,而在斐波那契查找中分割点mid=begin+F[k-1]-1。通过上面的学习我们知道,查找表arr现在的元素个数为F[k]-1,mid将查找表分成了两部分,左边的长度为F[k-1]-1,则右边的长度为F[k]-1-(F[k-1]-1)=F[k-2]-1。如图4所示。
图4 查找表arr元素分布
接下来我们就通过斐波那契查找法来查找arr中是否存在元素102,其过程如下所示:
(1)通过上述分析得知:arr由10个元素扩充到12个,此时k=7,则mid=begin+F[k]-1=0+F[7-1]-1=7,也就是arr从角标7处一分为二,如图5所示。
图5 mid=7
(2)求得mid=7,如果arr[mid]值比key=102小,则begin往后移:
begin=mid+1;k=k-2;
如果arr[mid]值比key=102大,则last往前移:
last=mid-1;k=k-1;
而此时arr[mid]=arr[7]=90,它要比key值小,则重新为begin和key赋值,begin=mid+1=7+1=8; k=k-2=7-2=5; 则再次求得mid=begin+F[k-1]-1=8+F[5-1]-1=8+3-1=10(原先arr已经由10个元素扩展到12个),即从角标10处将arr一分为二,如图6所示。
图6 mid=10
(3)mid=10,则arr[mid]=130,比key值大,则last往前移,last=mid-1=10-1=9; k=k-1=5-1=4;则再次求得mid=begin+F[k-1]-1=8+F[4-1]-1=8+2-1=9,则从角标为9处将arr一分为二,如图7所示。
图7 mid=9
(4)mid=9,则arr[mid]=130,大于key值,则last往前移,last=mid-1=9-1=8; k=k-1=4-1=3; 则再次求得mid=begin+F[k-1]-1=8+F[3-1]-1=8+1-1=8,则从角标8处将arr一分为二,如图8所示。
图8 mid=8
此时,arr[mid]=arr[8]=102,元素查找成功。
通过了上面的学习,斐波那契查找的算法实现也简单了,其代码如下所示:
int F[] = {0,1,1,2,3,5,8,13,21,34,55,89}; //当然也可以构造别的斐波那契数列
int Fibonacci_Search(int *arr, int n, int key)
{
int begin = 0;
int last = n - 1;
int k = 0;
while (n>F[k] - 1) //计算n位于斐波那契数列的位置
++k;
for (int i = n; i < F[k] - 1; i++) //扩展赋值
arr[i] = arr[n - 1];
while (begin <= last)
{
int mid = begin + F[k - 1] - 1;
if (key<arr[mid])
{
last = mid - 1;
k -= 1;
}
else if (key>arr[mid])
{
begin = mid + 1;
k -= 2;
}
else
{
if (mid<n)
return mid; //若相等则说明mid即为查找到的位置
else
return n - 1; //若mid>=n则说明是扩展的数值,返回n-1
}
}
return -1;
}
关于斐波那契查找, 如果要查找的记录在右侧,则左侧的数据都不再判断,不断反复进行下去,对处于当中的大部分数据,其工作效率要高一些。所以尽管斐波那契查找的时间复杂度也为O(logn),但就平均性能来说,斐波那契查找要优于折半查找。可惜如果是最坏的情况,比如这里key=1,那么始终都处于左侧在查找,则查找效率低于折半查找。
还有关键一点,折半查找是进行加法与除法运算的(mid=(low+high)/2),插值查找则进行更复杂的四则运算(mid = low + (high - low) * ((key - a[low]) / (a[high] - a[low]))),而斐波那契查找只进行最简单的加减法运算(mid = low + F[k-1] - 1),在海量数据的查找过程中,这种细微的差别可能会影响最终的效率。