深入理解递归(二)

【转载】递归(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);

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值