當代程序員必備技能(算法)之:遞歸詳解

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸是一種非常重要的算法思想,無論你是前端開發,還是後端開發,都需要掌握它。在日常工作中,統計文件夾大小,解析xml文件等等,都需要用到遞歸算法。它太基礎太重要了,這也是爲什麼面試的時候,面試官經常讓我們手寫遞歸算法。本文呢,將跟大家一起學習遞歸算法~"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"什麼是遞歸?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸的特點"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸與棧的關係"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸應用場景"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸解題思路"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"leetcode案例分析"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸可能存在的問題以及解決方案"}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"什麼是遞歸?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸,在計算機科學中是指一種通過重複將問題分解爲同類的子問題而解決問題的方法。簡單來說,遞歸表現爲函數調用函數本身。在知乎看到一個比喻遞歸的例子,個人覺得非常形象,大家看一下:"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"❝"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸最恰當的比喻,就是查詞典。我們使用的詞典,本身就是遞歸,爲了解釋一個詞,需要使用更多的詞。當你查一個詞,發現這個詞的解釋中某個詞仍然不懂,於是你開始查這第二個詞,可惜,第二個詞裏仍然有不懂的詞,於是查第三個詞,這樣查下去,直到有一個詞的解釋是你完全能看懂的,那麼遞歸走到了盡頭,然後你開始後退,逐個明白之前查過的每一個詞,最終,你明白了最開始那個詞的意思。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"❞"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來試試水,看一個遞歸的代碼例子吧,如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public int sum(int n) {\n    if (n <= 1) {\n        return 1;\n    } \n    return sum(n - 1) + n; \n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"遞歸的特點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際上,遞歸有兩個顯著的特徵,終止條件和自身調用:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自身調用:原問題可以分解爲子問題,子問題和原問題的求解方法是一致的,即都是調用自身的同一個函數。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"終止條件:遞歸必須有一個終止的條件,即不能無限循環地調用本身。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結合以上demo代碼例子,看下遞歸的特點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ed/ed0df7a912bda93939cec59b575875d4.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"遞歸與棧的關係"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實,遞歸的過程,可以理解爲出入棧的過程的,這個比喻呢,只是爲了方便讀者朋友更好理解遞歸哈。以上代碼例子計算sum(n=3)的出入棧圖如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6e/6e9d053109ebbf4157ec21cdab40293d.jpeg","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了更容易理解一些,我們來看一下 函數sum(n=5)的遞歸執行過程,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ba/ba956794dbb7669b03947a5531895a39.jpeg","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"計算sum(5)時,先sum(5)入棧,然後原問題sum(5)拆分爲子問題sum(4),再入棧,直到終止條件sum(n=1)=1,就開始出棧。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sum(1)出棧後,sum(2)開始出棧,接着sum(3)。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後呢,sum(1)就是後進先出,sum(5)是先進後出,因此遞歸過程可以理解爲棧出入過程啦~"}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"遞歸的經典應用場景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"哪些問題我們可以考慮使用遞歸來解決呢?即遞歸的應用場景一般有哪些呢?"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"階乘問題"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"二叉樹深度"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"漢諾塔問題"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"斐波那契數列"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"快速排序、歸併排序(分治算法也使用遞歸實現)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遍歷文件,解析xml文件"}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"遞歸解題思路"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決遞歸問題一般就三步曲,分別是:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一步,定義函數功能"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二步,尋找遞歸終止條件"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二步,遞推函數的等價關係式"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個遞歸解題三板斧理解起來有點抽象,我們拿階乘遞歸例子來喵喵吧~"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"1.定義函數功能"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定義函數功能,就是說,你這個函數是幹嘛的,做什麼事情,換句話說,你要知道遞歸原問題是什麼呀?比如你需要解決階乘問題,定義的函數功能就是n的階乘,如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//n的階乘(n爲大於0的自然數)\nint factorial (int n){\n\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"2.尋找遞歸終止條件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸的一個典型特徵就是必須有一個終止的條件,即不能無限循環地調用本身。所以,用遞歸思路去解決問題的時候,就需要尋找遞歸終止條件是什麼。比如階乘問題,當n=1的時候,不用再往下遞歸了,可以跳出循環啦,n=1就可以作爲遞歸的終止條件,如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//n的階乘(n爲大於0的自然數)\nint factorial (int n){\n    if(n==1){\n      return 1;\n    }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"3.遞推函數的等價關係式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸的"},{"type":"text","marks":[{"type":"strong"}],"text":"「本義」"},{"type":"text","text":",就是原問題可以拆爲同類且更容易解決的子問題,即"},{"type":"text","marks":[{"type":"strong"}],"text":"「原問題和子問題都可以用同一個函數關係表示。遞推函數的等價關係式,這個步驟就等價於尋找原問題與子問題的關係,如何用一個公式把這個函數表達清楚」"},{"type":"text","text":"。階乘的公式就可以表示爲 f(n) = n * f(n-1), 因此,階乘的遞歸程序代碼就可以寫成這樣,如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"int factorial (int n){\n    if(n==1){\n      return 1;\n    }\n    return n * factorial(n-1);\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「注意啦」"},{"type":"text","text":",不是所有遞推函數的等價關係都像階乘這麼簡單,一下子就能推導出來。需要我們多接觸,多積累,多思考,多練習遞歸題目滴~"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"leetcode案例分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來分析一道leetcode遞歸的經典題目吧~"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"❝"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原題鏈接在這裏哈:https://leetcode-cn.com/problems/invert-binary-tree/"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"❞"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「題目:」"},{"type":"text","text":" 翻轉一棵二叉樹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"輸入:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"     4\n   /   \\\n  2     7\n / \\   / \\\n1   3 6   9\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"輸出:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"     4\n   /   \\\n  7     2\n / \\   / \\\n9   6 3   1\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們按照以上遞歸解題的三板斧來:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「1. 定義函數功能」"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"函數功能(即這個遞歸原問題是),給出一顆樹,然後翻轉它,所以,函數可以定義爲:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//翻轉一顆二叉樹\npublic TreeNode invertTree(TreeNode root) {\n}\n\n/**\n * Definition for a binary tree node.\n * public class TreeNode {\n *     int val;\n *     TreeNode left;\n *     TreeNode right;\n *     TreeNode(int x) { val = x; }\n * }\n */\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「2.尋找遞歸終止條件」"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這棵樹什麼時候不用翻轉呢?當然是當前節點爲null或者當前節點爲葉子節點的時候啦。因此,加上終止條件就是:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//翻轉一顆二叉樹\npublic TreeNode invertTree(TreeNode root) {\n    if(root==null || (root.left ==null && root.right ==null)){\n       return root;\n    }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「3. 遞推函數的等價關係式」"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原問題之你要翻轉一顆樹,是不是可以拆分爲子問題,分別翻轉它的左子樹和右子樹?子問題之翻轉它的左子樹,是不是又可以拆分爲,翻轉它左子樹的左子樹以及它左子樹的右子樹?然後一直翻轉到葉子節點爲止。嗯,看圖理解一下咯~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/62/629e73062cc04032b14c5cc10e50a54a.jpeg","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,你要翻轉根節點爲4的樹,就需要"},{"type":"text","marks":[{"type":"strong"}],"text":"「翻轉它的左子樹(根節點爲2)和右子樹(根節點爲7)」"},{"type":"text","text":"。這就是遞歸的"},{"type":"text","marks":[{"type":"strong"}],"text":"「遞」"},{"type":"text","text":"的過程啦"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/24/2417fc5e61de859cfc41f2625779ce22.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後呢,根節點爲2的樹,不是葉子節點,你需要繼續"},{"type":"text","marks":[{"type":"strong"}],"text":"「翻轉它的左子樹(根節點爲1)和右子樹(根節點爲3)」"},{"type":"text","text":"。因爲節點1和3都是"},{"type":"text","marks":[{"type":"strong"}],"text":"「葉子節點」"},{"type":"text","text":"了,所以就返回啦。這也是遞歸的"},{"type":"text","marks":[{"type":"strong"}],"text":"「遞」"},{"type":"text","text":"的過程~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2c/2cba5c2df58b2cc4c8f09b949f7a832c.jpeg","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同理,根節點爲7的樹,也不是葉子節點,你需要翻轉"},{"type":"text","marks":[{"type":"strong"}],"text":"「它的左子樹(根節點爲6)和右子樹(根節點爲9)」"},{"type":"text","text":"。因爲節點6和9都是葉子節點了,所以也返回啦。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/91/91948f237d924d47913a5df071222d0f.jpeg","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"左子樹(根節點爲2)和右子樹(根節點爲7)都被翻轉完後,這幾個步驟就"},{"type":"text","marks":[{"type":"strong"}],"text":"「歸來」"},{"type":"text","text":",即遞歸的歸過程,翻轉樹的任務就完成了~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6e/6e632c462e9a952d4fcbdec20365afd4.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然,"},{"type":"text","marks":[{"type":"strong"}],"text":"「遞推關係式」"},{"type":"text","text":"就是:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"invertTree(root)= invertTree(root.left) + invertTree(root.right);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是,很容易可以得出以下代碼:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//翻轉一顆二叉樹\npublic TreeNode invertTree(TreeNode root) {\n    if(root==null || (root.left ==null && root.right ==null){\n       return root;\n    }\n    //翻轉左子樹\n    TreeNode left = invertTree(root.left);\n    //翻轉右子樹\n    TreeNode right= invertTree(root.right);\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏代碼有個地方需要注意,翻轉完一棵樹的左右子樹,還要交換它左右子樹的引用位置。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":" root.left = right;\n root.right = left;\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,leetcode這個遞歸經典題目的"},{"type":"text","marks":[{"type":"strong"}],"text":"「終極解決代碼」"},{"type":"text","text":"如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"class Solution {\n    public TreeNode invertTree(TreeNode root) {\n         if(root==null || (root.left ==null && root.right ==null)){\n           return root;\n         }\n         //翻轉左子樹\n         TreeNode left = invertTree(root.left);\n         //翻轉右子樹\n         TreeNode right= invertTree(root.right);\n         //左右子樹交換位置~\n         root.left = right;\n         root.right = left;\n         return root;\n    }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"拿終極解決代碼去leetcode提交一下,通過啦~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/db/dbb6d119362ce9e695d81a8b5ec7900e.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"遞歸存在的問題"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸調用層級太多,導致棧溢出問題"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遞歸重複計算,導致效率低下"}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"棧溢出問題"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每一次函數調用在內存棧中分配空間,而每個進程的棧容量是有限的。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當遞歸調用的層級太多時,就會超出棧的容量,從而導致調用棧溢出。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實,我們在前面小節也討論了,遞歸過程類似於出棧入棧,如果遞歸次數過多,棧的深度就需要越深,最後棧容量真的不夠咯"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「代碼例子如下:」"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/**\n * 遞歸棧溢出測試\n */\npublic class RecursionTest {\n\n    public static void main(String[] args) {\n        sum(50000);\n    }\n    private static int sum(int n) {\n        if (n <= 1) {\n            return 1;\n        }\n        return sum(n - 1) + n;\n    }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「運行結果:」"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"Exception in thread \"main\" java.lang.StackOverflowError\n at recursion.RecursionTest.sum(RecursionTest.java:13)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"怎麼解決這個棧溢出問題?首先需要"},{"type":"text","marks":[{"type":"strong"}],"text":"「優化一下你的遞歸」"},{"type":"text","text":",真的需要遞歸調用這麼多次嘛?如果真的需要,先稍微"},{"type":"text","marks":[{"type":"strong"}],"text":"「調大JVM的棧空間內存」"},{"type":"text","text":",如果還是不行,那就需要棄用遞歸,"},{"type":"text","marks":[{"type":"strong"}],"text":"「優化爲其他方案」"},{"type":"text","text":"咯~"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"重複計算,導致程序效率低下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們再來看一道經典的青蛙跳階問題:一隻青蛙一次可以跳上1級臺階,也可以跳上2級臺階。求該青蛙跳上一個 n 級的臺階總共有多少種跳法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"絕大多數讀者朋友,很容易就想到以下遞歸代碼去解決:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"class Solution {\n    public int numWays(int n) {\n    if (n == 0){\n       return 1;\n     }\n    if(n <= 2){\n        return n;\n    }\n    return numWays(n-1) + numWays(n-2);\n    }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是呢,去leetcode提交一下,就有問題啦,超出時間限制了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ab/abda71c75fc1293c1f2759cfbfbb2ad2.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼超時了呢?遞歸耗時在哪裏呢?先畫出"},{"type":"text","marks":[{"type":"strong"}],"text":"「遞歸樹」"},{"type":"text","text":"看看:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/56/566a40a9e116deb184077fe31232073e.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要計算原問題 f(10),就需要先計算出子問題 f(9) 和 f(8)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後要計算 f(9),又要先算出子問題 f(8) 和 f(7),以此類推。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一直到 f(2) 和 f(1),遞歸樹才終止。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先來看看這個遞歸的時間複雜度吧,"},{"type":"text","marks":[{"type":"strong"}],"text":"「遞歸時間複雜度 = 解決一個子問題時間*子問題個數」"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個子問題時間 = f(n-1)+f(n-2),也就是一個加法的操作,所以複雜度是 "},{"type":"text","marks":[{"type":"strong"}],"text":"「O(1)」"},{"type":"text","text":";"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"問題個數 = 遞歸樹節點的總數,遞歸樹的總結點 = 2^n-1,所以是複雜度"},{"type":"text","marks":[{"type":"strong"}],"text":"「O(2^n)」"},{"type":"text","text":"。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,青蛙跳階,遞歸解法的時間複雜度 = O(1) * O(2^n) = O(2^n),就是指數級別的,爆炸增長的,"},{"type":"text","marks":[{"type":"strong"}],"text":"「如果n比較大的話,超時很正常的了」"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回過頭來,你仔細觀察這顆遞歸樹,你會發現存在"},{"type":"text","marks":[{"type":"strong"}],"text":"「大量重複計算」"},{"type":"text","text":",比如f(8)被計算了兩次,f(7)被重複計算了3次...所以這個遞歸算法低效的原因,就是存在大量的重複計算!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「那麼,怎麼解決這個問題呢?」"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然存在大量重複計算,那麼我們可以先把計算好的答案存下來,即造一個備忘錄,等到下次需要的話,先去"},{"type":"text","marks":[{"type":"strong"}],"text":"「備忘錄」"},{"type":"text","text":"查一下,如果有,就直接取就好了,備忘錄沒有才再計算,那就可以省去重新重複計算的耗時啦!這就是"},{"type":"text","marks":[{"type":"strong"}],"text":"「帶備忘錄的解法」"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來看一下"},{"type":"text","marks":[{"type":"strong"}],"text":"「帶備忘錄的遞歸解法」"},{"type":"text","text":"吧~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般使用一個數組或者一個哈希map充當這個"},{"type":"text","marks":[{"type":"strong"}],"text":"「備忘錄」"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設f(10)求解加上"},{"type":"text","marks":[{"type":"strong"}],"text":"「備忘錄」"},{"type":"text","text":",我們再來畫一下遞歸樹:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「第一步」"},{"type":"text","text":",f(10)= f(9) + f(8),f(9) 和f(8)都需要計算出來,然後再加到備忘錄中,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/24/24e055469abd833d2bea295b906fd29a.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「第二步,」"},{"type":"text","text":" f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因爲 f(8) 已經在備忘錄中啦,所以可以省掉,f(7),f(6)都需要計算出來,加到備忘錄中~"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/78/78c73d347ca88ca81b7933721f9c5afa.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"「第三步,」"},{"type":"text","text":" f(8) = f(7)+ f(6),發現f(8),f(7),f(6)全部都在備忘錄上了,所以都可以剪掉。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c1/c12311d7ce64469e853078f4fe2b2442.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以呢,用了備忘錄遞歸算法,遞歸樹變成光禿禿的樹幹咯,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/54/5453f27a34a96f960ea474d8a2a62227.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"帶「備忘錄」的遞歸算法,子問題個數=樹節點數=n,解決一個子問題還是O(1),所以"},{"type":"text","marks":[{"type":"strong"}],"text":"「帶「備忘錄」的遞歸算法的時間複雜度是O(n)」"},{"type":"text","text":"。接下來呢,我們用帶「備忘錄」的遞歸算法去擼代碼,解決這個青蛙跳階問題的超時問題咯~,代碼如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public class Solution {\n    //使用哈希map,充當備忘錄的作用\n    Map tempMap = new HashMap();\n    public int numWays(int n) {\n        // n = 0 也算1種\n        if (n == 0) {\n            return 1;\n        }\n        if (n <= 2) {\n            return n;\n        }\n        //先判斷有沒計算過,即看看備忘錄有沒有\n        if (tempMap.containsKey(n)) {\n            //備忘錄有,即計算過,直接返回\n            return tempMap.get(n);\n        } else {\n            // 備忘錄沒有,即沒有計算過,執行遞歸計算,並且把結果保存到備忘錄map中,對1000000007取餘(這個是leetcode題目規定的)\n            tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);\n            return tempMap.get(n);\n        }\n    }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e4/e4296af607984df697f7f23d9fc0d876.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"​"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e4/e4296af607984df697f7f23d9fc0d876.png","alt":"程序員必備的基本算法:遞歸詳解","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"寫在最後"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然你們能看到這裏說明這篇文章對你們的幫助還是有的,小編可不可以給你們索要一個小小的贊呢。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"延伸學習閱讀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://blog.csdn.net/javachengzi/article/details/109516189","title":null},"content":[{"type":"text","text":"還在爲算法煩惱?那你應該還沒看過這份Github上70k標星的筆記"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"視頻:"},{"type":"link","attrs":{"href":"https://www.bilibili.com/video/BV1WA411j7go/","title":null},"content":[{"type":"text","text":"暴力遞歸算法"}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"看完三件事❤️"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"========"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"點贊,轉發,有你們的 『點贊和評論』,纔是我創造的動力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關注公衆號 『 Java鬥帝 』,不定期分享原創知識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時可以期待後續文章ing🚀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章