04. 递归
04. 递归
小小递归,大大世界
从前有座山,山上有座庙,庙里有个老和尚和一个小和尚,
有一天,老和尚给小和尚讲故事,讲的什么故事呢?
讲的是......
void 讲故事(){
if(困了) return;
讲故事();
回去睡觉();
}
我们把 “内部操作直接或间接地调用了自己的函数”称为递归函数。
程序中的递归函数与生活中的递归现象有相似之处,又有不同之处。
相似之处在于都调用了自己,不同之处在于生活中有些递归现象是无限递归,而递归函数有终止条件。
归纳起来,递归函数有两大要素:
(1)递归关系式:如何继续递归下去;
(2)递归终止条件:什么时候递归结束。
void 讲故事(){
if(困了) return; //递归结束条件
讲故事(); //递归关系式
回去睡觉();
}
【例】递归实现求:n! = 1×2×3…×n。(n<20)
状态定义 :f(n) 表示 n!
则有 :f(n-1) 表示 (n-1)!
递归关系式 :f(n) = f(n-1) * n;
递归终止条件 :f(1)=1;
long long f(int n){
if(n==1) return 1; //递归结束条件
return f(n-1)*n; //递归关系式
}
【例】有 N 阶楼梯,可以一步上一阶,也可以一步上二阶。
到第 N 阶共有多少种不同的走法(N<20)。
状态定义 :f(n) 表示到第 N 阶的方案数
递归关系式 :f(n) = f(n-1)+f(n-2);
递归终止条件 :f(1)=1, f(2)=2;
int f(int n){
if(n<=2) return n; //递归终止条件
return f(n-1)+f(n-2); //递归关系式
}
int dp(int n){ // dynamic programming 动态规划
int f[40]={0,1,2};
for(int i=3; i<=n; i++) f[i]=f[i-1]+f[i-2];
return f[n];
}
【例】给出正整数 n ,用递归法求斐波那契数列第 n 项模1000的值(n<20)。
(1)递归终止条件:fib(0)=0,fib(1)=1;
(2)递归关系式:fib(n)=fib(n-1)+fib(n-2), (n>=2);
int fib(int n) {
if(n<=1) return n;
else return (fib(n-1)+fib(n-2))%1000;
}
递归的执行过程
程序在执行函数A的函数体的过程中又调用了一个函数B,
此前当前执行的位置我们称之为 “当前代码行”,由于要跳出去执行函数B,
此处会形成一个断点,当B函数执行结束后会回到断点处继续执行函数A。
调用栈:为了更好的理解该过程,我们简单介绍一下调用栈。
调用栈描述的是函数之间的调用关系。
它由多个栈帧组成,每个栈帧对应着一个未运行完的函数。
栈帧中保存了该函数的返回地址和局部变量,不同的函数对应着不同的栈帧,
因而不仅能在执行完毕后找到正确的返回地址,
还很自然的保证了不同函数间的局部变量互不相干。
【例】用递归方法求m和n两个数的最大公约数。
定义函数 gcd(n,m) 计算两个数的最大公约数,根据辗转相除法得到。
递归关系式:gcd(n,m)=gcd(m,n%m)。
递归终止条件:gcd(n,0)=n。
int gcd(int n, int m){
if(n==0) return m;
return gcd(n, m%n);
}
爬楼梯
【例】树老师爬楼梯,他可以每次走1级或者2级,输入楼梯的级数,求不同的走法数。
例如:楼梯一共有3级,他可以每次都走一级;
或者第一次走一级,第二次走两级;
也可以第一次走两级,第二次走一级,一共3种走法。
输入格式:若干行,每行包含一个正整数N(1<=N<=30),代表楼梯级数。
输出格式:不同的走法数,每行输入对应一行输出。
输入样例:5 8 10
输出样例:8 34 89
- 参考程序
#include <iostream>
using namespace std;
int solve(int x) {
if (x == 1) return 1;
if (x == 2) return 2;
return solve(x - 1) + solve(x - 2);
}
int main() {
int n;
while (cin >> n) {
cout << solve(n) << endl;
}
return 0;
}
Pell数列
【例】Pell数列 a1, a2, a3, ...,
定义是这样的:a1=1, a2=2, ... , an=2*an-1+an-2,(n>2)。
给出一个正整数k,要求Pell数列的第k项模上32767是多少。
输入格式:第一个数是测试数据的组数 n;后面跟着n正整数 k(1<=k<1000000)。
输出格式:每个整数 k 对应一个输出,中间以空格隔开。
输入样例:2 1 8
输出样例:1 408
- 参考程序
#include <iostream>
using namespace std;
const int mod = 32767;
int solve(int x) {
if (x == 1) return 1;
if (x == 2) return 2;
return (2 * solve(x - 1) % mod + solve(x - 2)) % mod;
}
int main() {
int T; cin >> T;
while (T--) {
int n; cin >> n;
cout << solve(n) << endl;
}
return 0;
}
数根
【例】数根可以通过把一个数的各个位上的数字加起来得到。
如果得到的数是一位数,那么这个数就是数根。
如果结果是两位数或者包括更多位的数字,那么再把这些数字加起来。
如此进行下去,直到得到的是一位数为止。
比如对于 24 来说,把 2 和 4 相加得到 6,
由于 6 是一位数,因此 6 是 24 的数根。
再比如 39,把 3 和 9 加起来得到 12,由于 12 不是一位数,
因此还得把 1 和 2 加起来,最后得到 3,
这是一个一位数,因此 3 是 39 的数根。
输入格式:一个正整数(小于101000)
输出格式:一个数字,即输入数字的数根
输入样例:24
输出样例:6
- 参考程序
#include <iostream>
using namespace std;
char s[1010];
int n;
int solve(int x) {
if (x < 10) return x;
int sum = 0;
while (x) {
sum += x % 10;
x /= 10;
}
return solve(sum);
}
int main() {
cin >> s;
for (int i = 0; s[i]; i++) {
n += s[i] - '0';
}
cout << solve(n) << endl;
return 0;
}
汉诺塔
【例】汉诺塔由编号为1到n个大小不同的圆盘和三根柱子abc组成,编号越小,盘子越小。
开始时,这n个圆盘由大到小依次套在a柱上,要求把a柱上的n个圆盘按下述规则移动到c柱上:
1,一次只能移动一个圆盘,它必须位于某个柱子的顶部。
2,圆盘只能在三个柱子上存放。
3,任何时刻不允许大盘压小盘。
将这n个盘子用最少的移动次数从a柱移到c柱上,输出每一步的移动方法。
输出格式为”步数.Move 盘子编号 from 原柱 to 目标柱”。
【分析】汉诺塔
如何移动才能使得移动次数最少,考虑1到n这n个盘子哪一个盘子先移动到c的位置?
显然是n号圆盘!要想把n号圆盘移到c柱上,必须满足两个条件:
- a柱n号圆盘上面的1-n号圆盘都已经被挪走。
- c柱上没有其他圆盘。
题目要求输出原柱和目标柱的状态,我们重新整理一下题目要求,就是:
“用最少的移动次数把1-n号圆盘从b柱经过a柱移到c柱”,根据上面分析我们需要三步走:
- 用最少的次数把1到n-1号圆盘从a柱经过c柱移到b柱。
- 把n号圆盘从a柱挪到c柱。
- 用最少的次数把1到n-1号圆盘从b柱经过a柱移到c柱。
观察发现,第一步和第三步与原问题的本质是一样的,
只是圆盘数量在减少,原柱,中间柱,目标柱的状态发生了变化。
至此,递归关系比较明显,递归终止条件就是当n=1时,直接从a柱移到c柱即可。
接下来只需要把上面的自然语言描述转变一下就可以了。
我们定义 hanoi(n,a,b,c) 函数用来输出 “用最少的移动次数把1到n号圆盘从a柱经过b柱移动到c柱”的移动序列,n>1时依次执行:
- hanoi(n-1,a,c,b)
- 输出 "Move n from a to c"。
- hanoi(n-1,b,a,c)
- 参考程序
#include<cstdio>
int step = 0;
void hanoi(int n,char a,char b,char c){
if(n==1) printf("%2d.Move %2d from %c to %c\n", ++step, n, a, c);
else{
hanoi(n-1,a,c,b);
printf("%2d.Move %2d from %c to %c\n", ++step, n, a, c);
hanoi(n-1,b,a,c);
}
}
int main(){
int n=3;
hanoi(n, 'a', 'b', 'c');
return 0;
}