数据结构与算法的重温之旅(番外篇1)——谈谈斐波那契数列

在讲斐波那契数列之前,我们先回顾一下之前在第一篇文章讲复杂度分析里,谈到时间复杂度的时候,讲到时间复杂度有七种,分别是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;到第三年的时候,第二代的兔子成年并生了一对小兔,算上第一代生的一对小兔,第三年则生了两对小兔。第四年的时候,第三代兔子成年生了一对小兔,算上今年第一代和第二代小兔生的兔子,第四年总共生了三队小兔,下面我们用图来表示一下这关系:

这里我们可以看到,从第三个开始,每一项都等于前两个数的和。如果用作递推公式来表示的话我们可以用下面这条公式来表示:

n=1:  f(n) = f(1)

n=2:  f(n) = f(2)

n>2:  f(n) = f(n - 1) + f(n - 2)

看到这条公式是不是觉得有点似曾相识。在第八篇文章讲递归的时候走台阶游戏其实就是斐波那契数列。现在一听是不是觉得只要了解背后的本质,也就是这条递推公式,不管你怎么变,其实核心都只是斐波那契数列而已。那如何用代码实现呢?在第八篇文章的时候其实有讲过他的实现方法,解法如下:

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的增大呈现指数增长。下面有一张图来简单的表示:

在这张图我们可以看到,我们计算f(6)的时候树的层级为4,在这棵树当中,我们可以发现其实有很多计算都是重复的,这样的重复计算耗费了大量的时间,那有什么方法优化呢?这里可以利用非递归循环的思想来解斐波那契数列,代码如下:

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)。那么还有更快的吗?那肯定是有的,在数学里,求解斐波那契数列有一个通项公式,利用特征方程来求解的,公式如下:

f_{n}=\frac{1}{\sqrt{5}[(\frac{1+\sqrt{5}}{2})^{n}-(\frac{1-\sqrt{5}}{2})^{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),求幂运算里嵌套一个求根运算,就是logn^{2},可以转换为2logn,虽然说去掉常数2最后的时间复杂度也是O(logn),但是为了更快我们可以通过矩阵运算,来构建斐波那契数列的矩阵形态,然后通过矩阵乘法的结合性,把斐波那契转换成矩阵的幂运算,这一点我们可以把非递归循环的方法加以改写,通过矩阵乘法来求解,然后再利用上面的求幂公式得到结果。按照这个思路,我们假设一个矩阵x,使得a1矩阵乘以x等于a2矩阵。公式如下:

\begin{bmatrix} a &b \\ 0 &0 \end{bmatrix}*x=\begin{bmatrix} b &a+b \\ 0 & 0 \end{bmatrix}

通过矩阵乘法,我们可以求得x的值如下:

\begin{bmatrix} a &b \\ 0 &0 \end{bmatrix}*\begin{bmatrix} 0&1 \\ 1& 1 \end{bmatrix}=\begin{bmatrix} b &a+b \\ 0 & 0 \end{bmatrix}

得到这个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)。

 

发布了72 篇原创文章 · 获赞 44 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/Tank_in_the_street/article/details/97187760