日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

10 | 递归:如何用三行代码找到“最终推荐人”?

發(fā)布時(shí)間:2023/12/10 编程问答 47 豆豆
生活随笔 收集整理的這篇文章主要介紹了 10 | 递归:如何用三行代码找到“最终推荐人”? 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

什么是遞歸?

遞歸是一種應(yīng)用非常廣泛的算法(或者編程技巧)。很多數(shù)據(jù)結(jié)構(gòu)和算法的編碼實(shí)現(xiàn)都要用到遞歸,比如 DFS 深度優(yōu)先搜索、前中后序二叉樹(shù)遍歷等等。

去的過(guò)程叫“遞”,回來(lái)的過(guò)程叫“歸”

場(chǎng)景

周末你帶著女朋友去電影院看電影,女朋友問(wèn)你,咱們現(xiàn)在坐在第幾排啊?電影院里面太黑了,看不清,沒(méi)法數(shù),現(xiàn)在你怎么辦?別忘了你是程序員,這個(gè)可難不倒你,遞歸就開(kāi)始排上用場(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ò)程叫“歸”?;旧?#xff0c;所有的遞歸問(wèn)題都可以用遞推公式來(lái)表示。剛剛這個(gè)生活中的例子,我們用遞推公式將它表示出來(lái)就是這樣的:
f(n)=f(n-1)+1 其中,f(1)=1

遞歸需要滿(mǎn)足三個(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ù)詢(xún)問(wèn)任何人,就知道自己在哪一排,也就是 f(1)=1,這就是遞歸的終止條件

如何寫(xiě)遞歸代碼:大問(wèn)題分解為小問(wèn)題、寫(xiě)出遞推公式、找到終止條件,將遞推公式轉(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ù)第一步的走法把所有走法分為兩類(lèi),第一類(lèi)是第一步走了 1 個(gè)臺(tái)階,另一類(lè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é):寫(xiě)遞歸代碼的關(guān)鍵就是找到如何將大問(wèn)題分解為小問(wèn)題的規(guī)律,并且基于此寫(xiě)出遞推公式,然后再推敲終止條件,最后將遞推公式和終止條件翻譯成代碼。

遞歸理解和定勢(shì)思維誤區(qū)

  • 計(jì)算機(jī)擅長(zhǎng)做重復(fù)的事情,所以遞歸正合它的胃口。而我們?nèi)四X更喜歡平鋪直敘的思維方式。當(dāng)我們看到遞歸時(shí),我們總想把遞歸平鋪展開(kāi),腦子里就會(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)?;蛘咛摂M機(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ù)雜度高

遞歸代碼改寫(xiě)為非遞歸代碼

遞歸有利有弊,利是遞歸代碼的表達(dá)力很強(qiáng),寫(xiě)起來(lái)非常簡(jiǎn)潔;而弊就是空間復(fù)雜度高、有堆棧溢出的風(fēng)險(xiǎn)、存在重復(fù)計(jì)算、過(guò)多的函數(shù)調(diào)用會(huì)耗時(shí)較多等問(wèn)題。

那是不是所有的遞歸代碼都可以改為這種迭代循環(huán)的非遞歸寫(xiě)法呢?籠統(tǒng)地講,是的。因?yàn)檫f歸本身就是借助棧來(lái)實(shí)現(xiàn)的,只不過(guò)我們使用的棧是系統(tǒng)或者虛擬機(jī)本身提供的,我們沒(méi)有感知罷了。如果我們自己在內(nèi)存堆上實(shí)現(xiàn)棧,手動(dòng)模擬入棧、出棧過(guò)程,這樣任何遞歸代碼都可以改寫(xiě)成看上去不是遞歸代碼的樣子。但是這種思路實(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)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。