折半查找是从中间分,也就是说,每一次查找总是一分为二,无论数据偏大还是偏小,很多时候这都未必就是最合理的做法。除了插值查找,我们再介绍一种有序查找,斐波那契查找(Fibonacci Search),它是利用了黄金分割原理来实现的。
斐波那契数列我们在前面4.8节讲递归时,也详细地介绍了它。如何利用这个数列来作为分割呢?
为了能够介绍清楚这个查找算法,我们先需要有一个斐波那契数列的数组。
下面我们根据代码来看程序是如何运行的:
/* 斐波那契查找 */
int Fibonacci_Search(int *a, int n, int key)
{
int low, high, mid, i, k;
/*定义最低下标为记录首位 */
low = 1;
/*定义最高下标为记录末位 */
high = n;
k = 0;
/* 计算n位于斐波那契数列的位置 */
while (n > F[k] - 1)
k++;
/* 将不满的数值补全 */
for (i = n; i < F[k] - 1; i++)
a[i] = a[n];
while (low <= high)
{
/* 计算当前分隔的下标 */
mid = low + F[k - 1] - 1;
/* 若查找记录小于当前分隔记录 */
if (key < a[mid])
{/* 最高下标调整到分隔下标mid-1处 */
high = mid - 1;
/* 斐波那契数列下标减一位 */
k = k - 1; }
/* 若查找记录大于当前分隔记录 */
else if (key > a[mid])
{/* 最低下标调整到分隔下标mid+1处 */
low = mid + 1;
/* 斐波那契数列下标减两位 */
k = k - 2;
}else
{
if (mid <= n)
/* 若相等则说明mid即为查找到的位置 */
return mid;
else
/* 若mid>n说明是补全数值,返回n */
return n;
}
}
return 0;
}
下面对代码进行详细解释说明:
首先定义了函数
Fibonacci_Search
,输入参数为一个整型数组a
,数组长度为n
,要查找的关键字为key
。定义了变量
low
和high
分别表示最低下标和最高下标,初始值分别为数组的第一个和最后一个元素的下标。定义了变量
k
为0,用于计算关键字所在的斐波那契数列位置。计算关键字所在的斐波那契数列位置。通过循环找到满足
n > F[k] - 1
的最小k
值。将数组的长度扩展到满足斐波那契数列的要求。通过循环将数组的最后一个元素复制到扩展后的位置。
进入查找过程的主循环,当
low
小于等于high
时循环执行。计算当前分隔的下标
mid
,通过low + F[k - 1] - 1
计算得到。如果关键字小于当前分隔记录,说明关键字在分隔的左侧,将
high
调整到mid - 1
处,同时将k
减1。如果关键字大于当前分隔记录,说明关键字在分隔的右侧,将
low
调整到mid + 1
处,同时将k
减2。如果关键字等于当前分隔记录,分两种情况处理: a. 如果
mid
小于等于n
,说明mid
即为查找到的位置,返回mid
。 b. 如果mid
大于n
,说明扩展的位置是补全的数值,返回n
。如果循环结束时仍未找到关键字,返回0。
综上所述,该代码实现了使用斐波那契数列进行查找的算法,通过将数组长度扩展到满足斐波那契数列的要求,并以斐波那契数列的划分进行二分查找的方式,提高了查找效率。
斐波那契查找算法的核心在于:
- 1)当key=a[mid]时,查找就成功;
- 2)当key<a[mid]时,新范围是第low个到第mid-1个,此时范围个数为F[k-1]-1个;
- 3)当key>a[mid]时,新范围是第m+1个到第high个,此时范围个数为F[k-2]-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),在海量数据的查找过程中,这种细微的差别可能会影响最终的查找效率。
应该说,三种有序表的查找本质上是分隔点的选择不同,各有优劣,实际开发时可根据数据的特点综合考虑再做出选择。