结论:
尾递归就是自身递归语句
在自身函数的最后一行
的特殊递归函数的称谓。
1、什么是递归?
递归是指一种 循环调用 函数自身 的方式,这种递归调用可能会产生一个返回值,然后利用递归调用的返回值来计算结果。
2、递归的组成?
递归算法一般由递归前进段
和递归返回段
两大部分组成,递归函数的判断则确定了这一次调用递归函数会进入前进段还是返回段。
递归就像子孙寻找自己的亲戚,首先寻找自己的父辈,然后寻找自己父辈的父辈,最后还是会通过其他的父子关系,到达与我们同辈的地方,如果能找到这么一个人,我们就可以说这个人是我们同辈的亲戚。当我们向父辈调用的时候,这个时候我们称作递归前进段,当从父辈的关系链下降到我们这一辈的时候,我们称为递归返回段。
3、递归程序示例
#include <iostream>
using namespace std;
void my_print(int N) {
if (N) {
my_print(N - 1);
cout << N << endl;
}
}
int main() {
int input = 0;
cin >> input;
my_print(input);
}
这是一个简单的 C++ 语言的程序,它的作用是打印从 1 开始,以 1 为间隔,到我们输入的数这个范围内全部的数。
当输入的数足够小的时候,程序可以正常运行,当时当我们输入的数的规模变大的时候呢?程序会怎么样?
我们输入 10000 ,这个时候,IDE 提示我 Stack OverFlow(栈溢出),如下图所示(这里使用的是 Debug 模式,它默认关闭了编译器的优化)
我们发现在这个时候,递归调用函数的值为 5209,说明程序在进行到 5209 的时候出现了错误,这个时候我们查看程序使用了多少内存。
我们发现,程序只使用了 920KB 的内存空间,我们电脑的可用内存那么多,为什么程序会崩溃呢?这就要提到 C/C++ 语言程序运行时会占用那些空间了。我在我之前的一片博文中解释过,大家可以去看一下。
我们注意这里的报错就会发现,报错名称为栈溢出,栈由于需要一块连续的存储空间,所以一般的来说,在Windows 操作系统下,栈的空间一般在 1MB 至 2MB 左右。
我是用的软件是 Visual Studio 2019,这款软件跟大家以前使用的 VC++ 一样都是微软开发的,而它们的默认栈空间均为1MB
这也就解释了为什么程序只占用了 920KB 的内存资源,而程序却崩溃了的这个事情。
4、如何优化递归?
- 为什么递归会消耗大量的栈空间?
因为在函数调用自身的时候,它目前有一定的状态需要保存,所以需要用栈空间来保存当前的状态,这种保存的当前状态我们称为栈帧。
比如我们做番茄炒蛋,当蛋下锅以后,我们需要番茄了,调用产生番茄的函数,这个时候我们要用停止时间的东西储存住已经下锅的蛋的状态,然后我们去处理番茄。
锅
比作一次调用递归函数
,下锅炒的蛋可以比作递归函数执行的位置
,去取番茄可以比作调用生成番茄的函数,这样,在函数内调用多少次,就需要多少份这样的栈空间来保存当前的状态,这就是为什么栈空间会溢出的原因。
-
尾递归的原理:
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的栈帧(活动记录)而不是在栈中去创建一个新的栈帧,这样就可以避免栈空间的开销,使递归函数消耗的栈空间大大的缩小了。 -
简单的来说:
如果调用函数的语句本身,就是该函数的结束语句,那不就不需要保存状态了,因为后面已经没有需要执行的了,函数的返回值只却决于调用的最后一层函数的返回值,这种优化方法称为尾递归优化。
5、代码实现以及对比
还是原来的输出 1 ~ N 的数字的问题,常规递归函数为 my_print ,尾递归优化函数为 tail_my_print ,代码如下
#include <iostream>
using namespace std;
void my_print(int N) { // 原始的普通递归函数
if (N) {
my_print(N - 1);
cout << N << endl;
}
}
void tail_my_print(int start, int N) { // 尾递归优化函数
if (start) {
cout << N - start << endl;
tail_my_print(--start, N);
}
}
int main() {
int input = 0;
cin >> input;
//tail_my_print(input, input); // 交替注释这两行语句观察运行结果
my_print(input); // 交替注释这两行语句观察运行结果
}
由于尾递归优化需要开启编译器的优化选项,所以我们选择的模式为Release x64 模式,在 Visual Studio 如下图所示。
由于开启了优化,所以常规的递归函数要想使栈空间溢出就变得更加困难了,所以我们这次加大测试用例的规模,我们使用 30000作为测试的 N 值。
经测试,使用递归函数在 30000 的时候无法正确输出,栈溢出的时候 N 的值为 8524 ,也就是执行了 30000 - 8524 = 21476 次 递归函数。
而使用尾递归函数时的输出一切正常,如下图所示。