认识递归
递归是程序设计和算法学习中经常遇到的一类问题,递归解题本身也是很多比赛中常用的一种解题方法,那么什么是递归呢,我们不妨从下面的小视频开始我们的递归之旅。
相信通过上面的小动画,同学们应该对递归有了一个初步的了解了,等你学完本节课再回头看看这个小动画,我相信你对递归肯定有一个更深入的理解。那么究竟什么是递归,递归又能干什么呢?
何为递归
递归是指**在函数中调用函数本身**的现象。
以阶乘函数为例, 在 factorial 函数中存在着 factorial(n - 1) 的调用,所以此函数是递归函数。
int factorial(int n)
{
if (n < =1)
return 1;
return n * factorial(n - 1);
}
Copy
进一步解释递归问题。“递”的意思是将问题拆解成子问题来解决, 子问题再拆解成子子问题,.....,直到被拆解的子问题无需再拆分成更细的子问题(即可以求解),“归”是说最小的子问题解决了,那么它的上一层子问题也就解决了,上一层的子问题解决了,上上层子问题自然也就解决了,....,直到最开始的问题解决,文字说可能有点抽象,那我们就以阶层 f(6) 为例来看下它的“递”和“归”。
求解问题 f(6), 由于 f(6) = n * f(5), 所以 f(6) 需要拆解成 f(5) 子问题进行求解,同理 f(5) = n * f(4) ,也需要进一步拆分,... ,直到 f(1), 这是“递”,f(1) 解决了,由于 f(2) = 2 f(1) = 2 也解决了,.... f(n)到最后也解决了,这是「归」,所以递归的本质是能把问题拆分成具有相同解决思路的子问题,......,直到最后被拆解的子问题再也不能拆分,解决了最小粒度可求解的子问题后,在“归”的过程中自然顺其自然地解决了最开始的问题。
递归算法解题思路
根据以上分析,不难发现递归问题有以下特点:
一个问题可分解成具有相同解决思路的子问题、子子问题,即这些问题都可调用同一个函数。
经过层层分解的子问题最后一定有一个不能再分解的固定值(即终止条件),如果没有的话就无穷无尽地分解子问题,问题显然是无解的。
解递归题的关键在于我们首先需要根据以上特点判断题目是否可以用递归来解。经判断可以用递归后,接下来看看用递归解题的基本套路:
递归解题的基本套路
(1)先定义一个函数,明确这个函数的功能,由于递归的特点是问题和子问题都会调用函数自身,所以这个函数的功能一旦确定了, 之后只要找寻问题与子问题的递归关系即可。
(2)接下来寻找问题与子问题间的关系(即递推公式),由于问题与子问题具有相同解决思路,只要子问题调用步骤 1 定义好的函数,问题即可解决。所谓的关系最好能用一个公式表示出来,比如 f(n) = n * f(n-1) , 发现递推关系后,要寻找最终不可再分解的子问题的解,即(临界条件),确保子问题不会无限分解下去。由于第一步我们已经定义了这个函数的功能,所以当问题拆分成子问题时,子问题可以调用步骤 1 定义的函数,符合递归的条件(函数里调用自身)。
(3)将第二步的递推公式用代码表示出来补充到步骤 1 定义的函数中。
(4)最后也是很关键的一步,根据问题与子问题的关系,推导出时间复杂度,如果发现递归时间复杂度不可接受,则需转换思路对其进行改造,看下是否有更靠谱的解法。
实例说明
实例:输入一个正整数n,输出n!的值。其中n!=1*2*3*…*n,即求阶乘。
(1)定义这个函数,明确这个函数的功能,我们知道这个函数的功能是求 n 的阶乘, 之后求 n-1, n-2 的阶乘就可以调用此函数了。
int factorial(int n)
{
}
Copy
(2)寻找问题与子问题的关系,阶乘关系比较简单, 以 f(n) 来表示 n 的阶乘, 显然 f(n) = n * f(n - 1), 同时临界条件是 f(1) = 1。
(3)将第二步的递推公式用代码表示出来补充到步骤 1 定义的函数中。
int factorial(int n) {
// 第二步的临界条件
if (n < =1)
{
return 1;
}
// 第二步的递推公式
return n * factorial(n-1)
}
Copy
(4)求时间复杂度 由于 f(n) = n * f(n-1) = n * (n-1) * .... * f(1),总共作了 n 次乘法,所以时间复杂度为 n。
递归的简单总结
分析递归问题时我们需要采用自上而下的思维,而解决问题有时候采用自下而上的方式能让算法性能得到极大提升。若要熟练掌握还需要进行一些题目的训练才可以。
下面我们对递归做一个简单的小总结:
概念:一个函数在定义过程中,自己调用自己称为递归。
思想:把一个大型的原问题层层转化为一个与原问题相似的规模较小的子问题求解。递归不能无限地进行下去,否则程序会陷入死循环。
核心:递归公式和递归出口(边界)。递归公式即确定递归的方式,即原问题是如何分解为子问题的?递归出口(边界)即确定递归到何时终止,即递归的结束条件是什么?
缺点:递归算法解题的运行效率较低,容易超时。在这种情况下,通常将递归算法转化为非递归算法,如递推或模拟。
下面通过一些例题,让我们更加深入的了解和学习什么是递归。
例题1:菲波拉契数列
菲波那契数列是指这样的数列: 数列的第一个和第二个数都为1,接下来每个数都等于前面2个数之和。
给出一个正整数k,要求菲波那契数列中第k个数是多少。
输入格式:输入一行,包含一个正整数k。(1 <= k <= 46)
输出格式:输出一行,包含一个正整数,表示菲波那契数列中第k个数的大小
输入样例1:
19
Copy
输出样例1:
4181
Copy
解题思路:略。
#include <bits/stdc++.h>
using namespace std;
int fit(int n)
{
if(n<=2)
return 1;
else
return fit(n-1)+fit(n-2);
}
int main()
{
int n;
cin>>n;
int y=fit(n);
cout<<y<<endl;
return 0;
}
Copy
例题2:求阶乘
n的阶乘可表示为n!=1*2*3*4*......*(n-1)*n。输入一个人整数n(n<=30);输出n的阶乘。
解题思路:略。
#include <bits/stdc++.h>
using namespace std;
int f[110];
long long int fun(int n)
{
if(n==1)
return 1;
else
return n*fun(n-1);
}
int main()
{
int n;
cin>>n;
cout<<fun(n)<<endl;
return 0;
}
Copy
例题3:最大公约数和最小公倍数
输入两个正整数,求这两个正整数的最大公约数和最小公倍数。
输入格式:一行:两个正整数m和n(1<m<=500,1<n<=500)。
输出格式:一行:它们的最大公约数和最小公倍数。
输入样例1:
8 12
Copy
输出样例1:
4 24
Copy
解题思路:略。
#include <bits/stdc++.h>
using namespace std;
int get_gcd(int m,int n)
{
if(n==0)
return m;
else
return get_gcd(n,m%n);
}
int get_lcm(int m,int n)
{
return m*n/get_gcd(m,n);
}
int main()
{
int m,n;
cin>>m>>n;
cout<<get_gcd(m,n)<<" "<<get_lcm(m,n);
return 0;
}
Copy
例题4:判断字符是否为回文
输入一个字符串,输出该字符串是否回文。回文是指顺读和倒读都一样的字符串。
输入格式:输入为一行字符串(字符串中没有空白字符,字符串长度不超过100)。
输出格式:如果字符串是回文,输出yes;否则,输出no。
输入样例1:
abcdedcba
Copy
输出样例1:
yes
Copy
解题思路:略。
#include <bits/stdc++.h>
using namespace std;
string a;
bool ishuiwen(int i,int j)
{
if(i>j)
return true;
if(a[i]!=a[j])
return false;
return ishuiwen(++i,--j);
}
int main()
{
cin>>a;
if(ishuiwen(0,a.size()-1))
cout<<"yes"<<endl;
else
cout<<"no"<<endl;
return 0;
}
Copy
例题5:10进制转x进制
将一个10进制整数n,转换为一个x进制整数并输出(2≤x≤16)。
输入格式:一行两个整数,分别代表被转换的10进制整数n和要转换的进制x
输出格式:一行十进制n转换后的x进制数(10进制以上:分别使用A , B, C, D, E, F代表10, 11, 12, 13, 14, 15,)。
输入样例1:
8 2
Copy
输出样例1:
1000
Copy
解题思路:略。
#include <bits/stdc++.h>
using namespace std;
string number="0123456789ABCDEFG";
string a;
void change(int n,int x)
{
if(n==0)
return ;
a.push_back(number[n%x]);
return change(n/x,x);
}
int main()
{
int n,x;
cin>>n>>x;
change(n,x);
reverse(a.begin(),a.end());
cout<<a<<endl;
return 0;
}
Copy
例题6:汉诺塔问题
约19世纪末,在欧州的商店中出售一种智力玩具,在一块铜板上有三根杆,最左边的杆上自上而下、由小到大顺序串着由64个圆盘构成的塔。目的是将最左边杆上的盘全部移到中间的杆上,条件是一次只能移动一个盘,且不允许大盘放在小盘的上面。
由于条件是一次只能移动一个盘,且不允许大盘放在小盘上面,所以64个盘的移动次数是:18,446,744,073,709,551,615
这是一个天文数字,若每一微秒可能计算(并不输出)一次移动,那么也需要几乎一百万年。我们仅能找出问题的解决方法并解决较小N值时的汉诺塔,但很难用计算机解决64层的汉诺塔。
假定圆盘从小到大编号为1, 2, ...
输入格式:输入为一个整数后面跟三个单字符字符串。整数是盘子的数目(1≤N≤16),后三个字符表示三个杆子的编号。
输出格式:输出每一步移动盘子的记录。一次移动一行。
每次移动的记录为例如 a->3->b 的形式,即把编号为3的盘子从a杆移至b杆。
输入样例1:
2 a b c
Copy
输出样例1:
a->1->c
a->2->b
c->1->b
Copy
解题思路∶当n>=2时,移动n个盘子共三个步骤∶
第一步∶ 从a柱子通过c柱子,将n-1个盘子移动到柱子;
第二步∶ 从a柱子将第n个盘子移动到c柱子;
第三步∶ 从b柱子通过a柱子,将n-1个盘子移动到c柱子;
我们发现第一步和第三步是和原问题相同的子问题,只是参数n和柱子的顺序有变动。 这就是递归公式。
当n==1时,直接从a柱子移动到c柱子 这就是递归边界
下面链接是关于汉诺塔的小动画,大家可以看一看,帮助了解汉诺塔相关题目。
https://pic1.zhimg.com/50/v2-39f39bc301a10f500b0e663b79184675_hd.webp?source=1940ef5c(汉诺塔)
完整的代码如下:
#include <bits/stdc++.h>
using namespace std;
void move(int n,char a,char b,char c) //用c柱作为协助过渡,将a柱上的n片移到b柱上
{
if(n==1)
printf("%c->%d->%c\n",a,n,b);
else
{
move(n-1,a,c,b); //用b柱作为协助过渡,将a柱上的n-1片移到c柱上
printf("%c->%d->%c\n",a,n,b);
move(n-1,c,b,a); //用a柱作为协助过渡,将c柱上的n-1片移到b柱上
}
}
int main()
{
int n;
char a,b,c;
cin>>n>>a>>b>>c;
move(n,a,b,c);
return 0;
}