幾個月以前就想寫這樣一篇文章分享給你們,因爲本身有心而力不足,沒有把真正的學到的東西沉澱下來,因此一直在不斷的自學。多是由於在一所三流大學,資源也比較少,只能本身在網搜索相關資料,在互聯網上遇到了一些朋友的幫助下去深刻理解,而後本身抽出大量時間作題總結、概括,纔會把已有的知識概念所被本身吸取和理解,造成了本身的技術思想體系。javascript
而後本身又用了一個星期的時間去整理、分類,纔有了這篇 8000 字有關遞歸知識的分享,但願可以幫助正在學習遞歸的小夥伴們。並且有了這篇文章的支撐和動力,日後還會寫出關於數據結構與算法一些難懂的概念簡單化。若是文章中有錯誤的地方,但願你們指正,可以爲他人分享出更有質量的內容!java
看了不少關於遞歸的文章,也總結了不少遞歸的文章,也看了多篇文章下方讀者的評論。有的讀者評論到文章清晰易懂,有的卻噴做者寫的存在不少錯誤,埋怨做者寫出來很垃圾,還不如不寫。我想從理性的角度說一下,創做者寫文章的最初好意是可以幫助別人對此知識點有進一步的瞭解,並不表明必定可以知足每一個人的要求。git
另外一方面,每篇文章的做者可能理解的不夠透徹,不少地方可能存在許多錯誤,包括理解上的錯誤,筆誤等,這也是寫文章的第二個目的,可以讓別人挑出本身文章中的不足,可以達到與別人共同進步的目的,一箭雙鵰,一箭雙鵰。github
接下來分享的文章是關於遞歸的,這篇文章不僅僅分享遞歸的一切,我以爲更重要的是向每位讀者傳遞一個思想。思想?對的,沒錯!這篇文章不能說包含遞歸的邊邊角角,可是經過本身的理論上的學習和實踐,有了本身的一套遞歸思想。算法
什麼問題該用遞歸,什麼問題用遞歸簡潔,什麼問題就不能使用遞歸解決,以及對於特定的問題用遞歸解決的陷阱,能不能進一步對遞歸進行二次優化,這些都是今天小鹿分享的內容。編程
遞歸,顧名思義,有遞有歸才叫遞歸,有遞無歸,有歸無遞那叫 「耍流氓」 。
咱們學習一門技術也好,編程語言也好,首先學習以前咱們知道它將能給咱們帶來什麼,能幫助咱們解決什麼樣的問題,這也是激勵咱們去學習它的動力所在。數組
從數組到鏈表、散列表,再到基本算法等,直到遇到遞歸以後,感受很是的難理解。我相信每一個人都有這種感受,一開始以爲很是難,經歷了九九八十一難以後,仍是沒有弄懂遞歸裏邊的貓膩,而後就天然而然的跳過了。數據結構
後來我就開始刷了一個月的 LeetCode 題,發現遞歸在數據結構與算法中有着一席之地,統治着江山。大部分的題均可以用遞歸去解決,如:二叉樹的遍歷、回溯算法、0-1 揹包問題、深度優先遍歷、回溯算法等等,我整理了至少二三十到關於遞歸的題,才發現遞歸的重要性,因此不得不從新深刻遞歸學習,全部有了今天這篇文章。編程語言
上方我對遞歸「耍流氓」式的定義並不能讓你準確的理解遞歸是什麼,那麼咱們就來活生生的舉個生活中的例子。
好比你和小鹿我同樣,在大學裏喜歡插隊打飯(做爲一個三好學生,我怎麼能幹這種事呢?哈哈),那麼隊伍後邊的同窗本數着本身前邊還有 5 個同窗就改輪到本身了,因爲前邊同窗不斷的插隊,這時他發現,怎麼以爲本身離着打飯的窗口愈來愈遠呢?這時若是他想知道本身在隊隊列中的的第幾個(前提是前邊再也不有人插隊),用遞歸思想來解決,咱們怎麼作呢?函數
因而他問前邊的同窗是第幾位,前邊的同窗也不僅到呀,因而前邊的同窗問他前邊的同窗是第幾位,直到前邊第二個同窗問到第一個正在打飯的同窗是隊伍的第幾個(有點小尷尬)。打飯的同窗不耐煩的說,沒看到我是第一個正在打飯嗎?這個過程實際上是就是一個遞歸中「遞」的過程。
而後前邊打飯的第二個同窗不耐煩的又告訴第三個同窗,我是第二個,沒看單我前邊有個傢伙正在打飯嗎?而後第三個傳給第四個,之後日後傳,直到那位逐漸遠離窗口的同窗的前一我的告訴他是第幾個以後,他知道了本身目前在隊伍中的第幾個位置。這個過程咱們能夠理解爲遞歸中「歸」的過程。
「打飯的同窗不耐煩的說,沒看到我是第一個正在打飯嗎?」,在遞歸中,咱們稱爲終止條件。
1)問題雖然是層層遞歸的分析,可是用程序表示的時候,不要層層的在大腦中調用遞歸代碼去想,這樣可能會使你徹底陷入到 「遞」 的過程當中去,「歸」 的時候,歸不出來了,這些都是咱們交給計算機乾的事情。
2)那咱們在寫程序的時候怎麼理解遞歸呢?咱們只找問題之間存在的關係,屏蔽掉遞歸的細節,具體看(五)分析。
經過上方的例子,咱們能夠很容易的總結出知足遞歸的三個條件。
想知道本身在隊伍中的位置,將其問題分解爲「每一個人所處隊伍中的位置」這樣的多個子問題。
想要知道本身當前的位置,就要問前邊人所處的位置。那麼前邊人想要知道本身所處的位置,就要知道他前邊人的位置。因此說,該問題和子問題的解決思路相同,知足第二個條件。
第一個正在打飯的同窗說本身是隊伍中的第一人,這就是所謂的終止條件,找到終止條件以後就開始進行「歸」的過程。
若是你對遞歸有了必定的瞭解,上邊的例子對你來講小菜一碟,下邊還有更大的難度來進行挑戰。那麼問題分析清楚了,怎麼根據問題編寫出遞歸代碼來呢?
寫遞歸公式最重要的一點就是找到該問題和子問題的關係,怎麼找到之間存在的關係呢?這裏我要強調注意的一點就是不要讓大腦試圖去想層層的遞歸過程,畢竟大腦的思考方式是順勢思考的(一開始學習遞歸老是把本身繞繞進去,歸的時候,就徹底亂套的)。那怎麼找到每一個子問題之間存在的某種關係呢?
咱們只想其中一層(第一層關係),以上述爲例,若是我想知道當前隊伍的位置,因此我要以前前一我的的位置,而後 +1
就是個人位置了。對於他在什麼位置,我絲絕不用關係,而是讓遞歸去解決他的位置。咱們能夠寫出遞推公式以下:
// f(n) 表明當前我在隊伍中的位置 // f(n-1) 表明我前邊那我的的位置 // 遞推公式 f(n) = f(n-1) + 1
※ 注意:這個式子的含義就是f(n)
求當前 n 這我的的位置,f(n-1) + 1
表明的就是前一我的的位置+ 1
就是n
的位置。
遞推公式咱們很輕鬆的寫出來了,可是沒有終止條件的遞推公式會永遠的執行下去的,因此咱們要有一個終止條件終止程序的運行。那麼怎麼找到終止條件呢?
所謂的終止條件就是已知的條件,好比上述的排隊打飯的例子中,第一我的正在窗口打飯,他的前邊是沒有人的,因此他是第一個。第一我的的位置爲 1,咱們應該怎麼表示呢?
// 終止條件 f(1) = 1;
※ 注意:有的問題終止條件不止一個哦,好比:斐波那契數列。具體問題具體分析。
遞推公式和終止條件咱們分析出來了,那麼將遞推公式轉化爲遞歸代碼很是容易了。
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三、2一、34...,去除第一個和第二個數據外,其他的數據等於前兩個數據之和(如:2 = 1 + 1
,8 = 3 + 5
,34 = 21 + 13
)。你能夠嘗試着根據「知足遞歸的三個條件」以及「怎麼寫出遞歸代碼」的步驟本身動手動腦親自分析一下。
我也在這裏稍微作一個分析:
1)第一步:首先判斷能不能將問題分解爲多個子問題,上邊我也分析過了,除了第一個和第二個數據,其餘數據是前兩個數據之和。那麼前兩個數據怎麼知道呢?一樣的解決方式,是他們前兩個數之和。
2)第二步:找到終止條件,若是不斷的找到前兩個數之和,直到最前邊三個數據 0、一、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
實現的代碼,相信學過編程的小夥伴基本的代碼邏輯均可以看懂。根據上方總結的遞歸分析知足的三個條件以及怎麼寫出遞歸代碼的步驟,一步步來分析八皇后問題。
一、將問題分解爲多個子問題
在上述的代碼分析和算法思路分析中,咱們能夠大致知道怎麼分解該問題了,枚舉出八個皇后(棋子)全部的知足狀況能夠分解爲,先尋找每一種知足的狀況這種子問題。好比,每一個子問題的算法思路就是上方列出的四個步驟。
二、找出終止條件
當遍歷到第八行的時候,遞歸結束。
// 終止條件 if(row === 8){ // 打印第 n 種知足的狀況 console.log(result) n++; return; }
三、寫出遞推公式
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; }
四、轉換爲遞歸代碼
// 變量 // 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)遞歸的本質就是重複調用自己的過程,自己是什麼?固然是一個函數,那好,函數中有參數以及一些局部的聲明的變量,相信不少小夥伴只會用函數,而不知道函數中的變量是怎麼存儲的吧。不要緊,等你聽我分析完,你就會了。
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; }
有些遞歸問題中,存在重複計算問題,好比求斐波那契數列,咱們畫一下遞歸樹以下圖,咱們會發現有不少重複遞歸計算的值,重複計算會致使程序的時間複雜度很高,並且是指數級別的,致使咱們的程序效率低下。
以下圖遞歸樹中,求斐波那契數列 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; }
由於遞歸時函數的變量的存儲須要額外的棧空間,當遞歸深度很深時,須要額外的內存佔空間就會不少,因此遞歸有很是高的空間複雜度。
好比: f(n) = f(n-1)+1
,空間複雜度並非 O(1)
,而是 O(n)
。
咱們一塊兒對遞歸作一個簡單的總結吧,若是你仍是沒有徹底明白,不要緊,多看幾遍,說實話,我這我的比較笨,前期看遞歸還不知道看了幾十遍纔想明白,吃飯想,睡覺以前想,相信最後總會想明白的。
不要用大腦去想每一層遞歸的實現,記住這是計算機應該作的事情,咱們要作的就是弄懂遞歸之間的關係,從而屏蔽掉層層遞歸的細節。
最後可能說的比較打雞血,不少人一遇到遞歸就會崩潰掉,好比我,哈哈。不管之後遇到什麼困難,不要對它們產生恐懼,而是當作一種挑戰,當你通過長時間的戰鬥,突破層層困難,最後突破挑戰的時候,你會感激曾經的本身當初困難面前沒有放棄。這一點我深有感觸,有時候對於難題感到很無助,雖然本身沒有在一所好的大學,沒有好的資源,更沒有人去專心的指導你,可是我一直相信這都是老天給我發出的挑戰書,我會繼續努力,寫出更多高質量的文章。
若是以爲本文對你有幫助,點個贊,我但願可以讓更多處在遞歸困惑的人看到,謝謝各位支持!下一篇我打算出一篇完整關於鏈表的文章,終極目標:將數據結構與算法每一個知識點寫成一系列的文章。
做者:小鹿
座右銘:追求平淡不平凡,一輩子追求作一個不甘平凡的碼農!
本文首發於 Github ,轉載請說明出處:https://github.com/luxiangqiang/Blog/blob/master/articel/數據結構與算法系列/數據結構與算法之遞歸系列.md
我的公衆號:一個不甘平凡的碼農。