10 2 1 用尾遞迴避免棧溢位(續 )

2021-06-27 22:44:52 字數 2560 閱讀 6826

10.2.1 用尾遞迴避免棧溢位(續!)

naïve

不像是英語,不知道什麼意思。

第六章中的列表處理函式並不是尾遞迴。如果我們傳遞很大的列表,就會因棧溢位而失敗。我們將用尾遞迴重寫兩個函式(map 和 filter),將改正這個問題。為了對照,在清單 10.8 中包括了原來的實現。為了避免名字衝突,已經改名為 mapn 和 filtern。

清單10.8 naïve 列表處理函式 (f#)

// naïve 'map' implementation 

let rec mapn f list = 

match list with 

| x::xs -> let xs = (mapn fxs)     [1]

f(x) :: xs                   [2]

// naïve 'filter' implementation

let rec filtern f list =

match list with

| x::xs -> let xs = (filtern fxs)    [1]

if f(x) then x::xs else xs       [2]

兩個函式都包含乙個遞迴呼叫[2],但不是尾遞迴;遞迴呼叫每個分支都跟關乙個額外的操作[2]。一般的模式是,函式首先把列表分解成頭和尾;然後,遞迴處理尾,用頭執行一些動作。更確切地說,mapn 應用 f 函式到頭中的值,filtern 決定頭中的值是否應包括在結果列表中。最後的操作是把新的頭中的值(或者篩選分支中沒有值)追加到遞迴處理的尾中,且在遞迴呼叫後必須處理。

要把這些函式變成尾遞迴,使用前面曾看到的、同樣的累加器引數方法。在遍歷列表時,收集元素(篩選或對映),並把它們存放在累加器中;一旦到達終點,就可以返回已經收集到的元素。清單 10.9 顯示了對映和篩選的尾遞迴實現。

清單10.9 尾遞迴列表處理函式 (f#)

// tail-recursive 'map'implementation 

let map f list = 

let rec map' f list acc = 

match list with 

| ->list.rev(acc)          [1]

| x::xs -> let acc =f(x)::acc       [2]

map'f xs acc        [3]

map' f list

// tail-recursive 'filter'implementation 

let filter f list = 

let rec filter' f list acc= 

match list with 

| ->list.rev(acc)      [1]

| x::xs -> let acc =if f(x) then x::acc else acc    [2]

filter'f xs acc    [3]

filter' f list

像通常實現尾遞迴函式一樣,這兩個函式都包含本地的工具函式,有乙個額外的累加器引數。這一次,我們在函式名上增加了乙個單引號('),起初看起來有點怪。f# 把單引號當作標準的字元,可以在名稱中使用,因此,也沒有什麼神奇的事情。

我們首先看一下終止遞迴的分支[1],我們說是僅返**集到的元素,但實際上先是呼叫 list.rev,倒轉元素的順序。這是因為我們收集到的元素順序是「錯誤的」。新增到累加器列表中的元素總是在前面,成為新的頭,所以,最終我們處理的第乙個元素,是累加器中的最後乙個元素。呼叫 list.rev 函式倒轉列表,這樣,最終返回結果的順序就正確了。這種方法比我們將在 10.2.2 節中見到的,追加元素到尾部,更有效率。

現在,處理 cons cell 的分支是尾遞迴。第一步處理頭中的元素,並更新累加器[2],生成遞迴呼叫[3],立即返回結果。f# 編譯器可以知道遞迴呼叫是最後一步,可以採用尾遞迴優化。

如果我們將它們貼上到 f# interactive  中,嘗試處理大列表,很容易發現兩個版本之間的區別。對於這些函式,遞迴深度與列表的長度相同,所以,如果我們用naïve 版本,就會遇到問題:

> let large = [ 1 .. 100000 ] 

val large : int list = [ 1; 2; 3; 4; 5;...]

> large |> map (fun n ->n*n);; 

val it : int list = [1; 4; 9; 16; 25; ...]

> large |> mapn (fun n ->n*n);; 

process is terminated due tostackoverflowexception.

可以發現,對於遞迴處理函式來說,尾遞迴是一項重要技術。當然,f# 庫包含了處理列表的尾遞迴函式,所以,並不真的要自己寫像在這裡實現的對映和篩選。在第

六、七和八章,我們已經看到,設計自己的資料結構,寫函式來處理它們,是函式程式設計的關鍵。

將要建立的許多資料結構,都相當小,但是,而處理的資料量相當大,尾遞迴是一項重要技術。使用尾遞迴,可以寫出能夠正常處理大型資料集的**。當然,正因為函式不會棧溢位,不能保證函式在合理的時間內完成任務,這就是為什麼還需要考慮更有效地處理列表的原因。

10 1 1避免尾遞迴的堆疊溢位

10.1.1避免尾遞迴的堆疊溢位 對於每乙個函式呼叫,執行時分配乙個棧幀 stack frame 這些幀儲存在由系統維護的棧中 呼叫完成,棧幀被刪除 如果函式呼叫其他函式,那麼,乙個新的幀新增到這個棧的頂部。棧的大小是有限的,所以,太多的巢狀函式呼叫會耗光了給其他棧幀的空間,就不能再呼叫下乙個函式了...

遞迴和棧溢位。

遞迴確實是很多演算法的基礎思想。但外部因素導致遞迴會棧溢位。但卻是不甘心如此簡練的有效的演算法,放棄不用。所以一般有2中方式來使用大資料的遞迴思路 1 用棧型別放入引數,模擬遞迴呼叫。2 把大資料分割為一批適中的資料,就可以直接使用遞迴函式。用快速排序,測試並總結了下。1 本例大概 排序30000個...

遞迴呼叫的棧溢位

如下 include include int recurse int x int main int argc,char ar 22執行結果如下 hxl hxl virtual machine 桌面 task code r 100 x 100.a at 0x7ffcfce0afd0 x 99.a at...