java走楼梯递归,从爬楼梯到递归和堆栈溢出

有n个楼梯,每次可以走一阶或两阶,请问有多少种走法?

面试官说出这个的时候,我笑了笑,斐波那契?

反手就是一个

复制代码

递归求解

function MapSort(num){

if(num==1||num==2){

return 1

}

return MapSort(num-1) + MapSort(num-2)

}

复制代码

这种性能不够好,可以优化吗,面试官继续问道。我:不能。。

如果再回到那一天,我想我会好好跟他解释解释什么叫做递归的优化

复制代码

什么是递归

百科上的解释是:程序调用自身的编程技巧称为递归

复制代码

嗯,看了等于没看

一般来说递归至少有2个要点

* 不能无限制地调用本身,须有个出口,化简为非递归状况处理(递归结束条件)

* 子问题须与原始问题为同样的事,且更为简单(找到函数的规律)

我们拿一个阶乘来分析

6! 计算6的阶乘

function fact(n) {

if (n <= 0) { //递归结束条件

return 1;

}

return n * fact(n - 1); //找到函数的规律

}

fact(6) //得出6的阶乘

复制代码

当我们计算 fact(6) 的时候,会产生如下展开

6 * fact(5)

6 * (5 * fact(4))

6 * (5 * (4 * fact(3))))

//疯狂调用自身

6 * (5 * (4 * (3 * (2 * (1 * 1)))))) // <= 一步步的递进,完全展开

复制代码

展开不是为了好看,是为了计算

6 * (5 * (4 * (3 * (2 * 1)))))

6 * (5 * (4 * (3 * 2))))

6 * (5 * (4 * 6)))

....

720 // <= 一步步的归并,得出结果

复制代码

通过递进和最后的归并,我们完成了我们的递归。但是结果并不完美。

单纯的阶乘,当数量到达5w级别的时候 直接报错,没错,是堆栈溢出

46b3a6ecaa8140092328da08f5d0b042.png

复杂度更高的算法,比如上例的斐波那契使用递归,数字50的计算时间都需要100s+

堆栈溢出

在解决问题之前,我们应该先找到问题,为什么会堆栈溢出,我们不是有垃圾回收机制吗?

这个在这里简单解释一下,详细的连接放在下面有兴趣的可以自己去查看

首先垃圾回收机制有两种机制

* 引用计数(如果一个对象指向它的引用数为0,那么他就会被回收)

这种情况机制下,如果两个对象相互引用,即使已经不在使用,但还是无法被垃圾回收机制清除

* 标记清除(从根部root去能到达的一切都不会被清除)

总的来的说,做好以下几点就可以垃圾回收机制所清除

* 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;

* 注意程序逻辑,避免“死循环”之类的 ;

* 避免创建过多的对象 原则:不用了的东西要及时归还

复制代码

而在我们上述的递归方法中,我们由于每一次递归运算都会新建并保存之前的堆栈,会导致我们的垃圾回收机制无法正常运行

所以我们首先考虑,有很多值已经运算过了,那可不可以利用起来呢

说干就干!

利用私有变量存储运算结果

让我们在看一眼之前的写法

function MapSort(num){

if(num==1||num==2){

return 1

}

return MapSort(num-1) + MapSort(num-2)//会产生大量重复的运算

}

fib(45) //看一下运算时间

复制代码

f67ab0d8220f8e7cb1502a567a5dbf68.png

比如 MapSort(3) 会计算 MapSort(2) + MapSort(1),

而 MapSort(2) 又会计算 MapSort(1) + MapSort(0)。

这个 MapSort(1) 就是完全重复的计算,不应该为它再递归调用一次,而是应该在第一次求解除它了以后,就把他“记忆”下来。

把已经求得的解放在 Map 里,下次直接取,而不去重复结算。

这里用立即执行函数函数形成一个闭包,保留了 memo 这个私有变量,这是一个小技巧

let fib = (function(){

let memo = new Map();

return function(n){

//如果memo有值就使用,不重复计算

let memorize = memo.get(n)

if(memorize){

return memorize

}

if(n==1||n==2){

return 1

}

memo.set(n-1,fib(n-1))

memo.set(n-2,fib(n-2))

return fib(n-1) + fib(n-2)

}

})()

//fib(45) //可以看到,使用变量缓存甚至能够提高上万倍的效率

复制代码

60a21248d4ea810c180566c4a05b0225.png

问题解决了吗?并没有。当我们传入万级的数据时,依然会栈溢出,

就像刚刚提到的,这种方法借用闭包保存变量的特性,虽然减少了重复运算,但是依然无法被垃圾回收机制清理

复制代码

尾调用优化

我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame)

保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,

还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失

如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,

就形成一个"调用栈"(call stack)

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

复制代码

递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误

上面计算6的阶乘是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

复制代码

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function fact(n, total) {

if (n === 1) return total;

return fact(n - 1, n * total);

}

fact(5, 1) // 120

复制代码

严格模式

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

arguments:返回调用时函数的参数。

func.caller:返回调用当前函数的那个函数。

复制代码

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效

本文只是提供一个思想,有的地方没有特别详细,有错误的地方希望能被指正

复制代码

参考文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值