5.1.4 使用一個棧來實現遞迴
5.1.4 使用一個棧來實現遞迴
正如這個思想所展示的那樣,我們能實現任何的迭代的過程,通過指定一個暫存器機器,
讓它有一個暫存器來對應於過程的每個狀態變數。機器則重複地執行一個控制器的迴圈,
改變暫存器的內容,直到一些中止的條件被滿足。在控制器序列的任何一個點上,機器的
狀態(表示迭代過程的狀態)是完全取決於暫存器的內容(狀態變數的值)。
實現遞迴過程,然而,需要一種額外的機制。考慮如下的計算斐波那些數的遞迴方法,
它是我們首先在1.2.1部分中舉的例子:
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1) ) )
)
)
正如從這個程式中看到的,計算N!需要計算(N-1)!。我們GCD機器 以如下的程式建模。
(define (gcd a b)
(if (= b 0)
a
(gcd b (remainder a b))))
它是相似的,它不得不計算另一個GCD。但是在gcd程式和 factorial程式之間,
這有一個重要的區別。gcd程式把原來的計算歸結為新的計算,factorial程式
要求另一個factorial作為自己的子問題。在gcd計算中,新的gcd計算的答案就是原來的
gcd計算的答案。為了計算下一個gcd,我們簡化了在GCD機器中的輸入的暫存器處放置
新的實際引數的位置,並且通過執行相同的控制器序列而重用了機器的資料路徑。
當機器解決了最後的GCD問題,它就完成了整個計算。
在斐波那些數的例子中,(或者任何遞迴的過程)新的斐波那些數的子問題的答案
並不是原來的問題的答案。(n-1)!的值必須乘以n才能得到最終的答案。如果我們
試圖使用GCD的設計,解決斐波那些數的子問題通過把n的暫存器的值減一,重執行
斐波那些數的機器時,我們不再有可用的n的原來的值了,這個值需要用來計算最終的
結果。我們所以需要第二個斐波那些數的機器來計運算元問題。第二個計算本身有一個子問題,
它需要第三個斐波那些數的機器,等等。因為任何一個機器包含著另一個機器,總機器包含
著相似的機器的無限巢狀,並且因此不能被組裝成一個固定的,有限的部分。
然而,如果我們安排使用相同的元件,為了機器的每一個巢狀的例項,我們能以一個暫存器機器
實現斐波那些數的計算過程。特別是,機器計算n的階乘,應該使用相同的元件來計算
n-1的階乘 ,n-2的階乘等等。這是可行的,因為儘管斐波那些數的執行過程顯示出為了執行
這個計算需要這個機器上的無數個程式副本,但是在給定的某個時候,這些副本中僅有一個是需要處於活躍的狀態中的。當機器遇到了一個遞迴的子問題時,它能在主問題上暫停工作,重用相同的物理部分來處理子問題,然後再繼續做剛才暫停的計算。
在子問題上,暫存器的內容不同於它們在主問題上的內容。(在這個例子中,暫存器n的值是減1的)。為了能夠繼續被暫停的計算,機器必須儲存任何暫存器的值,在子問題被解決之後,它們是被需要的,為了讓它們被用來恢復被暫停的計算。在斐波那些數的例子中,我們將儲存n的原來
的值,為了當我們完成了子問題後用來恢復被暫停的主問題。
因為在巢狀的遞迴呼叫的深度上沒有一個先知的限制,我們可能需要儲存很多的暫存器的值。
這些值必須被恢復以它們的被儲存時的相反的順序,因為在遞迴的巢狀中,最後的子問題被
最行解決。這顯示了一個棧的使用,或者是“後進,先出”的資料結構,來儲存暫存器的值。
通過新增兩個型別的指令,我們能擴充套件暫存器的機器語言來包括棧:值被放在棧上,使用一個
儲存指令,從棧上取出用來恢復使用一個恢復的指令。一個值的序列被儲存在棧上之後,一個恢復的序列將以相反的順序檢索這些值。
在棧的輔助下,為了任何一個斐波那些數的子問題,我們能重用斐波那些數的
機器的資料路徑的一個副本。在重用控制器的序列來操作資料路徑方面,有一個
相似的設計問題。為了重執行斐波那些數的計算,控制器不能簡單地回溯到開頭,
正如 一個迭代的過程,因為解決了(n-1)的階乘這個子問題後,機器必須還把結果乘以n.
控制器必須暫停它的n的階乘的計算,解決 (n-1)的階乘這個子問題,然後再繼續 n的階乘
的計算。斐波那些數的計算的視角建議使用子程式的機制,這在5.1.3部分中有描述,在那裡,
有控制器使用一個繼續的暫存器來轉換序列的部分,解決子問題,然後再繼續主問題中的剩下
的部分。我們能因此把一個子程式的返回的入口點儲存在一個繼續的暫存器中。圍繞著任何一個子程式的呼叫,我們儲存和恢復暫存器,正如我們對暫存器n的做法。也就是說,斐波那些數的子程式必須在它呼叫了子程式後把一個新的值放入繼續暫存器,但是將需要舊值為了返回用。
圖5.11 顯示了實現了遞迴的斐波那些數的程式的機器的資料路徑和控制器。
機器有一個棧和三個暫存器,叫做n,val,continue.為了化簡資料路徑的圖,我們
沒有為暫存器賦值的按鈕命名,僅為棧操作的按鈕命名了(sc 和sn 儲存暫存器,rc 和rn恢復暫存器)。為了操作機器,我們把我們期望計算的斐波那些數的結果放在暫存器n中,啟動了機器。當
機器到了fact-done,計算完成了,答案在val暫存器中。在控制器序列中,n和continue被儲存
在任何的遞迴呼叫之前,因為在子程式返回之後,val的舊值沒有用了。僅有新值,被子計算生成的是被需要的。儘管在原則上斐波那些數的計算需要一個無限的機器,在圖5.11中的機器是實際上有限的,除了棧的部分,它是潛在的無上限的。一個棧的任何特定的物理的實現,然而將是有限的大小,這將限制能被機器處理的遞迴呼叫的深度。斐波那些數的實現顯示了為了實現遞迴演算法的通用的策略正如普通的暫存器機器用棧來作實際的引數。當遇到了一個遞迴的子問題時,我們在棧上儲存暫存器的值,它的當前的值在子問題被解決後將被使用,解決子問題,然後恢復儲存的暫存器,並且繼續在主問題上執行。繼續的暫存器必須總是被儲存。其它的暫存器是否需要被儲存依賴於特定的機器,因為並不是所有的遞迴計算都需要暫存器的原始的值,在子問題的解決過程中暫存器的值被修改了。(見練習5.4)
*一個雙遞迴
讓我們檢查一個更復雜的遞迴的過程,斐波那些數的樹形遞迴的計算,它是我們在
1.2.2部分中介紹過的:
(define (fib n)
(cond ((= n 0) 0)
((= n 1 ) 1)
(else (+ (fib (- n 1))
(fib (- n 2))
)
)
)
)
正如斐波那些數,我們能實現遞迴的斐波那些數計算作為一個暫存器機器
帶有暫存器n,val,continue.這個機器是更復雜的,因為在控制器的序列中有兩
個地方我們需要執行遞迴呼叫,一個是計算fib(n-1),另一個是計算fib(n-2)。
為了安裝這些呼叫中的每一個,我們儲存暫存器,它的值在稍後需要用到,
設定n暫存器為我們需要遞迴計算的(n-1或者n-2),並且給continue賦值為
在主序列中的入口點,這是用來在(afterfib-n-1 或者 afterfib-n-2)的返回時用的。
我們然後到了fib-loop.當我們從遞迴的呼叫中返回時,答案在val。圖5.12顯示了這個機器的
控制器序列。
(controller
(assign continue (label fact-done))
fact-loop
(test (op =) (reg n) (const 1))
(branch (label base-case))
(save continue)
(save n)
(assign n (op -) (reg n) (const 1))
(assign continue (label after-fact))
(goto (label fact-loop))
after-fact
(restore n)
(restore continue)
(assign val (op *) (reg n) (reg val))
(goto (reg continue))
base-case
(assign val (const 1))
(goto (reg continue))
fact-done
)
圖5.11 一個遞迴的斐波那些數的機器
(controller
(assign continue (label fib-done))
fib-loop
(test (op <) (reg n) (const 2))
(branch (label immediate-answer))
(save continue)
(assign continue (labelfib-n-1))
(save n)
(assign n (op -) (reg n) (const 1))
(goto (label fib-loop))
afterfib-n-1
(restore n)
(restore continue)
(assign n (op -) (reg n) (const 2))
(save continue)
(assign continue (label afterfib-n-2))
(save val)
(goto (label fib-loop))
afterfib-n-2
(assign n (reg val))
(restore val)
(restore continue)
(assign val (op +) (reg val) (reg n))
(goto (reg continue))
immediate-answer
(assign val (reg n))
(goto (reg continue))
fib-done
)
圖5.12 計算斐波那些數的機器的控制器
練習5.4
指定一個暫存器機器實現如下的程式。對於任何一個機器,
寫出它的控制器指令序列和畫一個圖顯示它的資料路徑。
a.遞迴過程的程式如下:
(define (expt b n)
(if (= n 0)
1
(* b (expt b (- n 1)))
)
)
b.迭代過程的程式如下:
(define (expt b n)
(define (expt-iter counter product)
(if (= counter 0)
product
(expt-iter (- counter 1))
))
(expt-iter n 1)
)
練習5.5
手動模擬斐波那些數和斐波那些數的機器,使用一個非正常的輸入
(需要至少執行一次遞迴呼叫)。在執行的任何一個關鍵的點上,
顯示棧的內容。
練習5.6
苯注意到斐波那些數的機器的控制器序列有一個特殊的儲存和恢復指令,
為了讓機器執行的更快,它們可以被刪除,這些指令在哪?