簡體   English   中英

JavaScript的尾遞歸優化?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM