什么是尾递归

时间:2022-04-07 02:41:19

简介:

想必大家都知道递归是什么,第一次接触尾递归,首先要从它的定义说起:

尾递归:当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作

举一个简单的例子,用递归算阶乘:

int factorial(int n)
{
if(n == 0 || n == 1) {
return 1;
} else {
return n * factorial(n-1);
}
}

现在我们模拟一下程序的过程,例如 n = 5 的时候:

factorial(5)
5 * factorial(4)
5 * (4 * factorial(3))
5 * (4 * (3 * factorial(2)))
5 * (4 * (3 * (2 * factorial(1))))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120

上述递归过程中,“最长”的部分,即代表着此刻占用的栈内存最大
所以在一般的递归中,它的内存占用,先是增大,后来到达一个峰值,然后缩小

下面给出尾递归的实现方法:
看到这段代码后,第一印象会发现函数除了 n ,还多了一个参数 tmp
事实上 tmp 代表着 1!(1的阶乘的值)也就是 1 ,于是在使用函数的时候,第二个参数传递一个 1

int factorial(int n, int tmp)
{
if(n == 0) {
return 1;
} else if (n == 1) {
return tmp;
} else {
return factorial(n - 1, n * tmp);
}
}

我们同样模拟一下程序的过程,例如 n = 5 的时候:

factorial(5, 1)
factorial(4, 5)
factorial(3, 20)
factorial(2, 60)
factorial(1, 120)
120

通过这个过程,我们可以发现,它完全对应着尾递归的定义:

当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作

它比线性递归(第一个例子称为线性递归)多一个参数,这个参数是上一次调用函数得到的结果,并且每次递归的时候都会保留着上次递归的结果,它不属于表达式的一部分,而且它需要回归的数据,本身已经通过参数携带,所以回归过程中不用做任何操作。

所以,相比较线性递归,尾递归占用的栈内存是恒定的

总结:

既然尾递归相比线性递归解决了线性递归致命的缺点(stack overflow风险),是不是更提倡使用尾递归呢?

参考数据结构与算法分析-C语言描述中的一个例子,打印一个链表:

/* Bad use of recursion : Printing a linked list */
/* No header */

void PrintList(List L)
{
if (L != NULL) {
PrintElement(L->Element);
PrintList(L->Next);
}
}

在这里使用尾递归就是一个不好的例子,因为没有什么需要存储,在递归结束调用的时候,实际上并没有需要存储的值,因此,我们就可以带着在第一次递归调用中已经用过的那些值, goto 到函数的顶部,下面是改进后的程序(记住,你应该更加自然的使用 while 循环结果去去除尾递归,这里使用 goto 只是为了说明编译器是如何自动地去除尾递归)

/* Printing a linked list non-recursively */
/* Uses a mechanical translation */
/* No header */

void PrintList(list L)
{
top:
if (L != NULL) {
PrintElement(L->Element);
L = L->Next;
goto top;
}
}

不用递归去打印一个链表,事实上,尾递归的去除是如此的简单,以至于某些编译器可以自动完成,所以这些工作不用你去完成,但是即使如此,最好还是你的程序自己去去除它。


参考:
1、What is tail recursion - *?
2、数据结构与算法分析:C语言描述(原书第2版) - Mark Allen Weiss



个人浅见,欢迎指正!