遞迴演算法小結

2021-07-07 03:06:38 字數 4534 閱讀 4264

寫下這個題目,心裡還是有點兒發虛的,自己作為乙個演算法新手,在這個地方大談遞迴演算法實在是有點兒不知道天高地厚的感覺。

先說這篇文章的性質以及適合人群,這篇文章是個人學習演算法過程中的乙個總結,沒有太多高深的東西,但也盡量能夠做到生動具體並對遞迴的精髓有所觸及。如果你是演算法大神,那恐怕會讓你失望,如果你也是剛剛接觸遞迴演算法,倒是可以讀一讀,我保證是不會有壞處的。

背景介紹:遞迴是一種程式設計方法,更恰當的說是一技巧。既然是技巧就會有一些問題,那就是當用得合適時,會有事半功倍的效果,但大多數情況下這種技巧是不適合的。遞迴程式相比於一般的迭代程式會有更多的程式開銷,所以在使用遞迴前要衡量好簡化**和增加開銷之間的關係。

從定義來看,只要是函式自己呼叫了自己,那就是應用了遞迴。而從我這些天的學習來看,這種遞迴呼叫大致分為三類:第一類是簡單遞迴,用於處理一些數學公式中的遞迴定義。常見的例子有求n的階乘、斐波那契數列等;第二類是代迴圈遞迴,也就是遞迴的使用是來代替迴圈的。我們要知道,所有的迴圈設計都是可以用遞迴的方法來實現的;第三種是「假如」遞迴(為什麼起這個名字,看完就明白了),這種遞迴是最難的遞迴使用,也是遞迴演算法的精髓所在。一會兒我們分別舉例說明這三類遞迴程式,在此之前先來看一下遞迴的設計思路和設計方法:

設計思路:

遞迴演算法的設計思路就是要解決乙個規模為n的問題,先看規模為n-1(或者n-k或者n/2,總之是比原問題規模小)的問題是否和原問題有同樣的性質,如果性質相同,那這個問題應該可以用遞迴演算法解決(規模為1的問題幾乎總是可以解決的)。這麼說可能還是有點兒抽象,不要著急,知識都是這樣,直接告訴你結論會讓你感覺這都是廢話。

設計方法:

從設計思路來看,我們在設計程式時要找出兩件東西:

(1)基礎步:也就是問題的出口,這裡假設a

1是問題p(1)的解,我們前面提到了,無論多複雜的問題,當其規模為1時也幾乎總是可以輕鬆解決的。

(2)歸納步:也就是如何實現遞迴過程,是設計的關鍵。我們假設已經知道了問題p(k)的解為a

k,那就要找到p(k+1)的解a

k+1=p(a

k)。這裡注意大p代表問題,而小p代表對問題解的一種處理過程,也就是說,知道了問題p(k)的解a

k,那就通過p來處理一下a

k,就可以得到問題p(k+1)的解a

k+1了。當然這個p就是我們要找的對應關係。

好了,下面我們用例子的形式來分別對三種遞迴方法做一些介紹。先說明一下,這裡的例子都是從鄭宗漢老師編寫的《演算法設計與分析(第二版)》中找到的,在這裡感謝鄭老師和他的書,個人認為寫的很不錯,大家有機會一定要讀一讀。

(1)簡單遞迴

這種遞迴是屬於那種「一眼就可以看出來的遞迴」,因為問題的數學描述公式就是遞迴定義的,這裡我們還是用那個最簡單的例子來說明。

例1:計算階乘函式n!

什麼是階乘就不囉嗦了,這個例子應該是遞迴演算法的老**了,其實其迴圈演算法很容易得到,並不需要動用遞迴這種核**。當然,剛開始拿乙個簡單的開刀也是完全可以理解的。

怎麼入手呢?先不要急著敲**,不是讓你來做題的,要養成演算法分析的習慣。

基礎步:當n=0,那n!=1,即f(0)=1

歸納步:當n!=0,那由階乘的定義知n!=n*(n-1)!,即f(n)=n*f(n-1)

(注意上面這兩行字是要寫在紙上的,不要太相信你的大腦了!這是演算法設計成敗的關鍵!它們是如此重要,以至於我都不得不為它們起個名字了,就叫「兩行字」吧)

有了兩行字,那就可以著手寫**了:

int factorial(int n)

}

注意:寫遞迴程式,一般先寫基礎步,再寫歸納步。

時間複雜度:取乘法作為演算法的基本步驟,則f(0)=0,f(n)=f(n-1)+1。則時間複雜度為o(n),怎麼來的就不多說了,可以檢視別的資料。

這裡的編碼問題沒有什麼好說的,**幾乎完全是兩行字的變形,甚至連變形都算不上。

(2)代迴圈遞迴

這種遞迴方法是最受人詬病的,因為如果乙個問題可以用迭代的方法來解決。那原則上是不允許使用遞迴的。當然事無絕對,一些問題雖然可以用迭代來解決,但解決起來是比較費力的,比如生成類演算法(常見的例子有生成n個數的全排列),這種演算法如果不用遞迴棧進行變數暫存那就要自己設計容器來儲存生成的中間結果,實在讓人頭疼。還有一種問題是問題本身邏輯比較複雜,需要多種遞迴策略的結合使用(常見的例子有漢諾塔問題),這種時候代迴圈遞迴就派上用場了。

說了那麼多,代迴圈遞迴有什麼特點呢?其實很簡單,一句話就可以概況,那就是遞迴語句在歸納步的最後。從演算法上來看,遞迴語句在歸納步的最後,說明我們對當前步的處理放在遞迴前面,也就是說我們解決問題的思路是這樣的:先處理眼前這一部分,然後呼叫遞迴處理剩下的部分,這顯然是迭代可以完成的功能。下面還是舉個例子吧。

例子2:基於遞迴的插入排序

什麼是插入排序也不多說了,其迭代版本相信大家都見到過或者自己完成過,現在來看一下其遞迴實現。

這個例子和上面的求階乘不同,求階乘有明確的數學算式,程式**是把數學公式挪乙個地方就完成了。而這一題裡沒有明確的數學算式,你也不可能自己寫出對應的數學算式。這時候要用自己的語言來寫兩行字。

基礎步:n=1時,就乙個元素,不用排序。

歸納步:n!=1時,將當前元素與前面元素一一比較,將其插入合適位置(插入排序的基本思想).然後n加1遞迴呼叫自身。

下面我們可以寫出其遞迴**了。

void insert_sort_rec(type a,int m,int n)

else

a[index+1] = temp;

insert_sort_rec(a,m+1,n);

}}

(這裡說明一下,**實現和兩行字直接有乙個小小的轉化,即**中的m是歸納步中的n,而程式中的n用來表示元素總個數(下標)了。實際上二者是完全對應的。)

時間複雜度:取移動元素為基本操作,可得f(0)=0,f(n)=f(n-1)+(n-1),則時間複雜度為o(n^2),這裡不再多說。

再用這個例子來重申一下這種代迴圈遞迴的策略:我們的解決方案是,從陣列頭開始(即m=0,這一點就跟迭代方法吻合了),先呼叫遞迴排第二個,再呼叫遞迴排第三個……直到m=n,看出來了吧,其演算法實質就是迭代,就是迴圈。

(3)「假如」遞迴

前面已經說了,這種遞迴是比較難掌握的,它之所以難學難用,和人的思維過程有直接關係。人類的正常思維都是遞推式的,即由條件a得到結果b,再從結果b推導出結果c……也就是順著事情或邏輯的發展進行的。而這種遞迴演算法正好與此相反,是「反著來的」。要想知道a,假如已經得到b該怎麼做呢?要想明白b,假如已經搞到c該怎麼做呢?……無根性和脫節性(自己創的詞,能理解就好)是很多人學習這種遞迴演算法時的感受:總感覺沒把握,這樣就可以了嗎?你說假如就能假如嗎?,,,這些感受都是正常的,因為你的大腦就不是為處理這種問題設計的,而計算機的大腦是可以的,計算機通過棧可以很容易的實現這種過程。(棧這種資料結構和遞迴演算法有著天然的聯絡,學習棧的工作過程對理解遞迴演算法有至關重要的幫助)

為了對比,我們這裡還用上面的例子

例子3:基於遞迴的插入排序

我們現在用「假如」的思想來重寫兩行字。

基礎步:n=0時,沒有元素不用排序,直接返回

歸納步:n!=0時。假如(假如二字值千金,所以精髓都在這上面)前n-1個元素已經排序,那應該將第n個元素與前面元素一一對比,將其插入合適位置。

下面我們來寫程式

void insert_sort_rec(int a,int n)

a[k+1] = a;

}}

取移動元素為基本操作,可得f(0)=0,f(n)=f(n-1)+(n-1),則時間複雜度為o(n^2),這裡不再多說。

這裡要著重比較一下前後兩種演算法,後面這種演算法的「假如」是怎麼實現的呢?就是通過先呼叫小引數的自己來實現的,然後在實現的基礎上來做下一步操作,體現在**上就是遞迴呼叫在歸納步的前面。(這裡很多人會感覺彆扭,寫了就實現了嗎?我做後面的操作時前面已經排好序了嗎?沒錯,寫了就實現了,你寫後面的操作時前面已經排好序了)

好了,三種策略都介紹完了,都是用了很簡單的例子,在實際應用中,問題會比這些複雜很多,有些是這三種的結合,即先遞迴呼叫自己幹一些事,然後處理當前步,再呼叫自己幹一些事,再處理當前步(如漢諾塔問題)……這時候你也要學會遞迴的理解問題,哪些東西是代迴圈的,哪些東西是假如的,他們之間有時候還是相對的……

你可能感覺更迷糊了,前面稍微提到了一點,在沒有足夠感性經驗作為鋪墊之前,理解理性的知識是很難的,甚至可以說是不可能的,你的感覺就都是廢話。要改善這種現狀,那方法只有乙個,就是大量積累感性經驗,說白了就是多做題,多分析各種各樣的遞迴演算法。另外本人的能力有限,所以只能寫成這樣了。要想真正了解遞迴演算法,一定是要找一本演算法書,熟悉理論,然後大量分析例子的,這個過程誰也替不了你。

最後,還得以一段理性結論作為結語。這段話原文出自嚴蔚敏老師的《資料結構(c語言版)》對遞迴算總結得很到位,與君共勉吧:遞迴設計的實質是,當乙個複雜的問題可以分解成若干子問題來處理時,其中某些子問題與原問題有相同的特徵屬性,則可以利用和原問題相同的分析處理方法;反之,這些子問題解決了,原問題也就迎刃而解了。由於遞迴函式的設計用的是歸納思維的方法,則在設計遞迴函式時,應注意:(1)首先應書寫函式的首部和規格說明,嚴格定義函式的功能和介面(遞迴呼叫的介面),對求精函式中所得的和原問題性質相同的子問題,只要介面一致,便可進行遞迴呼叫;(2)對函式中的每乙個遞迴呼叫都看成只是乙個簡單的操作,只要介面一致,必能實現規格說明中定義的功能,切忌想得太深太遠。

遞迴學習小結

眾所周知,遞迴思想在許多演算法裡尤為重要,尤其是遍歷和搜尋裡,所以總結下 遞迴演算法的實質是 把求解問題轉化為規模縮小了的同類問題的子問題,然後遞迴呼叫函式 或子過程 來表示問題的解,通過多次遞迴呼叫,最終可求出最小問題的解,然後通過這個最小問題的解返回上層呼叫,再求出次小問題的解,再返回上層呼叫,...

遞迴與遞推小結

時間限制 1 sec 記憶體限制 64 mb 題目描述 九連環是由九個彼此套接的圓環和一根橫桿組成,九個環從左到右依次為l 9,每個環有兩種狀 態 1和0,1表示環在杆上,0表示環不在杆上。初始狀態是九個環都在杆上,即 111111111,目標狀態是九個環都不在杆上,即 000000000,由初始狀...

演算法 遞迴演算法

遞迴演算法的概念,就是通過不斷地呼叫自身,最終達到解決問題的目的。遞迴有兩個點需要注意 1.要不斷的呼叫自身 2.這個遞迴要有出口,不能成為死迴圈 看下面的例子。很多介紹遞迴演算法的,都會用遞迴來做乙個題目 計算乙個數的階層。例如 計算5的階層,5 5 x 4 x 3 x 2 x 1 用遞迴來實現 ...