斐波那契查找

斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],将原查找表扩展为长度为Fn,完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。

定义

斐波那契查找也是有序查找的一种,它是利用黄金分割原理来实现的。该查找方法的精髓在于采用最接近查找长度的斐波那契数值来确定拆分点。

斐波那契数列

既然叫斐波那契查找,首先得弄明白什么是斐波那契数列。相信大家对这个著名的数列也并不陌生,无论是C语言的循环、递归,还是高数的数列,斐波那契数列都是一个重要的存在。而此处主要是用到了它的一条性质:前一个数除以相邻的后一个数,比值无限接近黄金分割。
 
斐波那契数列指的是这样一个数列 1,1,2,3,5,8,13, 21,34,55, 89,144,233……这个数列从第3项开始,每一项都等于前两项之和。如果我们用数学函数来定义是:

 
当数列趋向于无穷大时,前一项与后一项的比值越来越逼近黄金分割0.618(或者说后一项与前一项的比值小数部分越来越逼近0.618)。
 
1÷1=1,1÷2=0.5,2÷3=0.666…,3÷5=0.6,5÷8=0.625…,55÷89=0.617977…,144÷233=0.618025…,越到后面,这些比值越接近黄金比。

斐波那契查找原理

斐波那契查找是一种在有序表中高效查找指定元素的算法,比折半查找要复杂一些,主要复杂在要多做不少准备工作。下面看它的工作流程:

  1. 计算并保存一个斐波那契序列的数组,方便以后取值。数组名记为F,例如F[1]=1,F[2]=1,F[3]=2,F[4]=3,F[5]=5,F[6]=8,F[7]=13,F[8]=21
  2. 把有序数组的长度扩充到A.length=F[k]-1,k是满足条件的最小值,比如数组长度为13,那么就把它长度扩充到F[8]-1=20,所有在末尾添加的扩充元素都是原数组最后一个元素的复制品。
  3. 找到mid元素,不断进行二分比较,直到找到目标元素为止,这一步的做法与折半查找一模一样,仅仅是计算mid的公式从(low+high)/2改为low+(F[k-1]-1)。

举个例子来讲,现有长度为9的数组,要对它进行拆分,对应的斐波那契数列(长度先随便取,只要最大数大于9即可){1,1,2,3,5,8,13,21,34},不难发现,大于9且最接近9的斐波那契数值是f[6]=13,为了满足所谓的黄金分割,所以它的第一个拆分点应该就是f[6]的前一个值f[5]=8,即待查找数组array的第8个数,对应到下标就是array[7],依次类推。

推演到一般情况,假设有待查找数组array[n]和斐波那契数组F[k],并且n满足n>=F[k]-1&&n<F[k+1]-1,则它的第一个拆分点middle=F[k]-1。

这里得注意,如果n刚好等于F[k]-1,待查找数组刚好拆成F[k-1]和F[k-2]两部分,那万事大吉你好我好;然而大多数情况并不能尽人意,n会小于F[k]-1,这时候可以拆成完整F[k-1]和残疾的F[k-2]两部分,那怎么办呢?

聪明的前辈们早已想好了解决办法,对了,就是补齐,用最大的数来填充F[k-2]的残缺部分,如果查找的位置落到补齐的部分,那就可以确定要找的那个数就是最后一个最大的了。
image

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public class FbonacciSearch {

/**
* <p>name: main</p>
* <p>description: </p>
* <p>return: void</p>
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] array = { 1, 5, 15, 22, 25, 31, 39, 42, 47, 49, 59, 68, 88, 88,
88, 88, 88 };
System.out.println("result: " + fbSearch(array, 31));
}

/**
* <p>
* name: fbSearch
* </p>
* <p>
* description: 斐波那契查找实现
* </p>
* <p>
* return: int
* </p>
*/
public static int fbSearch(int[] array, int a) {
if (array == null || array.length == 0) {
return -1;
} else {
int length = array.length;
int[] fb = makeFbArray(20);// 制造一个长度为10的斐波数列
int k = 0;
while (length > fb[k] - 1) {// 找出数组的长度在斐波数列(减1)中的位置,将决定如何拆分
k++;
}
int[] temp = Arrays.copyOf(array, fb[k] - 1);// 构造一个长度为fb[k] - 1的新数列
for (int i = length; i < temp.length; i++) {
if (i >= length) {
temp[i] = array[length - 1];
}
}
int low = 0;
int hight = array.length - 1;
while (low <= hight) {
int middle = low + fb[k - 1] - 1;
if (temp[middle] > a) {
hight = middle - 1;
k = k - 1;
} else if (temp[middle] < a) {
low = middle + 1;
k = k - 2;
} else {
if (middle <= hight) {
return middle;// 若相等则说明mid即为查找到的位置
} else {
return hight;// middle的值已经大于hight,进入扩展数组的填充部分,即最后一个数就是要查找的数。
}
}
}
return -1;
// return recurse(array, fb, a, low, hight, k);
}
}

/**
* <p>
* name: makeFbArray
* </p>
* <p>
* description: 生成指定长度的斐波数列
* </p>
* <p>
* return: int[]
* </p>
*/
public static int[] makeFbArray(int length) {
int[] array = null;
if (length > 2) {
array = new int[length];
array[0] = 1;
array[1] = 1;
for (int i = 2; i < length; i++) {
array[i] = array[i - 1] + array[i - 2];
}
}
return array;
}

/**
* <p>
* name: recurse
* </p>
* <p>
* description: 递归实现,可以来代替while (low <= hight)遍历
* </p>
* <p>
* return: int
* </p>
*/
public static int recurse(int[] array, int[] fb, int a, int low, int hight,
int k) {
if (array == null || array.length == 0 || a < array[low]
|| a > array[hight] || low > hight) {
return -1;
}
int middle = low + fb[k - 1] - 1;
if (a < array[middle]) {
return recurse(array, fb, a, low, middle - 1, k - 1);
} else if (a > array[middle]) {
return recurse(array, fb, a, middle + 1, hight, k - 2);
} else {
if (middle <= hight) {
return middle;
} else {
return hight;
}
}
}
}

总结

斐波那契查找算法的核心在于:

  1. 当 key=a[mid]时,查找就成功;
  2. 当 key<a[mid)时,新范围是第 Iow 个到第 mid-l 个,此时范围个数为 F[k-1]-1 个;
  3. 当 key>a[mid]时,新范围是第 m+l 个到第 high 个,此时范围个数为 F[k-2] -1 个。

也就是说,如果要查找的记录在右侧,则左侧的数据都不用再判断了,不断反复进行下去,对处于当中的大部分数据,其工作效率要高一些。所以尽管斐波那契查找的时间复杂也为0(logn), 但就平均性能来说,斐波那契查找要优于折半查找。可惜如果是最坏情况,比如这里 key=l , 那么始终都处于左侧长半区在查找,则查找效率要低于折半查找。