用歸納法來理解遞迴
數學都不差的我們,第一反應就是遞迴在數學上的模型是什麼。畢竟我們對於問題進行數學建模比起**建模拿手多了。 自己觀察遞迴,我們會發現,遞迴的數學模型其實就是歸納法。即:歸納法適用於想解決乙個問題轉化為解決他的子問題,而他的子問題又變成子問題的子問題,而且我們發現這些問題其實都是乙個模型,也就是說存在相同的邏輯歸納處理項。當然有乙個是例外的,也就是遞迴結束的哪乙個處理方法不適用於我們的歸納處理項,當然也不能適用,否則我們就無窮遞迴了。這裡又引出了乙個歸納終結點以及直接求解的表示式。如果運用列表來形容歸納法就是:
步進表示式:問題蛻變成子問題的表示式
結束條件:什麼時候可以不再是用步進表示式
直接求解表示式:在結束條件下能夠直接計算返回值的表示式
邏輯歸納項:適用於一切非適用於結束條件的子問題的處理,當然上面的步進表示式其實就是包含在這裡面了。
分治策略一般性描述
把上面的設計思想加以歸納,可以得到分治演算法的一般描述.設p是待求解的問題,|p|代表問題的輸入規模,一般的分治演算法divide-and-conquer偽碼描述如下:
演算法 divide-and-conquer(p)
1. if |p| < c or |p| = c then s(p)
2. divide p into p1,p2,p3,...,pk
3. for i = 1 to k do
4. yi ← divide-and-conquer(pi)
5.return merge(y1,y2,y3,....,yk)
需要滿足的兩個條件
程式設計人員還是停留在「自己呼叫自己」的程度上。這其實這只是遞迴的表象(ps:嚴格來說連表象都概括得不全面,因為除了「自己呼叫自己」的遞迴外,還有互動呼叫的遞迴)。而遞迴的思想遠不止這麼簡單。遞迴,並不是簡單的「自己呼叫自己」,也不是簡單的「互動呼叫」。它是一種分析和解決問題的方法和思想。簡單來說,遞迴的思想就是:把問題分解成為規模更小的、具有與原問題有著相同解法的問題。比如二分查詢演算法,就是不斷地把問題的規模變小(變成原問題的一半),而新問題與原問題有著相同的解法。有些問題使用傳統的迭代演算法是很難求解甚至無解的,而使用遞迴卻可以很容易的解決。比如hanoi塔問題。但遞迴的使用也是有它的劣勢的,因為它要進行多層函式呼叫,所以會消耗很多堆疊空間和函式呼叫時間。
既然遞迴的思想是把問題分解成為規模更小且與原問題有著相同解法的問題,那麼是不是這樣的問題都能用遞迴來解決呢?答案是否定的。並不是所有問題都能用遞迴來解決。那麼什麼樣的問題可以用遞迴來解決呢?一般來講,能用遞迴來解決的問題必須滿足兩個條件:
可以通過遞迴呼叫來縮小問題規模,且新問題與原問題有著相同的形式。
存在一種簡單情境,可以使遞迴在簡單情境下退出。
如果乙個問題不滿足以上兩個條件,那麼它就不能用遞迴來解決。為了方便理解,如斐波那契數列來說下:求斐波那契數列的第n項的值。這是乙個經典的問題,說到遞迴一定要提到這個問題。斐波那契數列這樣定義:f(0) = 0, f(1) = 1, 對n > 1, f(n) = f(n-1) + f(n-2)
這是乙個明顯的可以用遞迴解決的問題。讓我們來看看它是如何滿足遞迴的兩個條件的:
對於乙個n>2, 求f(n)只需求出f(n-1)和f(n-2),也就是說規模為n的問題,轉化成了規模更小的問題;
對於n=0和n=1,存在著簡單情境:f(0) = 0, f(1) = 1。
因此,我們可以很容易的寫出計算費波納契數列的第n項的遞迴程式:
int fib(n)
在編寫遞迴呼叫的函式的時候,一定要把對簡單情境的判斷寫在最前面,以保證函式呼叫在檢查到簡單情境的時候能夠及時地中止遞迴,否則,你的函式可能會永不停息的在那裡遞迴呼叫了。
兩個熟悉的例子
先看兩個熟悉的例子:字串回文現象的遞迴判斷和二分查詢演算法
字串回文現象的遞迴判斷
回文是一種字串,它正著讀和反著讀都是一樣的。比如level,eye都是回文。用迭代的方法可以很快地判斷乙個字串是否為回文。用遞迴的方法如何來實現呢?首先我們要考慮使用遞迴的兩個條件:
這個問題是否可以分解為形式相同但規模更小的問題?
如果存在這樣一種分解,那麼這種分解是否存在一種簡單情境?
先來看第一點,是否存在一種符合條件的分解。容易發現,如果乙個字串是回文,那麼在它的內部一定存在著更小的回文。 比如level裡面的eve也是回文。 而且,我們注意到,乙個回文的第乙個字元和最後乙個字元一定是相同的。所以我們很自然的有這樣的方法:先判斷給定字串的首尾字元是否相等,若相等,則判斷去掉首尾字元後的字串是否為回文,若不相等,則該字串不是回文。注意,我們已經成功地把問題的規模縮小了,去掉首尾字元的字串當然比原字串小。
接著再來看第二點, 這種分解是否存在一種簡單情境呢?簡單情境在使用遞迴的時候是必須的,否則你的遞迴程式可能會進入無止境的呼叫。對於回文問題,我們容易發現,乙個只有乙個字元的字串一定是回文,所以,只有乙個字元是乙個簡單情境,但它不是唯一的簡單情境,因為空字串也是回文。這樣,我們就得到了回文問題的兩個簡單情境:字元數為1和字元數為0。
綜上兩點分析,滿足分治策略需要滿足的兩個條件了.即編寫出解決回文問題的遞迴實現如下**所示.:
int is_palindereme(char *str, int n)
}執行結果
二分查詢演算法的遞迴實現
典型的遞迴例子是對已排序陣列的二分查詢演算法。現在有乙個已經排序好的陣列,要在這個陣列中查詢乙個元素,以確定它是否在這個陣列中,很一般的想法是順序檢查每個元素,看它是否與待查詢元素相同。這個方法很容易想到,但它的效率不能讓人滿意,它的複雜度是o(n)的。現在我們來看看遞迴在這裡能不能更有效。
還是考慮上面的兩個條件:
第一:這個問題是否可以分解為形式相同但規模更小的問題?
第二:如果存在這樣一種分解,那麼這種分解是否存在一種簡單情境?
考慮條件一:我們可以這樣想,如果想把問題的規模縮小,我們應該做什麼?可以的做法是:我們先確定陣列中的某些元素與待查元素不同,然後再在剩下的元素中查詢,這樣就縮小了問題的規模。那麼如何確定陣列中的某些元素與待查元素不同呢? 考慮到我們的陣列是已經排序的,我們可以通過比較陣列的中值元素和待查元素來確定待查元素是在陣列的前半段還是後半段。這樣我們就得到了一種把問題規模縮小的方法。
接著考慮條件二:簡單情境是什麼呢?容易發現,如果中值元素和待查元素相等,就可以確定待查元素是否在陣列中了,這是一種簡單情境,那麼它是不是唯一的簡單情境呢? 考慮元素始終不與中值元素相等,那麼我們最終可能得到了乙個無法再分的小規模的陣列,它只有乙個元素,那麼我們就可以通過比較這個元素和待查元素來確定最後的結果。這也是一種簡單情境。
這個問題可以用遞迴來解決,二分法的**如下:
void selectionsort(int data, int count)
}int binary_search(int *data, int n, int key)
else
else}}
程式執行結果:
這個演算法的複雜度是o(logn)的,顯然要優於先前提到的樸素的順序查詢法。
小結
遞迴演算法與分治策略
關於遞迴的學習 1 遞迴演算法的基本思想是 把規模大的 較難解決的問題變成規模較小的的問題。規模較小的問題又變成規模更小的問題,並且小到一定程度可以直接得出它的解,從而得到原來問題的解。遞迴是一種直接或間接呼叫自身的函式的一種演算法,很常用,一般用於解決三類問題 資料的定義按遞迴定義的。fibona...
遞迴的基本運用與實踐
簡單的說 遞迴就是方法自己呼叫自己,每次呼叫時傳入不同的變數。遞迴有利於程式設計者解決複雜的問題,同時可以讓 變得簡潔。入門案例 累加 實現 public static intaccumulation int n 階乘 實現 public static intfactorial int n 遞迴用於...
演算法分析與設計 遞迴與分治策略
直接或間接地呼叫自身的演算法稱為遞迴演算法。用函式自身給出定義的函式稱為遞迴函式。在計算機演算法設計與分析中,使用遞迴技術往往使函式的定義和演算法的描述簡潔且易於理解。例1 階乘函式 可遞迴地定義為 其中 n 0 時,n 1為邊界條件 n 0 時,n n n 1 為遞迴方程 邊界條件與遞迴方程是遞迴...