当前位置: 首页 > backend >正文

C——函数递归

在 C 语言里,函数递归是一种函数调用自身的编程技术。下面开始逐一介绍。

一、什么是递归?

递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。

写⼀个史上最简单的C语⾔递归代码:
#include <stdio.h>
int main()
{
    printf("hehe\n");
    main();//main函数中⼜调⽤了main函数
    return 0;
}
上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演⽰递归的基本形式,不是为了解决问
题,代码最终也会陷⼊死递归,导致栈溢出(Stack overflow)。
1.递归的思想:
把⼀个⼤型复杂问题层层转化为⼀个与原问题相似,但规模较⼩的⼦问题来求解;直到⼦问题不能再被拆分,递归就结束了。所以递归的思考⽅式就是把⼤事化⼩的过程。
递归中的递就是递推的意思,归就是回归的意思。
2.递归的限制条件
递归在书写的时候,有2个必要条件:
递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续。
每次递归调⽤之后越来越接近这个限制条件。
二、递归举例
1.求n的阶乘
⼀个正整数的阶乘(factorial)是所有⼩于及等于该数的正整数的积,并且0的阶乘为1。
⾃然数n的阶乘写作 n!。
题⽬:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。
1.1代码的分析和实现
n的阶乘的公式: n! = n ∗ (n − 1)!
举例:
5!= 5 * 4 * 3 * 2 * 1
4!= 4 * 3 * 2 * 1
所以:5!= 5 * 4!
从这个公式不难看出:如何把⼀个较⼤的问题,转换为⼀个与原问题相似,但规模较⼩的问题来求解的。
n的阶乘和n-1的阶乘是相似的问题,但是规模要少了n。有⼀种有特殊情况是:当 n==0 的时候,n的阶乘是1,⽽其余n的阶乘都是可以通过上⾯的公式计算。
这样就可以写出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);
}
测试:
#include <stdio.h>
int Fact(int n)
{
    if (n == 0)
        return 1;
    else
        return n * Fact(n - 1);
}
int main()
{
    int n = 0;
    scanf("%d", &n);
    int ret = Fact(n);
    printf("%d\n", ret);
     return 0;
}
结果:
(这里不考虑n太大的情况,n太大容易溢出)
2.画图演练
2.1举例
顺序打印⼀个整数的每⼀位
输⼊⼀个整数m,按照顺序打印整数的每⼀位。
⽐如:
输⼊:1234 输出:1 2 3 4
输⼊:125 输出:1 2 5
2.2代码的分析和实现
拿到这个题目,我们需要想的第一件事就是如何得到每一位数。
如果n是⼀位数,n的每⼀位就是n⾃⼰
n是超过1位数的话,就得拆分每⼀位
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);
}

int main()
{
    int m = 0;
    scanf("%d", &m);
    Print(m);
    return 0;
}

输入和输出的结果:

在这里,我们运用到了大事化小的思路。

把Print(1234) 打印1234每⼀位,拆解为⾸先Print(123)打印123的每⼀位,再打印得到的4
把Print(123) 打印123每⼀位,拆解为⾸先Print(12)打印12的每⼀位,再打印得到的3
直到Print打印的是⼀位数,直接打印就⾏。
2.3画图演练
以1234每一位的打印来推演一下

三、递归与迭代

递归是⼀种很好的编程技巧,但是和很多技巧⼀样,也是可能被误⽤的,就像举例1⼀样,看到推导的公式,很容易就被写成递归的形式:
int Fact(int n)
{
    if (n == 0)
        return 1;
    else
        return n * Fact(n - 1);
}
Fact函数是可以产⽣正确的结果,但是在递归函数调⽤的过程中涉及⼀些运⾏时的开销。
在C语⾔中每⼀次函数调⽤,都需要为本次函数调⽤在内存的栈区,申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归函数调⽤都会开辟属于⾃⼰的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出的问题。
所以如果不想使⽤递归,就得想其他的办法,通常就是迭代的⽅式(通常就是循环的⽅式)。
⽐如:计算 n 的阶乘,也是可以产⽣1~n的数字累计乘在⼀起的。
1.举例
求第n个斐波那契数
以计算 factorial(5) 为例,执行过程如下:
factorial(5) 调用 5 * factorial(4)。
factorial(4) 调用 4 * factorial(3)。
factorial(3) 调用 3 * factorial(2)。
factorial(2) 调用 2 * factorial(1)。
factorial(1) 满足基本情况,返回 1。
然后逐步回溯计算:
factorial(2) 返回 2 * 1 = 2。
factorial(3) 返回 3 * 2 = 6。
factorial(4) 返回 4 * 6 = 24。
factorial(5) 返回 5 * 24 = 120。
由此可以看出,这里不适合使用递归,用迭代的方式效率更高。
递归的优缺点
优点:
代码简洁:递归可以让代码更加简洁、易读,尤其是处理一些具有递归结构的问题,如树的遍历、分治算法等。
易于理解:对于某些问题,递归的思路更贴合人类的思维方式,更容易设计和实现。
缺点:
性能问题:递归调用会频繁地进行函数调用和返回操作,带来额外的开销,可能会导致性能下降。
栈溢出风险:如果递归深度过大,会导致栈空间耗尽,引发栈溢出错误。
注意事项
要确保递归函数包含基本情况,防止无限递归。
注意递归深度,避免栈溢出。在必要时,可以考虑使用迭代(循环)来替代递归。
迭代是指基于一个初始值,按照特定的规则反复执行一系列操作,每一次执行都会对前一次的结果进行更新,直到满足某个终止条件为止。这个过程可以看作是对一个状态不断进行修改和演进的循环过程。
递归和迭代对比
执行效率:
迭代:通常情况下,迭代的执行效率相对较高,因为它避免了函数调用栈的频繁操作,只是在循环结构内进行简单的指令重复执行,减少了额外的开销。
递归:递归由于不断地进行函数调用和返回,会有一定的时间和空间开销,尤其是在递归深度较大时,可能会出现栈溢出等性能问题。
代码可读性:
迭代:对于一些简单逻辑,迭代代码的结构清晰,易于理解和调试,直接通过循环条件和循环体就能明白代码的执行流程。
递归:递归代码在处理具有递归结构的问题(如树的遍历、分治算法等)时,代码可能更简洁,更符合人们对问题的分解式思考方式,但对于初学者或者复杂逻辑,理解起来可能有一定难度。

迭代和递归各有优缺点,在实际编程中,需要根据问题的特点、性能要求和代码的可读性来选择合适的方法。有时候,迭代和递归可以相互转换,通过合理的设计和优化,可以提高代码的性能和可维护性。

http://www.xdnf.cn/news/4549.html

相关文章:

  • 软考冲刺——OSPF简答题
  • 仿真系统-学生选课管理
  • 数字化转型是往哪转?怎么转?
  • 第35周Zookkeeper+Dubbo Dubbo
  • 【前端笔记】CSS 选择器的常见用法
  • Cron 用法
  • 数据管道的解耦艺术:Dagster I/O管理器实现存储与逻辑分离
  • 第二章:MySQL 索引优化与高级应用
  • python的异常处理
  • CODESYS开发环境下的快捷键和软件操作汇总
  • 《C++ Templates》:有关const、引用、指针的一些函数模板实参推导的例子
  • Ubuntu 安装 Keepalived
  • Linux 系统的进阶指令详解
  • 【软件设计师:算法】3.排序算法
  • 微信小程序pinia的应用
  • 对redis的深入了解
  • 【每日刷题】第2天
  • 互联网大厂Java求职面试:AI集成与云原生架构设计
  • Go 面向对象,封装、继承、多态
  • 拆解 Prompt 工程:五大场景驱动 DeepSeek 超越 ChatGPT
  • AUTOSAR图解==>AUTOSAR_SWS_WirelessEthernetTransceiverDriver
  • 【AI入门】CherryStudio入门3:结合FastMCP创建自己的MCP服务,实现哔哩视频查询
  • 梅特卡夫法则——AI与思维模型【97】
  • 单片机-STM32部分:7、GPIO输入 按键
  • ()初始化 和 { }初始化
  • PostgreSQL中“参数默认值实现伪重载“详解
  • Unable to ping server at localhost:1099解决
  • 【Linux庖丁解牛】—程序地址空间【进程地址空间 | 虚拟地址空间】
  • 每日一题洛谷P1025 [NOIP 2001 提高组] 数的划分c++
  • Python打卡 DAY 18