1.普通递归
也称线性递归,在递归过程中会为每一层的调用在栈中开辟空间用于存储返回点和变量。所以递归次数过多容易造成栈的溢出。
2.尾递归
顾名思义,在函数尾部调用自身,并且不需要为返回点开辟新的栈空间,但往往需要将当层的计算结果作为参数传递到下一层的调用中去。所以需要为参数开辟额外但恒量的空间。不需要传递当层计算结果的尾递归调用习惯是极为不好的。
二者区别:
对于某些实现(如斐波那契),普通递归可能会有重复执行的调用,而尾递归实现不会有隐藏的重复调用。但尾递归的使用要极为小心。
普通递归和尾递归的例子:
Fibonacci不同递归形式的实现:
1.普通递归方式:
int Fibonacci_Recursive(int n)
{
if (n < 2)
return n;
return Fibonacci_Recursive(n - 1) + Fibonacci_Recursive(n - 2);
}
此种递归方式隐藏有重复执行的调用,例如计算n-1时,实际上同时计算过n-2。另外在计算n-1时需要n-2的调用返回和n-1的结果相加,所以需要为每层的调用开辟栈空间用于保存返回点和计算结果。
2.尾递归实现:
int Fibonacci_TailRecursive(int n, int ret1, int ret2)
{
if (n == 0)
return ret1,
else
return Fibonacci_TailRecursive(n - 1, ret2, ret1 + ret2);
}
上述尾递归传参使用形式是Fibonacci_TailRecursive(n, 0, 1)
尾递归方式,已经将当前调用层的计算结果传递给了下一层调用,不会有隐藏的重复执行次数,但是需要开辟两个整形变量来保存当前层的计算结果并传递给下一层调用,相比普通递归为每层调用都要开辟栈空间,这两个整形变量的增加是极小的。
怎样判断或者写一个尾递归?
尾递归一般是在函数的尾部调用自身,但这不是判断一个递归是尾递归的充分条件。实际上判断一个递归是尾递归的充分条件是在调用下一层时需不需要为返回点或者计算结果开辟栈空间。
例如:
return tailrec(x + 1);是尾递归,因为它不需要保存返回点的结算结果,而是将当层的计算结果传递给了下一层。
return tailrec(x) + 1;不是尾递归,因为它需要保存当前调用层的计算结果然后加1。
注意:不需要使用当前调用层计算结果或者当前调用层没必要返回的尾递归实现都是不好的,应该避免使用,并使用for/while或者goto去优化掉。例如:
PrintList(List L)
{
if (L != NULL) {
PrintElement(L->Element);
PrintList(L->Next);
}
}
在上面的尾递归实现中,如果L的长度过长,比如有几万个则很有可能导致栈的溢出,应该使用while/for或者goto等语句优化。例如:
void PrintList(List L)
{
top:
if (L != NULL) {
PrintElement(L->Element);
L = L->Next;
goto top;
}
}
题外话:
像上面不当的尾递归实现,C语言编译器有可能会帮助我们优化掉,但不是每个编译器都会这么做,最好的方式是避免使用。
另外,像python,java等不支持尾递归优化,只能使用for/while等避免。