遞歸優化
遞歸在我們平時擼碼中會經常用到,不過可能很多人不知道遞歸的弊端,就是會導致調用棧越來越深。如果沒有節制的使用遞歸可能會導致調用棧溢出。
- 那什么是遞歸呢?
遞歸調用是一種特殊的嵌套調用,是某個函數調用自己或者是調用其他函數后再次調用自己的,只要函數之間互相調用能產生循環的則一定是遞歸調用,遞歸調用一種解決方案,一種是邏輯思想,將一個大工作分為逐漸減小的小工作,比如說一個和尚要搬50塊石頭,他想,只要先搬走49塊,那剩下的一塊就能搬完了,然后考慮那49塊,只要先搬走48塊,那剩下的一塊就能搬完了,遞歸是一種思想,只不過在程序中,就是依靠函數嵌套這個特性來實現了。 - 那什么又是調用棧呢?
下面的是我寫的一個簡單的遞歸調用,通過斷點我們可以看到每執行一個test函數,調用棧就會多一個test函數。
當我們執行到i=0的時候,這個時候調用棧是最深的有11個test函數,之后又會逐個移除test函數,可以看圖二的動圖,可以看出調用棧是先進后出的
function test (i) {
if (i < 0) return
test(--i)
}
// 這個會調用自身11次
test(10)
圖1:
圖2:
那怎么對遞歸進行優化呢,既能起到遞歸的作用又不會加深調用棧
這里會用到while循環的思想,調用棧之所以會加深主要是因為方法內調用方法,必須等待方法執行完成這個任務才算是真正的結束,就像A同學有個任務1,這個任務是讓B同學完成任務2,在B同學沒有完成之前,A同學一直處理工作狀態。
那while循環是什么原理呢,可以理解為將有調用關系的方法平鋪為同一級別。這需要引入額外的方法來做調度,本來test方法需要調用自己10次的,現在用方法b通過標記的方法來決定是否需要調用test方法
- 下面的的例子就是實現遞歸優化的實現方法(這里復制於阮一峰的es6教程)
- 這是一個很巧妙的方法,我說下它的實現步驟:
- 利用閉包將f方法保留(這里的f方法就是我們需要遞歸調用的方法)
- 創建value、active、accumulated三個變量,並利用了閉包原理避免被垃圾回收
- accumulated是保存每次f方法調用后需要傳入f的新的形參,active是標記f方法是否執行到了最后一次循環,value是記錄需要返回的值
- 下面的因為tco會return一個新的函數accumulator,所以sum=accumulator,然后再accumulator內只要accumulated長度不為0,while就會一直執行,每次執行sum方法就會accumulated.push(arguments)方法,這樣accumulated長度就不會為0。所以只要f.apply(this, accumulated.shift())執行的時候一旦不調用sum(x + 1, y - 1)方法,accumulated就不會有push操作,這時while就會停止。然后就是active,我們看到if (!active) {...}這個操作,這里保證了只有第一次調用accumulator方法時會進入while循環,剩下的只是起到accumulated.push(arguments)的作用。直到while循環停止,return出來的就是經過n次調用f方法后返回的值了。
- 這樣就可以把一個遞歸調用轉換為while循環實現了
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
// 這里accumulated將形參入棧
accumulated.push(arguments);
// 這里保證只有第一次調用才會進入
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)