文章目录
递归入门
线性递归
例题: 计算给定的n个数的和
分析: 显然当 n=0 的 a[0] 为所求,因此可以将前 n 项可以看做是前 n-1 即(A[0,n - 2]) 项的和加上第 n 项
int sum( int A[], int n){
if ( n < 1 )
return 0;
else
return sun( A, n - 1) + A[n - 1];
}
如上算法中的 sum( ) 在进行递归调用的时对自身的调用最多只会进行一次,也就是在每一层次上至多只有一个实例,且构成一个线性结构,因此称为线性递归 (linear recursing)
线性递归往往对应所谓 减而治之(decrease-and-conquer) 即在递归每深入一层,待求解的问题就缩小一个常数级别,知道最终蜕化为一个简单问题
二分递归
分而治之: 将一个规模庞大的问题分解为若干规模更小的子问题,再通过递归机制,将子问题继续分解,知道子问题分解为平凡情况,这种方法就是 分而治之(divide-and-conquer) 和减而治之一样,在这里需要保证每一个子问题需要和原问题在形式上保持一致,而在递归的每层中都可能做多次递归操作,所以也称为 多路递归(multi-way recursion),又因为通常采用将问题一分为二,所以也称为 二分递归(binary recursion)
将上述例题以二分递归的模式进行分析:
分析: 以居中元素为界限,将数组一分为二,递归对子数组求和,最后数组之和即原数组的总和int sum( int A[], int lo, int hi ){ if(lo == hi) return A[lo]; else { int mi = (lo + hi) >> 1; return sum( A, lo, mi) + sum( A, mi + 1, hi ); } }
当 n=8 的时候我们可以得到如上图所示的执行过程,而如图所示的层次结构称为 二叉树(将在后序章节学习),我们可以很容易发现原 sum(lo, hi) 被分为了 sum(lo, mi) 和 sum(mi+1, hi) 因此没经历一次递归操作我们的数组区间的长度都会缩减为原来的一半.
在本算法下我们可以得知在经历了 次的递归调用后,数组区间的长度会从最初的 缩减至 因此我们可以得知本算法的时间复杂度为 而在之前的线性递归的时间复杂度为 所以二分递归更加优秀.
Fibonacci数
经典的递归问题,形如 这样的数列,叫做Fibonacci数列
查询Fibonacci数列中的第 个数我们可以用以下方法:
已知Fibonacci数列的递归表达式为:
因此可以采用
long long fib(int n){
return (2 > n)?(long long)n : fib(n - 1) + fib(n - 2);
}
但是我们不难发现采用此算法需要运行 的时间,因此不适合在实际生活中运用,我们可以对算法进行分析,可以发现列入在算第 8 个Fibonacci数的时候,我们需要先行计算第 6 个和第 7 个Fibonacci数,而在计算第 7 个Fibonacci数的时候会先计算第 5 和第 6 个Fibonacci数,因此我们可以发现我们计算了 2遍 第 6 个Fibonacci数,所以进行了重复操作,因此我们可以采取一定优化策略
即用时间换取空间,在各个子问题求解之后,借助一些存储空间去记录下对应的结果
- 我们可以从原问题出发,每当遇到一个子问题的时候我们都优先检验该问题是否已经计算过,若已经计算过,我们直接调阅记录获得答案,从而避免重复计算
- 我们也可以从递归基出发,依次求出各子问题的解,直到最终求得原问题的解
第一种方法我们称为制表(tabulation)或记忆(memoization)第二种方法我们成为动态规划(dynamic programming)
以下是两种方法的C++代码实现
long long fib(int n,long long &prev){
if(n==0){
prev = 1;
return 0;
}
else{
long long prevPrev; //定义prevPrev为n-2的结果
prev = fib(n-1, prevPrev);
return prev+prevPrev;
}
}
#include<iostream>
using namespace std;
long long fib(int n){
long long per_fib_num = 1, fib_num = 0; //per_fib_num表示前一个fib数
while(n--){
fib_num += per_fib_num;
per_fib_num = fib_num-per_fib_num;
}
return fib_num;
}