斐波那契数列
定义: 斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
在数列:0、1、1、2、3、4、5、8…中,我们发现任意前两项之和,等于第三项,即 0 + 1 = 1、1 + 1 = 2、1 + 2 = 3、2 + 3 = 5。这样的数列就是斐波那契数列。而在这个数列中的数,就叫斐波那契数。
它和黄金分割有什么关系呢?我们发现,斐波那契数列越往后,前一项与后一项的比值原来越趋近于 “黄金分割” (即0.618)。比如:13 / 21 = 0.619… ; 21 / 34 = 0.617…; 34 / 55 = 0.618…;
斐波那契查找
斐波那契查找是基于 二分查找 (不懂二分查找的点这里!)改进的一种查找方式,同样要求查找的数据是 有序的。总体思想和二分查找相同,不同点是对于每次中间值的选取。二分查找每次是取正中间的值,使得原数组被划分为 0.5 :0.5 平均的两部分。斐波那契查找是选取黄金分割点,使得原数组被划分为 0.382 :0.618 的两部分。
实现前的细节问题
在具体说查找实现之前,我们明确两个问题:
1.为什么要将数组长度扩充到刚好等于某个斐波那契数 - 1 ?
2.问题1中的 - 1 是怎么来的?
问题1:我们要将数组每次划分成 0.382 :0.618 的两部分,但是数组的长度应该是个整数,如果直接用小数代入算式中,那被舍弃的小数将会使得误差越来越大,即你划分开来的两部分,离黄金分割要求的 0.618 越来越偏。而我们观察斐波那契数列,里面的数既符合整数的要求,而且也符合黄金分割的要求,所以我们将数组长度扩充,借助斐波那契数列帮我们完成分割。
问题2:我们举个例子,现在你有一个数组:[1,2,3,4,5]长度为5,正好是一个斐波那契数,我们要先选出一个值进行比较。那么如果比较完,不符合,需要进行划分,被选出且比较过的值,我们还需要再划分进来吗?当然不需要!这样只会造成浪费,重复比较。所以拿掉这个数后,剩下4个数,按照斐波那契数列,应该划分为2和3两部分,可是4个数,不好划分。
如果我们数组是[1,2,3,4]长度为4,那么选取完一个去比较后,剩下3个数,那么我们可以划分为 2 - 1 和 3 - 1两部分,这样他们依旧保持着某个斐波那契数 - 1的形式!
5 - 1(原数组) = (3 - 1)(划分后前面部分) +(2 - 1)(划分后后面部分)+ 1(拿出去比较的数)。
具体实现步骤
1.构建一个斐波那契数列。(我们需要斐波那契数列,不伸手,自己造。)
2.寻找出大于要查找数组长度的斐波那契数。
3.将查找数组的长度扩容,使其长度=某个斐波那契数-1。扩容后,将原数组最后一个数复制,填满扩容后出现的空位。
4.开始像二分查找那样循环查找,但要注意每次选取的比较值方式是和二分查找不同的。
5.如果判断的值相等,要判断索引结果是否大于原数组的最大索引,如果大于,要返回的是原数组的最大索引。因为后面扩容的数据都是原数组最后一个数复制上去的。而且不能返回一个比要查找数组索引值之外的结果回去(除了-1表示找不到)。
复杂度分析
最坏情况下,时间复杂度为O( l o g 2 log_2 log2N),且其期望复杂度也为O( l o g 2 log_2 log2N)。
该查找mid = left + f[k-1] - 1;不涉及除法,如果该数列长度正好等于一个斐波那契数-1,那么理论情况要比有除法的二分查找法快。
适用情况:查找数组是有序的,且长度正好等于某个斐波那契数-1。
代码实现
此次实现是基于Java,jdk1.8。
1.构建一个斐波那契数列。我们将其定义为一个静态方法,方便调用。用循环很快就可以构建出来。
public static int[] fib(int size) {
int[] fib = new int[size];
fib[0] = 1;
fib[1] = 1;
for (int i = 2; i < fib.length; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib;
}
2 . 寻找出大于要查找数组长度的斐波那契数。首先要调用构建斐波那契数列的方法,返回一个斐波那契数列。然后寻找出大于等于查找数组长度的斐波那契数。
public int fibonacciSearch(int[] data, int target) {
int[] f = fib(20);
while (f[f.length - 1] < data.length) {
f = fib(f.length * 2);
}
int k = 0;
while (data.length > f[k] - 1) {
k++;
}
}
3 . 将查找数组的长度扩容,使其长度=某个斐波那契数-1。扩容后,将原数组最后一个数复制,填满扩容后出现的空位。
int left = 0;
int right = data.length - 1;
int mid = 0;
int[] temp = Arrays.copyOf(data, f[k]-1);
// 后边多余的部分,用data[right]补充
for (int i = right + 1; i < temp.length; i++) {
temp[i] = temp[right];
}
4 . 开始像二分查找那样循环查找,mid = left + f[k - 1] - 1;
while (left <= right) {
// 黄金分割,一共有f[k]-1个,左边是f[k-1]-1个,
// 右边是f[k-1]-1个,所以中间元素的下标为mid = left + f[k-1]-1;
// 比如[1,2,3,4],
// 前面f[k-1]-1个,就是2个,后面f[k-2]-1个,就是1个,那么中间元素3的下标正好是f[k
// -1]-1+left=2+0=2
// f[k-1]-1 + f[k-2]-1 + 1(mid) = f[k-1]
mid = left + f[k - 1] - 1;
if (temp[mid] > target) {
right = mid - 1;
// 第一次分割后,左边剩下f[k-1]-1个,下一次分割f[k-1]=f[k-2]+f[k
// -3],所以k--;
// 换个理解,现在是f[k-1],要变成f[k-2],那就f[k-1-1],
k--;
} else if (temp[mid] < target) {
left = mid + 1;
// 右边剩下f[k-2]-1个,下一次分割f[k-2]=f[k-3]+f[k-4],
// 所以k-=2;
// 换个理解,现在是f[k-1],要变成f[k-3],那就f[k-2-1],
k -= 2;
} else {
// 如果找到了,要注意,因为temp后面可能是补了好几个data[right
// ]的,所以我们要返回第一个data[right]的下标,所以mid不能超过right。
if (mid <= right) {
return mid;
} else {
return right;
}
}
}
5 . 判断一下找到的结果索引,是否大于原数组最大索引,大于则返回原数组最大索引作为结果。
if (mid <= right) {
return mid;
} else {
return right;
}
完整代码
//斐波那契查找
//该查找mid = left + f[k-1] - 1;不涉及除法,如果该数列长度正好等于一个斐波那契数-1,那么理论情况要比有除法的二分查找法快。
//同样要求有序数列,也是对二分查找的一种改进,用黄金分割来拆分。复杂度分析:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。
//黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1
// .618:1。
//斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89……
// .(从第三个数开始,后边每一个数都是前两个数的和)。随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618
//斐波那契数就是斐波那契数列中的一个值,比如21在数列中,就是一个斐波那契数,22不在,就不是。
import java.util.Arrays;
public class FibonacciSearch {
// 首先要构建一个斐波那契数列,因为我们要知道哪些数是一个斐波那契数。
// 只有它是一个斐波那契数,才能使用黄金分割。比如21是,那么可以分割成8和13。
public static int[] fib(int size) {
int[] fib = new int[size];
fib[0] = 1;
fib[1] = 1;
for (int i = 2; i < fib.length; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib;
}
public int fibonacciSearch(int[] data, int target) {
// 先去获取一个斐波那契数列,初始默认值20,如果20个斐波那契数列的数都小于查找数组长度,那就获取个长度翻倍的斐波那契数列。
int[] f = fib(20);
while (f[f.length - 1] < data.length) {
f = fib(f.length * 2);
}
int left = 0;
int right = data.length - 1;
int k = 0;
int mid = 0;
// 找到大于数组长度的斐波那契数
// 为什么这里是f[k]-1呢?
// 1.首先我们找mid时,就用掉一个,如果直接使用斐波那契数(比如k=4,f[k]=5),
// 那么分割后应该是f[k-1]=3,f[k-2]=2,这是用掉的mid应该从前段还是后段扣掉?
// 这将导致子序列的元素个数可能为f(k-1),f(k-1)-1,f(k-2),f(k-2)-1
// 2.所以我们使用f[k]-1,这样用掉一个mid后,剩下f[k]-2,那么直接分割成f[k-1]-1,f[k-2]-1。
while (data.length > f[k] - 1) {
k++;
}
// 构建一个长度为斐波那契数-1的数组
int[] temp = Arrays.copyOf(data, f[k]-1);
// 后边多余的部分,用data[right]补充
for (int i = right + 1; i < temp.length; i++) {
temp[i] = temp[right];
}
while (left <= right) {
// 黄金分割,一共有f[k]-1个,左边是f[k-1]-1个,
// 右边是f[k-1]-1个,所以中间元素的下标为mid = left + f[k-1]-1;
// 比如[1,2,3,4],
// 前面f[k-1]-1个,就是2个,后面f[k-2]-1个,就是1个,那么中间元素3的下标正好是f[k
// -1]-1+left=2+0=2
// f[k-1]-1 + f[k-2]-1 + 1(mid) = f[k-1]
mid = left + f[k - 1] - 1;
if (temp[mid] > target) {
right = mid - 1;
// 第一次分割后,左边剩下f[k-1]-1个,下一次分割f[k-1]=f[k-2]+f[k
// -3],所以k--;
// 换个理解,现在是f[k-1],要变成f[k-2],那就f[k-1-1],
k--;
} else if (temp[mid] < target) {
left = mid + 1;
// 右边剩下f[k-2]-1个,下一次分割f[k-2]=f[k-3]+f[k-4],
// 所以k-=2;
// 换个理解,现在是f[k-1],要变成f[k-3],那就f[k-2-1],
k -= 2;
} else {
// 如果找到了,要注意,因为temp后面可能是补了好几个data[right
// ]的,所以我们要返回第一个data[right]的下标,所以mid不能超过right。
if (mid <= right) {
return mid;
} else {
return right;
}
}
}
return -1;
}
}