【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)

news2024/12/24 2:11:17

文章目录

  • 递归是什么
    • 递归的思想
    • 递归的限制条件
  • 递归举例
    • 求n的阶乘
      • 分析和代码实现
      • 画图推演
    • 顺序打印一个整数的每一位
      • 分析和代码实现
      • 画图推演
  • 递归与迭代
    • 递归求第n个斐波那契数
    • 迭代求第n个斐波那契数
  • 拓展练习
    • 青蛙跳台阶问题
      • 递归求解
      • 迭代求解
    • 汉诺塔问题

递归是什么

递归是学习C语⾔函数绕不开的⼀个话题,那什么是递归呢?

递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。 写⼀个史上最简单的C语⾔递归代码:

#include <stdio.h>
int main()
{
    printf("hehe\n");
    main();//main函数中⼜调⽤了main函数 
    return 0;
}

上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演⽰递归的基本形式,不是为了解决问题,代码最终也会陷⼊死递归,导致栈溢出(Stack overflow)。

在这里插入图片描述

递归的思想

把⼀个⼤型复杂问题层层转化为⼀个与原问题相似,但规模较⼩的⼦问题来求解;直到⼦问题不能再被拆分,递归就结束了。所以递归的思考⽅式就是把⼤事化⼩的过程。

递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。

递归的限制条件

递归在书写的时候,有2个必要条件:

  • 递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续
  • 每次递归调⽤之后越来越接近这个限制条件

在下⾯的例⼦中,我们逐步体会这2个限制条件


递归举例

求n的阶乘

⼀个正整数的阶乘(factorial)是所有⼩于及等于该数的正整数的积,并且0的阶乘为1。 ⾃然数n的阶乘写作n!。

分析和代码实现

我们知道n的阶乘的公式:n! = n ∗ (n − 1)!

当 n==0 的时候,n的阶乘是1,其余n的阶乘都是可以通过公式计算。

在这里插入图片描述

那我们就可以写出函数Fact求n的阶乘,假设Fact(n)就是求n的阶乘,那么Fact(n-1)就是求n-1的阶乘,函数如下:

int Fact(int n)
{
    if(n==0)
        return 1;
    else
        return n*Fact(n-1);
}
  • 要求n的阶乘,那就是求n-1的阶乘再乘以n

这样的思路就是把⼀个较⼤的问题,转换为⼀个与原问题相似,但规模较⼩的问题来求解的。

画图推演

在这里插入图片描述


顺序打印一个整数的每一位

输⼊⼀个整数m,按照顺序打印整数的每⼀位。

⽐如: 输⼊:1234 输出:1234

输⼊:52 输出:52

分析和代码实现

在这之前学习循环的时候我们通过不断模10除10可以逆序打印整数的每一位

1234%10就能得到4,然后1234/10得到123,这就相当于去掉了4

然后继续对123%10,就得到了3,再除10去掉3,以此类推

不断的 %10 和 /10 操作,直到1234的每⼀位都得到;

但现在要求我们顺序打印,该怎么实现呢?

发现其实⼀个数字的最低位是最容易得到的,通过%10就能得到

那我们假设想写⼀个函数Print来打印n的每⼀位,如下表⽰:

Print(n)
如果n是1234,那表⽰为
Print(1234) //打印1234的每⼀位 
其中1234中的4可以通过%10得到,那么
Print(1234)就可以拆分为两步:
1. Print(1234/10) //打印123的每⼀位 
2. printf(1234%10) //打印4 
完成上述2步,那就完成了1234每⼀位的打印
那么Print(123)⼜可以拆分为Print(123/10) + printf(123%10)

以此类推下去,就有:

Print(1234)
==>Print(123) +                       printf(4)
==>Print(12) +             printf(3)
==>Print(1) +   printf(2)
==>printf(1) 

直到被打印的数字变成⼀位数的时候,就不需要再拆分,递归结束。 那么代码完成也就⽐较清楚:

void Print(int n)
{
    if(n>9)
    {
        Print(n/10);
    }
    printf("%d ", n%10);
}

在这个解题的过程中,我们就是使⽤了⼤事化⼩的思路:

把Print(1234)打印1234每⼀位,拆解为⾸先Print(123)打印123的每⼀位,再打印得到的4

把Print(123)打印123每⼀位,拆解为⾸先Print(12)打印12的每⼀位,再打印得到的3

直到Print打印的是⼀位数,直接打印就⾏。

画图推演

在这里插入图片描述


递归与迭代

递归是⼀种很好的编程技巧,但是和很多技巧⼀样,也是可能被误⽤的,就像练习一求阶乘一样,看到推导的公式,很容易就被写成递归的形式:

但是,但是

在递归函数调⽤的过程中涉及⼀些运⾏时的开销。

在C语⾔中每⼀次函数调⽤,都需要为本次函数调⽤在内存的栈区,申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。

函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归 函数调⽤都会开辟属于⾃⼰的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。 所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stackoverflow)的问题。

所以如果不想使⽤递归,就得想其他的办法,通常就是迭代的⽅式(通常就是循环的⽅式)。

⽐如:计算n的阶乘,也是可以产⽣1~n的数字累计乘在⼀起的。

int Fact(int n)
{
    int i = 0;
    int ret = 1;
    for(i=1; i<=n; i++)
    {
        ret *= i;
    }
    return ret;
}

上述代码是能够完成任务,并且效率是⽐递归的⽅式更好的。

事实上,我们看到的许多问题是以递归的形式进⾏解释的,这只是因为它⽐⾮递归的形式更加清晰, 但是这些问题的迭代实现往往⽐递归实现效率更⾼。

但如果当⼀个问题⾮常复杂,难以使⽤迭代的⽅式实现时,此时递归实现的简洁性便可以补偿它所带来的运⾏时开销。

递归求第n个斐波那契数

在这里插入图片描述

看到这公式,很容易诱导我们将代码写成递归的形式,如下所⽰:

int Fib(int n)
{
    if(n<=2)
        return 1;
    else
        return Fib(n-1)+Fib(n-2);
}

如果当我们输入50以上的数字n时,需要很⻓时间才能算出结果,这个计算所花费的时间,是我们很难接受的, 这也说明递归的写法是⾮常低效的,那是为什么呢

在这里插入图片描述

其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计 算,⽽且递归层次越深,冗余计算就会越多。我们可以作业测试:

#include <stdio.h>

int count = 0;
int Fib(int n)
{
    if(n == 3)
        count++;//统计第3个斐波那契数被计算的次数 
    if(n<=2)
        return 1;
    else
        return Fib(n-1)+Fib(n-2);
}

int main()
{
    int n = 0;
    scanf("%d", &n);
    int ret = Fib(n);
    printf("%d\n", ret); 
    printf("\ncount = %d\n", count);
    return 0;
}

这⾥我们看到了,在计算第40个斐波那契数的时候,使⽤递归⽅式,第3个斐波那契数就被重复计算了 39088169次,这些计算是⾮常冗余的。

所以斐波那契数的计算,使⽤递归是⾮常不明智的,我们就得 想迭代的⽅式解决

迭代求第n个斐波那契数

我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从⼩到⼤计算就⾏了。

这样就有下⾯的代码:

int Fib(int n)
{
    int a = 1;
    int b = 1;
    int c = 1;
    while(n>2)
    {
        c = a+b;
        a = b;
        b = c;
        n--;
    }
    return c;
}

迭代的⽅式去实现这个代码,效率就要⾼出很多了。

有时候,递归虽好,但是也会引⼊⼀些问题,所以我们⼀定不要迷恋递归,适可⽽⽌就好。


拓展练习

青蛙跳台阶问题

一只青蛙一次可以跳上 1 级台阶,也可以跳上2 级。求该青蛙跳上一个n 级的台阶总共有多少种跳法?

递归求解

和斐波那契数列很相似,要求跳上第n级的台阶(n>3),无非就是从n-2跳两级或者n-1的台阶跳1级,那总方法就是f(n)=f(n-2)+f(n-1)

int Fn(int n) {
    if (n <= 2) {
        return n;
    }
    else if (n > 2) {
        return Fn(n - 1) + Fn(n - 2);
    }
}

显然递归求解也涉及到很多的重复计算问题,效率十分之低

迭代求解

斐波那契数列是:1 1 2 3 5 8 13······

青蛙跳台阶数列是:1 2 3 5 8 13······

只需做适当更改即可:

int Fn(int n) {
    int a = 1;
    int b = 2;
    int c = 0;
    if (n == 1) {
        return 1;
    }
    else if (n == 2) {
        return 2;
    }
    else{
        while(n>2){
            c = a + b;
            a = b;
            b = c;
            n--;
        }
    }
    return c;
}

汉诺塔问题

汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放大圆盘,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?

注意:求得是最少移动次数

在这里插入图片描述

  • 当n=1时,直接将a移到c即可
  • 当n=2时,a->b,a->c,b->c
  • 当n=3时,a->c,a->b,c->b,a->c,b->a,b->c,a->c

当n越来越大时,如果我们要一次一次想再把它每一步写出来,是没有意义的了

所以我们先不关注每一步细节,思考一下移动的核心逻辑是什么

核心逻辑就是每一次移动都是把最底下的盘子移动到目标盘,此时只关注剩下的n-1个盘子,又变为了新的汉诺塔问题

例如当n=3的时候,(从下到上依次设定为盘子1,2,3)我们前四步进行的就是把盘子3移动到c,此时初始柱为a,中转柱为b,目标柱为c。

此时3已经移动到最后的正确位置了,直接忽略,而接下来要做的就是把b柱上的盘子1和2移到c,这不就是n==2的汉诺塔问题吗,此时初始柱变成了b,中转柱变成了a,目标柱就是c,我们在第5-7所做的事就是把盘子2移动到c

最后一步,即变为了只有一个盘子的汉诺塔问题,直接将盘子1移动到c即可。

上述分析仍然太过复杂,不妨这样考虑:

  • 第一步,n-1个盘子移动到中转柱,这其实何尝不是一个汉诺塔问题呢

  • 第二步,最底下的盘子移动到目标柱

  • 第三步,中转柱的n-1个盘子移动到目标柱,这又何尝不是一个汉诺塔问题呢

并且,这种思维还能帮我们推导出n个盘子移动所需要的最少步数

手写推导如下:在这里插入图片描述

就是高中数学很简单的数列题

如果想用数学归纳法求解也是可以的,也很简单,这里就不过多赘述

代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
/*
pos1:初始柱
pos2::中转柱
pos3:目标柱
*/
void move(char x,char y)
{
    printf("%c->%c ", x, y);
}
void hanoi(int n, char pos1, char pos2, char pos3)
{
    if (n == 1)
        move(pos1, pos3);
    else
    {
        hanoi(n - 1, pos1, pos3, pos2);//把n-1个盘子移动到中转柱pos2上,
        //是一个汉诺塔问题,对于这n-1个盘子来说,
        //初始柱为pos1,中转柱为pos3,目标柱为pos2

        move(pos1,pos3);//把初始柱最下面盘子移动到目标柱

        //此时n-1个盘子都在pos2上,
        //这也是一个汉诺塔问题
        //初始柱为pos2,中转柱为pos1,目标柱为pos3
        hanoi(n - 1, pos2, pos1, pos3);
    }
}

int main()
{
    int n = 0;
    scanf("%d", &n);
    hanoi(n, 'a', 'b', 'c');
    return 0;
}

如果我们更多关注每一次移动的细节会发现:

要保持最小的步数,每一次汉诺塔问题(无论是最初还是递归过程中的),如果此时初始柱盘子数为偶数,我们第一步是把最上面的盘子移动到中转柱,如果为奇数,我们第一步则是将其移动到目标柱。(当做一个归纳结论了解一下就行)

以上就是有关递归的详细介绍啦,各位大佬有什么问题欢迎在评论区指正,您的支持是我创作的最大动力!❤️
请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1990850.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FPGA使用sv生成虚拟单音数据

FPGA使用sv生成虚拟单音数据 之前一直使用matlab生成虚拟的数据&#xff0c;导出到txt或是coe文件中&#xff0c;再导入到fpga中进行仿真测试。 复杂的数据这样操作自然是必要的&#xff0c;但是平日使用正弦数据进行测试的话&#xff0c;这样的操作不免复杂&#xff0c;今日…

CentOS 安装Redis

在 CentOS 安装 Redis 操作系统&#xff1a;centos-7.9.2009-Core 1. 更新系统 首先&#xff0c;确保你的系统是最新的&#xff1a; sudo yum update -y2. 安装 EPEL 仓库 Redis 可能不在默认的 CentOS 仓库中&#xff0c;因此你需要安装 EPEL&#xff08;Extra Packages f…

【源码+文档+调试讲解】活力健身馆管理系统

摘 要 活力健身馆管理系统的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品&#xff0c;体验高科技时代带给人们的方便&#xff0c;同时也能让用户体会到与以往常规产品不同的体验风格。 与安卓&#xff0c;iOS相比较起来&am…

springboot狱内罪犯危险性评估系统的设计与实现论文源码调试讲解

第一章系统成功运行案例 第2章 程序开发技术 2.1 Mysql数据库 开发的程序面向用户的只是程序的功能界面&#xff0c;让用户操作程序界面的各个功能&#xff0c;那么很多人就会问&#xff0c;用户使用程序功能生成的数据信息放在哪里的&#xff1f;这个就需要涉及到数据库的知识…

CTFHUB-web-RCE-eval执行

开启题目 查看源码发现直接用蚁剑连接就可以&#xff0c;连接之后发现成功了

冒烟测试:快速初步检测软件稳定性的关键步骤

目录 前言1. 冒烟测试的定义1.1 冒烟测试的起源1.2 冒烟测试的特点 2. 冒烟测试的重要性2.1 确保构建稳定性2.2 早期发现重大问题2.3 提高测试效率 3. 冒烟测试的实施方法3.1 制定测试计划3.2 选择测试用例3.3 执行测试3.4 分析测试结果 4. 冒烟测试的最佳实践4.1 自动化冒烟测…

Opera浏览器与IPXProxy代理IP集成步骤详解

​对于经常需要使用公共WiFi网络进行网络操作的人来说&#xff0c;安全性是至关重要的。作为Opera浏览器的用户&#xff0c;我非常关注隐私安全&#xff0c;也尝试过各种提高安全性的方法&#xff0c;其中使用IPXProxy代理IP是不错的方法之一。下面为大家带来Opera浏览器与IPXP…

【java】升级jetty-client解决Too many open files问题

文章目录 升级jetty-client解决Too many open files问题问题背景排查原因解决lsof命令拓展 升级jetty-client解决Too many open files问题 问题背景 生产环境的采集经过一段时间就会报错 Too many open files&#xff0c;导致接下来的采集都会失败&#xff0c;已经严重影响到…

NSSCTF练习记录:[SWPUCTF 2021 新生赛]include

题目&#xff1a; 随便传入一个file 因为存在include_once函数&#xff0c;可以使用php伪协议获取flag.php源码&#xff0c;再通过base64解码得到flag。 php:// 访问各个输入/输出流&#xff0c;常用php://filter和php://input&#xff0c;php://filter用于读取源码&#xff…

gin框架 自定义404错误页面,自定义500等服务端异常,业务异常,根据不同异常类型显示不同的异常页面方法 整理

在gin框架中&#xff0c;要显示自定义的异常页面&#xff0c;首先需要通过gin路由对象中的LoadHTMLFiles或者LoadHTMLGlob方法加载自定义的错误页面模板文件&#xff0c; 然后定义符合 gin.HandlerFunc 类型的路由处理函数/方法 &#xff0c;即只有一个参数(c *ginx.XContext)的…

如何理解供应链控制塔?详解供应链控制塔类型与架构!

随着经济全球化的不断深入&#xff0c;企业供应链的复杂性也在不断增加。从供应商到制造商&#xff0c;再到分销商和消费者&#xff0c;全球供应链网络的每一个环节都充满了动态变化和不确定性。在这样的背景下&#xff0c;传统的供应链管理模式已难以满足现代企业的需求&#…

Ackites/Killwxapkg

自动化反编译微信小程序&#xff0c;小程序安全评估工具&#xff0c;发现小程序安全问题&#xff0c;自动解密&#xff0c;解包&#xff0c;可还..自动化反编译微信小程序&#xff0c;小程序安全评估工具&#xff0c;发现小程序安全问题&#xff0c;自动解密&#xff0c;解包&a…

【消息队列】kafka如何保证消息不丢失?

&#x1f44f;大家好&#xff01;我是和风coding&#xff0c;希望我的文章能给你带来帮助&#xff01; &#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&#x1f44d;一下博主哦 &#x1f4dd;点击 我的主页 还可以看到和风的其他内容噢&#x…

查看有无XSS漏洞验证

实验环境 操作机&#xff1a;Win10 用户名&#xff1a;wangan 密码&#xff1a;123靶机&#xff1a;Apache PHP实验地址&#xff1a;http://ip/xss/level1.php?nametest 实验原理 构造弹窗的代码提交&#xff0c;浏览器在执行该代码后就会执行弹框的操作&#xff0c;弹框的目…

Python请求API的简明教程

前言 随着微服务流行开来&#xff0c;API正在成为数据获取的主要渠道&#xff0c;我们可以通过Java的HttpClient完成数据请求&#xff0c;当然也可以通过Python工具完成数据请求。 本博将对Python如何请求API进行举例&#xff0c;保你一文掌握。 1. 准备工具 在使用Python请…

Java面试题--JVM大厂篇之针对频繁的Minor GC问题,有哪些优化对象创建与使用的技巧可以分享?

目录 引言&#xff1a; 正文&#xff1a; 1. 了解Minor GC的痛点 2. 使用对象池&#xff08;Object Pool&#xff09; 3. 避免不必要的对象创建 4. 使用StringBuilder替代字符串拼接 5. 合理设置对象的作用域 6. 使用软引用和弱引用 结束语&#xff1a; 引言&#xff…

Python | Leetcode Python题解之第330题按要求补齐数组

题目&#xff1a; 题解&#xff1a; class Solution:def minPatches(self, nums: List[int], n: int) -> int:patches, x 0, 1length, index len(nums), 0while x < n:if index < length and nums[index] < x:x nums[index]index 1else:x << 1patches …

分享一款老软件RealPlayer 16.0.1.18

一款怀旧老软件&#xff0c;功能还算强大测试系统win7/10&#xff0c;win11以上系统自行测试&#xff01;这种老软件非常适合配置低的老电脑&#xff0c;尤其是单位办公电脑。

LVS实战演练

一.LVS简介 LVS&#xff08;Linux Virtual Server&#xff09;是Linux虚拟服务器的简称&#xff0c;是一种基于Linux内核的开源负载均衡技术。 <1>.工作原理 LVS&#xff08;Linux Virtual Server&#xff09;的工作原理可以概括为通过负载均衡技术将客户端的请求分发到…

论文解读 | ACL 2024:自我蒸馏在语言模型微调中架起分布差异的桥梁

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 杨兆瑞 浙江大学CAD&CG全国重点实验室博士生 导师为陈为教授 概述 大型语言模型&#xff08;LLMs&#xff09;的兴起彻底改变了自然语言处理领域&#xff0c;但对它们进行特定任务的微调常常面临在平衡性能…