怎樣寫出一個遞歸程序

    做爲小白,我看到遞歸程序只是能看懂,可是本身寫不出來,我知道要有一個臨界條件(這個並不難找),但我不知道怎麼演進,這讓我十分頭疼,所以找到了一篇我的認爲寫的不錯的文章以下,根據我對遞歸的理解和疑問對原文作了一些標註,歡迎各位大佬,寫下本身對遞歸的理解,本小白感激涕零。算法

如何寫一個遞歸程序編程

  老是聽到大大們說遞歸遞歸的,本身寫程序的時候卻用不到遞歸。其中的緣由,一個是懼怕寫遞歸,另外一個就是不知道何時用遞歸。這篇文章就淺析一下,但願看完以後再也不懼怕遞歸,這就是本文最大的目的。
  遞歸到底有什麼意義?
  在說怎麼寫遞歸以前必需要說一下它的意義,其實這就是爲何大多數人在看了許多遞歸的例子後仍是不明因此的緣由。能夠確定的是,遞歸是個十分強大的工具,有許多算法若是不用遞歸可能很是難寫。不少地方介紹遞歸會用階乘或者斐波那契數列做例子,這徹底是在誤導初學者。儘管用遞歸實現階乘或者斐波那契數列是能夠的,可是這是沒有意義的。
數組

  先掉一下書袋,遞歸的定義是這樣的:程序調用自身的編程技巧稱爲遞歸( recursion)。在函數調用的過程當中是有一個叫函數調用棧的東西存在的。調用一個函數,首先要把原函數的局部變量等壓入棧中,這是爲了保護現場,保證調用函數完成後可以順利返回繼續運行下去。當調用函數返回時,又要將這些局部變量等從棧中彈出。在普通的函數調用中,通常調用深度最多不過十幾層,可是來到了遞歸的世界狀況就不同了。先看一段隨便從網上就能找到的階乘程序:函數

double fab(int n) 
{ 
   if(n == 0 || n == 1){ 
     return 1; 
   }else{ 
     return n*fab(n-1); 
   } 
} 
工具

       若是n = 100,很顯然這段程序須要遞歸地調用自身100次。這樣調用深度至少就到了100。棧的大小是有限的,當n變的更大時,有朝一日總會使得棧溢出,從而程序崩潰。除此以外,每次函數調用的開銷會致使程序變慢。因此說這段程序十分很差。那什麼是好的遞歸,先給出一個結論,接着看下去天然會明白。結論是若是遞歸可以將問題的規模縮小,那就是好的遞歸
  怎樣纔算是規模縮小了呢。舉個例子,好比要在一個有序數組中查找一個數,最簡單直觀的算法就是從頭至尾遍歷一遍數組,這樣必定能夠找到那個數。若是數組的大小是N,那麼咱們最壞狀況下須要比較N次,因此這個算法的複雜度記爲O(N)。有一個大名鼎鼎的算法叫二分法,它的表達也很簡單,因爲數組是有序的,那麼找的時候就從數組的中間開始找,若是要找的數比中間的數大,那麼接着查找數組的後半部分(若是是升序的話),以此類推,知道最後找到咱們要找的數。稍微思考一下能夠發現,若是數組的大小是N,那麼最壞狀況下咱們須要比較logN次(計算機世界中log的底幾乎老是2),因此這個算法的複雜度爲O(logN)。當N變大後,logN $amp;
  簡單的分析一下二分法爲何會快。能夠發現二分法在每次比較以後都幫咱們排除了一半的錯誤答案,接下去的一次只須要搜索剩下的一半,這就是說問題的規模縮小了一半。而在直觀的算法中,每次比較後最多排除了一個錯誤的答案,問題的規模幾乎沒有縮小(僅僅減小了1)。這樣的遞歸就稍微像樣點了。
  從新看階乘的遞歸,每次遞歸後問題並無本質上的減少(僅僅減少1),這和簡單的循環沒有區別,但循環沒有函數調用的開銷,也不會致使棧溢出。因此結論是若是僅僅用遞歸來達到循環的效果,那仍是改用循環吧。
  總結一下,遞歸的意義就在於將問題的規模縮小,而且縮小後問題並無發生變化(二分法中,縮小後依然是從數組中尋找某一個數),這樣就能夠繼續調用自身來完成接下來的任務。咱們不用寫很長的程序,就能獲得一個十分優雅快速的實現。
google


  怎麼寫遞歸程序?
  終於進入正題了。不少初學者都對遞歸心存畏懼,其實遞歸是符合人思考方式的。寫遞歸程序是有套路的,總的來講遞歸程序有幾條法則的。
  用二分查找做爲例子,先給出函數原型:
編碼

                 int binary_search(int* array, int start, int end, int num_wanted)
spa

返回值是元素在數組中的位置,若是查找失敗返回-1。.net


  1. 基準狀況
  基準狀況其實就是遞歸的終止條件。其實在實際中,這是十分容易肯定的。例如在二分查找中,終止條件就是找到了咱們想要的數或者搜索完了整個數組(查找失敗)。
if(end < start){ 
   return -1; 
}else if(num_wanted == array[middle]){ 
   return middle; 
} 
設計

2. 不斷演進
   演進的過程就是咱們思考的過程,二分查找中,就是繼續查找剩下的一半數組。
if(num_wanted > array[middle]){ 
   index = binary_search(array, middle+1, end, num_wanted); 
}else{ 
   index = binary_search(array, start, middle-1, num_wanted); 
} 

固然這是比較簡單的演進方式,其餘的好比快速排序、樹、堆的相關算法中會出現更復雜一點的演進過程(其實也複雜不到哪裏去)。


  3. 用人的思考方式設計
  這條法則我認爲是很是重要的,它不會出如今編碼中,但倒是理解遞歸的一條捷徑。它的意思是說,在通常的編程實踐中,咱們一般須要用大腦模擬電腦執行每一條語句,從而肯定編碼的正確性,然而在遞歸編碼中這是不須要的。遞歸編碼的過程當中,只須要知道前兩條法則就夠了。以後咱們就會看到這條法則的如何工做的了。


  4. 不要作重複的事情
  在任何編碼中,這都是不該該出現的事情,可是在遞歸中這更加可怕,可能因爲一次多餘的遞歸使得算法增長數級複雜度。以後也會看到相關的例子。
  如今咱們能夠寫出咱們完整的二分法的程序了

int binary_search(int* array, int start, int end, int num_wanted) 
{ 
  int middle = (end - start)/2 + start; // 1
  if(end < start){ 
    return -1; 
  }else if(num_wanted == array[middle]){ 
    return middle; 
  } 
  int index; 
  if(num_wanted > array[middle]){ 
    index = binary_search(array, middle+1, end, num_wanted); // 2
  }else{ 
    index = binary_search(array, start, middle-1, num_wanted); // 3
  } 
  return index; // 4
} 

程序中除了1和4都已經在前兩條法則的實現中了。1沒必要多說,4是一個比較關鍵的步驟,常常容易被忘記。這裏就用到第3條法則,

編寫的時候只要認爲2或者3必定會正確運行,而且馬上返回,不要考慮2和3內部是如何運行的,由於這就是你如今在編寫的。

這樣4該如何處理就是顯而易見的了,在這裏只須要將找到的index返回就能夠了。
  第4條法則在這個例子裏並無出現,咱們能夠看一下斐波那契數列的遞歸實現

long int fib(int n) 
{ 
   if(n <= 1){ 
     return 1; 
   }else{ 
     return fib(n-1) + fib(n-2); // 1
   } 
} 

乍看之下,這段程序很精練,它也是一段正確的遞歸程序,有基準條件、不斷推動。可是若是仔細分析一下它的複雜度能夠發現,

若是咱們取n=N,那麼每次fib調用會增長額外的2次fib調用(在1處),即fib的運行時間T(N) = T(N-1) + T(N-2),能夠獲得其複雜度是

O(2^N),幾乎是可見的複雜度最大的程序了(其中詳細的計算各位有興趣能夠google一下,這裏就不展開了^_^)。因此若是在一個

遞歸程序中重複屢次地調用自身,又不縮小問題的規模,一般不是個好主意。
  PS. 你們能夠比較一下二分法與斐波那契數列的遞歸實現的區別,儘管二分法也出現了2次調用自身,可是每次運行只有其中一個會被真正執行。


  到此其實你已經能夠寫出任何一個完整的遞歸程序了,雖然上面的例子比較簡單,可是方法老是這樣的。不過咱們能夠對遞歸程序再進一步分析。二分查找的遞歸算法中咱們注意到在遞歸調用以後僅僅是返回了其返回值,這樣的遞歸稱做尾遞歸。儘管在編寫的時候沒必要考慮遞歸的調用順序,但真正運行的時候,遞歸的函數調用過程能夠分爲遞和歸兩部分。在遞歸調用以前的部分稱做遞,調用以後的部分稱做歸。而尾遞歸在歸的過程當中實際上不作任何事情,對於這種狀況能夠很方便的將這個遞歸程序轉化爲非遞歸程序(好處就是不會致使棧的溢出)。

轉自:http://blog.csdn.net/u010697982/article/details/45875913

相關文章
相關標籤/搜索