LeetCode劍指offer二叉樹系列

LeetCode劍指offer二叉樹系列

07 重建二叉樹

題目

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。
例如,給出
前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]
返回如下的二叉樹:

    3
   / \
  9  20
    /  \
   15   7

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof
著作權歸領釦網絡所有。商業轉載請聯繫官方授權,非商業轉載請註明出處。

解題思路:

前序遍歷的性質:節點按照【根節點|左子樹|右子樹】排序

中序遍歷的性質:節點按照【左子樹|根節點|右子樹】排序

以題目示例爲例:

前序遍歷劃分 [ 3 | 9 | 20 15 7 ]
中序遍歷劃分 [ 9 | 3 | 15 20 7 ]
根據以上性質,可得出以下推論:

前序遍歷的首元素 爲 樹的根節點 node 的值。
在中序遍歷中搜索根節點 node 的索引 ,可將 中序遍歷 劃分爲 [ 左子樹 | 根節點 | 右子樹 ] 。
根據中序遍歷中的左 / 右子樹的節點數量,可將 前序遍歷 劃分爲 [ 根節點 | 左子樹 | 右子樹 ] 。

以上子樹的遞推性質是 分治算法 的體現,考慮通過遞歸對所有子樹進行劃分。

分治算法解析:
遞推參數: 根節點在前序遍歷的索引 root 、子樹在中序遍歷的左邊界 left 、子樹在中序遍歷的右邊界 right ;

終止條件: 當 left > right ,代表已經越過葉節點,此時返回 nullnull ;

遞推工作:

建立根節點 node : 節點值爲 preorder[root] ;
劃分左右子樹: 查找根節點在中序遍歷 inorder 中的索引 i ;
爲了提升效率,本文使用哈希表 dic 存儲中序遍歷的值與索引的映射,查找操作的時間複雜度爲 O(1)O(1)

構建左右子樹: 開啓左右子樹遞歸;

i - left + root + 1含義爲 根節點索引 + 左子樹長度 + 1

返回值: 回溯返回 node ,作爲上一層遞歸中根節點的左 / 右子節點;

代碼

package com.leetcode.offer.tree;

import com.labuladong.preDefine.TreeNode;

import java.util.HashMap;

public class BuildTree {
    HashMap<Integer, Integer> map = new HashMap<>();//標記中序遍歷
    int[] preorder;//保留的先序遍歷,方便遞歸時依據索引查看先序遍歷的值

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        //將中序遍歷的值及索引放在map中,方便遞歸時獲取左子樹與右子樹的數量及其根的索引
        for (int i = 0; i < inorder.length; i++) {
            map.put(inorder[i], i);
        }
        //三個索引分別爲
        //當前根的的索引
        //遞歸樹的左邊界,即數組左邊界
        //遞歸樹的右邊界,即數組右邊界
        return recur(0,0,inorder.length-1);
    }

    TreeNode recur(int pre_root, int in_left, int in_right){
        if(in_left > in_right) {
            return null;// 相等的話就是自己
        }
        TreeNode root = new TreeNode(preorder[pre_root]);//獲取root節點
        int idx = map.get(preorder[pre_root]);//獲取在中序遍歷中根節點所在索引,以方便獲取左子樹的數量
        //左子樹的根的索引爲先序中的根節點+1
        //遞歸左子樹的左邊界爲原來的中序in_left
        //遞歸右子樹的右邊界爲中序中的根節點索引-1
        root.left = recur(pre_root+1, in_left, idx-1);
        //右子樹的根的索引爲先序中的 當前根位置 + 左子樹的數量 + 1
        //遞歸右子樹的左邊界爲中序中當前根節點+1
        //遞歸右子樹的有邊界爲中序中原來右子樹的邊界
        root.right = recur(pre_root + (idx - in_left) + 1, idx+1, in_right);
        return root;
    }
}

ps:感覺這道題好難啊。

劍指 Offer 26. 樹的子結構

題目

輸入兩棵二叉樹A和B,判斷B是不是A的子結構。(約定空樹不是任意一個樹的子結構)

B是A的子結構, 即 A中有出現和B相同的結構和節點值。

例如:
給定的樹 A:

     3
    / \
   4   5
  / \
 1   2
給定的樹 B:

   4 
  /
 1
返回 true,因爲 B 與 A 的一個子樹擁有相同的結構和節點值。

示例 1:

輸入:A = [1,2,3], B = [3,1]
輸出:false
示例 2:

輸入:A = [3,4,5,1,2], B = [4,1]
輸出:true

解題思路:

根據性質來講,若樹B是樹A的子結構,則子結構的根節點可能爲樹A的任意一個節點。因此判斷樹B是否是A的子結構需要完成以下兩步工作:

  1. 先序遍歷A中的每個節點nA(對應函數isSubStructure(A,B)
  2. 判斷A中以nA爲根節點的子樹是否包括樹B(對應函數recur(A,B))

recur(A, B) 函數:

  1. 終止條件
    1. 當節點 B 爲空:說明樹 BB 已匹配完成(越過葉子節點),因此返回 true;
    2. 當節點 A 爲空:說明已經越過樹 A 葉子節點,即匹配失敗,返回 false ;
    3. 當節點 A 和 B 的值不同:說明匹配失敗,返回 false ;
  2. 返回值:
    1. 判斷 A 和 B 的左子節點是否相等,即 recur(A.left, B.left) ;
    2. 判斷 A 和 B 的右子節點是否相等,即 recur(A.right, B.right) ;

isSubStructure(A, B) 函數:

  1. 特例處理: 當 樹 AA 爲空 或 樹 BB 爲空 時,直接返回 falsefalse ;
  2. 返回值: 若樹 BB 是樹 AA 的子結構,則必滿足以下三種情況之一,因此用或 || 連接;
    1. 以 節點 AA 爲根節點的子樹 包含樹 BB ,對應 recur(A, B);
    2. 樹 BB 是 樹 AA 左子樹 的子結構,對應 isSubStructure(A.left, B);
    3. 樹 BB 是 樹 AA 右子樹 的子結構,對應 isSubStructure(A.right, B);

以上 2. 3. 實質上是在對樹 AA先序遍歷

代碼:

class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        return (A != null && B != null) && (recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B));
    }
    boolean recur(TreeNode A, TreeNode B) {
        if(B == null) return true;
        if(A == null || A.val != B.val) return false;
        return recur(A.left, B.left) && recur(A.right, B.right);
    }
}

劍指 Offer 27. 二叉樹的鏡像

題目

請完成一個函數,輸入一個二叉樹,該函數輸出它的鏡像。

例如輸入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9
鏡像輸出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1
示例 1:

輸入:root = [4,2,7,1,3,6,9]
輸出:[4,7,2,9,6,3,1]

解題思路:

二叉樹鏡像定義: 對於二叉樹中任意節點 root,設其左 / 右子節點分別爲 left, right ;則在二叉樹的鏡像中的對應 root 節點,其左 / 右子節點分別爲 right, left 。

代碼

方法一:遞歸

根據二叉樹鏡像的定義,考慮遞歸遍歷dfs二叉樹,交換每個節點的左右子節點,即可生成二叉樹的鏡像

遞歸解析

  1. 終止條件:當節點root爲空,則返回null
  2. 遞推工作
    1. 初始化temp節點,用於暫存root的左節點
    2. 開始遞歸右子節點mirrorTree(root.right).並將返回值作爲root的左節點
    3. 開始遞歸左子節點mirrorTree(temp),並將返回值作爲root的右子節點
  3. 返回值:返回當前節點

爲什麼要暫存root的左子節點?

在遞歸右子節點root.left = mirrorTree(root.right)執行完畢之後,root.left的值已經發生改變,此時遞歸左子節點會出現問題。

class Solution {
    public TreeNode mirrorTree(TreeNode root) {
        if(root == null) return null;
        TreeNode tmp = root.left;
        root.left = mirrorTree(root.right);
        root.right = mirrorTree(tmp);
        return root;
    }
}

方法二:棧方法

利用棧(或隊列)遍歷樹的所有節點node ,並交換每個node 的左 / 右子節點。
算法流程:

  1. 特例處理: 當 root 爲空時,直接返回null ;
  2. 初始化: 棧(或隊列),本文用棧,並加入根節點 root 。
  3. 循環交換: 當棧 stack 爲空時跳出;
    1. 出棧: 記爲 node ;
    2. 添加子節點: 將 node 左和右子節點入棧;
    3. 交換: 交換node 的左 / 右子節點。
  4. 返回值: 返回根節點 root 。
代碼
package com.leetcode.offer.tree;

import com.labuladong.preDefine.TreeNode;

import java.util.Stack;

public class MirrorTree_2 {

    public TreeNode mirrorTree(TreeNode root) {
        if(root==null){
            return null;
        }
        Stack<TreeNode> stack = new Stack<>();
        //首先加入根節點
        stack.push(root);
        // 循環交換
        while (!stack.isEmpty()){
            TreeNode node = stack.pop();
            //添加子節點,將node左右子節點加入棧中
            if(node.left!=null){
                stack.push(node.left);
            }
            if(node.right!=null){
                stack.push(node.right);
            }
            //交換節點
            TreeNode temp = node.left;
            node.left=node.right;
            node.right = temp;
        }
        return root;
    }
    public static void main(String[] args) {
        TreeNode a = new TreeNode(4);
        TreeNode al = new TreeNode(2);
        TreeNode ar = new TreeNode(7);
        TreeNode all = new TreeNode(1);
        TreeNode alr = new TreeNode(3);
        TreeNode arl = new TreeNode(6);
        TreeNode arr = new TreeNode(9);
        a.left= al;
        a.right=ar;
        al.left=all;
        al.right=alr;
        ar.left= arl;
        ar.right=arr;
        System.out.println(new MirrorTree_2().mirrorTree(a));
    }
}

劍指 Offer 28. 對稱的二叉樹

題目:

請實現一個函數,用來判斷一棵二叉樹是不是對稱的。如果一棵二叉樹和它的鏡像一樣,那麼它是對稱的。

例如,二叉樹 [1,2,2,3,4,4,3] 是對稱的。

    1
   / \
  2   2
 / \ / \
3  4 4  3
但是下面這個 [1,2,2,null,3,null,3] 則不是鏡像對稱的:

    1
   / \
  2   2
   \   \
   3    3

 

示例 1:

輸入:root = [1,2,2,3,4,4,3]
輸出:true
示例 2:

輸入:root = [1,2,2,null,3,null,3]
輸出:false

解題思路:

對稱二叉樹定義: 對於樹中 任意兩個對稱節點 L 和 R,一定有:

  1. L.val = R.valL.val=R.val :即此兩對稱節點值相等。
  2. L.left.val = R.right.valL.left.val=R.right.val :即 L 的 左子節點 和 R 的 右子節點 對稱;
  3. L.right.val = R.left.valL.right.val=R.left.val :即 L的 右子節點 和 R 的 左子節點 對稱。

根據以上規律,考慮從頂至底遞歸,判斷每對節點是否對稱,從而判斷樹是否爲對稱二叉樹。

算法流程:

isSymmetric(root) :

  • 特例處理: 若根節點 root 爲空,則直接返回 true 。
  • 返回值: 即 recur(root.left, root.right) ;

recur(L, R)

終止條件

  1. 當 L 和 R同時越過葉節點: 此樹從頂至底的節點都對稱,因此返回 true ;
  2. 當 L 或 R 中只有一個越過葉節點: 此樹不對稱,因此返回 false ;
  3. 當節點 L 值 != 節點 R 值: 此樹不對稱,因此返回 false ;

遞推工作

  1. 判斷兩節點 L.left和 R.right是否對稱,即 recur(L.left, R.right) ;
  2. 判斷兩節點 L.right和 R.left是否對稱,即 recur(L.right, R.left) ;

返回值: 兩對節點都對稱時,纔是對稱樹,因此用與邏輯符 && 連接。

代碼:

package com.leetcode.offer.tree;

import com.labuladong.preDefine.TreeNode;
public class IsSymmetric {

    public boolean isSymmetric(TreeNode root) {
        if(root==null){
            return true;
        }
        return recur(root.left, root.right);
    }

    private boolean recur(TreeNode left, TreeNode right) {
        if(left==null &&right==null){
            return true;
        }
        if(left==null || right ==null|| left.val!=right.val){
            return false;
        }
        return recur(left.left, right.right) && recur(left.right, right.left);
    }
}

參考:

劍指 Offer 27. 二叉樹的鏡像(遞歸 / 輔助棧,清晰圖解)

面試題28. 對稱的二叉樹(遞歸,清晰圖解)

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