文章目录
前言
递归是一种计算机程序设计技术,它是指在一个函数或子程序的定义中直接或间接地调用自身的过程。递归的核心特点是解决问题时通过将问题分解为规模更小但结构与原问题相似的子问题来逐步求解,直到子问题足够简单可以直接得出答案。
通过这种方式,递归提供了一种优雅的解决方案来处理那些可以通过分解而变得更易于管理的问题。本文将通过几个具体的例子来介绍递归的基础概念及其在数值类问题中的应用。
欢迎关注本专栏《C++从零基础到信奥赛入门级(CSP-J)》
学习路线:C++从入门到NOI学习路线
学习大纲:C++全国青少年信息学奥林匹克竞赛(NOI)入门级-大纲
一、概念
1.1 导入
大家都听过愚公移山这个故事吧?传说之中有两座大山,名字叫太行和王屋。
这两座山本来是在冀州南边,黄河北岸的北边。在北山的山下住着一个人,别人都叫他愚公。
愚公平时也没啥爱好,就爱刷刷抖音看看剧,打打游戏。
那愚公不好好的享受老年生活,干嘛非要去移山呢?
因为这两座山刚好在他家的对面。太行和王屋非常的高大,周围七百里,高七八千丈。
这就导致了一个问题—信号不好!!!!
于是愚公就开始了移山。
愚公有一个邻居,叫做智叟。
智叟发现了愚公的谜之操作。
为啥说是谜之操作呢?搬家不比搬山轻松?
智叟就问他(愚公):“你咋不搬家呢? ”,呸,”你都90岁了,少玩两天手机呗”。
愚公回答到:”那不行,我的队友还等我呢!“
智叟:“这么一大座山你也挖不完啊!”
愚公嘴角微微一翘,“虽我之死,有子存焉;子又生孙,孙又生子;子又有子,子又有孙;子子孙孙无穷匮也,而山不加增,何苦而不平?”
智叟听到愚公的回答,撇撇嘴,愤愤地走了。
故事就先说到这里,我比较好奇的是为什么同一件事,会有两种不同的态度呢?
浅浅的分析一下:
-
智叟觉得办不到,是因为8000米高的太行、王屋山想要搬走实在是太难了。
-
愚公觉得可以做到,是因为他认为山是不会长高的,而子孙是无穷无尽的。
愚公要是知道有大陆漂移说,肯定会被气死。
扯远了,为什么要讲这个故事呢?愚公的思想就是典型的递归思想。
咳咳~ 将复杂的问题转换成简单的问题,然后逐步求解。这就是递归的思想。
愚公把8000米的山看成是一个8000个1米的小土堆(假设能挖掉1米,别较真),他和他的后代只需要重复8000次就能将整座山移走。
在编程中,递归的实现通常包含两个重要部分:
-
基本情况(Base Case):这是递归结束的条件,没有更小的子问题需要解决的情况。
-
递归步骤(Recursive Step):这是将大问题分解为小问题并调用自身的过程。
1.2 递归的过程
我们以愚公移山来展示递归的过程:
步骤1:定义递归函数
什么是递归函数呢?说白了就是立个flag。
- 首先,我们需要定义一个“移山”的递归函数。这个函数接收一个参数(对象),就是打算把哪座山要移掉。
void 移山(山名 太行山):
...
步骤2:确定基本情况(Base Case)
接下来,需要确定基本情况。
你问我什么是基本情况?
基本情况就是递归结束的条件,没有更小的子问题需要解决的情况。
基本情况就是打算干到什么时候退休、不干了。
在愚公移山中,估计把太行山挖空,愚公一家就能退休了。
- 因此基本情况是:
void 移山(山名 太行山):
如果 太行山 为空:
输出 "报告,太行山已经欧克!"
返回
愚公一家到这里就可以松一口气了。
不对!
还有王屋山。延迟退休!!!
延迟退休:就是递归函数被再一次调用。
步骤3:定义递归步骤(Recursive Step)
立了flag,设定了退休条件。接下来需要做什么呢?
没错,工作绩效。
愚公移山这件事,说白了就是愚公突然头脑发热。
他的后代可能并不想。。。他们可能只想。。。
所以需要设置一个每天最少要干多少事的目标。
愚公:那就每天挖1米吧。
没办法,老祖宗发话,得干啊。
void 移山(山名 太行山):
如果 太行山 为空:
输出 "报告,太行山已经欧克!"
返回
否则:
挖掉 太行山的1米
输出 "今日工作内容已完成,over!"
调用 移山(剩余的太行山)
注意在递归步骤这一部分结尾处我们一定要重新调用递归函数。
因为:明天的事情明天做!
我们已经完成了我们今天的工作绩效,加班是不可能的。
关于愚公移山的递归函数我们就写完了。
总结一下,递归的过程就是不断将问题规模缩小到基本情况,通过反复调用自身来解决小规模的问题,最终累积起来解决原来的大规模问题。
1.3 使用递归改造循环
递归的求解效率较低,且比较消耗内存,因此如果递归能够转化为循环,尽量用循环,这里是出于教学递归!!!
递归的求解效率较低,且比较消耗内存,因此如果递归能够转化为循环,尽量用循环,这里是出于教学递归!!!
递归的求解效率较低,且比较消耗内存,因此如果递归能够转化为循环,尽量用循环,这里是出于教学递归!!!
改造题目:1002 - 编程求解1+2+3+…+n
类型:简单循环
题目描述:
编程求解下列式子的值: S=1+2+3+⋯+n。
输入:
输入一行,只有一个整数 n(1≤n≤1000) 。
输出:
输出只有一行(这意味着末尾有一个回车符号),包括 1 个整数。
样例:
输入:
100
输出:
5050
解题思路:
请不要笑,我当然知道对于学到这里的你们,这题简直不要太容易。
我们按照之前递归的过程来改造这个题。
步骤1:定义递归函数
- 定义一个“mySum”的递归函数。这个函数接收一个参数(对象),就是需要被累计和的数的最大值,类型为整数(int),参数名可以任意取。
- 最后确定返回类型为int,把计算结果告诉调用我们的人。
int mySum(int m){
}
步骤2:确定基本情况(Base Case)
我们这个函数运行到什么时候就停止呢?
假设是计算1到100的和,我们将问题规模缩小:
1到100的和=100+1到99的和。
进一步呢?
1到100的和=100+1到99的和=100+99+1到98的和。
持续下去…
最终我们的问题就变成了100+99+98+…+3+2+1。
所以我们的函数应该在等于1的时候停下。
int mySum(int m) {
if(1==m){
return 1;
}
}
步骤3:定义递归步骤(Recursive Step)
同理,在刚才的分析中,我们已经把递归步骤也一起总结出来了。
即:累计和=m+mySum(m-1)。
int mySum(int m) {
if(1==m){
return 1;
}else{
return m+mySum(m-1);
}
}
欧克,搞定。
全部代码:
#include <bits/stdc++.h> // 包含标准输入输出库
using namespace std; // 使用标准命名空间
// 定义一个名为mySum的递归函数,接受一个整数n作为参数
int mySum(int m){
// 基本情况:当n等于1时,返回1(因为1+0=1)
if(1==m){
return 1;
}
// 递归步骤:当m不等于1时,返回n加上mySum(m-1)的结果
// 这意味着每次调用mySum都会将问题规模缩小为m-1,并不断递归直到达到基本情况
else{
return m + mySum(m - 1);
}
}
int main(){
// 数据定义部分
int n, sum; // 定义整数变量n表示要累加到的数值,sum用于存储计算结果
// 数据输入部分
cin >> n; // 从标准输入读取一个整数赋值给n
// 数据计算部分
sum = mySum(n); // 调用mySum函数计算累加和,并将结果存入sum
// 输出结果部分
cout << sum; // 将计算得到的累加和输出到标准输出
return 0; // 主函数返回0,表示程序正常结束
}
二、例题讲解
问题:1004 - 编程求1 * 2 * 3 * … * n
类型:简单循环
题目描述:
编程求 1×2×3×⋯×n 。
输入:
输入一行,只有一个整数 n(1≤n≤10);
输出:
输出只有一行(这意味着末尾有一个回车符号),包括 1 个整数。
样例:
输入:
5
输出:
120
1.分析问题
- 已知:一个整数 n(1≤n≤10);
- 未知:编程求 1×2×3×…×n 。
- 关系:递归
2.定义变量
- 定义了两个整型变量 n 和 res,其中 n 用于存储用户输入的值,res 用于存储阶乘的结果。
//二、定义变量(已知、未知、关系)
int n,res;
3.输入数据
- 使用 cin 从用户那里获取输入的整数 n。
//三、输入已知
cin>>n;
4.数据计算
- 一个递归函数,用于计算阶乘。如果传入的参数 d 等于 1,则返回 1;否则返回 d 与 d-1 的阶乘的乘积。
int dg(int d){
if(d==1){
return 1;
}else{
return d*dg(d-1);
}
}
- 调用 dg 函数计算阶乘并将结果存储在 res 中。
res = dg(n);
5.输出结果
- 使用 cout 输出阶乘的结果。
//五、输出未知
cout<<res;
完整代码如下:
#include<bits/stdc++.h> // 包含所有标准库头文件,但在实际使用中应尽量避免这样做
using namespace std; // 使我们可以直接使用 std::cin 和 std::cout 等,而无需 std:: 前缀
// 定义一个名为 dg 的函数,用来计算 n 的阶乘
int dg(int d) {
// 如果 d 等于 1,则返回 1 (因为 1 的阶乘是 1)
if(d == 1) {
return 1;
} else {
// 否则,返回 d 乘以 (d-1) 的阶乘
return d * dg(d-1);
}
}
int main() {
// 定义变量 n 和 res,n 用来存储用户输入的整数,res 存储计算结果
int n, res;
// 输入已知的整数 n
cin >> n;
// 根据关系计算阶乘
res = dg(n);
// 输出未知的结果,即阶乘的值
cout << res;
return 0; // 主函数结束,程序正常退出
}
欢迎关注本专栏《C++从零基础到信奥赛入门级(CSP-J)》
问题:1053 - 求100+97+……+4+1的值。
类型:简单循环
题目描述:
求 100+97+⋯+4+1 的值。
输入:
无。
输出:
输出一行,即求到的和。
1.分析问题
- 未知:求 100+97+?+4+1 的值。
2.定义变量
- 定义res 用于存储结果。
//二、定义变量(已知、未知、关系)
int res;
3.输入数据
无。
4.数据计算
- 如果 d 小于等于 1,返回 1(这是递归的基本情况)。
- 否则,返回 d 加上 d - 3 的和,递归调用 dg 函数。
int dg(int d){
if(d<=1){
return 1;
}else{
return d+dg(d-3);
}
}
- 调用 dg 函数计算序列的和。
//四、根据关系计算
res=dg(100);
5.输出结果
- 使用 cout 输出结果。
//五、输出未知
cout<<res;
完整代码如下:
#include<bits/stdc++.h>
using namespace std;
// 修正后的递归函数,用于计算 100 + 97 + ... + 4 + 1 的和
int dg(int d) {
if (d <= 1) {
return 1; // 序列的最小项是 1,这里返回 1
} else {
return d + dg(d - 3); // 累加 d,并递归调用 dg 函数减少 d 的值
}
}
int main() {
// 分析问题
// 未知:求 100 + 97 + ... + 4 + 1 的值。
// 定义变量
int res;
// 输入已知 - 在这个例子中没有输入
// 根据关系计算
res = dg(100);
// 输出未知
cout << res << endl;
return 0;
}
问题:1241 - 角谷猜想
类型:有规律的循环、递归。
题目描述:
日本一位中学生发现一个奇妙的定理,请角谷教授证明,而教授无能为力,于是产生了角谷猜想。
猜想的内容:任给一个自然数,若为偶数则除以 2 ,若为奇数则乘 3 加 1 ,得到一个新的自然数后按上面的法则继续演算。若干次后得到的结果必为 1 。
请编写代码验证该猜想:求经过多少次运算可得到自然数 1 。
如:输入 22 ,则计算过程为。
22/2=11
11×3+1=34
34/2=17
17×3+1=52
52/2=26
26/2=13
13×3+1=40
40/2=20
20/2=10
10/2=5
5×3+1=16
16/2=8
8/2=4
4/2=2
2/2=1
经过 15 次运算得到自然数 1 。
输入:
一行,一个正整数 n 。( 1≤n≤20000 )
输出:
一行,一个整数,表示得到 1 所用的运算次数。
样例:
输入:
22
输出:
15
1.分析问题
- 已知:一个正整数 n 。
- 未知:得到 1 所用的运算次数。
- 关系:角谷猜想。
2.定义变量
- 定义并读入一个整数n;
//二、数据定义
int n;
3.输入数据
//三、数据输入
cin>>n;
4.数据计算
-
定义全局变量c用于记录操作次数。
-
定义一个名为op的递归函数,参数为需要进行运算的整数n。
-
当n不等于1时,进入递归:
-
如果n是偶数,则对n进行除以2的操作,并以结果作为新的n调用op函数;
-
如果n是奇数,则对n进行乘以3再加1的操作,并以结果作为新的n调用op函数;
-
每进行一次上述操作(无论是除以2还是乘以3加1),全局计数器c加1。
int c=0;
void op(int n){
if(n!=1){
if(n%2==0){
op(n/2);
}else{
op(n*3+1);
}
++c;
}
}
- 调用op(n)开始执行角谷猜想的计算流程;
//四、数据计算
op(n);
5.输出结果
- 输出操作次数c,即从输入的n到达1所需经过的步骤数。
//五、输出结果
cout<<c;
return 0;
完整代码如下:
#include <bits/stdc++.h> // 引入C++标准库的头文件,包含大部分常用函数和数据结构
using namespace std; // 使用std命名空间,方便调用其中的标准库函数
int c = 0; // 定义全局变量c,用于记录执行操作(变换)的次数
// 定义递归函数op,参数为整数n
void op(int n) {
if (n != 1) { // 如果当前数值n不等于1,则继续进行以下操作
if (n % 2 == 0) { // 如果n是偶数
op(n / 2); // 将n除以2后作为新的输入值调用op函数(遵循角谷猜想的规则)
} else { // 否则,即当n是奇数时
op(n * 3 + 1); // 根据角谷猜想将n乘以3并加1后作为新的输入值调用op函数
}
++c; // 在每次执行完一次操作(无论是否改变n的值)后,计数器c增加1
}
}
int main() {
// 分析问题:实现角谷猜想(Collatz Conjecture),即对于任意正整数n,通过特定规则变换,最终都能到达1
int n; // 定义变量n,用于存储用户输入的初始数值
// 数据输入
cin >> n;
// 数据计算
op(n); // 调用op函数对给定的n执行角谷猜想的操作流程
// 输出结果
cout << c; // 输出从初始数值n到1所经历的操作次数
return 0; // 程序正常结束,返回0
}
问题:1146. 求S的值
类型:递归基础、函数
题目描述:
求 S=1+2+4+7+11+16…的值刚好大于等于 5000 时 S 的值。
输入:
无。
输出:
一行,一个整数。
1.分析问题
- 未知:求 S=1+2+4+7+11+16…的值刚好大于等于 5000 时 S 的值。
2.定义变量
- 定义了两个整型变量 s 和 i,其中 s 用于存储累加和,i 用于索引 dg 函数。
//二、定义变量(已知、未知、关系)
int s=0,i=1;
3.输入数据
无。
4.数据计算
- 递归函数 dg:
- 如果 d 等于 1,则返回 1。
- 否则,返回 dg(d - 1) + d - 1。
int dg(int d){
int res;
if(d==1){
res=1;
}else{
res=dg(d-1)+d-1;
}
return res;
}
- 使用 while 循环来累加 dg(i) 的值,直到累加和 s 大于等于 5000。
- 在每次循环中,s 增加 dg(i) 的值,然后 i 自增 1。
//四、根据关系计算
while(s<5000){
s+=dg(i);
++i;
}
5.输出结果
- 使用 cout 输出累加和首次超过 5000 的值。
//五、输出未知
cout<<s;
完整代码如下:
#include<bits/stdc++.h> // 包含所有标准库头文件
using namespace std; // 使我们可以直接使用 std::cin 和 std::cout 等,而无需 std:: 前缀
// 定义一个名为 dg 的递归函数
int dg(int d) {
int res;
if (d == 1) {
res = 1; // 如果 d 等于 1,返回 1
} else {
res = dg(d - 1) + d - 1; // 否则,返回前一项的结果加上 d-1
}
return res;
}
int main() {
// 一、分析问题
// 未知:求 S=1+2+4+7+11+16…的值刚好大于等于 5000 时 S 的值。
// 二、定义变量(已知、未知、关系)
int s = 0; // 累加和,初始值为 0
int i = 1; // 序列的索引,初始值为 1
// 三、输入已知 - 本例中没有输入
// 四、根据关系计算
while (s < 5000) { // 当累加和小于 5000 时继续循环
s += dg(i); // 累加 dg(i) 的值到 s 中
++i; // 增加索引值
}
// 五、输出未知
cout << s; // 输出累加和首次超过 5000 的值
return 0; // 主函数结束,程序正常退出
}
问题:1147. 求1/1+1/2+2/3+3/5+5/8+8/13+13/21……的前n项的和
类型:函数
题目描述:
求1/1+1/2+2/3+3/5+5/8+8/13+13/21+21/34…的前 n 项的和。
输入:
输入一个整数 n(1≤n≤30)。
输出:
输出一个小数,即前 n 项之和(保留 3 位小数)。
样例:
输入:
20
输出:
12.660
1.分析问题
- 未知:求1/1+1/2+2/3+3/5+5/8+8/13+13/21+21/34…的前 n 项的和。
2.定义变量
- 声明一个整数变量 n,用于存储用户输入的项数。
- 声明一个双精度浮点数 res 并初始化为 0,用于累加每一项的值。
//二、定义变量(已知、未知、关系)
int n;
double res=0;
3.输入数据
//三、输入已知
cin>>n;
4.数据计算
- 斐波那契数列函数:fblq 函数通过递归的方式计算斐波那契数列中的第 f 项。
int fblq(int f){
int r;
if(f==1||f==2){
r=1;
}else{
r=fblq(f-1)+fblq(f-2);
}
return r;
}
- 对于 1 到 n 的每个整数 i 执行循环。
- 计算每一项的值并累加到 res 中。
//四、根据关系计算
for(int i=1;i<=n;++i){
res+=fblq(i)*1.0/fblq(i+1);
}
5.输出结果
//五、输出未知
cout<<fixed<<setprecision(3)<<res;
完整代码如下:
#include<bits/stdc++.h>
using namespace std;
int fblq(int f){ // 定义一个名为 fblq 的函数,它接受一个整数参数 f 并返回一个整数。
int r; // 声明一个整数变量 r 用于存储结果。
if(f==1||f==2){ // 如果 f 等于 1 或者 2,则执行下面的语句。
r=1; // 将 r 设置为 1。
}else{ // 否则,如果 f 大于 2,则执行以下递归计算。
r=fblq(f-1)+fblq(f-2); // 计算 f-1 和 f-2 的值,然后将它们相加并将结果赋给 r。
}
return r; // 返回计算得到的 r 的值。
}
int main(){ // 主函数开始。
// 一、分析问题
// 未知:求 1/1 + 1/2 + 2/3 + 3/5 + 5/8 + 8/13 + 13/21 + 21/34 … 的前 n 项的和。
// 二、定义变量(已知、未知、关系)
int n; // 声明一个整数变量 n,用于存储用户输入的项数。
double res=0; // 声明一个双精度浮点数 res 并初始化为 0,用于累加每一项的值。
// 三、输入已知
cin>>n; // 从标准输入读取一个整数并将其存储到 n 中。
// 四、根据关系计算
for(int i=1;i<=n;++i){ // 对于 1 到 n 的每个整数 i 执行循环。
res+=fblq(i)*1.0/fblq(i+1); // 计算每一项的值并累加到 res 中。
}
// 五、输出未知
cout<<fixed<<setprecision(3)<<res; // 输出 res 的值,保留三位小数。
return 0; // 主函数结束,返回 0 表示程序正常退出。
}
问题:1088 - 求两个数M和N的最大公约数
类型:需要找规律的循环。
题目描述:
求两个正整整数 M 和 N 的最大公约数(M,N都在长整型范围内)
输入:
输入一行,包括两个正整数。
输出:
输出只有一行,包括1个正整数。
样例:
输入:
45 60
输出:
15
1.分析问题
- 已知:两个正整数。
- 未知:最大公约数。
- 关系:递归 、辗转相除法原理(即欧几里得算法),gcd(m, n) = gcd(n, m % n)。
2.定义变量
- 定义变量m、n存储输入的两个整数,result用于存储计算得到的最大公约数。
//二、数据定义
long long int m,n,result;
3.输入数据
//三、数据输入
cin>>m>>n;
4.数据计算
- 4.1 调用gcd函数计算m和n的最大公约数
//四、数据计算
result=gcd(m,n);
- 4.2 定义一个名为gcd的递归函数,接受两个长整型参数m和n
long long int gcd(long long int m, long long int n){
// 基本情况:当m能被n整除时(即m%n等于0),返回n作为最大公约数
if(0 == m % n){
return n;
}
// 递归步骤:当m不能被n整除时,继续调用gcd函数,并将n和m对n取余的结果作为新的参数传递给下一次函数调用
else{
return gcd(n, m % n); // 调用gcd函数,此时的问题规模变为了求解n和m%n的最大公约数
}
}
5.输出结果
- 输出计算得到的最大公约数。
- 主函数返回0,表示程序正常结束。
//五、输出结果
cout<<result;
return 0;
完整代码如下:
#include <bits/stdc++.h> // 包含标准输入输出库
using namespace std; // 使用标准命名空间
// 定义一个名为gcd的递归函数,接受两个长整型参数m和n
long long int gcd(long long int m, long long int n){
// 基本情况:当m能被n整除时(即m%n等于0),返回n作为最大公约数
if(0 == m % n){
return n;
}
// 递归步骤:当m不能被n整除时,继续调用gcd函数,并将n和m对n取余的结果作为新的参数传递给下一次函数调用
else{
return gcd(n, m % n); // 调用gcd函数,此时的问题规模变为了求解n和m%n的最大公约数
}
}
int main(){
// 数据定义部分
long long int m, n, result; // 定义变量m、n存储输入的两个整数,result用于存储计算得到的最大公约数
// 数据输入部分
cin >> m >> n; // 从标准输入读取两个正整数赋值给m和n
// 数据计算部分
result = gcd(m, n); // 调用gcd函数计算m和n的最大公约数
// 输出结果部分
cout << result; // 输出计算得到的最大公约数
return 0; // 主函数返回0,表示程序正常结束
}
三、总结
本文介绍了递归的基本概念及其在数值类问题中的应用。通过“愚公移山”的例子,我们理解了递归是如何通过不断地将问题规模缩小至最基本的情况来求解的。此外,我们还探讨了如何将简单的数学运算如求和、阶乘及最大公约数等问题转换成递归的形式,并给出了具体的C++实现代码。尽管递归提供了一种直观且自然的解决问题的方式,但它也有其局限性,比如效率较低和可能消耗更多的内存资源。因此,在实际应用中,应当根据具体情况权衡是否采用递归方法。希望本文能帮助初学者掌握递归的基本原理,并激发探索更深层次的算法知识的兴趣。
四、感谢
欢迎关注本专栏《C++从零基础到信奥赛入门级(CSP-J)》
如若本文对您的学习或工作有所启发和帮助,恳请您给予宝贵的支持——轻轻一点,为文章点赞;若觉得内容值得分享给更多朋友,欢迎转发扩散;若认为此篇内容具有长期参考价值,敬请收藏以便随时查阅。
每一次您的点赞、分享与收藏,都是对我持续创作和分享的热情鼓励,也是推动我不断提供更多高质量内容的动力源泉。期待我们在下一篇文章中再次相遇,共同攀登知识的高峰!