導言java
對於通常的二叉樹問題,咱們總能想到的是深度優先搜索這個算法,繼續想下去就是遞歸,可是其實對於深度優先搜索,有不少不同的思考方向和實現細節,在這基礎上,咱們能夠推導、總結出一些其餘的高級算法,例如分治、動態規劃等等,把這些算法聯繫在一塊兒,更有助於咱們理解一些核心的、本質的問題算法
LeetCode 104. Maximum Depth of Binary Tree數組
給定一個二叉樹,求這個二叉樹的最大深度,一道很簡單的二叉樹問題,題目一理解,咱們很容易就知道,咱們要遞歸去求解,可是這裏仍是須要思考的是,是否是這道題就一種遞歸思路?遞歸實現的代碼每每很是簡潔,可是僅僅是一個地方的細微差異,反應出來的是兩種徹底不同的思路。咱們一塊兒來看看。bash
最開始作這道題,我想的很是簡單,思路是:把整個二叉樹遍歷一遍,每一個節點都記錄一下當前的深度,而後對比求出最大深度便可。因而我寫出了下面的代碼:數據結構
private int max = 1;
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
helper(root, 1);
return max;
}
private void helper(TreeNode root, int currentDepth) {
if (root == null) {
return;
}
max = Math.max(max, currentDepth);
helper(root.left, currentDepth + 1);
helper(root.right, currentDepth + 1);
}
複製代碼
你能夠看到這裏我定義了一個全局變量 max 來記錄當前訪問過的全部節點中的最大深度,最後遍歷完全部節點,max 就是題目要求的解。這麼作從時間空間複雜度分析其實都沒有啥毛病,可是這麼寫確實會讓代碼變得有點冗餘,通過思考以後,改進獲得下面的代碼函數
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
複製代碼
這裏沒有定義額外的全局變量或者輔助函數,遞歸和以前不一樣的點僅僅是 「更新值」 前後的問題,並且有一點特別重要的是這裏的遞歸是帶返回值的,以前的遞歸是不帶返回值的。那這說明什麼呢?是說明帶返回值的遞歸必定就比不帶返回值的遞歸更優嗎?其實不是,咱們要根據具體狀況具體分析,針對這道題,這兩種解法確實第二種來的更爲簡潔,可是明白思路更加的重要,第一種的思路是有點相似遍歷,可是這裏用的是遞歸去遍歷,並非咱們一般使用的 for 循環,每到一個樹節點就去作一下相應的記錄,而後去到下一個樹節點作相似的記錄,最後把全部的記錄彙總就是咱們要的答案,第二種思路其實就是分治,它的核心是先分再合,每一個節點只負責分跟合,這裏的分就是當前樹節點若是有子節點就分下去,合是指將子節點的結果以及當前的值進行統1、合併。你可能會以爲分治就必定比以前的遞歸遍歷更優,先別急着下這個結論,看看樹的中序遍歷吧,LeetCode 94. Binary Tree Inorder Traversal,思考一下,試着用兩種不一樣的思路去解,相信你會得出和這道題徹底相反的結論。post
以前作了挺多的深度優先相關的算法題,像是排列問題,組合問題,N 皇后問題,這些題目都是回溯的思想,條件知足就更新,你不多會去關注當前層和上面一層的聯繫,這裏的遞歸也不須要任何的返回值,緣由很簡單,每一層不須要向上一層反應狀況,操做都是基於全局變量或者堆內存的。可是反觀分治則狀況大不相同,能夠舉一個咱們工做生活中的例子來加以說明:spa
老闆
/ | \
經理...經理
/ | \ / | \
員工...員工 員工...員工
複製代碼
這裏一個公司只有一個老闆,老闆管理着不少的部門,每接到項目,老闆都會將這些項目交給不一樣的部門去作,咱們這裏假設部門之間相互沒有聯繫(分治算法中不存在重複子問題),每一個部門由一個經理來負責,經理會將項目拆分紅小任務並分配給不一樣的員工去處理,到這裏,分配就結束了。員工作完了分配的任務後,向上彙報狀況,經理將全部員工彙報的狀況整合,繼續向上彙報,最後老闆根據全部部門經理彙報的狀況來產生出公司的策略,也就是最後的解。這個例子很好的解釋了分治算法的思想,不同的是,這個例子中的員工、經理、老闆作的是不同的事情,可是分治算法會更加的簡單,每一層作的事情都是同樣的,只是根據子問題獲得的數據不同,於是結果就會不同。你能夠看到分治其實就是先分再合,自底向上傳遞結果的過程。由於要傳遞結果,因此遞歸函數每每就須要有返回值,可是這並不絕對,像快速排序這樣利用分治思想的算法的遞歸函數就沒有返回值,這是由於它的結果都會記錄在同一個數組中。code
看完上面的內容你可能會有一個疑問,是否是深度優先搜索必須依靠遞歸來實現?其實並非,函數遞歸本質上是函數調用函數本身,在系統的底層,咱們藉助的是函數棧來保存以前的函數,也就是上一層的內容,若是不使用遞歸,那麼就是說咱們不能依靠系統爲咱們提供的函數棧,所以咱們須要手動創建一個棧來保存上一層須要的內容,對於這道簡單的二叉樹問題,代碼以下:排序
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
Stack<TreeNode> stackTree = new Stack<>();
Stack<Integer> stackDepth = new Stack<>();
stackTree.push(root);
stackDepth.push(1);
int max = 1;
while (!stackTree.isEmpty()) {
TreeNode curNode = stackTree.pop();
int curDepth = stackDepth.pop();
if (curNode.left != null) {
stackTree.push(curNode.left);
stackDepth.push(curDepth + 1);
}
if (curNode.right != null) {
stackTree.push(curNode.right);
stackDepth.push(curDepth + 1);
}
if (curNode.left == null && curNode.right == null) {
max = Math.max(max, curDepth);
}
}
return max;
}
複製代碼
這裏我用了兩個棧的緣由是有兩個變量須要保存,一個是節點,另外一個是節點對應的深度,固然你也能夠把他們合二爲一做爲一個新的 Object。本身手動實現一遍,相信會加深你對遞歸的理解。
其實在普通的深度優先搜索算法的基礎之上,咱們也能夠看到動態規劃的影子。通常的深度優先搜索是對以前的子問題的結果不進行保存的,就拿這道題爲例子,當你獲得最後的解的時候,這時你只知道整顆樹的最大深度,可是你並不知道左子樹,以及右子樹的最大深度,想要知道的話,就得從新再針對左子樹或者右子樹深度優先搜索走一遍,可是,其實你以前計算整顆樹的最大深度的時候,已經將左子樹和右子樹的最大深度計算過了,由於(maxDepth = Max(leftMaxDepth, rightMaxDepth))+ 1,若是咱們用一個數據結構,好比數組或者散列,去記錄這些子問題的解,用到的時候直接去這些數據結構中對應着找,那麼這樣的思想就是動態規劃,只是這時它是以遞歸的形式呈如今這裏。固然在這道題當中,記不記錄並無區別,由於沒有重複的子問題,換句話說就是除根節點外,一個節點有且僅有一個父節點。能夠看以前我分析過的一個算法題 LeetCode 312 Burst Balloons 思路分析總結,這裏面提到了一個很好的分析搜索類,以及動態規劃類問題的思路步驟就是:
這些步驟並非對於每到題都要走完的,對於像排列、組合這類問題到第二步就結束了,可是對於不少動態規劃問題咱們須要一直走完五個步驟,雖然繁瑣了些,可是確實能夠增強咱們方向和思路。以我往常的經驗,動態規劃問題怕就怕在沒有思路,沒有思路就會步履維艱。
總體來看,深度優先搜索的涵蓋面確實太廣了,一方面是由於它比較好的和遞歸進行告終合,另外一方面是藉助它,不少其餘算法的思想獲得了體現,文章的內容總結以下