作為小白,我看到遞迴程式只是能看懂,但是自己寫不出來,我知道要有乙個臨界條件(這個並不難找),但我不知道怎麼演進,這讓我十分頭疼,因此找到了一篇個人認為寫的不錯的文章如下,根據我對遞迴的理解和疑問對原文做了一些標註,歡迎各位大佬,寫下自己對遞迴的理解,本小白感激不盡。
如何寫乙個遞迴程式
總是聽到大大們說遞迴遞迴的,自己寫程式的時候卻用不到遞迴。其中的原因,乙個是害怕寫遞迴,另乙個就是不知道什麼時候用遞迴。這篇文章就**一下,希望看完之後不再害怕遞迴,這就是本文最大的目的。
遞迴到底有什麼意義?
在說怎麼寫遞迴之前必須要說一下它的意義,其實這就是為什麼大多數人在看了許多遞迴的例子後還是不明所以的原因。可以肯定的是,遞迴是個十分強大的工具,有許多演算法如果不用遞迴可能非常難寫。很多地方介紹遞迴會用階乘或者斐波那契數列作例子,這完全是在誤導初學者。儘管用遞迴實現階乘或者斐波那契數列是可以的,但是這是沒有意義的。
先掉一下書袋,遞迴的定義是這樣的:程式呼叫自身的程式設計技巧稱為遞迴(
recursion)。在函式呼叫的過程中是有乙個叫函式呼叫棧的東西存在的。呼叫乙個函式,首先要把原函式的區域性變數等壓入棧中,這是為了保護現場,保證呼叫函式完成後能夠順利返回繼續執行下去。當呼叫函式返回時,又要將這些區域性變數等從棧中彈出。
在普通的函式呼叫中,一般呼叫深度最多不過十幾層,但是來到了遞迴的世界情況就不一樣了。先看一段隨便從網上就能找到的階乘程式:
double fab(int n)
else
} 如果n =
100,很顯然這段程式需要遞迴地呼叫自身100次。這樣呼叫深度至少就到了100。棧的大小是有限的,當n變的更大時,有朝一日總會使得棧溢位,從而程式崩潰。除此之外,每次函式呼叫的開銷會導致程式變慢。所以說這段程式十分不好。那什麼是好的遞迴,先給出乙個結論,接著看下去自然會明白。結論是如果遞迴能夠將問題的規模縮小,那就是好的遞迴。
怎樣才算是規模縮小了呢。舉個例子,比如要在乙個有序陣列中查詢乙個數,最簡單直觀的演算法就是從頭到尾遍歷一遍陣列,這樣一定可以找到那個數。如果陣列的大小是n,那麼我們最壞情況下需要比較n次,所以這個演算法的複雜度記為o(n)。有乙個大名鼎鼎的演算法叫二分法,它的表達也很簡單,由於陣列是有序的,那麼找的時候就從陣列的中間開始找,如果要找的數比中間的數大,那麼接著查詢陣列的後半部分(如果是公升序的話),以此類推,知道最後找到我們要找的數。稍微思考一下可以發現,如果陣列的大小是n,那麼最壞情況下我們需要比較logn次(計算機世界中log的底幾乎總是2),所以這個演算法的複雜度為o(logn)。當n變大後,logn
$amp;
簡單的分析一下二分法為什麼會快。可以發現二分法在每次比較之後都幫我們排除了一半的錯誤答案,接下去的一次只需要搜尋剩下的一半,這就是說問題的規模縮小了一半。而在直觀的演算法中,每次比較後最多排除了乙個錯誤的答案,問題的規模幾乎沒有縮小(僅僅減少了1)。這樣的遞迴就稍微像樣點了。
重新看階乘的遞迴,每次遞迴後問題並沒有本質上的減小(僅僅減小1),這和簡單的迴圈沒有區別,但迴圈沒有函式呼叫的開銷,也不會導致棧溢位。所以結論是如果僅僅用遞迴來達到迴圈的效果,那還是改用迴圈吧。
總結一下,遞迴的意義就在於將問題的規模縮小,並且縮小後問題並沒有發生變化(二分法中,縮小後依然是從陣列中尋找某乙個數),這樣就可以繼續呼叫自身來完成接下來的任務。我們不用寫很長的程式,就能得到乙個十分優雅快速的實現。
怎麼寫遞迴程式?
終於進入正題了。很多初學者都對遞迴心存畏懼,其實遞迴是符合人思考方式的。寫遞迴程式是有套路的,總的來說遞迴程式有幾條法則的。
用二分查詢作為例子,先給出函式原型:
int binary_search(int* array, int start, int end, int num_wanted)
返回值是元素在陣列中的位置,如果查詢失敗返回-1。
1. 基準情況
基準情況其實就是遞迴的終止條件。其實在實際中,這是十分容易確定的。例如在二分查詢中,終止條件就是找到了我們想要的數或者搜尋完了整個陣列(查詢失敗)。
if(end < start)else if(num_wanted == array[middle])
2. 不斷演進
演進的過程就是我們思考的過程,二分查詢中,就是繼續查詢剩下的一半陣列。
if(num_wanted > array[middle])else
當然這是比較簡單的演進方式,其他的比如快速排序、樹、堆的相關演算法中會出現更複雜一點的演進過程(其實也複雜不到**去)。
3. 用人的思考方式設計
這條法則我認為是非常重要的,它不會出現在編碼中,但卻是理解遞迴的一條捷徑。它的意思是說,在一般的程式設計實踐中,我們通常需要用大腦模擬電腦執行每一條語句,從而確定編碼的正確性,然而在遞迴編碼中這是不需要的。遞迴編碼的過程中,只需要知道前兩條法則就夠了。之後我們就會看到這條法則的如何工作的了。
4. 不要做重複的事情
在任何編碼中,這都是不應該出現的事情,但是在遞迴中這更加可怕,可能由於一次多餘的遞迴使得演算法增加數級複雜度。之後也會看到相關的例子。
現在我們可以寫出我們完整的二分法的程式了
int binary_search(int* array, int start, int end, int num_wanted)
else if(num_wanted == array[middle])
int index;
if(num_wanted > array[middle])else
return index; // 4
} 程式中除了1和4都已經在前兩條法則的實現中了。1不必多說,4是乙個比較關鍵的步驟,經常容易被忘記。這裡就用到第3條法則,
編寫的時候只要認為2或者3一定會正確執行,並且立刻返回,不要考慮2和3內部是如何執行的,因為這就是你現在在編寫的。
這樣4該如何處理就是顯而易見的了,在這裡只需要將找到的index返回就可以了。
第4條法則在這個例子裡並沒有出現,我們可以看一下斐波那契數列的遞迴實現
long int fib(int n)
else
} 乍看之下,這段程式很精練,它也是一段正確的遞迴程式,有基準條件、不斷推進。但是如果仔細分析一下它的複雜度可以發現,
如果我們取n=n,那麼每次fib呼叫會增加額外的2次fib呼叫(在1處),即fib的執行時間t(n) = t(n-1) + t(n-2),可以得到其複雜度是
o(2^n),幾乎是可見的複雜度最大的程式了(其中詳細的計算各位有興趣可以google一下,這裡就不展開了^_^)。所以如果在乙個
遞迴程式中重複多次地呼叫自身,又不縮小問題的規模,通常不是個好主意。
ps. 大家可以比較一下二分法與斐波那契數列的遞迴實現的區別,儘管二分法也出現了2次呼叫自身,但是每次執行只有其中乙個會被真正執行。
到此其實你已經可以寫出任何乙個完整的遞迴程式了,雖然上面的例子比較簡單,但是方法總是這樣的。不過我們可以對遞迴程式再進一步分析。二分查詢的遞迴演算法中我們注意到在遞迴呼叫之後僅僅是返回了其返回值,這樣的遞迴稱作尾遞迴。儘管在編寫的時候不必考慮遞迴的呼叫順序,但真正執行的時候,遞迴的函式呼叫過程可以分為遞和歸兩部分。在遞迴呼叫之前的部分稱作遞,呼叫之後的部分稱作歸。而尾遞迴在歸的過程中實際上不做任何事情,對於這種情況可以很方便的將這個遞迴程式轉化為非遞迴程式(好處就是不會導致棧的溢位)。
**:
怎樣寫出更快的C程式
由於儲存器的設計和語言的儲存安排,乙個具有良好區域性性的程式往往可以更快的執行。總結自 深入理解計算機系統 按照上述思想結合 定時器,以及c語言關於陣列的儲存方式。我們實驗比較按照列訪問二維陣列和按照行訪問二維陣列的效果差別。sumarray.c include include timer.h in...
怎樣寫乙個lemon的spj
所有的都從argv裡面讀入 argv 1 輸入檔案 argv 2 選手輸出檔案 argv 3 標準輸出檔案 argv 4 單個測試點分值 argv 5 輸出最終得分的檔案 argv 6 輸出錯誤報告的檔案 include using namespace std ifstream fin,fout,f...
寫出乙個死鎖程式!!
寫出乙個死鎖的例子!產生死鎖的四個必要條件 1 互斥條件 乙個資源每次只能被乙個程序使用。2 請求與保持條件 乙個程序因請求資源而阻塞時,對已獲得的資源保持不放。3 不剝奪條件 程序已獲得的資源,在末使用完之前,不能強行剝奪。4 迴圈等待條件 若干程序之間形成一種頭尾相接的迴圈等待資源關係。auth...