递归法(Recursion)是一种在函数或方法中调用自身的编程技术,在计算机方法中,使用递归技术往往使函数的定义和算法的描述简洁且易于理解。任何可以用计算机求解的问题所需要的计算时间都与其规模有关。而且规模越小,解题所需要的计算时间通常越小,从而比较容易处理。
简而言之,递归思想就是用与自身问题相似但规模较小的问题来描述自己。
例如,兔子出生两个月后就有繁殖能力,一对兔子每个月能生出一对兔子来。如果所有兔子都不死,那么一年以后可以繁殖多少对兔子?
第一个月小兔子没有繁殖能力,所有还是一对;两个月后,生下一对,兔子共2对;三个月后,老兔子又生下一对,因为小兔子还没有繁殖能力,所以一共是三对。
以此类推,如下所示。
所经过月数:0 1 2 3 4 5 6 7 8 9 10 11 12
兔子对数: 1 1 2 3 5 8 13 21 34 55 89 144 233
表中数字1,1,2,3,5,8^233构成一个序列。这个数列有个明显的特点,即前面两项之和构成后一项。用数学表示这个无穷数列称为裴波那契数列,形式化描述为:
1 , n=0
F(n)= 1 ,n=1
F(n-1)+F(n-2) ,n>1
该数列就是个典型递归数列。
#include<iostream> using namespace std; int fib(int n) { int f=0; if(n==1) return 0; if(n==2) return 1; f=fib(n-1)+fib(n-2); return f; } int main() { int n,i,m=0; cin>>n; m=fib(n); cout<<"第"<<n<<"项是"<<m<<endl; m=0; for(i=1;i<=n;i++) m=fib(i)+m; cout<<"前"<<n<<" 项和是"<<m<<endl; return 0; }
递归算法的特性:
(1)求解规模为n的问题可以转换成一个或多个结果相同、规模较小的问题,然后从这些小问题的解能方便地构造出大问题的解。
(2)递归调用的次数必须是有限的。
(3)必须有结束递归的条件(边界条件)来终止递归。即当规模为1时,能够直接得解。
递归的执行过程:
递归算法的执行过程划分为递推和回归两个阶段。在递推阶段,把规模为n的问题求解推到比原问题的规模较小的问题求解,且必须要有终止递推的情况。在回归阶段,当获得最简单情况的解后,逐渐返回,依次得到规模较大问题的解。
需要注意的是,用递归描述问题并不表示程序也一定要直接用递归实现。
递归算法的优点:结构清晰、可读性强,而且容易用数学归纳法来证明算法的正确性,因此它为设计算法、调试程序带来很大方便。
递归算法的缺点:运行效率相对较低,无论是耗费的计算时间还是占用的存储空间都比非递归算法要多。通常解决方法是消除递归算法中的递归调用,使递归算法转换为非递归算法。
理论上递归算法都可以转换为非递归算法。方法有如下3种
(1)通过分析,跳过分解部分,直接用循环结构实现求值过程。
(2)用栈保存程序的运行过程,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。
(3)利用栈保存参数,由于栈的后进先出特性与递归算法的执行过程相似,因而可以用非递归算法替代递归算法。
1汉诺塔问题求解
假设有n个金盘,并且金盘由小到大一次编号为1,2,3……n。要把放在A针上的n个金盘移到目的针C上。当只有一个金盘,即n=1时,只要将编号为1的金盘从A针直接移到C针上即可。当n>1时,可以把最上面的n-1个金盘看作一个整体。这样n个金盘就分成了两个部分:上面n-1个金盘和最下面的编号为n的金盘。移动金盘的问题可以转换为如下3个步骤:
1)借助C针,将n-1个金盘从A针移到B针上;
2)将编号为n的金盘直接从A针移到C针上;
3)借助A针,将B针上的n-1个金盘移到C针上。
其中,第二步只移动一个金盘,容易解决。第一、三步不能直接解决,但由于已经把移到n个金盘问题变成了移到n-1个金盘的问题,问题规模变小。如果再把第一、三步分别变成类似的三个子问题,移到n-1个金盘的问题就可以变成移到n-2个金盘的问题,依次类推,从而将问题加以解决。
#include<iostream> using namespace std; void Move(int n,char x,char y) { cout<<"把"<<n<<" 号从 "<<x<<"挪动到 "<<y<<endl; } void Hannoi(int n,char a,char b,char c) { if(n==1) Move(1,a,c); else { Hannoi(n-1,a,c,b); Move(n,a,c); Hannoi(n-1,b,a,c); } } int main() { cout<<"以下是3层汉诺塔的解法"<<endl; Hannoi(3,'a','b','c'); cout<<"输出完毕!"<<endl; return 0; }
2 八皇后问题
在8*8国际象棋盘上放置8个黄昏,使任何一个皇后都不能吃掉另一个。即任意两个皇后都不能处于同一行、同一列或者同一斜线上,问共有多少种放置方案。
假设当前生成全排列中的第t个数字大于n,其中,t是每一行从左到右的标号,n是棋盘的行数,则说明生成了一个全排列,否则:
1)从1~8中依次取出一个数字来尝试,如果此数字前面已经出现,则取下一个数字,直到找到第一个没有出现过的数字;
2)将这个数字标记为已出现,然后以同样的方法生成全排列中的下一个数字;
3)将这个数字标记为未出现;
显然,这是一个递归定义,递归的结束条件为t>n。
#include<iostream> using namespace std; int result=1; int chess[8]; //根据前面几行的棋子,检查这一行所放的棋子是否合法 int check(int n) { int i; for(i=1;i<=n-1;i++) { if(chess[n] == chess[i]+(n-i) || chess[n]==chess[i]-(n-i) || chess[n]==chess[i]) return 0; } return 1; } void show_chess(void) { int i; cout<<endl<<"Result -"<<result<<endl; for(i=1;i<=8;i++) { cout<<i<<" : "<<chess[i]<<"\t"; } ++result; } //递归函数:放棋子 void putchess(int n) { int i; if(n<=8) { for(i=1;i<=8;i++) {//将第n行从第一格(i)开始往下放 chess[n]=i; if(check(n) == 1) {//若可放,则检查是否放满 if(n==8) show_chess();//若已放满到8行时,则表示找出一种解,打印出来 else putchess(n+1);//若没放满则放下一行 putchess(n+1) } } } } int main() { cout<<"This is for 8*8 matrix"<<endl; putchess(1); return 0; }