摘要:終止條件遞推公式遞歸的分類通過做大量的題,根據遞歸解決不同的問題,引申出來的幾種解決和思考的方式。我們通過層與層之間的計算關系用遞推公式表達出來做計算,經過層層的遞歸,最終得到結果值。
前言
幾個月之前就想寫這樣一篇文章分享給大家,由于自己有心而力不足,沒有把真正的學到的東西沉淀下來,所以一直在不斷的自學。可能是因為在一所三流大學,資源也比較少,只能自己在網搜索相關資料,在互聯網上遇到了一些朋友的幫助下去深入理解,然后自己抽出大量時間做題總結、歸納,才會把已有的知識概念所被自己吸收和理解,形成了自己的技術思想體系。
然后自己又用了一個星期的時間去整理、分類,才有了這篇 8000 字有關遞歸知識的分享,希望能夠幫助正在學習遞歸的小伙伴們。而且有了這篇文章的支撐和動力,往后還會寫出關于數據結構與算法一些難懂的概念簡單化。如果文章中有錯誤的地方,希望大家指正,能夠為他人分享出更有質量的內容!
為什么要寫這篇遞歸文章看了很多關于遞歸的文章,也總結了很多遞歸的文章,也看了多篇文章下方讀者的評論。有的讀者評論到文章清晰易懂,有的卻噴作者寫的存在很多錯誤,埋怨作者寫出來很垃圾,還不如不寫。我想從理性的角度說一下,創作者寫文章的最初好意是能夠幫助別人對此知識點有進一步的了解,并不代表一定能夠滿足每個人的要求。
另一方面,每篇文章的作者可能理解的不夠透徹,很多地方可能存在許多錯誤,包括理解上的錯誤,筆誤等,這也是寫文章的第二個目的,能夠讓別人挑出自己文章中的不足,能夠達到與別人共同進步的目的,一舉兩得,兩全其美。
接下來分享的文章是關于遞歸的,這篇文章不單單分享遞歸的一切,我覺得更重要的是向每位讀者傳遞一個思想。思想?對的,沒錯!這篇文章不能說包含遞歸的邊邊角角,但是通過自己的理論上的學習和實踐,有了自己的一套遞歸思想。
什么問題該用遞歸,什么問題用遞歸簡潔,什么問題就不能使用遞歸解決,以及對于特定的問題用遞歸解決的陷阱,能不能進一步對遞歸進行二次優化,這些都是今天小鹿分享的內容。
什么是遞歸?遞歸,顧名思義,有遞有歸才叫遞歸,有遞無歸,有歸無遞那叫 “耍流氓” 。為什么要學習遞歸?
我們學習一門技術也好,編程語言也好,首先學習之前我們知道它將能給我們帶來什么,能幫助我們解決什么樣的問題,這也是激勵我們去學習它的動力所在。
從數組到鏈表、散列表,再到基本算法等,直到遇到遞歸之后,感覺非常的難理解。我相信每個人都有這種感覺,一開始覺得非常難,經歷了九九八十一難之后,還是沒有弄懂遞歸里邊的貓膩,然后就自然而然的跳過了。
后來我就開始刷了一個月的 LeetCode 題,發現遞歸在數據結構與算法中有著一席之地,統治著江山。大部分的題都可以用遞歸去解決,如:二叉樹的遍歷、回溯算法、0-1 背包問題、深度優先遍歷、回溯算法等等,我整理了至少二三十到關于遞歸的題,才發現遞歸的重要性,所以不得不重新深入遞歸學習,所有有了今天這篇文章。
怎么理解遞歸的過程?上方我對遞歸“耍流氓”式的定義并不能讓你準確的理解遞歸是什么,那么我們就來活生生的舉個生活中的例子。1、問題
比如你和小鹿我一樣,在大學里喜歡插隊打飯(作為一個三好學生,我怎么能干這種事呢?哈哈),那么隊伍后邊的同學本數著自己前邊還有 5 個同學就改輪到自己了,由于前邊同學不斷的插隊,這時他發現,怎么覺得自己離著打飯的窗口越來越遠呢?這時如果他想知道自己在隊隊列中的的第幾個(前提是前邊不再有人插隊),用遞歸思想來解決,我們怎么做呢?
2、“遞”于是他問前邊的同學是第幾位,前邊的同學也不只到呀,于是前邊的同學問他前邊的同學是第幾位,直到前邊第二個同學問到第一個正在打飯的同學是隊伍的第幾個(有點小尷尬)。打飯的同學不耐煩的說,沒看到我是第一個正在打飯嗎?這個過程其實是就是一個遞歸中“遞”的過程。
3、“歸”然后前邊打飯的第二個同學不耐煩的又告訴第三個同學,我是第二個,沒看單我前邊有個家伙正在打飯嗎?然后第三個傳給第四個,以后往后傳,直到那位逐漸遠離窗口的同學的前一個人告訴他是第幾個之后,他知道了自己目前在隊伍中的第幾個位置。這個過程我們可以理解為遞歸中“歸”的過程。
4、終止條件“打飯的同學不耐煩的說,沒看到我是第一個正在打飯嗎?”,在遞歸中,我們稱為終止條件。
5、怎么理解遞歸?1)問題雖然是層層遞歸的分析,但是用程序表示的時候,不要層層的在大腦中調用遞歸代碼去想,這樣可能會使你完全陷入到 “遞” 的過程中去,“歸” 的時候,歸不出來了,這些都是我們交給計算機干的事情。
2)那我們在寫程序的時候怎么理解遞歸呢?我們只找問題之間存在的關系,屏蔽掉遞歸的細節,具體看(五)分析。
通過上方的例子,我們可以很容易的總結出滿足遞歸的三個條件。1、一個問題能不能分解成多個子問題來解決
想知道自己在隊伍中的位置,將其問題分解為“每個人所處隊伍中的位置”這樣的多個子問題。
2、該問題是否和子問題的解決思路相同想要知道自己當前的位置,就要問前邊人所處的位置。那么前邊人想要知道自己所處的位置,就要知道他前邊人的位置。所以說,該問題和子問題的解決思路相同,滿足第二個條件。
3、該問題是否有終止條件第一個正在打飯的同學說自己是隊伍中的第一人,這就是所謂的終止條件,找到終止條件之后就開始進行“歸”的過程。
怎么編寫遞歸代碼?如果你對遞歸有了一定的了解,上邊的例子對你來說小菜一碟,下邊還有更大的難度來進行挑戰。那么問題分析清楚了,怎么根據問題編寫出遞歸代碼來呢?1、寫出遞推公式
寫遞歸公式最重要的一點就是找到該問題和子問題的關系,怎么找到之間存在的關系呢?這里我要強調注意的一點就是不要讓大腦試圖去想層層的遞歸過程,畢竟大腦的思考方式是順勢思考的(一開始學習遞歸總是把自己繞繞進去,歸的時候,就完全亂套的)。那怎么找到每個子問題之間存在的某種關系呢?
我們只想其中一層(第一層關系),以上述為例,如果我想知道當前隊伍的位置,所以我要之前前一個人的位置,然后 +1 就是我的位置了。對于他在什么位置,我絲毫不用關系,而是讓遞歸去解決他的位置。我們可以寫出遞推公式如下:
// f(n) 代表當前我在隊伍中的位置 // f(n-1) 代表我前邊那個人的位置 // 遞推公式 f(n) = f(n-1) + 1
※ 注意:這個式子的含義就是 f(n) 求當前 n 這個人的位置, f(n-1) + 1 代表的就是前一個人的位置 + 1 就是 n 的位置。2、找到終止條件
遞推公式我們很輕松的寫出來了,但是沒有終止條件的遞推公式會永遠的執行下去的,所以我們要有一個終止條件終止程序的運行。那么怎么找到終止條件呢?
所謂的終止條件就是已知的條件,比如上述的排隊打飯的例子中,第一個人正在窗口打飯,他的前邊是沒有人的,所以他是第一個。第一個人的位置為 1,我們應該怎么表示呢?
// 終止條件 f(1) = 1;
※ 注意:有的問題終止條件不止一個哦,比如:斐波那契數列。具體問題具體分析。3、轉換遞歸代碼
遞推公式和終止條件我們分析出來了,那么將遞推公式轉化為遞歸代碼非常容易了。
function f(n){ // 終止條件 if(n == 1) retun 1; // 遞推公式 return f(n-1) + 1; }遞歸的分類
通過做大量的題,根據遞歸解決不同的問題,引申出來的幾種解決和思考的方式。之所以將其分類,是為了能夠更好的理解遞歸在不同的問題下起著什么作用,如:每層遞歸之間存在的關系、計算,以及遞歸枚舉所有情況和面臨選擇性問題的遞歸。雖然分為了幾類,但是遞歸的本質是一成不變的。
將哪一類用遞歸解決的問題作為計算型呢?我簡單總結了為兩點,層層計算和并列計算。
層層計算,顧名思義,能夠用遞歸解決的問題都可以分為多個子問題,我們把每個子問題可以抽象成一層,子問題之間的關系可以表示為層與層之間的關系。我們通過層與層之間的計算關系用遞推公式表達出來做計算,經過層層的遞歸,最終得到結果值。
▉ 例子:
我們再那上方排隊打飯的例子來說明,我們的子問題已經分析出來了,就是我想知道當前在隊伍中的位置,就是去問我前邊人的位置加一就是我當前隊伍的位置,這為一層。而前邊這個人想知道當前自己的位置,需要用同樣的解決思路,作為另一層。
層與層之間的關系是什么(我當前隊伍中的位置與前邊人的位置存在什么樣的關系)?這時你會說,當前是 +1。這個大部分人都很容易找出,既然關系確定了,然后通過遞推公式很容易寫出遞歸代碼。
// f(n) 為我所在的當前層 // f(n-1) 為我前邊的人所在的當前層 // + 1 是層與層之間的計算關系 f(n) = f(n-1) + 1
▉ 總結:
我將以上一類遞歸問題命名為「遞歸計算型」的「層層計算類型」。
▉ 舉一反三:
求年齡的問題也是層層計算類型的問題,自己嘗試分析一下(一定要自己嘗試的去想,動手編碼,才能進一步領悟到遞歸技巧)。
問題一:有 5 個人坐在一起,問第 5 個人多少歲,他說比第 4 個人大 2 歲。問第 4 個人多少歲,他說比第 3 個人大2歲。問第 3 人多少歲,他說比第 2個 人大 2 歲。問第2個人多少歲,他說比第 1 個人大 2 歲。最后問第 1 個人,他說他是 10 歲。編寫程序,當輸入第幾個人時求出其對應的年齡。
問題二:單鏈表從尾到頭一次輸出結點值,用遞歸實現。
并列計算,顧名思義,問題的解決方式是通過遞歸的并列計算來得到結果的。層與層之間并沒有一定的計算關系,而只是簡單的改變輸入的參數值。
▉ 例子:
最經典的題型就是斐波那契數列。觀察這樣一組數據 0、 1、1、2、3、5、8、13、21、34...,去除第一個和第二個數據外,其余的數據等于前兩個數據之和(如:2 = 1 + 1,8 = 3 + 5,34 = 21 + 13)。你可以嘗試著根據「滿足遞歸的三個條件」以及「怎么寫出遞歸代碼」的步驟自己動手動腦親自分析一下。
我也在這里稍微做一個分析:
1)第一步:首先判斷能不能將問題分解為多個子問題,上邊我也分析過了,除了第一個和第二個數據,其他數據是前兩個數據之和。那么前兩個數據怎么知道呢?同樣的解決方式,是他們前兩個數之和。
2)第二步:找到終止條件,如果不斷的找到前兩個數之和,直到最前邊三個數據 0、1、1 。如果遞歸求第一個 1 時,前邊的數據不夠,所以這也是我們找到的終止條件。
3)第三步:既然我們終止條件和關系找到了,遞推公式也就不難寫出 f(n) = f(n-1) + f(n-2)(n 為要求的第幾個數字的值)。
4)轉化為遞歸代碼如下:
function f(n) { // 終止條件 if(n == 0) return 0; if(n == 1) return 1; // 遞推公式 return f(n-1) + f(n-2); }
▉ 總結:
我將上方的問題總結為并列計算型。也可以歸屬為層層計算的一種,只不過是 + 1 改成了加一個 f 函數自身的遞歸(說白了,遞歸的結果也是一個確切的數值)。之所謂并列計算 f(n-1) 和 f(n-2) 互不打擾,各自遞歸計算各的值。最后我們將其計算的結果值相加是我們最想要的結果。
▉ 舉一反三:
青蛙跳臺階的問題也是一種并列計算的一種,自己嘗試著根據上邊的思路分析一下,實踐出真知(一定要自己嘗試的去想,動手編碼,才能進一步領悟到遞歸技巧)。
問題:
一只青蛙一次可以跳上 1 級臺階,也可以跳上2 級。求該青蛙跳上一個n 級的臺階總共有多少種跳法。
遞歸枚舉型最多的應用就是回溯算法,枚舉出所有可能的情況,怎么枚舉所有情況呢?通過遞歸編程技巧進行枚舉。那什么是回溯算法?比如走迷宮,從入口走到出口,如果遇到死胡同,需要回退,退回上一個路口,然后走另一岔路口,重復上述方式,直到找到出口。
回溯算法最經典的問題又深度優先遍歷、八皇后問題等,應用非常廣泛,下邊以八皇后問題為例子,展開分析,其他利用遞歸枚舉型的回溯算法就很簡單了。
在 8 X 8 的網格中,放入八個皇后(棋子),滿足的條件是,任意兩個皇后(棋子)都不能處于同一行、同一列或同一斜線上,問有多少種擺放方式?
▉ 問題分析:
要想滿足任意兩個皇后(棋子)都不能處于同一行、同一列或同一斜線上,需要一一枚舉皇后(棋子)的所有擺放情況,然后設定條件,篩選出滿足條件的情況。
▉ 算法思路:
我們把問題分析清楚了之后,怎么通過遞歸實現回溯算法枚舉八個皇后(棋子)出現的所有情況呢?
1)我們在 8 X 8 的網格中,先將第一枚皇后(棋子)擺放到第一行的第一列的位置(也就是坐標: (0,0))。
2)然后我們在第二行安置第二個皇后(棋子),先放到第一列的位置,然后判斷同一行、同一列、同一斜線是否存在另一個皇后?如果存在,則該位置不合適,然后放到下一列的位置,然后在判斷是否滿足我們設定的條件。
3)第二個皇后(棋子)找到合適的位置之后,然后在第三行放置第三枚棋子,依次將八個皇后放到合適的位置。
4)這只是一種可能,因為我設定的第一個皇后是固定位置的,在網格坐標的(0,0) 位置,那么怎么枚舉所有的情況呢?然后我們不斷的改變第一個皇后位置,第二個皇后位置...... ,就可以枚舉出所有的情況。如果你和我一樣,看了這個題之后,如果還有點懵懵懂懂,那么直接分析代碼吧。
▉ 代碼實現:
雖然是用 javascript 實現的代碼,相信學過編程的小伙伴基本的代碼邏輯都可以看懂。根據上方總結的遞歸分析滿足的三個條件以及怎么寫出遞歸代碼的步驟,一步步來分析八皇后問題。
1、將問題分解為多個子問題
在上述的代碼分析和算法思路分析中,我們可以大體知道怎么分解該問題了,枚舉出八個皇后(棋子)所有的滿足情況可以分解為,先尋找每一種滿足的情況這種子問題。比如,每個子問題的算法思路就是上方列出的四個步驟。
2、找出終止條件
當遍歷到第八行的時候,遞歸結束。
// 終止條件 if(row === 8){ // 打印第 n 種滿足的情況 console.log(result) n++; return; }
3、寫出遞推公式
isOkCulomn() 函數判斷找到的該位置是否滿足條件(不能處于同一行、同一列或同一斜線上)。如果滿足條件,我們返回 true,進入 if 判斷,row 行數加一傳入進行遞歸下一行的皇后位置。直至遞歸遇到終止條件位置,column ++,將第一行的皇后放到下一位置,進行繼續遞歸,枚舉出所有可能的擺放情況。
// 每一列的判斷 for(let column = 0; column < 8; column++){ // 判斷當前的列位置是否合適 if(isOkCulomn(row,column)){ // 保存皇后的位置 result[row] = column; // 對下一行尋找數據 cal8queens(row + 1); } // 此循環結束后,繼續遍歷下一種情況,就會形成一種枚舉所有可能性 }
// 判斷當前列是否合適 const isOkCulomn = (row,column) =>{ // 左上角列的位置 let leftcolumn = column - 1; // 右上角列的位置 let rightcolumn = column + 1; for(let i = row - 1;i >= 0; i--){ // 判斷當前格子正上方是否有重復 if(result[i] === column) return false; // 判斷當前格子左上角是否有重復 if(leftcolumn >= 0){ if(result[i] === leftcolumn) return false; } // 判斷當前格式右上角是否有重復 if(leftcolumn < 8){ if(result[i] === rightcolumn) return false; } // 繼續遍歷 leftcolumn --; rightcolumn ++; } return true; }
4、轉換為遞歸代碼
// 變量 // result 為數組,下標為行,數組中存儲的是每一行中皇后的存儲的列的位置。 // row 行 // column 列 // n 計數滿足條件的多少種 var result = []; let n = 0 const cal8queens = (row) =>{ // 終止條件 if(row === 8){ console.log(result) n++; return; } // 每一列的判斷 for(let column = 0; column < 8; column++){ // 判斷當前的列位置是否合適 if(isOkCulomn(row,column)){ // 保存皇后的位置 result[row] = column; // 對下一行尋找數據 cal8queens(row + 1); } // 此循環結束后,繼續遍歷下一種情況,就會形成一種枚舉所有可能性 } } // 判斷當前列是否合適 const isOkCulomn = (row,column) =>{ // 設置左上角 let leftcolumn = column - 1; let rightcolumn = column + 1; for(let i = row - 1;i >= 0; i--){ // 判斷當前格子正上方是否有重復 if(result[i] === column) return false; // 判斷當前格子左上角是否有重復 if(leftcolumn >= 0){ if(result[i] === leftcolumn) return false; } // 判斷當前格式右上角是否有重復 if(leftcolumn < 8){ if(result[i] === rightcolumn) return false; } // 繼續遍歷 leftcolumn --; rightcolumn ++; } return true; } // 遞歸打印所有情況 const print = (result)=>{ for(let i = 0;i < 8; i++){ for(let j = 0;j < 8; j++){ if(result[i] === j){ console.log("Q" + " ") }else{ console.log("*" + " ") } } } } // 測試 cal8queens(0); console.log(n)
▉ 總結
上述八皇后的問題就是用遞歸來枚舉所有情況,然后再從中設置條件,只篩選滿足條件的選項。上述代碼建議多看幾遍,親自動手實踐一下。一開始解決八皇后問題,我自己看了好長時間才明白的,以及遞歸如何發揮技巧作用的。
▉ 舉一反三:
如果你想練練手,可以自己實現圖的深度優先遍歷,這個理解起來并不難,可以自己動手嘗試著寫一寫,我把代碼傳到我的 Github 上了。
分類三:遞歸選擇型所謂的遞歸選擇型,每個子問題都要面臨選擇,求最優解的情況。有的小伙伴會說,求最優解動態規劃最適合,對的,沒錯,但是遞歸通過選擇型「枚舉所有情況」,設置條件,求得問題的最優解也是可以實現的,所有我呢將其這一類問題歸為遞歸選擇型問題,它也是一個回溯算法。
0 - 1 背包問題,了解過的小伙伴也是很熟悉的了。其實這個問題也屬于回溯算法的一種,廢話不多說,直接上問題。有一個背包,背包總的承載重量是 Wkg。現在我們有 n 個物品,每個物品的重量不等,并且不可分割。我們現在期望選擇幾件物品,裝載到背包中。在不超過背包所能裝載重量的前提下,如何讓背包中物品的總重量最大?
▉ 問題分析:
如果你對該問題看懵了,沒關系,我們一點點的分析。假如每個物品我們有兩種狀態,總的裝法就有 2^n 種,怎么才能不重復的窮舉這些可能呢?
▉ 算法思路:
我們可以把物品依次排列,整個問題就分解為了 n 個階段,每個階段對應一個物品怎么選擇。先對第一個物品進行處理,選擇裝進去或者不裝進去,然后再遞歸地處理剩下的物品。
▉ 代碼實現:
這里有個技巧就是設置了條件,自動篩選掉不滿足條件的情況,提高了程序的執行效率。
// 用來存儲背包中承受的最大重量 var max = Number.MIN_VALUE; // i: 對第 i 個物品做出選擇 // currentw: 當前背包的總重量 // goods:數組,存儲每個物品的質量 // n: 物品的數量 // weight: 背包應承受的重量 const f = (i, currentw, goods, n, weight) => { // 終止條件 if(currentw === weight || i === n){ if(currentw > max){ // 保存滿足條件的最大值 max = currentw; } return ; } // 選擇跳過當前物品不裝入背包 f(i+1, currentw, goods, n, weight) // 將當前物品裝入背包 // 判斷當前物品裝入背包之前是否超過背包的重量,如果已經超過當前背包重量,就不要就繼續裝了 if(currentw + goods[i] <= weight){ f(i+1 ,currentw + goods[i], goods, n, weight) } } let a = [2,2,4,6,3] f(0,0,a,5,10) console.log(max)遞歸的缺點
雖然遞歸的使用非常的簡潔,但是也有很多缺點,也是我們在使用中需要額外注意的地方和優化的地方。1、遞歸警惕堆棧溢出
你可能會問,遞歸和系統中的堆棧有什么關聯?不要急,聽我慢慢細說。
1)遞歸的本質就是重復調用本身的過程,本身是什么?當然是一個函數,那好,函數中有參數以及一些局部的聲明的變量,相信很多小伙伴只會用函數,而不知道函數中的變量是怎么存儲的吧。沒關系,等你聽我分析完,你就會了。
2)函數中變量是存儲到系統中的棧中的,棧數據結構的特點就是先進后出,后進先出。一個函數中的變量的使用情況就是隨函數的聲明周期變化的。當我們執行一個函數時,該函數的變量就會一直不斷的壓入棧中,當函數執行完畢銷毀的時候,棧內的元素依次出棧。還是不懂,沒關系,看下方示意圖。
3)我們理解了上述過程之后,回到遞歸上來,我們的遞歸調用是在函數里調用自身,且當前函數并沒有銷毀,因為當前函數在執行自身層層遞歸進去了,所以遞歸的過程,函數中的變量一直不斷的壓棧,由于我們系統棧或虛擬機棧空間是非常小的,當棧壓滿之后,再壓時,就會導致堆棧溢出。
// 函數 function f(n){ var a = 1; var b = 2; return a + b; }
那么遇到這種情況,我們怎么解決呢?
通常我們設置遞歸深度,簡單的理解就是,如果遞歸超過我們設置的深度,我們就退出,不再遞歸下去。還是那排隊打飯的例子,如下:
// 表示遞歸深度變量 let depth = 0; function f(n){ depth++; // 如果超過遞歸深度,拋出錯誤 if(depth > 1000) throw "error"; // 終止條件 if(n == 1) retun 1; // 遞推公式 return f(n-1) + 1; }2、遞歸警惕重復元素
有些遞歸問題中,存在重復計算問題,比如求斐波那契數列,我們畫一下遞歸樹如下圖,我們會發現有很多重復遞歸計算的值,重復計算會導致程序的時間復雜度很高,而且是指數級別的,導致我們的程序效率低下。
如下圖遞歸樹中,求斐波那契數列 f(5) 的值,需要多次遞歸求 f(3) 和 f(2) 的值。
重復計算問題,我們應該怎么解決?有的小伙伴想到了,我們把已經計算過的值保存起來,每次遞歸計算之前先檢查一下保存的數據有沒有該數據,如果有,我們拿出來直接用。如果沒有,我們計算出來保存起來。一般我們用散列表來保存。(所謂的散列表就是鍵值對的形式,如 map )
// 斐波那契數列改進后 let map = new Map(); function f(n) { // 終止條件 if(n == 0) return 0; if(n == 1) return 1; // 如果散列表中存在當前計算的值,就直接返回,不再進行遞歸計算 if(map.has(n)){ return map.get(n); } // 遞推公式 let num = f(n-1) + f(n-2); // 將當前的值保存到散列表中 map.set(n,num) return num; }3、遞歸高空間復雜度
因為遞歸時函數的變量的存儲需要額外的棧空間,當遞歸深度很深時,需要額外的內存占空間就會很多,所以遞歸有非常高的空間復雜度。
比如: f(n) = f(n-1)+1 ,空間復雜度并不是 O(1),而是 O(n) 。
小結我們一起對遞歸做一個簡單的總結吧,如果你還是沒有完全明白,沒關系,多看幾遍,說實話,我這個人比較笨,前期看遞歸還不知道看了幾十遍才想明白,吃飯想,睡覺之前想,相信最后總會想明白的。1、滿足遞歸的三個條件
一個問題能不能分解成多個子問題來解決;
該問題是否和子問題的解決思路相同;
該問題是否有終止條件。
2、怎么寫出遞歸代碼尋找遞歸終止條件;
寫出遞推公式;
轉化成遞歸代碼。
3、怎么理解遞歸?不要用大腦去想每一層遞歸的實現,記住這是計算機應該做的事情,我們要做的就是弄懂遞歸之間的關系,從而屏蔽掉層層遞歸的細節。
4、遞歸的缺點遞歸警惕堆棧溢出
遞歸警惕重復計算
遞歸的高空間復雜度
最后想說的話最后可能說的比較打雞血,很多人一遇到遞歸就會崩潰掉,比如我,哈哈。無論以后遇到什么困難,不要對它們產生恐懼,而是當做一種挑戰,當你經過長時間的戰斗,突破層層困難,最后突破挑戰的時候,你會感激曾經的自己當初困難面前沒有放棄。這一點我深有感觸,有時候對于難題感到很無助,雖然自己沒有在一所好的大學,沒有好的資源,更沒有人去專心的指導你,但是我一直相信這都是老天給我發出的挑戰書,我會繼續努力,寫出更多高質量的文章。
如果覺得本文對你有幫助,點個贊,我希望能夠讓更多處在遞歸困惑的人看到,謝謝各位支持!下一篇我打算出一篇完整關于鏈表的文章,終極目標:將數據結構與算法每個知識點寫成一系列的文章。
作者:小鹿
座右銘:追求平淡不平凡,一生追求做一個不甘平凡的碼農!
本文首發于 Github ,轉載請說明出處:https://github.com/luxiangqiang/Blog/blob/master/articel/數據結構與算法系列/數據結構與算法之遞歸系列.md
個人公眾號:一個不甘平凡的碼農。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109627.html
摘要:空間復雜度方法是否為最大的冪的約數思路最大的的冪為,判斷是否是的約數即可。復雜度時間復雜度,一個整數統計二進制的復雜度,最壞的情況下是。 大廠算法面試之leetcode精講9.位運算視頻教程(高效學習):點擊學習目錄:1.開篇介紹2.時間空間復雜度3.動態規劃4.貪心5.二分查找6.深度優先&廣度優先7.雙指針...
摘要:動態規劃問題一定會具備最優子結構,才能通過子問題的最值得到原問題的最值。另外,雖然動態規劃的核心思想就是窮舉求最值,但是問題可以千變萬化,窮舉所有可行解其實并不是一件容易的事,只有列出正確的狀態轉移方程才能正確地窮舉。 大廠算法面試之leetcode精講3.動態規劃視頻教程(高效學習):點擊學習目錄:1.開篇介...
摘要:空間復雜度雙指針,循環數組,較小的那個先向內移動如果高的指針先移動,那肯定不如當前的面積大計算面積更新最大面積相交鏈表方法哈希表思路將鏈表存入中,第一個相同的節點就是重合的節點復雜度時間復雜度,分別是兩個鏈表的長度。 大廠算法面試之leetcode精講7.雙指針視頻教程(高效學習):點擊學習目錄:1.開篇介紹2...
摘要:常見的內部排序算法有插入排序希爾排序選擇排序冒泡排序歸并排序快速排序堆排序基數排序等。用一張圖概括歸并排序英語,或,是創建在歸并操作上的一種有效的排序算法,效率為。 常見的內部排序算法有:插入排序、希爾排序、選擇排序、冒泡排序、歸并排序、快速排序、堆排序、基數排序等。用一張圖概括: showImg(https://segmentfault.com/img/bVNwuO?w=966&h=...
摘要:常見的內部排序算法有插入排序希爾排序選擇排序冒泡排序歸并排序快速排序堆排序基數排序等。用一張圖概括歸并排序英語,或,是創建在歸并操作上的一種有效的排序算法,效率為。 常見的內部排序算法有:插入排序、希爾排序、選擇排序、冒泡排序、歸并排序、快速排序、堆排序、基數排序等。用一張圖概括: showImg(https://segmentfault.com/img/bVNwuO?w=966&h=...
閱讀 2694·2023-04-25 17:58
閱讀 2978·2021-11-15 11:38
閱讀 2377·2021-11-02 14:48
閱讀 1183·2021-08-25 09:40
閱讀 1822·2019-08-30 15:53
閱讀 1093·2019-08-30 15:52
閱讀 1031·2019-08-30 13:55
閱讀 2435·2019-08-29 15:21