您的位置:首页 > 其它

深入理解递归(二)

2018-02-03 23:37 183 查看
【转载】递归(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);

}
[/code]前者逆序打印链表,后者正序打印链表。
形象点,我们可以用 “吃冰糖葫芦” 来描述上面的两种方式:



自底向上就是从下面往上面吃;自顶向下就是从上面往下面吃。
那这两种方式有何不同呢?
处理数据的顺序:正如上面打印链表的例子,一个正序,一个逆序;

空间内存的消耗:这在接下来对 “尾调用” 的讲解会提及;

运行时间的消耗:这其实是由处理数据的顺序导致的,我能找到的一个例子就是有序链表转化为平衡的二分查找树

因此,在写递归程序的时候,自底向上和自顶向下这两个方式都是我们应该考虑在内的。

二、尾递归

在介绍尾递归前,需要先理解尾调用。(以下摘自阮一峰的尾调用优化,并作稍微修改)
它的概念非常简单,就是指某个函数的最后一步是调用另一个函数。
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);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  递归 算法