遞歸和數學歸納法


計算機就是數學的一個分支,不管你認不認同,你都會發現在編程的過程中,你能夠發現很多的數學思維的閃現,就比如遞歸,遞歸可以讓程序簡化,與非遞歸比較,簡單的遞歸函數省去了大段大段的代碼,讓人嘆服不已,遞歸往往能體現設計者頭腦的聰慧,但是遞歸的思想與數學又有什么相關呢?

本文將介紹遞歸與數學歸納法之間的聯系,希望給讀者一些啟迪。

要說遞歸得先說,數學歸納法,想必每一個程序員在高中的時候就應該學習了數學歸納法,當我們需要去證明一個證明題時,很可能就要用到數學歸納法,數學歸納法的思想如下:

一般地,證明一個與自然數n有關的命題P(n),有如下步驟:
(1)證明當n取第一個值n0時命題成立。n0對於一般數列取值為0或1,但也有特殊情況;
(2)假設當n=k( k≥n0,k為自然數)時命題成立,證明當n=k+1時命題也成立。
綜合(1)(2),對一切自然數n(≥n0),命題P(n)都成立。
 
其實,數學歸納法利用的是遞推的原理,形象地可以叫做多米諾原理。因為N+1的成立就可以向前向后遞推所有數都成立。

    而遞歸利用的也是遞推的原理,在整個程序中,反復實現的將是同一個原理。在這一點上,遞歸與數學歸納法本質是相同的。所以往往可以利用數學歸納法來設計遞歸的實現。計算機是數學應用的一個分支在這里體現的淋漓盡致。
 
 這里我們先來看一個例子,非常簡單,設計一程序,求自然數N的階乘N!:
 
現在已知N!=N*(N-1)*(N-2)*(N-3)*…*2*1

    首先可知當N=1時 N!=1

    第二步可設當R(N)=N!,R(N+1)=(N+1)!

    第三步,求R(N+1)與R(N)之間的關系。R(N)=N!,
    而R(N+1)=(N+1)!=(N+1)*(N)*(N-1)*…*2*1=(N+1)*N!=(N+1)*R(N)
    即:R(N+1)=(N+1)*R(N) =〉 R(N)=N*R(N-1) 

    現在根據這個公式草略地構造一個函數:
    factorial (int N)
    {
       return N * 
factorial (N - 1)   /*    遞歸部分    */
    }

    接下來補充截止部分,這一部分在整個過程中只使用一次,沒有它,程序就將無限遞歸下去。可以說它是程序運行棧的棧頂,到了它,就開始一步步退棧了。
    函數改為:

    factorial (int N)
    {
       if (N == 1)   return 1;
       return N * 
factorial (N - 1)   /*    遞歸部分    */
    }
 
  上面的步驟是可以顛倒的,而且首先設計截至部分還要好一些。

    現在來總結設計遞歸程序的步驟:
    一、用數學歸納法分析問題,根據數學歸納法的第一步得出截至部分。

    
二、根據數學歸納法的第三步來構造函數的遞歸部分。其實這個求解過程就是找出R(N)與R(N-1)的關系式。

    現在利用總結出的方法做一個練習,比較經典的漢諾塔。

    漢諾塔想必大家都知道:三個立柱(命名為from、temp、to,from為圓盤初始所在立柱、to是目標立柱),N個直徑不相等的
圓盤 ,將 圓盤 從from上一個一個移動在to上,要求,每次只能移動一個 圓盤 ,而且只能在三個立柱之間移動。不能出現大盤壓小盤的情況。

    首先用數學歸納法分析:
    當只有一個
圓盤的時候,我們可以確定唯一動作:直接將圓盤從from移動到to上。

    現在假設有N個圓盤在from上,而我們可以將這些圓盤最終按要求移動到to上(當然也可以移動到temp上)。

    那么我們可以證明如果有N+1個時候,我們也可以將圓盤全部按要求移動到to上
因為我們可以先將上面的N個移動到temp上(第二步已假設),再把剩下的最后一個移動到to上,再把temp上的移動到to上。

    
按照我們總結過的遞歸函數設計步驟來設計程序:
    首先,確定截至部分:當只有一個圓盤移動的時候,直接將它移動到to上。即:if (n == 1) move (n,  from, to);
    (這里的move函數意義是將n號圓盤,或者說初始狀態下從上面數第n個圓盤,從from移動到to)

     第二步確定遞歸部分,其實就是N+1與N的關系部分,就是紅色字體部分。現在開始把文字轉化為程序:

    設Hanoi (int n, int from, int temp, int to)函數就是我們要求的漢諾塔實現函數,意義是將按直徑遞增摞在一起的n個圓盤從from按要求移動到to上,temp為輔助圓盤。

    
可寫出代碼:
    Hanoi (n-1, from, temp);   /*
 先將上面的N個移動到temp上  */
    move (n, from, to);   /* 
剩下的最后一個移動到to上  */
    Hanoi (n-1, temp, to);   /* 
再把temp上的移動到to上  */

    第二步完成,最后合成函數:
    void
    Hanoi (int n, int from, int temp, int to)
    {
       if (n == 1)
          move (n, from, to);
       else
       {
          
Hanoi (n-1, from, temp);
          
move (n, from, to);
          
Hanoi (n-1, temp, to); 
       }
    }
 
下面我們來看下一次例子,鏈表逆置。
假如鏈表只有一個元素,必然可以逆置
1->null ---------null<-1
假如鏈表有k個元素,也可以逆置,逆置后返回逆置后的頭結點
那么只要證明鏈表有k+1個元素也可以逆置,新添加的元素在鏈表頭。
 
其實代碼很簡單:
Node* reverse(Node* head)
{
  Node* p;
//當鏈表只有一個元素的時候直接,直接返回,即逆置成功。
  if( head->next ==null)   
  {
    return head;
  }
  else
  {  
//假設k個節點的鏈表可以逆置成功,並返回逆置后的鏈表頭結點
    p = reverse(head->next);     
//判斷第k+1個節點(第k+1個節點位於鏈表頭)是否可以逆置?如何做呢,前提:我們知道k個結點逆置后的尾節點,同時還知道第k+1個結點,所以下面就是逆置第k+1個結點
    head->next->next = head;   
    head->next = null;
//因為要返回逆置后的頭結點,所以將尾節點向上傳遞
    return p;
  }
}   
    使用數學歸納法設計遞歸程序最大的好處就是可以使設計者擺脫對 遞推的顧慮 。因為你設計的代碼必定隱含着遞推的步驟。直接根據你的分析文字轉化為代碼即可。
    
    本文還有很多不足之處,歡迎讀者指教。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM