数据结构与算法——第一节课实验题解

第一次课实验题题解

第一节课的前三道实验题,题目要求“使用递归”,但递归好像对很多同学来说都比较陌生,再加上题目本身(尤其是第三题)真的很难,所以这篇文章将先大体讲一下递归是什么,然后以“从递推到递归”的思路完成前两道题,并给出递推版本题解和符合要求的递归版本题解。等大家对递归熟悉后,再直接运用递归解决最难的第三题。


递归

总而言之,递归就是函数自己调用自己。递归是一个很难理解的算法,我们先看下面这个引例来了解一下,对递归已经很熟悉的同学可以跳过这部分。

递归的引例

int num=0; //计数器,用于记录递归调用次数
int find(int n){
    if(n==1)return num; //递归结束,返回结果
    n=n-1; //n减小
    num++; //num++表示运行了一次
    return find(n); //递归调用
}
}
  • 如果n=1,函数在第一个if直接结束,此时num=0直接被返回;
  • 如果n=2,函数不会在第一个if时终止,而是执行它下面的语句,最后返回find(1),然后程序再掉过头来找find(1)是什么,这时候n经过一次n=n-1后,已经为1了,所以在第一个if里终止,而此时num也经历过一次增大,所以返回的num将是1

现在要求程序执行find(n),其中n=4,我们来手推这段程序:

  1. n=4因此不会触发return num,而是执行下面的语句,n=n-1=3num=1,下一句话是return find(3),但目前还不知道find(3)是什么,所以这个函数运行到这里先暂停,去找find(3)到底是什么

  2. n=3因此不会触发return num,而是执行下面的语句,n=n-1=2num=2,下一句话是return find(2),但目前还不知道find(2)是什么,所以这个函数运行到这里先暂停,去找find(2)到底是什么

  3. n=2因此不会触发return num,而是执行下面的语句,n=n-1=1num=3,下一句话是return find(1),但目前还不知道find(1)是什么,所以这个函数运行到这里先暂停,去找find(1)到底是什么

  4. n=1触发return num,此时的num=3,也就是说经历多次递归调用后,我们找到了这时候的find(1)=3,那么函数find(2)可以继续了,返回3;那么函数find(3)可以继续了,返回3;那么函数find(4)可以继续了,返回3;

综上,如果在主函数中这样写:

int main()
{
    int n=4;
    cout<<find(n);
    return 0;
}

输出:

3

递归大概就是这样的一个过程:函数执行的时候,遇到了“参数不同的它自己”,然后它先放下手里的其他工作,去探索这个新的“它自己”是什么,在探索过程中又遇到了一个“更新的它自己”,于是现在的工作暂停,出发去探索新的“它自己”是什么······等最后终于没有”新的自己“出现了,再把探索的结果一层一层地往回带,最后回到原来想要问题,得到结果。

从上面的过程中,我们可以总结出两个递推的普遍特点:

  1. 需要有一个递推终点,“新的自己”不应该是无限多的,这样程序才有停下来的可能,比如上面程序中的if(n==1)return num;
  2. 需要通过操作来靠近递推终点,比如上面程序中的n--;

递归的确不是很好懂,希望你看完上面的过程后,对它能有一个初步的理解,再通过后续的练习加深理解。

btw. 有句老话说的好,人理解递推,神理解递归


第一题——递归求和

题目链接:传送门

描述

递归是一种非常有效的程序设计方法,应用相当广泛,递归求和就是其中的一种。现在定义数列通项An = n * n,给定一个整数n(1 <= n <= 1000),要你求前n项和Sn,即Sn = 1 * 1 + 2 * 2 + … + n * n。要求使用递归的方法进行计算。

输入

输入只有一行,包括一个整数n,表示要求的项数。

输出

输出只有一行,为一个整数Sn,表示对应的前n项和。

样例输入

7

样例输出

140

解题思路

题意很清晰,数列An=n*n, 它还有个和数列Sn=A1+A2+...+An,我们的任务就是对一个给定的n,求Sn

递推解

因为我们对递归还不够熟悉,不妨先用递推写一下试试,可以得到递推关系:S(n)=S(n-1)+n*n

#include <iostream>
using namespace std;
int s[1010]; //和数列S
int main()
{
    s[1]=1;
    int n;
    cin>>n;
    for(int i=2;i<=n;i++)
        s[i]=s[i-1]+i*i; //递推关系
    cout<<s[n];
    return 0;
}

提交结果是Accepted

递归解

我们直接把递推公式以递归的形式表述出来(暂时不考虑递推终点和操作)

int s(int n){
    return s(n-1)+n*n;
}

代入递推公式后,我们发现它已经满足了第二个条件——n在不断减小,而n越小,题目越简单,当n=1的时候,我们知道结果是1,可以把它设为递推终点

int s(int n){
    if(n==1)return 1; //如果当前n=1,递归结束,返回1
    return s(n-1)+n*n; //否则,继续递归
}

上面这个递归函数,就能完美满足题目要求

附一下完整程序,提交结果Accepted

#include <iostream>
using namespace std;
int s(int n){
    if(n==1)return 1;
    return s(n-1)+n*n;
}
int main()
{
    int n;
    cin>>n;
    cout<<s(n);
    return 0;
}

第二题——递归求最大公约数

题目链接:传送门

描述

给定两个正整数,求它们的最大公约数

输入

输入一行,包含两个正整数,两数都在32位有符号整型的表示范围之内。(也就是在int范围内)

输出

输出一个正整数,即这两个正整数的最大公约数

样例输入

6 9

样例输出

3

提示

整数x和y的最大公约数是能够同时整除x和y的最大整数。编写递归函数gcd来返回x和y的最大公约数。x和y的gcd函数按如下方式进行递归定义:如果y等于0,那么gcd(x, y)等于x;否则gcd(x,y)等于gcd(y, x % y),这里%是求模运算符。

解体思路

老师给的提示,运用到的数学方法称为“辗转相除法”,是一种求两个数最大公约数的套路化方法,有兴趣了解其证明的同学可以自行百度,这里将直接对其进行运用,按照从递推到递归的思路给出题解

ps. gcd只是一个自己起的名字,你想把它改成任何名字都可以,只不过实际应用中为了增强代码的可读性,往往都把“用来求最大公因数的函数”取名为gcd

递推解

使用辗转相除法和while(r!=0)进行递推,当r=0while结束,输出b,详见下方代码:

#include <iostream>
using namespace std;
int main()
{
    int a, b; //待输入的两个数
    cin>>a>>b;
    int r=a%b; //定义r为a/b的余数
    while(r!=0){ //r为0时的b即为最大公约数
        a=b;
        b=r;
        r=a%b;
    }
    cout<<b;
    return 0;
}

有关辗转相除法,建议自己随便写两个数,然后手推一下,验证它的可行性,同时也加深记忆。这里给出一个手推例子:24和16

a=24,b=16,r=24%16=8,c!=0;
a=b=16,b=r=8,r=16%8=0,c==0;

r=0时,b=8即为24和16的最大公因数

提交结果Accepted

递归解

#include <iostream>
using namespace std;
int gcd(int x, int y){
    int r=x%y; //余数r
    if(r==0)return y; //递归终点
    x=y;
    y=r;
    return gcd(x, y); //执行新的递归
}

int main()
{
    int a,b;
    cin>>a>>b;
    cout<<gcd(a,b);
    return 0;
}

第三题——递归:爬楼梯

题目链接:传送门

描述

小明爬楼梯,他可以每次走1级或者2级,输入楼梯的级数,求不同的走法数。
例如:楼梯一共有3级,他可以每次都走一级,或者第一次走一级,第二次走两级;也可以第一次走两级,第二次走一级,一共3种方法。

输入

输入包含若干行正整数,第一行正整数K代表数据组数;后面K行,每行包含一个正整数N,代表楼梯级数,1 <= N <= 30

输出

不同的走法数,每一行输入对应一行输出

样例输入

3
5
8
9

样例输出

8
34
89

解题思路

这个题真的是宇宙无敌超级好题,我想详细讲一下这个题的递推公式是怎么来的,再顺便说一下

“输入包含若干行正整数,第一行正整数K代表数据组数;后面K行,每行包含…”

这种题目要求,应该如何处理

递推公式

我们现在考虑一般情况,用f(n)表示到第n阶的走法种类数,且我们假设已经算出来f(n-1)和它之前的所有项,那么问自己这样一个问题:

想要到达第n阶,有几种可能?

  1. 从第(n-1)阶出发,迈一步过来
  2. 从第(n-2)阶出发,迈两步过来

想要到第n阶,只有这两种可能性

等等,我从第n-2阶出发,先迈一步,再迈一步,不也是一种可能吗?

不是,因为你先迈一步,到了n-1阶以后,问题变成了“从第n-1阶到第n阶”,这种可能性已经被算入第一种情况

综上,到达第n阶的f(n)种走法被分成了两部分:

  • 来自第n-1阶的一部分,有f(n-1)
  • 来自第n-2阶的另一部分,有f(n-2)

也就是说,我们得到了递推公式:

f(n)=f(n-1)+f(n-2);

做到这,你已经可以用递推写出答案来了,但我们能不能尝试一下不经过递推,直接写递归?

我们之前给过一个假设,那就是“已经算出来f(n-1)和它之前的所有项”,但事实上我们根本不知道它们是多少,而是需要现算:

如果遇到f(n-1),我们需要用f(n-2)+f(n-3)去算它,而f(n-2)f(n-3)也不知道,也需要用这样的规律继续去算,直到不依赖规律,我们也能得到结果,比如算到f(3)=f(2)+f(1),我们知道f(1)=1f(2)=2

这其实跟计算机的递归调用过程是一样的。

int f(int n){
    if(n==1)return 1; //递归终点
    if(n==2)return 2; //递归终点
    return f(n-1)+f(n-2); //递归调用
}

n组输入&输出的处理

这个东西就像一个“套路”一样,只要记住就可以,并且也不难理解

int t; //t组数据
cin>>t; 
while(t--){
    ...
}

题解代码

#include <iostream>
using namespace std;
int f(int n){
    if(n==1)return 1;
    if(n==2)return 2;
    return f(n-1)+f(n-2);
}
int main()
{
    int t,n;
    cin>>t;
    while(t--){
        cin>>n;
        cout<<f(n)<<endl;
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/pyx2466079565/article/details/108480392