递归函数
例子1 阶乘
#include <iostream>
using namespace std;
int factorial(int n)
{
if(n <= 1)
return 1;
else
return n * factorial(n-1);
}
int main()
{
int i = 5;
cout<<factorial(i)<<endl;
return 0;
}
理解“函数调用栈”这个概念
例子2 (质)因数分解
#include <iostream>
#include <cmath>
using namespace std;
void Get_Divisior(int );
int main()
{
int num = 100;
Get_Divisior(num);
return 0;
}
void Get_Divisior(int number)
{
double sqrt_of_number = sqrt(number);
for(int i = 2;i<=sqrt_of_number;i++)
{
if(number%i==0){
cout<<i<<", " ;
Get_Divisior(number/i);
return ;
}
}
cout<<number<<endl;
return ;
}
例子3 最大公因数(greatest common factor)
最大的公共因数,比如15和25的最大公因数是5
20和10 2、5和10都是公因数,10是它们的最大公因数
在这里我们使用辗转相除法编程
辗转相乘法的好处就是便于使用递归,计算机会更喜欢辗转相除法而不是表格法或者短除法
关于辗转相除法可以查看这个视频
https://www.bilibili.com/video/BV19r4y127fu/?spm_id_from=333.788.recommend_more_video.6
过程就是
被除数 ÷ 除数 = 商 ······余数
除数成为新的被除数,而余数成为新的除数
···
当某一时刻
被除数 ÷ 除数 = 商 ······ 0时,此时的除数就是最大公因数
可以看到,这个就有个递归的思想在里面
“除数成为新的被除数,而余数成为新的除数”
#include <iostream>
#include <cmath>
using namespace std;
//指示非法输入用
int counter =0;
int Greatest_Common_Factor(int number1,int number2)
{
if((number1<=0 || number2 <=0) && counter == 0)
{
cout<<"invalid inputs"<<endl;
return false;
}
counter = 1;
if(number2 == 0)
{
return number1;
}
else
return Greatest_Common_Factor(number2, number1%number2);
}
int main()
{
int A = 0;
int B = 342;
int result;
result = Greatest_Common_Factor(A,B);
cout<<result<<endl;
return 0;
}
例子4 汉诺塔(Tower of Hanoi)
汉诺塔概述
汉诺塔有三个塔,第一个塔由一叠穿孔圆盘组成,圆盘由下到上依次变小,我们需要将这一叠圆盘从1塔移动到3塔,并需要遵守以下规则
- 一次只能移动一个盘
- 大盘不能叠在小盘上
递归的思路,就是假定已经知道如何移动n-1个盘(这时候我们还没开始移动盘),那么
我们为了完成任务,需要
- 将n-1和盘从源塔(1塔)移到2塔
- 将最底下的那个盘动源塔移动到目标塔(3塔)
- 将n-1个盘从2塔移动到目标塔
显然,第一个任务是假定已知的(虽然我们还不知道具体步骤),而第二个任务很无脑,
第三个任务则很艰巨,不过,既然假定已知如何移动n-1个塔,那当n变成n-1时,n-1-1个盘子的移动不是更简单吗
递归的英文是recurrence,意思就是”重复地发生“,n是一个可变的参数,但问题的整体结构不变
正如那句著名的论断
“历史不会重复,但是会惊人的相似”
“递归技术使得我们能在已知如何解决n-1的情况下解决n”
但递归有一个很重要的条件,就是一定要有退出机制
这里的退出机制也是很简单很显而易见的————n==1时
代码实现汉诺塔
#include <iostream>
using namespace std;
void Move_a_Ring( int src_tow, int dest_tow);
void Move_Rings(int n, int src_tow, int dest_tow, int other_tow);
int main()
{
int n =3;
Move_Rings(n,1,3,2);
return 0;
}
void Move_Rings(int n, int src_tow, int dest_tow, int other_tow)
{
if(n==1){
Move_a_Ring(src_tow, dest_tow);
}
else
{
Move_Rings(n-1,src_tow, other_tow, dest_tow);
Move_a_Ring(src_tow, dest_tow);
Move_Rings(n-1,other_tow, dest_tow, src_tow);
}
}
对代码的解读
表面上看,我们不过是仅仅是根据上文的叙述进行相应的编程而已
在假定已经知道如何移动n-1个盘的情况下
- 将n-1和盘从源塔(1塔)移到2塔
- 将最底下的那个盘动源塔移动到目标塔(3塔)
- 将n-1个盘从2塔移动到目标塔
代码最核心的部分在这一部分
Move_Rings(n-1,src_tow, other_tow, dest_tow);
Move_a_Ring(src_tow, dest_tow);
Move_Rings(n-1,other_tow, dest_tow, src_tow);
这三句话的意思就是,我这时候在源塔上有n个盘子
- 我先将n-1个盘子从源塔移到其他塔上
- 再将源塔上残留的那个当前n个盘子中最大最底下的那个盘子移到目标塔
- 再将n-1个盘子从其他塔移到目标塔上
注意到,在第三步中,有一个“将n-1个盘子从其他塔移到目标塔上”,这时候等于是
其他塔和源塔身份互换
而我们又假定我们是能够移动n-1个盘子的,这和n的大小无关。
因此我们想执行移动n个盘子的操作,就要执行两次n-1次盘子的操作
而要执行一次n-1个盘子的操作,又要执行两次移动n-2个盘子的操作,所以总的来说,n加一,输出行数就增加一倍
n增加一倍,操作就变成原来的2^n倍,是一个指数增长的过程
其时间复杂度为O(2^n)
总结
递归算法其实在于给了我们一个抽象的思考框架,简而言之,就是一句话
在假定已知(n-1)的情况可以解决时,我们只要解决了n,就解决了全部
比如,在阶乘的例子中,我们要算n!,便可以假定了factorial(n-1)已经可以算出来
那么我们唯一需要知道的,需要做的,就是factorial(n) = n*factorial(n-1);
factirial(n-1)中的n-1成为了新的n,而根据假定,factorial(n-2)的解决方法又是已知的
广而推之,最后就到了需要我们出手的时刻
factorial(1)
这个东西我们也需要知道怎么解决,否则递归就没办法退出,re—currence不是per-currence
一再出现不代表会一直出现,天下没有不散的宴席,量变总有质变的一天。
因此虽然时间复杂度增加了,但问题的解决变得无脑了——这就是递归
同理,在解决质因子分解的问题的时,我们看到
如果n不是一个质数,且能够被i整除(i是n的最小质因子),那么就可以假定
算法已知(n/i)的最小质因子如何计算,继而n/i成为了新的n(n’)
这也是递归的“在假定已知(n-1)的情况可以解决时,我们只要解决了n,就解决了全部”的原理
因为n的质因子分解被我们变成了i*因子(n/i)
若(n/i)的分解问题被假定已经解决,则整个问题就算解决了。
而对于n‘而言,如果它不是一个质数,且i’为其最小质因子,根据假定
算法也知道(n’/i’)的最小质因子如何计算
同样的,我们要设置一个退出机制:直到当n’‘是质数时,算法输出n’'是n的一个质因子并退出
在辗转相除法的例子中,我们要求两个正整数A、B(A>B)的最大公因数
根据辗转相除法的原理,最重要的一个结论就是
A,B的最大公因数和B,A%B的最大公因数是一样的,
这不就是完美的、递归的“在假定已知(n-1)的情况可以解决时,我们只要解决了n,就解决了全部”的原理的具体化吗?
若假定如何求(B,A%B)的方法是已知的,则问题便告解决
因此系统假定了如何求(B,A%B)的方法是已知的
因此求 A,B 的最大公约数就转化为求 B, A%B 的最大公约数
然后B 变成了A’, A%B变成了B‘
为了求 A’, B’ 的最大公约数,我们又需要求 B’, A’%B’
而根据假定,如何求 B’, A’%B’ 的最大公约数又是已知的
因此我们只需要知道退出机制是什么就可了。
退出机制就是,当出现一组(A’,B’),其中A’%B’ == 0,系统就得出B‘为A,B的最大公约数 并退出
在汉诺塔的例子中,有一点不太一样,就是增加了一个Move_a_Ring,当然这个操作是很简单很简单的
在汉诺塔的例子中,我们需要移动n个盘,因此我们就假定
如何移动n-1个盘的操作是已知的,而移动一个盘的操作也是已知的
因此问题就被分解为
- 将n-1和盘从源塔(1塔)移到2塔(移动n-1个盘)
- 将最底下的那个盘动源塔移动到目标塔(3塔)(移动一个盘)
- 将n-1个盘从2塔移动到目标塔(移动n-1个盘)
而要移动n-1个盘,根据假定
如何移动n-2个盘的方法也是已知的
因此,广而推之,就到了需要退出机制的时刻
刚好,我们前面已经知道怎么移动一个盘
而移动一个盘作为递归的退出机制再好不过了。
因此问题就解决了。