標籤:
關於遞迴相信大家已經熟悉的不能再熟悉了,所以筆者在這裡不就不多費口舌,不懂的讀者們可以在部落格園中找到很多與之相關的部落格。下面我們直接切入正題,開始介紹尾遞迴。
尾遞迴
普通遞迴和尾遞迴如果僅僅只是從代碼的角度出發來看,我們可能發現不了他的特點,所以筆者利用兩張堆棧上的圖來展示具體的差距在哪,首先我們來看看普通的遞迴調用的情況,如1.1所示:
假設這裡執行的函數是Func1,並且Func1中通過遞迴調用了自己,那麼我們可以看到棧上在每次調用Func1的時候都會重新將函數返回地址等其他參數放入棧中,在遞迴次數較少的情況下,這樣是不會有問題的。但如果遞迴調用次數達到一定的數量級,則會將棧空間消耗光。因此,就提出了尾遞迴。而尾遞迴的棧圖如1.2所示:
一樣還是遞迴,但是每次執行自身的時候並不會在棧空間中申請新的空間,類似於for迴圈的效果,面對遞迴次數很多的情況下也不會出現什麼問題。但是新的問題就出來了,在C#中編譯器不會做到這一步最佳化,而是在jit編譯器執行時才會進行最佳化。並且只有64位才進行最佳化。在語言的層面上我們也要遵守一定的原則,才能讓編譯器知道去最佳化。當然有些喜歡看部落格的人可能早就知道尾遞迴就是在最後return的時候調用自身。我們可以通過一串示意代碼來看尾調用:
int Func1()
{
return Func1();
}
當然上面這串代碼會形成一個死迴圈,因為這裡我們沒有基準條件。下面我們舉一個例子:
這個函數想必應該會比較熟悉,就是計算階乘的。但是我們可以發現函數sunfc最後的返回語句並不是直接調用函數本身,而是x*sfunc(x -1),恰恰就是因為前面這個x*就會導致編譯器無法最佳化,從而只能採用普通的遞迴調用的方式去執行,那麼我們就需要利用一些模式去改變,首先我們先介紹的是“累加器傳遞模式”,可能名字比較懸乎,其實就是將當前的計算結果傳遞給下一次調用函數中,這樣當到達基準條件後直接根據上次計算的結果算出最終結果返回即可,如果將上面的代碼採用這個模式就是下面這個樣子:
採用這個模式之後我們就變回了尾遞迴了,當執行到基準條件時,直接返回y的值即可。根本不需要回溯到以前。除了利用這種模式,我們還可以利用一種“後繼傳遞模式”,跟累加器傳遞模式一樣也需要修改函數簽名,增加一個參數,我們繼續修改上面這串代碼:
相比累加器傳遞模式,這種方式比較難理解,其實sfunc在到達基準條件時y就等同於下面這個lambda運算式:a => a*4*3*2,然後就是調用y(1)就直接計算最終的結果了。在簡單點就是y這個函數被封裝了了好幾層,比如上面這段函數執行結束時y的調用順序:
a為1傳遞給y(2 * a),結果就是y(2)。
a為2傳遞給y(3 * a),結果就是y(6)。
a為6傳遞給y(4 * a),結果就是y(24)。
a為24傳遞給x => x,輸出24。
如果還是不理解只能下斷點,調試自己琢磨琢磨了,實在不懂的可以Q問。
C#函數式編程之遞迴調用