前言
今天刷leetcode时看到爬楼梯这道题可以使用递归调用进行解题。
/*
假设你正在爬楼梯。需要 `n` 阶你才能到达楼顶。
每次你可以爬 `1` 或 `2` 个台阶。你有多少种不同的方法可以爬到楼顶呢?
*/
/*
思路:最后一步有两种选择选择走 1 或 2 个台阶,因此 n 阶楼梯的爬法就是 n - 1 与 n - 2 阶 楼梯的爬法只和,可以递归求解
*/
function climbStairs(n: number): number {
if (n === 1) return 1;
if (n === 2) return 2;
return climbStairs(n - 1) + climbStairs(n - 2);
};
但递归求解会出现超时和调用堆栈溢出的问题,可以考虑使用尾递归进行优化,因此总结一波。
尾调用
尾调用(Tail Call)作为函数式编程的一个重要概念,其概念很简单,即某个函数的最后一步操作总是返回另一个函数的调用结果。
function inner () {}
function outer () {
return inner();
}
尾调用仅是指函数的最后一步操作,因此函数调用不一定出现在函数尾部。
function outer (x) {
if (x > 0)return inner1();
return inner2();
}
尾调用的核心是指函数执行的是最后一步操作,本质是函数调用另一个函数时其本身是否被释放掉。我们知道函数在调用时内存会形成一个调用记录,也就是函数调用帧,保存着函数的执行位置与变量信息等内容。函数被执行时调用帧被入栈,执行完成之后会出栈。而尾调用的特殊之处在于其最后一步是调用另一个函数,也就意味着其本身的调用记录信息已不再需要,可以被内部函数的调用记录取代。这就是"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。"尾调用优化"的意义也在于此。
function inner1 () {
console.log('tail call');
}
function inner2 () {}
function outer () {
return inner2();
}
outer();
/*
* 执行js,把main主程序推入调用栈中并执行,发现需要调用outer
* 将 outer 入栈,执行 outer 发现需要调用 inner2
* 将 inner2 入栈,执行 inner2 发现需要调用 inner1
* 将 inner1 入栈,执行 inner1 发现需要调用 console.log
* 将 console.log 入栈,执行 console.log 并打印结果
* 出栈 console.log
* 出栈 inner1
* 出栈 inner2
* 出栈 outer
* 出栈 main js程序执行完毕
*/
尾递归
递归是指函数执行时调用自身,而尾递归是指尾调用自身,即是函数最后一步执行的是自身。尾递归之所以作为概念且被讨论,是因为我们都知道递归是是十分占用内存的,因此在运行时需要同时保存成百上千个调用记录,且特别容易造成“栈溢出”错误(stack overflow)。而尾递归由于仅需保存最后一步的调用记录,无需存储之前的调用记录,从而不会造成栈溢出问题,避免内存大量占用。
function climbStairs(n: number): number {
if (n === 1) return 1;
if (n === 2) return 2;
return climbStairs(n - 1) + climbStairs(n - 2);
};
如上是使用递归求解爬楼梯问题,由于需要保留每一步的调用记录,当 n 较大时极其容易造成溢出,且执行时间很容易超限。
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function climbStairsByTailCall(n: number, ac1 = 1 , ac2 = 1): number {
if (n <= 1) return ac2;
return climbStairsByTailCall(n - 1, ac2, ac1 + ac2);
}
function climbStairs(n: number): number {
return climbStairsByTailCall(n);
};
由此可见,“尾调用优化”对递归操作具有重要意义,因此一些函数式编程语言也将其写入了语言规格。其中 ECMAScript(6及以上) 也明确规定“尾调用优化”的实现。这就是意味着,ECMAScript(6及以上) 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。但目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 暂都不支持。