一、斐波那契查找算法概述
1.1 什么是斐波那契查找算法
斐波那契搜索(Fibonacci search) ,又称斐波那契查找,是区间中单峰函数的搜索技术。斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。
斐波那契查找同样是查找算法家族中的一员,它要求数据是有序的(升序或降序)。斐波那契查找采用和二分查找/插值查找相似的区间分割策略,都是通过不断的分割区间缩小搜索的范围。
1.2 什么是斐波那契数列
要想具体学习斐波那契查找算法,就不得不先了解一下斐波那契数列。
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为 “兔子数列”。
如下图所示,就是一个斐波那契数列:
在数学上,斐波那契数列被以如下递推的方法定义:
F
(
0
)
=
0
,
F
(
1
)
=
1
F(0) = 0, \ F(1) = 1
F(0)=0, F(1)=1
F ( n ) = F ( n − 1 ) + F ( n − 2 ) ( n ≥ 2 , n ∈ N ∗ ) F(n) = F(n-1) + F(n-2) \qquad (n≥2,n∈N^*) F(n)=F(n−1)+F(n−2)(n≥2,n∈N∗)
从斐波那契的递推公式我们可以总结出重要一点:斐波那契数列从第三项开始,每一项都等于前两项之和。
1.3 斐波那契查找算法的思想
由 1.2 我们可以得知斐波那契数列的特点,而我们可以利用它的特点来做区间分割:可以将一个长度为 F(n) 的数组看作左右两段,左边一段长度是 F(n-1),右边一段长度是 F(n-2)。
正是上面这样的区间分割想法,使斐波那契数列和数组联系到了一起。这种分割思想亦是斐波那契查找算法的基础。
斐波那契查找算法相对于二分查找和插值查找的基本思路是一致的,其中最重要的区别就是它们的查找点(或称中间值)的确定。斐波那契查找算法的查找点的计算公式如下:
m
i
d
=
l
e
f
t
+
F
(
n
−
1
)
−
1
mid = left+F(n-1)-1
mid=left+F(n−1)−1
【基本思想】
完整的斐波那契查找的基本思想如下:在斐波那契数列找一个等于或第一个大于查找表中元素个数的数 F[n],然后将原查找表的长度扩展为 Fn (如果要补充元素,则重复补充原查找表最后一个元素,直到原查找表中满足 F[n] 个元素);扩展完成后进行斐波那契分割,即 F[n] 个元素分割为前半部分 F[n-1] 个元素,后半部分 F[n-2] 个元素;接着找出要查找的元素在哪一部分并递归,直到找到。
二、斐波那契查找的基本步骤
从上面我们知道了斐波那契查找算法的基本思想,根据它的基本思想,斐波那契查找的基本步骤可以分为以下几步:
- 构建斐波那契数列;
- 找出查找表长度对应的斐波那契数列中的元素 F(n);
- 如果查找表长度小于斐波那契数列中对应的元素 F(n) 的值,则补充查找表(以查找表最后一个元素补充);
- 根据斐波那契数列特点对查找表进行区间分隔,确定查找点
mid = left+F(n-1)-1
(减 1 是因为数组下标从 0 开始); - 判断中间值
arr[mid]
和目标值的关系,确定下一步策略:- 如果目标值小于中间值,说明目标值在左区间。由于左区间长度为 F(n-1),因此 n 应该更新为 n-1,然后再次执行 4、5 两步;
- 如果目标值大于中间值,说明目标值在右区间。由于右区间长度为 F(n-2),因此 n 应该更新为 n-2,然后再次执行 4、5 两步;
- 如果目标值等于中间值,说明找到了目标值。但此时还需判别该目标值是原查找表中的元素还是填充元素:
- 如果是原查找表中的元素,直接返回索引;
- 如果是填充元素,则返回原查找表的最后一个元素的索引,即
arr.length-1
。(因为扩展数组是以原查找表最后一个元素来填充,如果目标值是填充元素,则说明原查找表最后一个元素值就是目标值)
三、斐波那契查找的代码实现
根据斐波那契查找算法的基本步骤,实现出的代码如下:
/**
* 斐波那契查找算法
* @param arr 查找表
* @param findValue 目标值
* @return int 目标值在查找表中的索引
*/
public static int fibonacciSearch(int[] arr, int findValue){
int i = 0;
int mid; // 中间值
int left = 0; // 区间左端
int right = arr.length-1; // 区间右端
// 1. 创建斐波那契数列
int[] fibonacci = getFibonacci(20);
// 2. 获取斐波那契数列中等于或者第一个大于数组长度的数
while(fibonacci[i] < arr.length){
i++;
}
// 3. 按照斐波那契数列中的元素长度拷贝一个查找表
int[] temp = Arrays.copyOf(arr, fibonacci[i]);
// 4. 以原查找表最后一个元素补齐临时查找表长度
for (int j=arr.length; j<temp.length; j++){
temp[j] = arr[arr.length-1];
}
// 5. 循环判断
while (left <= right){
mid = left + fibonacci[i-1]-1; // 计算查找点
if (temp[mid] < findValue){ // 如果查找点小于目标值,说明在右区间
left = mid + 1; // 右区间起点
i -= 2; // 右区间长度是 f[i-2],所以要把 i 换成 i-2
}else if (temp[mid] > findValue){ // 如果查找点大于目标值,说明在左区间
right = mid - 1; // 左区间终点
i -= 1; // 左区间长度是 f[i-1],根据所以要把 i 换成 i-1
}else{ // 如果相等,说明找到了
/* 找到存在两种可能:一是找到的是原查找表中的元素,二是找到的是填充值。因此需要判别*/
if (mid <= right){ // 如果是原查找表中的元素,直接返回索引
return mid;
}else{ // 如果找到的是填充值,则返回原查找表最后一个索引
return right;
}
}
}
return -1;
}
/**
* 创建斐波那契数列
* @param maxSize 斐波那契数列长度
* @return int[] 斐波那契数列
*/
public static int[] getFibonacci(int maxSize){
int[] fibonacci = new int[maxSize];
fibonacci[0] = 0;
fibonacci[1] = 1;
for (int i=2; i<maxSize; i++){
fibonacci[i] = fibonacci[i-1] + fibonacci[i-2];
}
return fibonacci;
}