WeetCode4 —— 二叉樹遍歷與樹型DP

Cuzzz 發表於 2023-01-23
演算法

一丶二叉樹的遍歷

1.二叉樹遍歷遞迴寫法與遞迴序

瞭解過二叉樹的朋友,最開始肯定是從二叉樹的遍歷開始的,二叉樹遍歷的遞迴寫法想必大家都有所瞭解。

public static void process(TreeNode node) {
    if (node == null) {
        return;
    }
    //如果在這裡列印 代表前序遍歷  ----位置1
    process(node.left); 
    //如果在這裡列印中序遍歷       ----位置2
    process(node.right);
    //如果在這裡列印 後序遍歷      ---位置3
}

process函式在不同的位置進行列印,就實現了不同的遍歷順序。

我們這裡引入一個概念遞迴序 —— 遞迴函式到達節點的順序

image-20230115115002428

process函式的遞迴序列是什麼呢

  1. 首先process(1)此時方法棧記為A,遍歷節點1(可以理解為A棧的位置1)
  2. 然後process(1.left) 再開闢一個棧記為B 來到2(可以理解為B棧的位置1)
  3. 接著process(2.left)為空 出棧 相當於來到了B棧的位置2 ,再次來到2
  4. 接著process(2.right)為空,出棧,來到B棧位置3,再次來到2
  5. 接著出棧,來到A棧位置2
  6. 然後process(1.right)再開闢一個棧記為C 來到3(可以理解為C棧的位置1)
  7. 接著process(3.left)為空 出棧 相當於來到了C棧的位置2 ,再次來到3
  8. 接著process(3.right)為空,出棧,來到C棧位置3,再次來到3
  9. 最後出棧,來到A棧的位置3,來到1

遞迴序為 1,2,2,2,1,3,3,3,1。可以看到每一個節點都將訪問3次。

  • 第一次訪問的時候列印

    1,2,3 ——先序遍歷

  • 第二次訪問的時候列印

    2,1,3——中序遍歷

  • 第三次訪問的時候列印

    2,3,1 ——後序遍歷

2.二叉樹遍歷非遞迴寫法

下面講解的二叉樹遍歷非遞迴寫法,都針對下面這棵樹

image-20230115120134790

2.1 先序遍歷

遞迴寫法告訴我們,列印結果應該是1,2,4,5,3

對於節點2,我們需要先列印2,然後處理4,然後處理5。棧先進後出,如果我們入棧順序是4,5 那麼會先列印5然後列印4,將無法實現先序遍歷,所有我們需要先入5後入4。

  • 當前列印的節點記憶為cur
  • 列印
  • cur的右節點(如果存在)入棧,然後左節點(如果存在)入棧
  • 彈出棧頂進行處理,迴圈往復

程式如下

public static void process1(TreeNode root) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stackMemory = new Stack<>();
    stackMemory.push(root);
    while (!stackMemory.isEmpty()) {
        TreeNode temp = stackMemory.pop();
        System.out.println(temp.val);
        if (temp.right != null) {
            stackMemory.push(temp.right);
        }
        if (temp.left != null) {
            stackMemory.push(temp.left);
        }
    }
}

2.2 中序遍歷

  1. 將樹的左邊界放入棧中

    image-20230115120134790

    這時候棧中的內容是 (棧底)1->2->4(棧頂)

  2. 然後彈出節點cur進行列印

    也就是列印4,如果cur具備右子樹,那麼將右子樹的進行步驟一

  3. 迴圈往復直到棧為空

為什麼這可以實現左->中->右列印的中序遍歷

首先假如當前節點是A,那麼列印A的前提是,左子樹列印完畢,在列印A的左子樹的時候,我們會把A左子節點的右樹入棧,這一保證了列印A之前,A的左子樹被處理完畢,然後列印A

列印完A,如果A具備右子樹,右子樹會入棧,然後彈出,保證了列印完A後列印其右子樹,從而實現左->中->右列印的中序遍歷

public static void process2(TreeNode root) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stackMemory = new Stack<>();
    do {
        
        //首先左子樹入棧
        //1
        while (root!=null){
            stackMemory.push(root);
            root = root.left;
        }
        
        //來到這兒,說明左子樹都入棧了
        //彈出 
        if (!stackMemory.isEmpty()){
            root = stackMemory.pop();
            System.out.println(root.val);
            
            //賦值為右子樹,右子樹會到1的程式碼位置,如果右子樹,那麼右子樹會進行列印
            root = root.right;
        }
    }while (!stackMemory.isEmpty()||root!=null);
}

2.3 後序遍歷

後續遍歷就是左->右->頭的順序,那麼只要我以頭->左->右的順序將節點放入收集棧中,最後從收集棧中彈出的順序,就是左->右->頭

public static void process3(TreeNode r) {
    if (r == null) {
        return;
    }
	//輔助棧
    Stack<TreeNode> help = new Stack<>();
	//收集棧
    Stack<TreeNode> collect = new Stack<>();
    
    help.push(r);
    while (!help.isEmpty()) {
        TreeNode temp = help.pop();
        collect.push(temp);
        if (temp.left != null) {
            help.push(temp.left);
        }
        if (temp.right != null) {
            help.push(temp.right);
        }
    }

    StringBuilder sb = new StringBuilder();
    while (!collect.isEmpty()) {
        sb.append(collect.pop().val).append(",");
    }
    System.out.println(sb);
}

3.二叉樹寬度優先遍歷

給你二叉樹的根節點 root ,返回其節點值的 層序遍歷 (也是寬度優先遍歷)即逐層地,從左到右訪問所有節點)。

img

此樹寬度優先遍歷——[3],[9,20],[15,7]

寬度優先遍歷可以使用佇列實現,最開始將佇列的頭放入到佇列中,然後當佇列不為空的時候,拿出佇列頭cur,加入到結果集合中,然後如果當前cur的左兒子,右兒子中不為null的節點放入到佇列中,迴圈往復

下面以LeetCode102為例子

image-20230123164114417

public List<List<Integer>> levelOrder(TreeNode root) {
    //結果集合
    List<List<Integer>> res = new ArrayList<>();
    if (root == null) {
        return res;
    }
    //佇列
    LinkedList<TreeNode> queue = new LinkedList<>();
    queue.addLast(root);
    //當前層的節點數量為1
    int curLevelNum = 1;
    
    while (!queue.isEmpty()) {
        //儲存當前層節點的值
        List<Integer> curLevelNodeValList = new ArrayList<>(curLevelNum);
        //下一層節點的數量
        int nextLevelNodeNum = 0;
        
        //遍歷當前層
        while (curLevelNum > 0) {
            TreeNode temp = queue.removeFirst();
            curLevelNodeValList.add(temp.val);
            
            //處理左右兒子,只要不為null 那麼加入並且下一次節點數量加1
            if (temp.left != null) {
                queue.addLast(temp.left);
                nextLevelNodeNum++;
            }
            if (temp.right != null) {
                queue.addLast(temp.right);
                nextLevelNodeNum++;
            }
            //當前層減少
            curLevelNum--;
        }
        //當前層結束了,到下一層
        curLevelNum = nextLevelNodeNum;
        //儲存結果
        res.add(curLevelNodeValList);
    }
    return res;
}

二丶樹型DP

1.從一道題開始——判斷一顆二叉樹是否是搜尋二叉樹

image-20230123164308491

1.1 中序遍歷解題

可以斷定我們可以使用中序遍歷,然後在中序遍歷的途中判斷節點的值是滿足升序即可

  • 遞迴中序遍歷判斷是否二叉搜尋樹

    public boolean isValidBST(TreeNode root) {
        if (root == null) {
            return true;
        }
    	
        //第二個引數記錄之前遍歷遇到節點的最大值
        //由於TreeNode 可能節點值為int 最小使用Long最小
        return check(root, new AtomicLong(Long.MIN_VALUE));
    }
    
    
    private boolean check(TreeNode node, AtomicLong preValue) {
        if (node == null) {
            return true;
        }
    	
        //左樹是否二叉搜尋樹
        boolean isLeftBST = check(node.left, preValue);
    
        //左樹不是 那麼返回false
        if (!isLeftBST) {
            return false;
        }
        //當前節點的值 大於之前遇到的最大值 那麼更改preValue
        if (node.val > preValue.get()) {
            preValue.set(node.val);
        } else {
            //不滿足升序那麼false
            return false;
        }
    	
        //檢查右樹
        return check(node.right, preValue);
    }
    
  • 非遞迴中序遍歷判斷是否二叉搜尋樹

    private boolean check(TreeNode root) {
        if (root == null) {
            return true;
        }
        //前面節點最大值,最開始為null
        Integer pre = null;
        Stack<TreeNode> stack = new Stack<>();
        do {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            if (!stack.isEmpty()) {
                root = stack.pop();
                
                //滿足升序那麼更新pre
                if (pre == null || pre < root.val) {
                    pre = root.val;
                } else {
                    return false;
                }
                root = root.right;
            }
        } while (!stack.isEmpty() || root != null);
    
        return true;
    }
    

1.2 引入 —— 樹形DP

如果當前位於root節點,我們可以獲取root左子樹的一些"資訊",root右子樹的一些資訊,我們們要如何判斷root為根的樹是否是二叉搜尋樹:

  1. root左子樹,右子樹必須都是二叉搜尋樹

  2. root的值必須大於左子樹最大,必須小於右子樹最小

  3. 根據1和2 我們可以得到"資訊"的結構

    static class Info {
        
        //當前子樹的最小值
        Integer min;
        //當前子樹最大值
        Integer max;
        //當前子樹是否是二叉搜尋樹
        boolean isBst;
    
        Info(Integer min, Integer max, boolean flag) {
            this.min = min;
            this.max = max;
            this.isBst = flag;
        }
    }
    

接下來的問題是,有了左右子樹的資訊,如何拼湊root自己的資訊?如果不滿足二叉搜尋樹的要求那麼返回isBst為false,否則需要返回root這棵樹的最大,最小——這些資訊可以根據左子樹和右子樹的資訊構造而來。程式碼如下

private Info process(TreeNode node) {
    //如果當前節點為null 那麼返回null
    //為null 表示是空樹
    if (node == null) {
        return null;
    }

    //預設現在是二叉搜尋樹
    boolean isBst = true;

    //左樹最大,右樹最小 二者是否bst ,從左右子樹拿資訊
    Info leftInfo = process(node.left);
    Info rightInfo = process(node.right);
    //左樹不為null 那麼 維護isBst識別符號
    if (leftInfo != null) {
        isBst = leftInfo.isBst;
    }
      //右樹不為null 那麼 維護isBst識別符號
    if (rightInfo != null) {
        isBst = isBst && rightInfo.isBst;
    }
    
    //如果左數 或者右樹 不為二叉搜尋樹 那麼返回
    if (!isBst){
        return new Info(null,null,isBst);
    }
    //左右是bst,那麼看是否滿足二叉搜尋樹的條件
    
    //左邊最大 是否小於當前節點
    if (leftInfo!=null && leftInfo.max >= node.val){
        isBst = false;
    }
    
    //右邊最小 是否小於當前節點
    if (rightInfo!=null && rightInfo.min <= node.val){
        isBst = false;
    }
    
    //如果不滿足 那麼返回
    if (!isBst){
        return new Info(null,null,isBst);
    }
    //說明node為根的樹是bst
	
    //那麼根據左右子樹的資訊返回node這課樹的資訊
    Integer min  = node.val;
    Integer max  = node.val;
    if (leftInfo!=null){
        min = leftInfo.min;
    }
    if (rightInfo!=null){
        max = rightInfo.max;
    }
    return new Info(min, max, true);
}

2. 樹型DP題目套路

之所以稱之為樹型DP,是因為這個套路用於解決 樹的問題。那麼為什麼叫DP,這是由於node節點的資訊,來自左右子樹的資訊,類似於動態規劃中的狀態轉移。

2.1樹型DP可以解決什麼問題

image-20220102125201549

怎麼理解:

對於1中判斷是否二叉搜尋樹的問題,S規則就是以node為根的這棵樹是否是二叉搜尋樹

最終整棵樹是否二叉搜尋樹,是依賴於樹中所有節點的——"最終答案一定在其中"

2.2 解題模板

image-20220102134222487

3.題目練習

3.1 二叉樹的最大深度

image-20230123172040318

需要的資訊只有樹的高度,我們可以向左子樹獲取,高度然後獲取右子樹的高度,然後二叉高度取max加上1就是當前節點為根的樹的高度

  public int maxDepth(TreeNode root) {
        if(root == null){
            return 0;
        }

        int leftH = maxDepth(root.left);
        int rightH = maxDepth(root.right);

        return Math.max(leftH,rightH)+1;
    }

3.2 判斷一顆樹是否二叉平衡樹

  • 需要什麼資訊:左右樹的高度,左右樹是否是平衡的
  • 怎麼根據左右構造當前樹的資訊:當前高度=max(左右高度)+1 ,當前是否平衡=左平衡右平衡且二者高度差不大於1
/***
 * 是否是平衡二叉樹
 * @return
 */
public static boolean isAVL(TreeNode root) {
    return process(root).getKey();
}

public static Pair<Boolean, Integer> process(TreeNode root) {
    //當前節點為null 那麼是平衡二叉樹
    if (root == null) {
        return new Pair<>(true, 0);
    }
    //右樹
    Pair<Boolean, Integer> rightData = process(root.right);
    //左樹
    Pair<Boolean, Integer> leftData = process(root.left);
    //右樹是否是平衡
    boolean rTreeIsAVL = rightData.getKey();
    //右樹高度
    int rHigh = rightData.getValue();
    //左樹是否平衡
    boolean lTreeIsAVL = leftData.getKey();
    //左樹高度
    int lHigh = rightData.getValue();
    //當前樹是平衡要求:左樹平衡 右樹平衡 且二者高度差小於1
    boolean thisNodeIsAvl = rTreeIsAVL
            && lTreeIsAVL
            && Math.abs(rHigh - lHigh) < 2;
    //返回當前樹的結果 高度樹是左右高度最大+1
    return new Pair<>(thisNodeIsAvl, Math.max(rHigh, lHigh) + 1);
}

3.3 判斷一棵樹是否滿二叉樹

滿二叉樹 樹的高度h和樹節點數目n具備 n = 2的h次方 -1 的特性

  • 需要左右樹的高度,左右樹的節點個數
  • 怎麼根據左右構造當前樹的資訊:當前高度=max(左高,右高)+1,當前節點個數=左個數+右個數+1
public static boolean isFullTree(TreeNode root) {
    Pair<Integer, Integer> rootRes = process(root);
    int height = rootRes.getKey();
    int nodeNums = rootRes.getValue();
    return nodeNums == Math.pow(2, height)-1;
}

//key 高度 v 節點個數
public static Pair<Integer, Integer> process(TreeNode node) {
    if (node == null) {
        return new Pair<>(0, 0);
    }
    Pair<Integer, Integer> rInfo = process(node.right);
    Pair<Integer, Integer> lInfo = process(node.left);
    int thisNodeHeight = Math.max(rInfo.getKey(), lInfo.getKey()) + 1;
    int thisNodeNum = rInfo.getValue() + lInfo.getValue() + 1;
    return new Pair<>(thisNodeHeight, thisNodeNum);
}