遞歸是算法設計中的一種基本而重要的算法。遞歸方法通過函數調用自身將問題轉化為本質相同但規模較小的子問題,是分治策略的具體體現。
遞歸算法的定義:如果一個對象的描述中包含它本身,我們就稱這個對象是遞歸的,這種用遞歸來描述的算法稱為遞歸算法。
先來看看大家熟知的一個的故事: 從前有座山,山上有座廟,廟里有個老和尚在給小和尚講故事,老和尚講:從前有座山,山上有座廟,廟里有個老和尚在給小和尚講故事,老和尚講:…… 上面的故事本身是遞歸的,用遞歸算法描述:
void bonze-tell-story ()
{
if (講話被打斷)
{ 故事結束; return; }
從前有座山,山上有座廟,廟里有個老和尚在給小和尚講故事;
bonze-tell-story();
}
從上面的遞歸事例不難看出,遞歸算法存在的兩個必要條件:(1)必須有遞歸的終止條件,如老和尚的故事一定要在某個時候應該被打斷,可以是小和尚聽煩了叫老和尚停止,或老和尚本身就只想重復講10遍等;(2)過程的描述中包含它本身。
遞歸是一種非常有用的程序設計技術。當一個問題蘊含遞歸關系且結構比較復雜時,采用遞歸算法往往比較自然、簡潔、容易理解。
遞歸思想的基本思想是把一個大型復雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解。遞歸策略只需少量的程序就可描述出解題過程所需要的多次重復計算,大大地減少了程序的代碼量。用遞歸思想寫出的程序往往十分簡潔易懂。 一般來說,遞歸需要有邊界條件、遞歸前進段和遞歸返回段。當邊界條件不滿足時,遞歸前進;當邊界條件滿足時,遞歸返回。
使用遞歸要注意以下幾點:
(1)遞歸就是在過程或函數里調用自身;
(2)在使用遞增歸策略時,必須有一個明確的遞歸結束條件,稱為遞歸出口。
【例1】用遞歸法計算pn。
(1)編程思想。
1)描述遞歸關系。
遞歸關系是這樣的一種關系。設{U1,U2,U3,…,Un,…}是一個序列,如果從某一項k開始,Un和它之前的若干項之間存在一種只與n有關的關系,這便稱為遞歸關系。
當n≥1時,pn=p*p(n−1)(n=0時,p0=1),這就是一種遞歸關系。對於特定的pk,它只與k與p(k−1)有關。
2)確定遞歸邊界。
在步驟1的遞歸關系中,對大於k的Un的求解將最終歸結為對Uk的求解。這里的Uk稱為遞歸邊界(或遞歸出口)。在本例中,遞歸邊界為k=0,即p0=1。對於任意給定的pn,程序將最終求解到p0。
確定遞歸邊界十分重要,如果沒有確定遞歸邊界,將導致程序無限遞歸而引起死循環。
例如以下程序:
int f(int x)
{ return(f(x−1)); }
int main()
{ cout<<f(5)<<endl; }
它沒有規定遞歸邊界,運行時將無限循環,會導致錯誤。
3)寫出遞歸函數。
將步驟1)和步驟2)中的遞歸關系與邊界統一起來用數學語言來表示,即
pn= p*pn−1 當n>=1時
pn= 1 當n=0時
再將這種關系翻譯為代碼,即一個函數:
int f(int p , int n)
{
if (n==0) return 1;
return p*f(p,n-1);
}
(2)源程序。
#include <iostream>
using namespace std;
int f(int p , int n)
{
if (n==0) return 1;
return p*f(p,n-1);
}
int main()
{
int p, n,k;
while (cin >>p>>n && p!=0)
{
k=f(p,n);
cout<<k<<endl;
}
return 0;
}
【例2】分蘋果
把 M 個同樣的蘋果分放在N 個同樣的盤子里,允許有的盤子空着不放,問共有多少種不同的分法?例如,M=7,N=3,則有(7,0,0)、(6,1,0)、(5,2,0)、(5,1,1)、(4,3,0)(4,2,1)、(3,3,1)和(3,2,2)共8種分法。注意:(5,1,1)和(1,5,1)是同一種分法。
(1)編程思路。
所有不同的擺放方法可以分為兩類:至少有一個盤子空着和所有盤子都不空。我們可以分別計算這兩類擺放方法的數目,然后把它們加起來。
對於至少空着一個盤子的情況,則N 個盤子擺放M 個蘋果的擺放方法數目與N-1 個盤子擺放M 個蘋果的擺放方法數目相同。對於所有盤子都不空的情況,則N 個盤子擺放M 個蘋果的擺放方法數目等於N 個盤子擺放M-N 個蘋果的擺放方法數目。我們可以據此來用遞歸的方法求解這個問題。
設f(m, n) 為m 個蘋果,n 個盤子的放法數目,則先對n 作討論,如果n>m,必定有n-m 個盤子永遠空着,去掉它們對擺放蘋果方法數目不產生影響;即
if(n>m) f(m,n) = f(m,m)。
當n <= m 時,不同的放法可以分成兩類:即有至少一個盤子空着或者所有盤子都有蘋果,前一種情況相當於f(m , n) = f(m , n-1); 后一種情況可以從每個盤子中拿掉一個蘋果,不影響不同放法的數目,即f(m , n) = f(m-n , n)。總的放蘋果的放法數目等於兩者的和,即 f(m,n) =f(m,n-1)+f(m-n,n) 。
整個遞歸過程描述如下:
int f(int m , int n)
{
if (n ==1|| m == 0) return 1;
if (n > m) return f (m, m);
return f (m , n-1)+f (m-n , n);
}
遞歸終止條件說明:當n=1時,所有蘋果都必須放在一個盤子里,所以返回1;當沒有蘋果可放時,定義為1種放法;遞歸的兩條路,第一條n 會逐漸減少,總會到達終止條件 n==1;第二條m 會逐漸減少,因為n>m 時,我們會return f(m , m) ,所以也會到達終止條件 m==0。
(2)源程序
#include <iostream>
using namespace std;
int f(int m , int n)
{
if (n==1 || m==0) return 1;
if (n > m) return f (m, m);
return f(m,n-1)+f(m-n,n);
}
int main()
{
int n, m,k;
while (cin >>m>>n && n!=0)
{
k=f(m,n);
cout<<k<<endl;
}
return 0;
}