做題目前隨便說點
-
樹是一種抽象數據類型,一種具有樹結構形式的數據集合。
-
節點個數確定,有層次關係。
-
有根節點。
-
除了根,每個節點有且只有一個父節點。
-
沒有環路。
-
所有數據結構都可以用鏈表表示或者用數組表示,樹也一樣。
面試題55 - I. 二叉樹的深度
輸入一棵二叉樹的根節點,求該樹的深度。從根節點到葉節點依次經過的節點(含根、葉節點)形成樹的一條路徑,最長路徑的長度爲樹的深度。
說明: 葉子節點是指沒有子節點的節點。
示例:
給定二叉樹 [3,9,20,null,null,15,7],3
/
9 20
/
15 7
返回它的最大深度 3 。來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/maximum-depth-of-binary-tree
著作權歸領釦網絡所有。商業轉載請聯繫官方授權,非商業轉載請註明出處。
解題:
審題:
-
也就是知道根節點的引用和屬性,和左右子樹的引用。求根節點所在的樹深度。
-
什麼是深度呢?題目給出了定義。從根節點到所有葉子的深度路徑長度中最長的那條路徑的長度。就是樹的深度。
-
那麼什麼是路徑長度呢?也就是說長度的單位是什麼?審題,審一下題目的例子可以得知,就是從根節點到葉子節點經過的能訪問到節點屬性的節點個數,這個約定不怎麼優化,有點拗口。但是比較容易理解。
-
換句話說,多少個葉子就有多少條路徑了。
-
我們要找出這些路徑中最長的那個。
-
我們可以把所有路徑長度放到一個數組中,然後從數組中找出最大的長度,就是樹的深度。
-
求數組最大值,這個不難用“打擂法”,什麼是打擂法,可以查一下資料。
-
我們可以簡單的理解打擂法就是,循環訪問數,然後把數組和一個初始值爲0的全局變量比較大小,如果比這個變量大就取代他的位子。等遍歷玩每一個數組元素之後,這個變量的值就是數組的最大值了。這個變量就好像擂臺的金腰帶。只有最強者纔可以拿到金腰帶。
-
還有一個問題要解決,怎麼獲取根節點到每個葉子節點的長度呢?這隻能使用二叉樹的遍歷,因爲我們得一個一個節點的樹,一頓操作下來,每個節點都被我們數了一遍。我們怎麼數呢?就是訪問一個節點就記一下。把上一次記的結果+1就是當前節點的長度數了。
-
記哪裏呢?當然是記數據結構了,樹的屬性就是用來記數據的。自然的,我把每個節點到都記錄到根節點的長度即可,而且有趣的事,我們只需要將父節點的長度+1就是當前沒節點的長度了。
-
另外按照樹的性質定義,如果該節點沒有葉子節點的話,那麼該節點就會葉子節點了。
-
其次我們得知道遞歸遍歷樹是深度優先遍歷算法的一種,所以很適合用來做深度計算。(廣度優先算法一樣可以解答這類題目。)
-
接下來可以開始寫代碼了,先找出代碼框架。
代碼框架如下:
class Solution {
TreeNode recursive(TreeNode node) {
// 邊界判斷
if(...) {
return node;
}
// 訪問節點,讀取或者修改屬性值。
// 遞歸訪問左右子樹
recursive(node.left);
recursive(node.right);
// 也可以在這裏讀取或者修改屬性值,這種是回溯思路。
// 返回值。
return node;
}
}
- 如果先遞歸左右子樹叫回溯法,因爲編譯成計算機指令程序的話,會先修改葉子的值再修改根的值。然後一步一步return。他是自下而上的update每個節點的屬性。
- 如果先訪問修改節點信息,然後才遞歸左右子樹,就是普通的非線性遞歸了。
(這裏不要扯複雜先。專心學習框架。暴力解題。)
開始解題:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public static int kingDepth = -1;
public static void setNodeFatherDepth(TreeNode node, int fatherDepth) {
if( node != null ) {
node.val = fatherDepth;
}
}
static TreeNode recursive(TreeNode node) {
// 邊界判斷
/* explain: 我們的任務是記錄節點的長度,所以如果節點引用不存在也就沒有記錄的地方了,所以直接返回;*/
if(node == null) {
return node;
}
// 訪問節點,讀取或者修改屬性值。
/* explain: 記錄node節點長度 = 父節點的單位長度 + 1, node.val表示父節點的單位長度*/
int nodeDepth = node.val + 1;
/* explain: 其次是要判斷該節點是否爲葉子節點,如果是和擂臺變量kingDepth比較. 同時如果該節點已經是葉子節點了,也就沒有進一步遞歸的必要了。直接return.*/
boolean isLeaf = (node.left == null && node.right == null);
if(isLeaf) {
boolean isBetterKing = nodeDepth > Solution.kingDepth;
Solution.kingDepth = isBetterKing?nodeDepth : Solution.kingDepth;
return node;
}
/* explain: 然後如果有子節點的話,我們要還有同步一下子節點的“父節點長度值”val */
Solution.setNodeFatherDepth(node.left, nodeDepth);
Solution.setNodeFatherDepth(node.right, nodeDepth);
// 遞歸訪問左右子樹
recursive(node.left);
recursive(node.right);
// 也可以在這裏讀取或者修改屬性值,這種是回溯思路。這裏也是【後續遍歷】的地方
// 返回值。
return node;
}
public int maxDepth(TreeNode root) {
// 初始化根節點的父節點長度;
Solution.setNodeFatherDepth(root, 0);
kingDepth = 0;
recursive(root);
return kingDepth;
}
}
總結:
該題目有趣,竟然用到了打擂法。
- 這道題目是求深度,也就是求所有節點到根節點的最長路徑的長度(經過的節點數)。
- 所以可以用動態規劃法,快速求解。動態規劃題目具備3個要素:重疊子問題,最優子結構,動態轉移方程。
- 重疊子問題,就是子節點的深度求解和根節點的深度求解是一樣的。(你可以說大部分“樹”求最值的題目都存在重疊子問題)
- 最優子結構,即要求解給定問題的答案有多個選擇,而且存在最優解。
- 剩下就是拼湊狀態轉移方程,我們需要確定3個概念,才能得出動態轉移方程。
- 狀態是什麼? (狀態通常是題目給定的參數變量)
確定了狀態後就可以構建dp表了。
這道題目要求我們求解的是二叉樹的深度。
那麼變量就是二叉樹的節點,dp值就是深度。
所以dp[二叉樹的節點n] = 第n個結構構成的樹的深度。
也就是說二叉樹的節點就是“狀態”
- base case是什麼?base case就是最簡單的狀態,且能夠顯而易見的得出深度。那麼這個狀態就是我們要找的【Base case】,base case 可以有多個,通常找出1~2個就可以,base case主要用於前期的遞推。
- 定義選項以及擇優策略。選項是由dp表的元素構成,定義選項相對靈活很多,沒有標準答案。而擇優策略則是確定一個規則,從多個選項中選擇最優,並賦值給下一個dp[狀態]。
-
動態規劃是自下而上的求解方式。
-
本題目,可以確定存在重疊子問題。這個無疑。畢竟是二叉樹求最優解。需要遍理所有節點,從而求深度。
-
也存在最優子結構,因爲從根節點到葉子節點的路徑有多條,而且存在最長路徑。
-
狀態就是dp[二叉樹的節點n] = 第n個結構構成的樹的深度。
-
base case 就是 dp[節點爲Null] = 0(或者 dp[葉子節點] = 1);
-
選項就是 選項1=( dp[左子節點] +1 )。選項2 = (dp[右子節點] + 1)
-
擇優策略就簡單了,看哪個選項大就選哪個。
-
狀態轉移方程最終就是長這個樣子:
dp[root] = max( dp[左子節點] + 1 ,dp[右子節點] + 1) , if 節點== null, return 0;
```
代碼如下:
```java
int maxDepth( TreeNode node ) {
if(node == null) {
return 0;
}
int leftDepth = maxDepth(node.left);
int rightDepth = maxDepth(node.right) ;
return Math.max(leftDepth + 1 , rightDepth + 1 );
}
```
- 和上面的算法比起來,其實就是一個後續遍理的回溯的過程。
- 我們的第一種解法採用的是,前序遍理,然後再遞推的過程累計節點長度。
- 而第二種揭發是採用後續遍理,在回溯的過程累計節點長度。
- 理論上我們並沒有真正的用動態規劃算法(我們用的是遞歸的回溯算法,因爲我們並沒有記錄dp表。),但是思想上是動態規劃的思想。
- 不管如何這道題目後續和前需遍理都可以求解。
- 前序遍理有利於我們對算法進行剪支,減少不必要的遍理(我們這裏沒有可以用剪支的場景)。
- 而後續遍理可以用回溯計算,減少函數局部變量的開銷(函數棧的開銷在遞歸算法裏是無法避免的,而且如果參數和返回值不大的話也不值一提)。但是無法剪支。
- 而中序遍歷普遍是二叉搜素樹會用到,先遍歷的子節點無法剪支,後遍歷的節點可以剪支。你問我什麼是剪支?剪支就是在遞歸函數調用前,加一個If判斷,如果不滿足條件就不遞歸了(不遞歸就等於被流程被我們if語句剪掉了)。