面試題55 - I. 二叉樹的深度-動態規劃遞推回溯

做題目前隨便說點

  • 樹是一種抽象數據類型,一種具有樹結構形式的數據集合。

  • 節點個數確定,有層次關係。

  • 有根節點。

  • 除了根,每個節點有且只有一個父節點。

  • 沒有環路。

  • 所有數據結構都可以用鏈表表示或者用數組表示,樹也一樣。

面試題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語句剪掉了)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章