【转载】递归(2):高级
一、递归的方式
递归的方式分为两个:自底向上和自顶向下。
我们以打印单链表为例:
/* 自底向上 */
void Print(Node * node)
{
if (node == nullptr)
return;
Print(node->next);
cout << node->data << " ";
}
/* 自顶向下 */
void Print(Node * node)
{
if (node == nullptr)
return;
cout << node->data << " ";
Print(node->next);
}
前者逆序打印链表,后者正序打印链表。
形象点,我们可以用 “吃冰糖葫芦” 来描述上面的两种方式:
自底向上就是从下面往上面吃;自顶向下就是从上面往下面吃。
那这两种方式有何不同呢?
处理数据的顺序:正如上面打印链表的例子,一个正序,一个逆序;
空间内存的消耗:这在接下来对 “尾调用” 的讲解会提及;
运行时间的消耗:这其实是由处理数据的顺序导致的,我能找到的一个例子就是有序链表转化为平衡的二分查找树。
因此,在写递归程序的时候,自底向上和自顶向下这两个方式都是我们应该考虑在内的。
二、尾递归
在介绍尾递归前,需要先理解尾调用。(以下摘自阮一峰的尾调用优化,并作稍微修改)
它的概念非常简单,就是指某个函数的最后一步是调用另一个函数。
TypeName f()
{
return g();
}
以下两种情况都不属于尾调用。
/* case 1 */
TypeName f()
{
TypeName t = g();
return t;
}
/* case 2 */
TypeName f()
{
return 1 + g(x);
}
尾调用不一定出现在函数尾部,只要是最后一步操作即可。
TypeName f()
{
if (x > 0)
return g();
return h();
}
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道,函数调用会在内存形成一个 “调用记录”,又称 “调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数 A 的内部调用函数 B,那么在 A 的调用记录上方,还会形成一个 B 的调用记录。等到 B 运行结束,将结果返回到 A,B 的调用记录才会消失。如果函数 B 内部还调用函数 C,那就还有一个 C 的调用记录栈,以此类推。所有的调用记录,就形成一个 “调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
TypeName f()
{
int m = 1;
int n = 2;
return g(m + n);
}
上面代码中,如果函数 g 不是尾调用,函数 f 就需要保存内部变量 m 和 n 的值、g 的调用位置等信息。但由于调用 g 之后,函数 f 就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g() 的调用记录。
这就叫做 “尾调用优化”(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是 “尾调用优化” 的意义。
而尾递归就是函数尾调用自身。
递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生 “栈溢出” 错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生 “栈溢出” 错误。
int Factorial(int n)
{
if (n == 0)
return 1;
return n * Factorial(n - 1);
}
上面代码是一个阶乘函数,计算 n 的阶乘,最多需要保存 n 个调用记录,空间复杂度为O(n)。
如果改写成尾递归,只保留一个调用记录,空间复杂度为O(1) 。
int Factorial(int n, int total)
{
if (n == 0)
return total;
return Factorial(n - 1, n * total);
}
由此可见,”尾调用优化” 对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算 5 的阶乘,需要传入两个参数 5 和 1?
有个方法可以解决这个问题。就是在尾递归函数之外,再提供一个正常形式的函数。
int Factorial(int n, int total)
{
if (n == 0)
return total;
return Factorial(n - 1, n * total);
}
int Factorial(int n)
{
return Factorial(n , 1);
}
/* you can use it like this */
Factorial(5);