1.函数调用在内存中的原理?什么是递归?
首先我们要知道我们程序一步步进行指令时是如何调用函数的?
当函数调用时,在堆栈区域要申请一片栈帧,函数的局部变量、参数都分配在栈帧上面;而函数返回时,堆栈上的栈帧就要释放了,可以说栈帧的分配和释放遵循后进先出(数据结构的栈的行为方式)。
那递归是什么?简单的来说就是一个函数它自己调用了它自己。
如下图所示,每当我们调用函数时就会形成栈帧,而如果我们不知道函数的返回值,func就会一直往下调用它自己,而内存的栈其实很小,所以会形成栈溢出这样的错误。因此,我们在设计递归时要设置一个出口,即能返回给上一个调用的func的值,这样才能逐渐返回并消除栈帧,回到main函数。
2.递归的使用要点
最常见的递归就是n的阶乘。我们知道 n!=n*(n-1)! ,而 (n-1)!=(n-1)*(n-2)! ,其实这就是每次求比它小1的数的阶乘,这就是一个递归。千万记得递归需要出口,1!=1 ,这样才能返回给2的阶乘,3的阶乘,......。
//n的阶乘
#include <cstdio>
int Factorial(int n){
if(n == 1){
return 1;
}
else {
return Factorial(n - 1) * n;
}
}
int main(){
int n;
scanf("%d",&n);
printf("%d\n", Factorial(n));
}
根据我们刚刚的分析我们可以发现递归的两个要点,一是把大问题化成小问题(我们不用真的去求n! ,而是把它化成了求 (n-1)! , (n-2)! , ... ,直到1! ;二是最小问题(往往都是我们很容易算出来的,这里 1!=1 我们都知道)。
3.递归例题
例题1 汉诺塔问题
题目描述:
约 19世纪末,在欧洲的商店中出售一种智力玩具:在一块铜板上有三根杆,最左边的杆自上而下、由小到大顺宇串 者由 64 个网盘构成的塔。日的是将最左边杆上的园猛全部移到右边的杆上,条件是一次只能移动一个圆盘,并且不允许大网盘放在小圆盘的上面。现在我们改变这个游戏的玩法:不允许直接从最左(右)边移动到最右(左)边(每次移动一定是移到中间杆或从中间杆移出),也不允许大圆盘放到小國盘的上面。Daisy 已经做过原来的汉诺塔问题和汉诺塔山问题,但碰到这个问题时,她想了很久都无法解决。请你帮助她。现在有N个圆盘,她至少需要多少次移动才能把这些圆盘从最左边移到最右边?
输入:
包含多组数据,每次输入一个N值 (1≤N≤35)。
输出:
对于每组数据,输出移动最小的次数。
题目分析:
(1)汉诺塔问题也是最经典的递归问题,1个2个3个我们可以通过自己打草稿移动得出结果,而一旦这个圆盘数N十分大时,我们是无法计算得出的。因此,对于这种大问题,我们就可以把它化为小问题,进行递归。
(2)如下图所示,我们可以把上面n-1块小的看作一个整体,这就类似于移动两块圆盘(第n块和那n-1块),然后问题就转化成了怎么移这n-1块,也就是把大问题化成了小问题,依此类推逐渐化小,最小问题即是怎么移1块圆盘,显然是移动两次。
(3)大问题化小问题:F(n) = 3*F(n-1)+2
(4)最小问题:F(1) = 2
代码示例:
//汉诺塔问题(递归:大问题化小问题,n-->n-1)
#include <cstdio>
long long hanoi(int n){
if(n == 1){
return 2;
}
else{
return 3*hanoi(n-1) + 2;
}
}
int main(){
int n;
while (scanf("%d",&n) != EOF){
printf("%lld\n", hanoi(n));
}
}
4.什么是分治法?
分而治之的思想就是运用到递归身上的。
(1)分(devide):大规模问题分解成若干个相似的小问题
(2)治(conquer):解决边界问题
(3)合:合并小问题的解决方案,得到大问题的解决
例题2 Fibonacci (上海交通大学复试上机题)
描述
The Fibonacci Numbers{0,1,1,2,3,5,8,13,21,34,55...} are defined by the recurrence: F0=0 F1=1 Fn=Fn-1+Fn-2,n>=2 Write a program to calculate the Fibonacci Numbers.
输入描述:
Each case contains a number n and you are expected to calculate Fn.(0<=n<=30) 。
输出描述:
For each case, print a number Fn on a separate line,which means the nth Fibonacci Number.
输入:
1
输出:
1
题目分析:
如下图,我们知道F(n)=F(n-1)+F(n-2),那么F(n-1)=F(n-2)+F(n-3);... ;F(2)=F(1)+F(0)。这样逐渐分解,我们会发现最小问题即是F(0)=0和F(1)=1。
代码示例:
//斐波那契数列
#include <cstdio>
int Fibonacci(int n){
if(n == 1){
return 1;
}
else if(n == 0){
return 0;
}
else{
return Fibonacci(n-1) + Fibonacci(n-2);
}
}
int main(){
int n;
while(scanf("%d",&n) != EOF){
printf("%d\n", Fibonacci(n));
}
}
例题3 二叉树 (北京大学复试上机题 )
如上所示,由正整数1,2,3……组成了一颗特殊二叉树。我们已知这个二叉树的最后一个结点是n。现在的问题是,结点m所在的子树中一共包括多少个结点。 比如,n = 12,m = 3那么上图中的结点13,14,15以及后面的结点都是不存在的,结点m所在子树中包括的结点有3,6,7,12,因此结点m的所在子树中共有4个结点。
输入描述:
输入数据包括多行,每行给出一组测试数据,包括两个整数m,n (1 <= m <= n <= 1000000000)。
输出描述:
对于每一组测试数据,输出一行,该行包含一个整数,给出结点m所在子树中包括的结点的数目。
输入:
3 12
0 0
输出:
4
题目分析
(1)通过下图可以发现,结点3所在子树中包括的结点数目=3的左子树的结点数+3的右子树的结点数+1(它自己),也就分解成了如何求它的左子树结点数和右子树结点数,这样又变成了求子树结点的问题,就可以一直递归下去(注意:序号为m的左孩子序号为2m,右孩子序号为2m+1)。
(2)那递归出口是什么呢?不妨想一想最小问题,也就是最小结点数目,显然就是0个,即左子树右子树和它自己都不存在,要这种情况发生就要该结点序号是大于整棵树节点的(m>n),即该结点不存在。
代码实例:
//二叉树
#include <cstdio>
int tree(int m, int n){
if(m > n){
return 0;
}
else{
return 1+tree(2*m,n)+tree(2*m+1,n);
}
}
int main(){
int a = 1;
int m,n;//起始的编号与树的规模
while (scanf("%d%d", &m,&n) != EOF){
if(m == 0){
break;
}
printf("%d\n",tree(m,n));
}
}