編寫遞迴程式有幾個重要的原則可以遵循:
1. 要解決的問題可拆分為幾個與原問題
類似的子問題(子問題仍可拆分)。
2. 每個子問題必須比原來問題的規模
更小(即使小一號也行,當然如果能夠迅速減小規模更好)。
3. 遇到足夠小的子問題時就直接解決之,防止問題無限細分下去,也就是防止無限遞迴(遞迴終止條件很重要)。
先看一個最簡單的遞迴程式,下面程式求整數n的階乘:
int factorial(int n)
{
return n <= 1 ? 1 : n*factorial(n-1);
}
所謂遞迴程式,就是程式在執行中又調用其自身,如上面函數factorial在其函數體內調用了函數factorial。函數在每次被調用時都會產生一個包含自身局部變數的
副本,即算是函數調用自身時也是如此。 遞迴程式主體主要由兩部分構成:一是遞迴執行部分,包含遞迴執行所需的條件;一是遞迴終止部分,含遞迴終止條件。上面求階乘的程式中把這兩部分寫在一行代碼裡面,把其分別抽出來便是:
int factorial(int n)
{
if(n <= 1) return 1; // 遞迴終止體
return n*factorial(n-1); // 遞迴執行體
}
下面以幾個經典演算法問題的遞迴程式求解為例來分析編寫遞迴程式中可以遵循的簡單規則。
1. 求正整數n所有可能的和式的組合,如給定正整數3,所有和加起來等於3的和式如下:
3 = 3 + 0
3 = 2 + 1
3 = 1 + 1 + 1
其中一個和式中的因子只能為自然數,因子允許重複出現。
這個問題是一個組合問題的變形,用遞迴程式實現如下(分析見注釋):
#define MAXSIZE 50
static int buff[MAXSIZE]; // 儲存和式因子組合的緩衝
/// 求當前和式中因子之和。
int Sum(int buff[],int i)
{
int sum = 0;
for(int m=0; m<=i; m++)
sum += buff[m];
return sum;
}
/// 列印當前滿足條件的和式。
void printFactor(int buff[],int i,const int number)
{
int count = 0, m;
for(m=0; m<=i; m++)
printf("%4d",buff[m]);
if(number==buff[0])
printf("%4d",0);
printf("/n");
}
/// 遞迴求解所有和式組合。
/// i - 當前可選自然數的最大值。
/// top - 當前組合中因子個數。
/// number - 問題中要求解和式的正整數。
void divide(int i,int top,int number)
{
for(int j=i; j>0; j--) // 注意這裡的迴圈條件,可以有效防止出現重複的組合
{
buff[top ] = j;
if(number == Sum(buff,top)) // 首先要確定遞迴中止條件
printFactor(buff,top,number);
else if(number < Sum(buff,top)) // 繼續增加和式中的因子
continue;
if(top >= number) // 和式中的因子不應該超過和的大小
continue;
divide(j,top+1,number); // 減小問題規模,繼續遞迴
}
return;
}
int main()
{
int i,top = 0;
int number = 8;
printf("input a souce number:");
scanf("%d",&number);
i = number;
divide(i,top,number);
return 0;
}
2. 給定正整數k,以及1-k共 k個正整數的一個排列,假如是 1,2,3,...,k,求所有和此排列“不相交”的排列。
所謂不相交,是指新的排列和已有排列在同一位置上的數都不相同。如假設k=3,則其排列:
1 2 3 和 3 2 1是相交的,因為第二個位置上都是2;而排列1 2 3 和 3 1 2則不相交。
問題分析:此問題是普通排列問題的變形,只要在排列問題的遞迴程式中加上位置的限制即可,程式實現如下,具體分析見注釋。
const int N = 5;
int range[9] = {1,2,3,4,5,6,7,8,9}; // 預定義好位置的排列
/// 判斷當前選擇的數j是否已經被使用。
bool selected(int j, int* buffer)
{
for(int i=0; i<N; i++)
if(buffer[i]==j)
return true;
return false;
}
/// 求不相交排列。
/// i - 當前排列的位置或者說當前排列元素個數。
/// buffer - 儲存當前排列的數組。
/// count - 儲存不相交排列的數目。
void arrange(int i, int* buffer, int& count)
{
if(i == 0) // 遞迴終止條件
{
for(int k=0; k<N; k++)
cout << buffer[k] << " ";
cout << endl;
count++;
return;
}
for(int j=1; j<=N; j++) // 迴圈挑選當前位置可以放置的數
{
if(selected(j,buffer)) // 判斷當前選定的數是夠已經用過
continue;
buffer[i-1] = j;
if(buffer[i-1] == range[i-1]) // 回溯的限制條件,同一位置的數不能和預定義的排列中相應位置的數相同
continue;
arrange(i-1,buffer,count); // 繼續遞迴
}
buffer[i-1] = 0; // 回溯,重新初始化當前位置
}
int main()
{
int buffer[N] = {0};
int count = 0;
arrange(N,buffer,count);
cout << count << endl;
return 0;
}
遞迴程式求解問題雖然優美簡潔,但是和相應的非遞迴程式比較,隨著問題規模的增大,程式的效率會逐漸下降,這也是遞迴程式的一大缺陷。
Copyright@戴維 2006.11 於北京