10 | 递归:如何用三行代码找到“最终推荐人”?
什么是遞歸?
遞歸是一種應(yīng)用非常廣泛的算法(或者編程技巧)。很多數(shù)據(jù)結(jié)構(gòu)和算法的編碼實(shí)現(xiàn)都要用到遞歸,比如 DFS 深度優(yōu)先搜索、前中后序二叉樹遍歷等等。
去的過(guò)程叫“遞”,回來(lái)的過(guò)程叫“歸”
場(chǎng)景
周末你帶著女朋友去電影院看電影,女朋友問(wèn)你,咱們現(xiàn)在坐在第幾排啊?電影院里面太黑了,看不清,沒(méi)法數(shù),現(xiàn)在你怎么辦?別忘了你是程序員,這個(gè)可難不倒你,遞歸就開始排上用場(chǎng)了。于是你就問(wèn)前面一排的人他是第幾排,你想只要在他的數(shù)字上加一,就知道自己在哪一排了。但是,前面的人也看不清啊,所以他也問(wèn)他前面的人。就這樣一排一排往前問(wèn),直到問(wèn)到第一排的人,說(shuō)我在第一排,然后再這樣一排一排再把數(shù)字傳回來(lái)。直到你前面的人告訴你他在哪一排,于是你就知道答案了。這就是一個(gè)非常標(biāo)準(zhǔn)的遞歸求解問(wèn)題的分解過(guò)程,去的過(guò)程叫“遞”,回來(lái)的過(guò)程叫“歸”。基本上,所有的遞歸問(wèn)題都可以用遞推公式來(lái)表示。剛剛這個(gè)生活中的例子,我們用遞推公式將它表示出來(lái)就是這樣的:
f(n)=f(n-1)+1 其中,f(1)=1
遞歸需要滿足三個(gè)條件
1. 一個(gè)問(wèn)題的解可以分解為幾個(gè)子問(wèn)題的解
子問(wèn)題就是數(shù)據(jù)規(guī)模更小的問(wèn)題。比如,前面講的電影院的例子,你要知道,“自己在哪一排”的問(wèn)題,可以分解為“前一排的人在哪一排”這樣一個(gè)子問(wèn)題。
2. 這個(gè)問(wèn)題與分解之后的子問(wèn)題,除了數(shù)據(jù)規(guī)模不同,求解思路完全一樣
求解“自己在哪一排”的思路,和前面一排人求解“自己在哪一排”的思路,是一模一樣的。
3. 存在遞歸終止條件
把問(wèn)題分解為子問(wèn)題,把子問(wèn)題再分解為子子問(wèn)題,一層一層分解下去,不能存在無(wú)限循環(huán),這就需要有終止條件。第一排的人不需要再繼續(xù)詢問(wèn)任何人,就知道自己在哪一排,也就是 f(1)=1,這就是遞歸的終止條件
如何寫遞歸代碼:大問(wèn)題分解為小問(wèn)題、寫出遞推公式、找到終止條件,將遞推公式轉(zhuǎn)換為代碼
假如這里有 n 個(gè)臺(tái)階,每次你可以跨 1 個(gè)臺(tái)階或者 2 個(gè)臺(tái)階,請(qǐng)問(wèn)走這 n 個(gè)臺(tái)階有多少種走法?如果有 7 個(gè)臺(tái)階,你可以 2,2,2,1 這樣子上去,也可以 1,2,1,1,2 這樣子上去,總之走法有很多,那如何用編程求得總共有多少種走法呢?我們仔細(xì)想下,實(shí)際上,可以根據(jù)第一步的走法把所有走法分為兩類,第一類是第一步走了 1 個(gè)臺(tái)階,另一類是第一步走了 2 個(gè)臺(tái)階。所以 n 個(gè)臺(tái)階的走法就等于先走 1 階后,n-1 個(gè)臺(tái)階的走法 加上先走 2 階后,n-2 個(gè)臺(tái)階的走法。用公式表示就是:
f(n) = f(n-1)+f(n-2)
終止條件:f(1)=1,f(2)=2
將遞推公式和終止條件整合到一起:
/** f(1) = 1; f(2) = 2; f(n) = f(n-1)+f(n-2) **/int f(int n) {if (n == 1) return 1;if (n == 2) return 2;return f(n-1) + f(n-2); }總結(jié):寫遞歸代碼的關(guān)鍵就是找到如何將大問(wèn)題分解為小問(wèn)題的規(guī)律,并且基于此寫出遞推公式,然后再推敲終止條件,最后將遞推公式和終止條件翻譯成代碼。
遞歸理解和定勢(shì)思維誤區(qū)
- 計(jì)算機(jī)擅長(zhǎng)做重復(fù)的事情,所以遞歸正合它的胃口。而我們?nèi)四X更喜歡平鋪直敘的思維方式。當(dāng)我們看到遞歸時(shí),我們總想把遞歸平鋪展開,腦子里就會(huì)循環(huán),一層一層往下調(diào),然后再一層一層返回,試圖想搞清楚計(jì)算機(jī)每一步都是怎么執(zhí)行的,這樣就很容易被繞進(jìn)去。
- 對(duì)于遞歸代碼,這種試圖想清楚整個(gè)遞和歸過(guò)程的做法,實(shí)際上是進(jìn)入了一個(gè)思維誤區(qū)。很多時(shí)候,我們理解起來(lái)比較吃力,主要原因就是自己給自己制造了這種理解障礙
- 正確的理解方式:如果一個(gè)問(wèn)題 A 可以分解為若干子問(wèn)題 B、C、D,你可以假設(shè)子問(wèn)題 B、C、D 已經(jīng)解決,在此基礎(chǔ)上思考如何解決問(wèn)題 A。而且,你只需要思考問(wèn)題 A 與子問(wèn)題 B、C、D 兩層之間的關(guān)系即可,不需要一層一層往下思考子問(wèn)題與子子問(wèn)題,子子問(wèn)題與子子子問(wèn)題之間的關(guān)系。屏蔽掉遞歸細(xì)節(jié),這樣子理解起來(lái)就簡(jiǎn)單多了
遞歸的弊端
遞歸代碼雖然簡(jiǎn)潔高效,但是也有不少的弊端
1、遞歸代碼要警惕堆棧溢出
- 函數(shù)調(diào)用會(huì)使用棧來(lái)保存臨時(shí)變量。每調(diào)用一個(gè)函數(shù),都會(huì)將臨時(shí)變量封裝為棧幀壓入內(nèi)存棧,等函數(shù)執(zhí)行完成返回時(shí),才出棧。系統(tǒng)棧或者虛擬機(jī)棧空間一般都不大。如果遞歸求解的數(shù)據(jù)規(guī)模很大,調(diào)用層次很深,一直壓入棧,就會(huì)有堆棧溢出的風(fēng)險(xiǎn)。
如何避免堆棧溢出
- 限制調(diào)用深度:遞歸調(diào)用超過(guò)一定深度(比如 1000)之后,我們就不繼續(xù)往下再遞歸了,直接返回報(bào)錯(cuò)
2、遞歸代碼要警惕重復(fù)計(jì)算
觀察如下示意圖,其中遞歸涉及到多處重復(fù)計(jì)算
優(yōu)化方案
為了避免重復(fù)計(jì)算,我們可以通過(guò)一個(gè)數(shù)據(jù)結(jié)構(gòu)(比如散列表)來(lái)保存已經(jīng)求解過(guò)的 f(k)。當(dāng)遞歸調(diào)用到 f(k) 時(shí),先看下是否已經(jīng)求解過(guò)了。如果是,則直接從散列表中取值返回,不需要重復(fù)計(jì)算,這樣就能避免剛講的問(wèn)題了。
public int f(int n) {if (n == 1) return 1;if (n == 2) return 2;// hasSolvedList可以理解成一個(gè)Map,key是n,value是f(n)if (hasSolvedList.containsKey(n)) {return hasSolvedList.get(n);}int ret = f(n-1) + f(n-2);hasSolvedList.put(n, ret);return ret; }3、函數(shù)調(diào)用耗時(shí)多
4、空間復(fù)雜度高
遞歸代碼改寫為非遞歸代碼
遞歸有利有弊,利是遞歸代碼的表達(dá)力很強(qiáng),寫起來(lái)非常簡(jiǎn)潔;而弊就是空間復(fù)雜度高、有堆棧溢出的風(fēng)險(xiǎn)、存在重復(fù)計(jì)算、過(guò)多的函數(shù)調(diào)用會(huì)耗時(shí)較多等問(wèn)題。
那是不是所有的遞歸代碼都可以改為這種迭代循環(huán)的非遞歸寫法呢?籠統(tǒng)地講,是的。因?yàn)檫f歸本身就是借助棧來(lái)實(shí)現(xiàn)的,只不過(guò)我們使用的棧是系統(tǒng)或者虛擬機(jī)本身提供的,我們沒(méi)有感知罷了。如果我們自己在內(nèi)存堆上實(shí)現(xiàn)棧,手動(dòng)模擬入棧、出棧過(guò)程,這樣任何遞歸代碼都可以改寫成看上去不是遞歸代碼的樣子。但是這種思路實(shí)際上是將遞歸改為了“手動(dòng)”遞歸,本質(zhì)并沒(méi)有變,而且也并沒(méi)有解決前面講到的某些問(wèn)題,徒增了實(shí)現(xiàn)的復(fù)雜度。
如何找到“最終推薦人”
偽代碼實(shí)現(xiàn)
long findRootReferrerId(long actorId) {Long referrerId = select referrer_id from [table] where actor_id = actorId;if (referrerId == null) return actorId;return findRootReferrerId(referrerId); }在實(shí)際項(xiàng)目中,上面的代碼并不能工作,為什么呢?這里面有兩個(gè)問(wèn)題
第一,如果遞歸很深,可能會(huì)有堆棧溢出的問(wèn)題。第二,如果數(shù)據(jù)庫(kù)里存在臟數(shù)據(jù),我們還需要處理由此產(chǎn)生的無(wú)限遞歸問(wèn)題。比如 demo 環(huán)境下數(shù)據(jù)庫(kù)中,測(cè)試工程師為了方便測(cè)試,會(huì)人為地插入一些數(shù)據(jù),就會(huì)出現(xiàn)臟數(shù)據(jù)。如果 A 的推薦人是 B,B 的推薦人是 C,C 的推薦人是 A,這樣就會(huì)發(fā)生死循環(huán)。
?
總結(jié)
以上是生活随笔為你收集整理的10 | 递归:如何用三行代码找到“最终推荐人”?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: java多元一次方程组求解_java 怎
- 下一篇: python爬虫实例之一