尾调用和尾递归

2017/02/25 Algorithm

今天在复习栈和队列的时候,突然出现这么一个概念,虽然从表面看好像是一个简单地不能再简单的问题,但是上网一查却发现这个知识点被很多人所研究过,于是打算顺便总结一下!

什么是尾调用

顾名思义,就是指某个函数的最后一步是调用另一个函数。如以下代码中,1属于尾调用,2、3都不属于尾调用。

// 1
function f(x) {
	return g(x);
}

// 2
function f(x) {
	let y = g(x);
	return y;
}

// 3
function f(x) {
	return g(x) + 1;
}

尾调用优化

函数调用会形成调用帧,保存调用位置和内部变量等信息。若A调用B,则A上方会形成B的调用记录,所有记录构成调用栈。尾调用不需保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了,这叫做尾调用优化,将大大节省内存,具体可以参考以下代码:

function f() {
	let m = 1;
    let n = 2;
    return g(m + n);
}
f();

// 等同于
function f() {
    return g(3);
}
f();

// 等同于
g(3);

尾递归

递归非常耗费内存,很容易发生栈溢出(stack overflow)。但尾递归由于只存在一个调用记录,所以不会栈溢出。

function factorial(n) {
    if (n === 1) 
    	return 1;
    return n * factorial(n - 1);
}

factorial(5) // 120

上面的函数最多需保存n个调用记录,复杂度O(n)。若改成尾递归,只需保留一个调用记录,复杂度O(1)。尾递归实现往往需要改递归函数,把所有用到的内部变量改写成函数参数,具体如下:

function factorial(n, total) {
    if (n === 1) 
  		return total;
	return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

Search

    Table of Contents