在讲斐波那契数列以前,咱们先回顾一下以前在第一篇文章讲复杂度分析里,谈到时间复杂度的时候,讲到时间复杂度有七种,分别是O(1),O(logn),O(n),O(nlogn),O(n^2),O(2^n),O(n!)。前面五种的话其实很容易写出对应的算法来实现相应时间复杂度。好比O(1)时间复杂度在数组的下标取值,链表的插入和删除都是这个时间复杂度;O(logn)时间复杂度能够经过二分查找来实现,二分查找会在以后的文章有讲;O(n)时间复杂度能够经过数组的遍历和链表的查询能够实现;O(nlogn)时间复杂度能够经过第十篇文章讲的快速排序和归并排序实现;O(n^2)时间复杂度则能够经过冒泡排序、插入排序、选择排序来实现。可是咱们在那篇文章中好像漏了O(2^n)和O(n!)时间复杂度是经过哪一种现实状况来实现,今天我主要讲O(2^n)时间复杂度是如何实现的,至于后面的O(n!)时间复杂度则留到下一篇讲排列组合的文章具体分析。算法
咱们先来看看斐波那契数列的定义是什么,斐波那契的定义是这个数列从第3项开始,每一项都等于前两项之和。一开始这个数列是应用与兔子繁殖的问题。好比一对成年兔子一年生一对小兔,小兔一年以后长成年。一开始只有一对小兔,求n年以后有多少只兔子。数组
咱们先来分析一下,第0年的时候没有生兔子,因此为0;第一年生了一对兔子,记为1;因为小兔要等一年才能成年,因此第二年老兔子仍然只生一对兔子,记为1;到第三年的时候,第二代的兔子成年并生了一对小兔,算上第一代生的一对小兔,第三年则生了两对小兔。第四年的时候,第三代兔子成年生了一对小兔,算上今年第一代和第二代小兔生的兔子,第四年总共生了三队小兔,下面咱们用图来表示一下这关系:bash
这里咱们能够看到,从第三个开始,每一项都等于前两个数的和。若是用做递推公式来表示的话咱们能够用下面这条公式来表示:函数
n=1: f(n) = f(1)优化
n=2: f(n) = f(2)ui
n>2: f(n) = f(n - 1) + f(n - 2)spa
看到这条公式是否是以为有点似曾相识。在第八篇文章讲递归的时候走台阶游戏其实就是斐波那契数列。如今一听是否是以为只要了解背后的本质,也就是这条递推公式,无论你怎么变,其实核心都只是斐波那契数列而已。那如何用代码实现呢?在第八篇文章的时候其实有讲过他的实现方法,解法以下:code
function func(val){
if (val === 1) return 1
if (val === 2) return 2
return func(val - 1) + func(val - 2)
}复制代码
那么这种解法有什么问题呢,这种解法的问题是时间复杂度很是高,是O(2^n),当你输入40的时候,计算结果已经要花费好几秒的时间了,为何会是这样呢,由于求解F(n),必须先计算F(n-1)和F(n-2),计算F(n-1)和F(n-2),又必须先计算F(n-3)和F(n-4)。。。。。。以此类推,直至必须先计算F(1)和F(0),而后逆推获得F(n-1)和F(n-2)的结果,从而获得F(n)要计算不少重复的值,在时间上形成了很大的浪费,算法的时间复杂度随着N的增大呈现指数增加。下面有一张图来简单的表示:cdn
在这张图咱们能够看到,咱们计算f(6)的时候树的层级为4,在这棵树当中,咱们能够发现其实有不少计算都是重复的,这样的重复计算耗费了大量的时间,那有什么方法优化呢?这里能够利用非递归循环的思想来解斐波那契数列,代码以下:blog
function func(n) {
if (n === 0) {
return 0
}
else if (n < 3) {
return 1
}
let a1 = 1, a2 = 1
for (let i = 1; i < n - 1; i++) {
[a1, a2] = [a2, a1 + a2]
}
return a2
}复制代码
经过这种算法,咱们减小了每次的重复计算的次数,使得时间复杂度压缩到O(n)。那么还有更快的吗?那确定是有的,在数学里,求解斐波那契数列有一个通项公式,利用特征方程来求解的,公式以下:
利用这项公式,咱们能够获得代码:
function func (n) {
return Math.round((Math.pow((1+Math.sqrt(5))/2, n) - Math.pow((1 - Math.sqrt(5))/2, n)) / Math.sqrt(5))
}复制代码
乍一看好像时间复杂度变为O(1),其实不是的,这里的使用了JavaScript函数内置的幂运算Math.pow方法,执行了n次幂,那n次幂的话时间复杂度是O(n)吗,也不是。在计算机中,求幂能够经过平方来不断的接近n,求根则能够经过二分来不断的接近要求解的数,求幂和求根的方法的时间复杂度都是O(logn),因此这里的时间复杂度是O(logn)。
其实因为IEEE754标准的问题,咱们每次经过计算所获得的值都要经过Math.round函数来进行一次四舍五入的运算,在计算机中,执行这个方法也是要耗费必定的时间的,其实咱们能够经过改写底层的Math.pow方法来使得改方法直接返回一个整数型的数值,这个时候就须要深刻到二进制了。
这个思路是这样的,对于咱们要求的幂,传入来的幂若是模以2有余数的时候,咱们则乘以对应次数的x倍,若是没有则乘以对应次数的n。代码以下:
function pow (x, n) {
var r = 1
var v = x
while (n) {
if (n % 2 == 1) {
r *= v
n -= 1
}
v *= v
n = n / 2
}
return r
}复制代码
就这样上面的通项公式代码能够改写成以下:
function func (n) {
return (pow((1+Math.sqrt(5))/2, n) - pow((1 - Math.sqrt(5))/2, n)) / Math.sqrt(5)
}
function pow (x, n) {
var r = 1
var v = x
while (n) {
if (n % 2 == 1) {
r *= v
n -= 1
}
v *= v
n = n / 2
}
return r
}复制代码
上面的方法其实也用到了JavaScript自带的Math.sqrt求根方法,上面也说到求根运算在计算机中也是时间复杂度为O(logn),求幂运算里嵌套一个求根运算,就是,能够转换为2logn,虽说去掉常数2最后的时间复杂度也是O(logn),可是为了更快咱们能够经过矩阵运算,来构建斐波那契数列的矩阵形态,而后经过矩阵乘法的结合性,把斐波那契转换成矩阵的幂运算,这一点咱们能够把非递归循环的方法加以改写,经过矩阵乘法来求解,而后再利用上面的求幂公式获得结果。按照这个思路,咱们假设一个矩阵x,使得a1矩阵乘以x等于a2矩阵。公式以下:
经过矩阵乘法,咱们能够求得x的值以下:
获得这个x的值咱们能够获得一个代码,以下所示:
function matrixMul (x, y) {
return [
[x[0][0] * y[0][0] + x[0][1] * y[1][0], x[0][0] * y[0][1] + x[0][1] * y[1][1]],
[x[1][0] * y[0][0] + x[1][1] * y[1][0], x[1][0] * y[0][1] + x[1][1] * y[1][1]]
]
}复制代码
紧接着稍微的改写一下求幂公式,得:
function pow (x, n) {
var r = [[1,0],[0,1]]
var v = x
while (n) {
if (n % 2 == 1) {
r = matrixMul(r, v)
n -= 1
}
v = matrixMul(v, v)
n = n / 2
}
return r
}复制代码
这下子咱们就能够上面说的矩阵乘法,求得等式右边的值,咱们最后只要右上角的元素,因此代码以下:
function func (n) {
if (n <= 0) {
return 0
}
else {
return matrixMul([[0,1],[0,0]], pow([[0,1],[1,1]], n - 1))[0][1]
}
}复制代码
就这样能够经过矩阵乘法,进一步的将时间复杂度稳定在O(logn)。