遞歸算法就是通過解決同一問題的一個或多個更小的實(shí)例來最終解決一個大問題的算法。為了在C語言中實(shí)現(xiàn)遞歸算法,常常使用遞歸函數(shù),也就是說能調(diào)用自身的函數(shù)。
遞歸程序的基本特征:
它調(diào)用自身(參數(shù)的值更?。哂薪K止條件,可以直接計算其結(jié)果。
一個遞歸模型為分治法,最本質(zhì)的特征就是:把一個問題分解成獨(dú)立的子問題。如果子問題并不獨(dú)立,問題就會復(fù)雜的多,主要原因是即使是這種最簡單算法的直接遞歸實(shí)現(xiàn),也可能需要難以想象的時間,使用動態(tài)規(guī)劃技術(shù)就可以避免這個缺陷。
例如,斐波那契數(shù)列的遞歸實(shí)現(xiàn)如下:
int F(int i)
{
if(i < 1) return 0;
if(i == 1) return 1;
return F(i-1) + F(i - 2);
}
千萬不要使用這樣的程序,因為它的效率極低,需要指數(shù)級時間。相比之下,如果首先計算前N個斐波那契數(shù),并把它們存儲在一個數(shù)組中,就可以使用線性時間(與N成正比)計算F。
F[0] = 0;F[1] = 1;
for(i = 2; i <= N; i++)
F[i] = F[i-1] + F[i-2];
這個技術(shù)給了我們一個獲取任何遞歸關(guān)系數(shù)值解的快速方法,在斐波那契數(shù)的例子中,我們甚至可以舍棄數(shù)組,只需要保存前兩個值。
由上面的討論我們可以得出這樣的結(jié)論:
我們可以按照從最小開始的順序計算所有函數(shù)值來求任何類似函數(shù)的值,在每一步使用先前已經(jīng)計算出的值來計算當(dāng)前值,我們稱這項技術(shù)為自底向上的動態(tài)規(guī)劃。只要有存儲已經(jīng)計算出的值的空間,就能把這項技術(shù)應(yīng)用到任何遞歸計算中,就能把算法從指數(shù)級運(yùn)行時間向線性運(yùn)行時間改進(jìn)。
自頂向下的動態(tài)規(guī)劃甚至是一個更簡單的技術(shù),這項技術(shù)允許我們執(zhí)行函數(shù)的代價與自底向上的動態(tài)規(guī)劃一樣(或更小),但是它的計算是自動的。我們實(shí)現(xiàn)遞歸程序來存儲它所計算的每一個值(正如它最末的步驟),并通過檢查所存儲的值,來避免重新計算它們的任何項(正如它最初的步驟)。這種方法有時也稱作為備忘錄法。
斐波那契數(shù)(動態(tài)規(guī)劃)
通過把所計算的值存儲在遞歸過程的外部數(shù)組中,明確地避免重復(fù)計算。這一程序計算的時間與N成正比。
int F(int i)
{
if(knownF[i] != unknown)
return knownF[i];
if(i == 0) t = 0;
if(i == 1) t = 1;
if(i > 1) t = F(i - 1) + F(i - 2);
return knownF[i] = t;
}
在自頂向下的動態(tài)規(guī)劃中,我們存儲已知的值;在自底向上的動態(tài)規(guī)劃中,我們預(yù)先計算這些值。我們常常選擇自頂向下的動態(tài)規(guī)劃而不選自底向上動態(tài)規(guī)劃,其原因如下:
1 自頂向下的動態(tài)規(guī)劃是一個自然的求解問題的機(jī)械轉(zhuǎn)化。
2 計算子問題的順序能自己處理。
3 我們可能不需要計算所有子問題的解。
我們不能忽視至關(guān)重要的一點(diǎn)是,當(dāng)我們需要的可能的函數(shù)值的數(shù)目太大以至于不能存儲(自頂向下)或預(yù)先計算(自底向上)所有值時,動態(tài)規(guī)劃就會變得低效。自頂向下動態(tài)規(guī)劃確實(shí)是開發(fā)高效的遞歸算法實(shí)現(xiàn)的基本技術(shù),這類算法應(yīng)納入任何從事算法設(shè)計與實(shí)現(xiàn)所需的工具箱。