[英]Tail Recursion optimization for JavaScript?
我向所有人道歉,因為以前版本的這個模糊不清。 有人決定對這個新女孩表示同情並幫我改寫這個問題 - 這是一個我希望能夠解決問題的更新(並且,感謝迄今為止所有那些慷慨解答的人):
問題
我是Uni的第一年,我是一名新的計算機科學專業的學生。 對於我的算法類的最終項目,我們可以選擇我們想要的任何語言,並實現一種“細化”/“效率”算法,該算法在本地(內部?)用另一種語言找到,但在我們選擇的語言中缺失。
我們剛剛在課堂上研究了遞歸,我的教授簡要地提到JavaScript沒有實現Tail Recursion 。 從我的在線研究中,新的ECMA腳本6規范包含此功能,但它目前不在任何(/大多數?)JavaScript版本/引擎中? (對不起,如果我不確定哪個是......我是新來的)。
我的任務是為缺少的功能提供2個(編碼) WORK AROUND的選項。
所以,我的問題是......是否有人,比我更聰明,更有經驗,有任何關於如何實現的想法或例子:
解決了缺乏尾遞歸優化?
一種可能的遞歸優化是懶惰地評估,即返回一個“計算”(=函數),它將返回一個值而不是計算並立即返回它。
考慮一個總結數字的函數(以一種相當愚蠢的方式):
function sum(n) {
return n == 0 ? 0 : n + sum(n - 1)
}
如果你用n = 100000
來調用它,它將超過堆棧(至少在我的Chrome中)。 要應用所述優化,首先將其轉換為true tail-recursive,以便該函數僅返回對自身的調用,僅此而已:
function sum(n, acc) {
return n == 0 ? acc : sum(n - 1, acc + n)
}
並使用“懶惰”函數包裝此直接自調用:
function sum(n, acc) {
return n == 0 ? acc : function() { return sum(n - 1, acc + n) }
}
現在,為了從中獲得結果,我們重復計算直到它返回一個非函數:
f = sum(100000, 0)
while(typeof f == "function")
f = f()
這個版本沒有問題,n = 100000,1000000等
正如我在評論中提到的,您總是可以將程序轉換為延續傳遞樣式,然后使用異步函數調用來實現真正的尾部調用優化。 為了推動這一點,請考慮以下示例:
function foldl(f, a, xs) {
if (xs.length === 0) return a;
else return foldl(f, f(a, xs[0]), xs.slice(1));
}
顯然這是一個尾遞歸函數。 所以我們需要做的第一件事就是將它轉換為延續傳遞樣式,這非常簡單:
function foldl(f, a, xs, k) {
if (xs.length === 0) k(a);
else foldl(f, f(a, xs[0]), xs.slice(1), k);
}
而已。 我們的功能現在是延續傳遞風格。 然而,仍然存在一個大問題 - 沒有尾部調用優化。 但是,使用異步函數可以輕松解決這個問題:
function async(f, args) {
setTimeout(function () {
f.apply(null, args);
}, 0);
}
我們的尾調用優化的foldl
函數現在可以寫成:
function foldl(f, a, xs, k) {
if (xs.length === 0) k(a);
else async(foldl, [f, f(a, xs[0]), xs.slice(1), k]);
}
現在你需要做的就是使用它。 例如,如果要查找數組的數字總和:
foldl(function (a, b) {
return a + b;
}, 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], function (sum) {
alert(sum); // 55
});
把它們放在一起:
function async(f, args) { setTimeout(function () { f.apply(null, args); }, 0); } function foldl(f, a, xs, k) { if (xs.length === 0) k(a); else async(foldl, [f, f(a, xs[0]), xs.slice(1), k]); } foldl(function (a, b) { return a + b; }, 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], function (sum) { alert(sum); // 55 });
當然,繼續傳遞風格是用JavaScript編寫的一種痛苦。 幸運的是,有一種叫做LiveScript的非常好的語言,可以讓回調更加有趣。 用LiveScript編寫的相同函數:
async = (f, args) ->
setTimeout ->
f.apply null, args
, 0
foldl = (f, a, xs, k) ->
if xs.length == 0 then k a
else async foldl, [f, (f a, xs.0), (xs.slice 1), k]
do
sum <- foldl (+), 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
alert sum
是的,這是一種編譯成JavaScript的新語言,但值得學習。 特別是因為回調(即<-
)允許您輕松編寫回調而無需嵌套函數。
許多最常見的語言缺乏尾遞歸優化,因為它們根本不希望您使用遞歸來解決線性問題。
尾遞歸優化僅適用於遞歸調用是函數執行的最后一項操作,這意味着無需查看當前堆棧內容,因此無需通過添加另一個堆棧幀來保留它。
任何這樣的算法都可以適應迭代形式。 例如(偽代碼):
int factorial(int x) {
return factTail(x,1);
}
int factTail(int x, int accum) {
if(x == 0) {
return accum;
} else {
return(factTail (x-1, x * accum);
}
}
...是factorial()
一個實現,它被定制以確保最后一個語句返回遞歸調用的結果。 知道TCO的引擎會對此進行優化。
以相同順序執行操作的迭代版本:
int factorial(int x) {
int accum = 1;
for(int i=x; i>0; i--) {
accum *= i;
}
return accum;
}
(我讓它向后計算以近似遞歸版本的執行順序 - 實際上你可能不會為factorial做這個)
如果你知道遞歸深度不會很大(在這個例子中, x
值很大),那么使用遞歸調用是很好的。
通常遞歸會導致非常優雅的解決方案規范。 擺弄算法以獲得尾調用會減損。 看看上面的factorial
比經典更難理解:
int factorial(int x) {
if(x == 1) {
return 1;
} else {
return factorial(x-1) * x;
}
}
...但這個經典形式是堆棧飢餓,對於一個不需要堆棧的任務。 因此,可以認為迭代形式是解決這一特定問題的最清晰方式。
由於編程的方式,現在大多數程序員對迭代形式比使用遞歸方法更舒服。 是否存在特定的遞歸算法?
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.