計算機就是數學的一個分支,不管你認不認同,你都會發現在編程的過程中,你能夠發現很多的數學思維的閃現,就比如遞歸,遞歸可以讓程序簡化,與非遞歸比較,簡單的遞歸函數省去了大段大段的代碼,讓人嘆服不已,遞歸往往能體現設計者頭腦的聰慧,但是遞歸的思想與數學又有什么相關呢?
本文將介紹遞歸與數學歸納法之間的聯系,希望給讀者一些啟迪。
要說遞歸得先說,數學歸納法,想必每一個程序員在高中的時候就應該學習了數學歸納法,當我們需要去證明一個證明題時,很可能就要用到數學歸納法,數學歸納法的思想如下:
一般地,證明一個與自然數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) /* 遞歸部分 */
}
首先可知當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);
}
}
現在來總結設計遞歸程序的步驟:
一、用數學歸納法分析問題,根據數學歸納法的第一步得出截至部分。
二、根據數學歸納法的第三步來構造函數的遞歸部分。其實這個求解過程就是找出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;
}
}
使用數學歸納法設計遞歸程序最大的好處就是可以使設計者擺脫對 遞推的顧慮 。因為你設計的代碼必定隱含着遞推的步驟。直接根據你的分析文字轉化為代碼即可。
本文還有很多不足之處,歡迎讀者指教。
使用數學歸納法設計遞歸程序最大的好處就是可以使設計者擺脫對 遞推的顧慮 。因為你設計的代碼必定隱含着遞推的步驟。直接根據你的分析文字轉化為代碼即可。
本文還有很多不足之處,歡迎讀者指教。