Java递归算法经典实例
簡單遞歸定義
?
什么叫遞歸?(先定義一個比較簡單的說法,為了理解,不一定對)
遞歸:無限調(diào)用自身這個函數(shù),每次調(diào)用總會改動一個關(guān)鍵變量,直到這個關(guān)鍵變量達(dá)到邊界的時候,不再調(diào)用。
?
比如說我要你先求一個N!的結(jié)果
你說我會用循環(huán)啊(沒錯,但是現(xiàn)在是學(xué)遞歸)
[AppleScript]?純文本查看?復(fù)制代碼
?
| 1 2 3 4 5 6 | private int factorial(int x,int ans) { if(x==1) return ans; factorial(x-1,ans*x); } |
怎么樣,對于Java基礎(chǔ)如果掌握的還行的話,這段代碼應(yīng)該很好理解。遞歸,顧名思義就是“遞”和“歸”。也就是說,寫每一個遞歸函數(shù)的時候,都應(yīng)該在寫之前考慮清楚,哪里體現(xiàn)了“遞”,哪里體現(xiàn)了“歸”。
但是常常遞歸函數(shù)會比較復(fù)雜, “遞”和“歸”看起來并不是那么明顯,這就需要我們進(jìn)一步來理解遞歸算法的思想。
?
比如說我現(xiàn)在要你用輾轉(zhuǎn)相除法求出兩個數(shù)的最大公約數(shù),遞歸函數(shù)如下:
?
[AppleScript]?純文本查看?復(fù)制代碼
?
| 1 2 3 4 | private int gcd(int a,int b) { return a%b==0?b:gcd(b,a%b); } |
這是一段很常用的代碼,我們知道,在學(xué)習(xí)過程中不求甚解是最不應(yīng)該的。因此現(xiàn)在來仔細(xì)看一看。這里的“遞”和“歸”放在同一行。首先進(jìn)行判斷a==b?(我們可以想象成“歸”的內(nèi)容,如果這個條件符合的話)。當(dāng)然,如果不符合這個判斷,那就繼續(xù)“遞”,也就是繼續(xù)進(jìn)行g(shù)cd(b,a%b);
看到這里,你就會發(fā)現(xiàn),遞歸不就是循環(huán)的另一種方式么?
?
說對了一半,不過遞歸是一種思想,現(xiàn)在還暫時不能說透,需要大家先比較一下循環(huán)和遞歸的相同點和不同點(飯一口一口吃,別著急)
?
遞歸與循環(huán)的區(qū)別與聯(lián)系
?
相同點:
-
都是通過控制一個變量的邊界(或者多個),來改變多個變量為了得到所需要的值,而反復(fù)而執(zhí)行的;
-
都是按照預(yù)先設(shè)計好的推斷實現(xiàn)某一個值求取;(請注意,在這里循環(huán)要更注重過程,而遞歸偏結(jié)果一點)
?
不同點:
遞歸通常是逆向思維居多,“遞”和“歸”不一定容易發(fā)現(xiàn)(比較難以理解);而循環(huán)從開始條件到結(jié)束條件,包括中間循環(huán)變量,都需要表達(dá)出來(比較簡潔明了)。
?
簡單的來說就是:用循環(huán)能實現(xiàn)的,遞歸一般可以實現(xiàn),但是能用遞歸實現(xiàn)的,循環(huán)不一定能。因為有些題目①只注重循環(huán)的結(jié)束條件和循環(huán)過程,而往往這個結(jié)束條件不易表達(dá)(也就是說用循環(huán)并不好寫);②只注重循環(huán)的次數(shù)而不注重循環(huán)的開始條件和結(jié)束條件(這個循環(huán)更加無從下手了)。
?
遞歸的經(jīng)典應(yīng)用
?
來看看“漢諾塔問題”
如圖,漢諾塔問題是指有三根桿子A,B,C。C桿上有若干碟子,把所有碟子從A桿上移到C桿上,每次只能移動一個碟子,大的碟子不能疊在小的碟子上面。求最少要移動多少次?
這是一個循環(huán)只注重循環(huán)次數(shù)的常見例子,我們知道,用循環(huán)有點無從下手(就目前作者水平來看),但是遞歸就很好寫了。
漢諾塔,什么鬼,我不會啊?
別急,慢慢來。
我們首先需要一點思維:解決n塊盤子從A移動到C,那么我只需要先把n-1塊盤子從A移到B,然后把最下面的第n塊盤子從A移到C,最后把n-1塊盤子從B移到C(這就完成了)。
?
等等,那么如何把n-1塊盤子從A移到B?
?
很好,這說明你已經(jīng)開始遞歸入門了。
同樣這樣去想:解決n-1塊盤子從A移動到B,那么我只需要先把n-2塊盤子從A移動到C,然后把倒數(shù)第二塊盤子從A移到B,最后把n-2塊盤子從C移到B(這就完成了)。
?
這就是遞歸的“遞”!
那么“歸”呢?n==1的時候?
?
?
實際上這里面已經(jīng)使用到了一點點棧的思想(即最上面的最先考慮變化),但其實遞歸有的時候就是真的可以理解為棧!
?
到了這一步,相信大家應(yīng)該已經(jīng)有所明白。循環(huán)其實就是一個控制變量從開始條件走到結(jié)束條件的過程(在循環(huán)的過程順帶把其他變量也改變一下),因此需要控制變量,開始條件,結(jié)束條件(缺一不可)。但是遞歸只要告訴你“歸”是什么,如何去“遞”,不管過程如何,只要計算結(jié)果即可。
?
遞歸可以是多個“遞”,也可以是多個“歸”;而循環(huán)由始至終都只由一個變量控制(就算有幾個變量同時控制)
也只有一個出口,每次循環(huán)也只是一個“遞”。
?
再看一個例子
用二分思想建立二叉樹(通常的是遞歸實現(xiàn)),比如說線段樹
[AppleScript]?純文本查看?復(fù)制代碼
?
| 01 02 03 04 05 06 07 08 09 10 11 | //root 節(jié)點序號 //left 節(jié)點維護(hù)的左邊界 //right 節(jié)點維護(hù)的右邊界 private void build(int root,int left,int right) { if(left==right) return ; int mid=(left+right)/2; build(root*2,left,mid); build(root*2+1,mid+1,right); } |
?
如果你是新手看不太懂也沒關(guān)系,現(xiàn)在最主要的是明白:在這個程序里面只有一個“歸”,但是有兩個“遞”。那么如果學(xué)過一點但是對這一塊還不明白的怎么辦呢?別急,聽我來解釋:
?
實際上,這兩個 “遞”是按照先后分別進(jìn)行的,等到第一個“遞”執(zhí)行完(也就是到了“歸”的條件之后),才開始執(zhí)行第二個“遞”。也就是說,通常在建樹的時候,都不是一層一層同時建的,而是先建一棵子樹,等到這棵子樹全部建完之后,才開始建立另外一棵子樹。
?
那就會有人問了,一棵子樹建完了之后root值不會變么,root值變了之后還怎么建另外一棵子樹呢?
?
root值不會變!大家請注意,這里root*2是寫在遞歸函數(shù)里面的,實際上并沒有賦值?為什么要這樣寫?因為如果不這樣寫,你直接寫在外邊的話,一棵子節(jié)點到達(dá)葉子節(jié)點之后,需要一層一層往上回溯(在這里提到了回溯的思想),而回溯就會無故產(chǎn)生很多不必要的時間復(fù)雜度,降低了遞歸效率(實際上遞歸的時間效率本來就有一點偏低)。
?
所以到目前為止,我只是介紹一些很常見的簡單的遞歸,但是在接下來,我就需要說一些比較深層一點的知識了。
?
首先要理解一下什么是回溯
?
回溯:在遞歸的過程中由于改變的量需要倒退到某一個位置而執(zhí)行的步驟。
?
先來看一個簡單的素數(shù)環(huán)問題:
給出1到n的n個連續(xù)的正整數(shù)(這里n暫時等于6),并且把這n個數(shù)填寫在如下圖的n個圓圈里面(當(dāng)然是不重復(fù)不遺漏了)。要求是每一個位置上面的數(shù)跟他相鄰的數(shù)之和都為一個素數(shù),打印并輸出最后滿足條件的情況。
?
?
首先明白,開始條件是 1,把1填寫在第一個位置,然后在剩下的n-1個數(shù)字里找到一個滿足與1的和是一個素數(shù)的數(shù)(當(dāng)然如果有多個,先靠前的先考慮)。接下來再繼續(xù)從剩下n-2個數(shù)字里找到一個與這個數(shù)的和又是一個素數(shù)的數(shù)(當(dāng)然如果有多個,同上。)。。。最后的一個數(shù)只要滿足與最開始的數(shù)1之和是一個素數(shù)的話,這個情況就滿足了(就可以打印輸出這樣一個例子了)
?
但事情并沒有想象的那么簡單。。。(告訴我如果在中途尋找的過程中從剩下的數(shù)里找不到與當(dāng)前數(shù)的的和是一個素數(shù)的情況出現(xiàn)怎么辦?在線等)
?
這就表明這樣一條路終歸是一條思路,你要往回走了!這就很符合我們給回溯的定義了,此時這個改變的量需要倒退的前面一步從另外一個方向?qū)ふ伊恕?#xff08;還是舉栗子吧)
?
比如說:
?
1->2->3->4 突然發(fā)現(xiàn)5和6都不滿足要求了
那么就倒退,準(zhǔn)備找另外滿足要求的數(shù)
1->2->3 又發(fā)現(xiàn)除了4以外3跟5或者3跟6也不滿足要求
那就繼續(xù)倒退,繼續(xù)準(zhǔn)備找另外滿足要求的數(shù)
1->2->5->6 接下來發(fā)現(xiàn)6跟3或者6跟4不滿足要求
…(還想繼續(xù)下去?乖,別這樣,我也累啊,看一兩個就行了,啊!) 最后發(fā)現(xiàn)滿足條件的一個是1->4->3->2->5->6
?
大家應(yīng)該已經(jīng)懂了,上面的倒退,實際上就是回溯。(暫時這樣簡單的理解吧,錯了也不能怪你們)
實際上,遞歸+回溯就已經(jīng)是dfs(深度優(yōu)先搜索)的內(nèi)容范疇了。
[AppleScript]?純文本查看?復(fù)制代碼
?
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | private void dfs(int x) { if(x==n+1&&prime(a[x-1]+a[1])) //如果滿足條件就可以打印輸出數(shù)據(jù)了,這里就是“歸” { for(int i=1;i<n;i++) cout<<a[i]<<" "; cout<<a[n]<<endl; } else //否則就繼續(xù)“遞” { for(int i=2;i<=n;i++) { if(!vis[i]&&prime(i+a[x-1])) { vis[i]=1; //vis[]是一個標(biāo)記數(shù)組,表示當(dāng)前的數(shù)字已經(jīng)被使用過了 a[x]=i; dfs(x+1); //“遞”的入口 vis[i]=0; //請注意,回溯點在這里 } } } } |
?
大家可能前面都看懂了,比如說“遞”和“歸”,vis[]標(biāo)記數(shù)組什么的。但是最后一個vis=0是啥意思?難道不多余么?
?
不多余!前面我已經(jīng)拿建樹給大家講過遞歸的“工作原理”,它是先無限遞歸,然后到達(dá)某個條件之后,回溯到上面一個位置,繼續(xù)向其他方向遞歸。而這個vis=0就是清楚當(dāng)前數(shù)字的標(biāo)記,表示從當(dāng)前節(jié)點開始,之后遞歸過的內(nèi)容統(tǒng)統(tǒng)清空(也就是回溯)。然后根據(jù)循環(huán),進(jìn)行下面一個方向的繼續(xù)遞歸。
?
總結(jié)
(1)把遞歸當(dāng)成復(fù)雜的循環(huán)來寫,如果不明白過程,多模擬幾遍數(shù)據(jù);
(2)把遞歸逆向?qū)懙臅r候當(dāng)做一個棧來實現(xiàn)(即符合先進(jìn)先出的思想);
(3)當(dāng)遞歸和回溯結(jié)合在一起的時候需要明白遞歸次數(shù)和統(tǒng)計次數(shù)之間的練習(xí)和區(qū)別;
(4)但遞歸有多個“遞”和“歸”的時候,選擇一個重點的“遞”和“歸”作為匹配,即時題目即時分析,注意隨機(jī)應(yīng)變即可。
總結(jié)
以上是生活随笔為你收集整理的Java递归算法经典实例的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ZOJ 2562 More Diviso
- 下一篇: 3.Java垃圾回收机制面试题