前文介绍了递归和迭代、减而治之和分而治之的概念。
数据结构(邓俊辉):递归和迭代、分而治之和减而治之(1.概念)
接下来举一些例子来解释:
以数组求和为例:
采用减而治之(线性递归)的办法:
int sum(int A[n], int n){
if(n > 1)
return sum(A, n-1) + A[n-1];
else //特殊情况 n<1 ,也就是数组为空的情况,这也是递归停止的条件
return 0;
}
由上述代码可以看出,该问题采用的是减而治之(规模为n-1的子问题sum(A, n-1)
和规模为1 的子问题A[n-1]
)的思路,同时必须考虑“递归基”用来停止递归。
很容易可以分析出来它的时间复杂度为:O(n)
,空间复杂度为:O(n)
。
下面采用分而治之(递归)的办法:
int sum(int A[], int lo, int hi){
if(lo > hi)
return false;
else if(lo == hi)
return A[lo]; //return A[hi]
else{
mid = (lo + hi)/2;//mid = (lo + hi) >> 1;
return sum(A, lo, mid) + sum(A, mid+1, hi);
}
return ;
}
从以上代码可以看出,它将问题分成两个均等的子问题,调用自身去解决。
很容易可以分析出来它的时间复杂度为:O(n)
,空间复杂度为:O(log)
。
下面举个斐波那契数列的例子来说明递归和迭代的效率问题。
斐波那契数列的定义:
fib(0) = 0
fib(1) = 1
fib(n) = fib(n-1)+fib(n-2) (n>=2)
(1)直接采用递归的办法:
int fib(int n){
if(n == 0)
return 0;
else if(n == 1)
return 1;
else(n >= 2)
return fib(n-1)+fib(n-2);
}
分析递归的时间复杂度:
采用递推公式来做:
其实粗糙的计算应该是把T(n)
的递推公式求出来,形式上和fib(n)
一样,那么最后的结果也是大致相同的。
但是这个O(2^n)
的复杂度太高了。其本质原因是每一次都要重新计算,那么就会把前面算过的都算一遍,重复浪费了。
如何解决这个问题:借助一定量的辅助空间,在子问题解决后,将它们及时保存下来。
为了避免这样计算,要么每次计算前,查下辅助空间里面有没有保存它的解;要么从最底层出发,自下而上地递推出个问题的解。前者是“制表”,后者是“记忆”也即“动态规划”。
接下来用制表的方法(线性迭代)来优化下:
int fib(int n, int &prev){
if ( n == 0){ //递归基
prev = 1;
return 0;
}else{
int prevPrev;
prev = fib(n-1, prevPrev);
return prev + prevPrev;
}
}
不同之处在于:prev = fib(n-1, prevPrev);
是在调用自身,可以一直调用下去O(n-1)
次,但是直到递归基才计算,并且开始自下而上地记录每个算子。
所以它的时间复杂度是:O(n)
。空间复杂度是O(n)
(因为每次都要额外的辅助空间记录)。
(2)采用迭代的方法:
int fib(int n){
int f = 0, g = 1;
while(n--){
g = f + g;
f = g - f;
}
return g;
}
我们可以将斐波那契数列视为走楼梯,一次可以走一个楼梯或者一次走两个楼梯。那么如果走到了第5层楼梯,有多少种方法?其实就是走到第4层楼梯的方法+第3层楼梯的方法。既然采取迭代的方法,那么就要找到最小的问题的解,自底向上求解。
while(n--){
g = f + g;
f = g - f;
}
这个迭代方法结合图来看,可以比喻为:当我处于第n层台阶的时候,也就是迭代了第n次(此时n=0),我一共有多少种方法,用g来表示。
而我的方法是:每次走一个台阶(g=1)或者不走台阶(f=0),n代表(倒数的)迭代次数,什么时候停止迭代?当n==0的时候。
(1)当n=n的时候,也就是在第一层台阶,途径是:第0层台阶+1,或者第一层台阶+0;也就是,我要走上第一层台阶,要么我走了一层(g = 1),要么我本身就在这一级台阶上(f = 0)。所以第一层台阶的方法有:g = g + f;
(2)当n = n-1的时候,也就是在第二层台阶的时候,我的方法的数目是由第一层台阶的方法数目+第0层(假设有)台阶的方法数目,第一层台阶的方法数目已经在(1)中得到了,第0层怎么表示呢?第0层意味着一开始就没有走,那么就是下一层(-1层)上来的次数:我这一层的总的方法数目是g
,又没有走(f = 0;
),那么实际上要上来的方法数(必须走上来台阶)就是:f = g - f;
这样的解释实际上我自己也觉得有一点拗口。再重新解释就是:我处于第某层台阶,我能够上来的途径就是:每次走一个台阶(下一层台阶的总方法数)或者不走台阶(这层台阶到上一层台阶的f=0方法数),当我选择不走台阶的时候,我的方法应该是f
的总和(这个时候f
应该理解为不走台阶的总方法数目);当我选择走台阶的时候,那么我应该知道下一层台阶的总方法数目即g
的总和;所以本台阶的总方法数为:g = g + f;
下一层台阶的方法数为:f = g - f;
。
实际上用物理意义(走台阶)来解释迭代反而不太好理解,应该回归到迭代的本质,迭代就是已知最原始最小的问题的解,依次扩展到原问题,这是一个从已知到未知的过程,运用迭代器来计数即可。
所以采用迭代的方法还可以这样写:
int fib(int n){
int f1 = 0, f2 = 1;
while(n--){
f2 = f1 + f2;
f1 = f2 - f1;
}
return g;
}
表示如图:
解释如下:
迭代本身就是代数的一种计算方法,化繁为简,逐步推进。
已知最开始的前一个数和后一个数,求迭代了n次的后一个数。将每次得到的新的数视为后一个数,在此之前的相邻的一个数视为前一个数。
接下来具体的操作是:每次先执行f2 = f1 + f2;
得到新的后一个数,此时它已经更新了f2
,而此时的f1
是现在的新的f2
的邻居的邻居,他们之间隔了一个数(这个数是原来的f2
),所以要继续更新f1
,也就是让它变成现在的新的f2
的邻居,那么执行f1 = f2 - f1;
就可以得到更新后的f1
了。
具体如图表示:
要记住:只有程序里只有两个变量:f1
和f2
,所以要时刻更新这两个数来求解。
接下来分析迭代法的复杂度:
int fib(int n){
int f1 = 0, f2 = 1; //执行2次
while(n--){ //执行n次
f2 = f1 + f2; //执行1次
f1 = f2 - f1; //执行1次
}
return g; //执行1次
}
//总共时间:O(2+2*n+1)=O(n),空间为O(1)(仅为f1和f2)
在leetcode里面也有题目:
斐波那契数列
斐波那契数列
(不过每次写题时,一定要考虑特殊情况,也就是考虑算法的退化性,更主要的原因是,不这样考虑通不过。)