第9讲:函数递归
第9讲:函数递归
XingJiのBlog正文开始
递归是什么?
递归是学习 C 语言函数绕不开的一个话题,那什么是递归呢?
递归其实是一种解决问题的方法,在 C 语言中,递归就是 函数自己调用自己 。
写一个史上最简单的 C 语言递归代码:(>这是一个错误的示范,会导致死循环,导致栈溢出。)
1 |
|
运行结果:
1 | hehe |
这个代码的作用是什么呢?
- 它会导致死循环,导致栈溢出。
- 它没有任何意义,只是打印”hehe”
上述就是一个简单的递归程序,只不过上面的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷入死递归,导致栈溢出(Stackoverflow)。
所以,递归的基本形式是:每一次函数调用,都会在栈上开辟一块内存,当递归层数太多时,会导致栈溢出。
递归的思想:
递归的思想是:把一个大型复杂问题——》分解成规模较小的子问题来求解
递归中的 递就是递推 的意思, 归就是回归 的意思。
递归的限制条件
递归在书写的时候,有 2 个必要条件:
递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
在下面的例子中,我们逐步体会这 2 个限制条件。
递归举例
举例 1 :求 n 的阶乘
一个正整数的 阶乘 ( factorial )是所有小于及等于该数的正整数的积,并且 0 的阶乘为 1 。自然数 n 的阶乘写作 n!。
题目:计算 n 的阶乘(不考虑溢出),n 的阶乘就是 1~n 的数字累积相乘。
分析和代码实现
我们知道 n 的阶乘的公式:n !=^ n ∗( n −^1 )!
1 | 举例: |
这样的思路就是把一个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解的。
当 n==0 的时候,n 的阶乘是 1 ,其余 n 的阶乘都是可以通过公式计算。
n 的阶乘的递归公式如下:
那我们就可以写出函数 Fact 求 n 的阶乘,假设 Fact(n)就是求 n 的阶乘,那么 Fact(n-1)就是求 n-1 的阶乘,函数如下:
1 | int Fact(int n) |
测试:
1 |
|
运行结果:(这里不考虑 n 太大的情况,n 太大存在溢出):
画图推演
举例 2 :顺序打印一个整数的每一位
输入一个整数 m,按照顺序打印整数的每一位。
比如:
1 | 输入:1234 输出:1 2 3 4 |
分析和代码实现
这个题目,放在我们面前,首先想到的是,怎么得到这个数的每一位呢?
1 | 如果n是一位数,n的每一位就是n自己 |
1 | 1234%10就能得到 4 ,然后1234/10得到 123 ,这就相当于去掉了 4 |
1 | //假设n是1234 |
但是我们有了灵感,我们发现其实一个数字的最低位是最容易得到的,通过% 10 就能得到那我们假设想写一个函数 Print 来打印 n 的每一位,如下表示:
1 | Print(n) |
以此类推下去,就有
1 | Print( 1234 ) |
直到被打印的数字变成一位数的时候,就不需要再拆分,递归结束。 那么代码完成也就比较清楚:
1 | /** |
输入和输出结果:
1 | 123 |
在这个解题的过程中,我们就是使用了大事化小的思路
把 Print(1234)打印 1234 每一位,拆解为首先 Print(123)打印 123 的每一位,再打印得到的 4
把 Print(123)打印 123 每一位,拆解为首先 Print(12)打印 12 的每一位,再打印得到的 3
直到 Print 打印的是一位数,直接打印就行。
画图推演
以 1234 每一位的打印来推演一下
递归与迭代
递归是一种很好的编程技巧,但是和很多技巧一样,也是可能被误用的,就像举例 1 一样,看到推导的
公式,很容易就被写成递归的形式:
1 | int Fact(int n) |
Fact 函数是可以产生正确的结果,但是在递归函数调用的过程中涉及一些运行时的开销。
递归函数调用的过程,是栈的一种数据结构,每一次函数调用,都会在栈上开辟一块内存,当递归层数太多时,会导致栈溢出。
在 C 语言中每一次函数调用,都需要为本次函数调用在内存的栈区,申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为 运行时堆栈 ,或者 函数栈帧 。
函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stackoverflow)的问题。
注:关于 函数栈帧 的详细内容,鹏哥录制了视频专⻔讲解的,下课导入课程,自行学习。
所以如果不想使用递归,就得想其他的办法,通常就是迭代的方式(通常就是循环的方式)。
比如:计算n的阶乘,也是可以产生1~n的数字累计乘在一起的。
1 | /** |
上述代码是能够完成任务,并且效率是比递归的方式更好的。
事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,
但是这些问题的迭代实现往往比递归实现效率更高。
当一个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
举例 3 :求第 n 个斐波那契数
我们也能举出更加极端的例子,就像计算第 n 个斐波那契数,是不适合使用递归求解的,但是斐波那契
数的问题通过是使用递归的形式描述的,如下:
看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:
1 | int Fib(int n) |
测试代码:
1 |
|
当我们 n 输入为 50 的时候,需要很⻓时间才能算出结果,这个计算所花费的时间,是我们很难接受的,
这也说明递归的写法是非常低效的,那是为什么呢?
其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,而且递归层次越深,冗余计算就会越多。我们可以作业测试:
1 |
|
输出结果:
这里我们看到了,在计算第 40 个斐波那契数的时候,使用递归方式,第 3 个斐波那契数就被重复计算了
39088169 次,这些计算是非常冗余的。所以斐波那契数的计算,使用递归是非常不明智的,我们就得想迭代的方式解决。
我们知道斐波那契数的前 2 个数都 1 ,然后前 2 个数相加就是第 3 个数,那么我们从前往后,从小到大计算就行了。
这样就有下面的代码:
1 | /** |
迭代的方式去实现这个代码,效率就要高出很多了。
有时候,递归虽好,但是也会引入一些问题,所以我们一定不要迷恋递归,适可而止就好。
拓展学习:
- ⻘蛙跳台阶问题
- 汉诺塔问题
以上 2 个问题都可以使用递归很好的解决,有兴趣可以研究。
课堂板书:
https://gitee.com/bitpg/class114training-camp-phase-5/blob/master/2024-04-22-%E6%9D%BF%E4%B9%A6.png
课堂代码:
https://gitee.com/bitpg/class114training-camp-phase-5/blob/master/test_4_22/test_4_22/test.c
完