二叉樹的子結構、深度以及重建二叉樹

bmilk發表於2020-07-06

目錄

  • 二叉樹的深度
  • 平衡二叉樹
  • 二叉樹的子結構
  • 二叉樹的重建
  • 總結
  • 參考資料

二叉樹相關的套路,除了四種遍歷方式,還有很多的內容,有二叉樹的深度,將一個陣列構建成為一個二叉樹。
今天接著搞定二叉樹。

二叉樹的深度

劍指offer第55-I題,Leetcode第104題:

輸入一棵二叉樹的根節點,求該樹的深度。從根節點到葉節點依次經過的節點(含根、葉節點)形成樹的一條路徑,
最長路徑的長度為樹的深度。

例如:給定二叉樹[3,9,20,null,null,15,7],返會他的最大深度。

    3
   / \
  9  20
    /  \
   15   7

二叉樹的深度,說白了就是二叉樹有幾層,因此可以使用層序遍歷,統計有多少層即可:

使用層序遍歷的方法:

public int maxDepth(TreeNode root) {
    if (root == null) return 0;
    Queue<TreeNode> queue=new LinkedList<>();
    queue.add(root);
    TreeNode node;
    int dep=1;
    while (!queue.isEmpty()){
        int len=queue.size();
        for (int i=0;i<len;i++){
            node=queue.remove();
            if(node.left!=null){
                queue.add(node.left);
            }
            if(node.right!=null){
                queue.add(node.right);
            }
        }
        dep++;
    }
    return dep;
}
  • 時間複雜度:將整個樹每個節點訪問了兩邊即O(n)
  • 空間複雜度:額外使用了一個棧來儲存狀態O(n)

除了層序遍歷,考慮遞迴的方法,對於根節點,分別求出左子樹的深度和右子樹的深度,取他們的最大值,
然後+1即是這棵樹的深度。而左子樹右子樹同樣,則有遞迴的方法如下:

public int maxDepth(ThreeNode root){
    //代表已經出樹的結構
    if(root==null) return 0;
    return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}

時間複雜度:遍歷了所有的樹的節點,O(n)
空間複雜度:當樹退化成單鏈,則遞迴的深度為n,因此空間複雜度O(n)

平衡二叉樹

平衡二叉樹定義:如果某二叉樹中任意節點的左右子樹的深度相差不超過1,那麼它就是一棵平衡二叉樹。

劍指offer第55-II題,Leetcode第110題:

輸入一棵二叉樹的根節點,判斷該樹是不是平衡二叉樹。

平衡二叉樹的概念講的很清楚,要求任意一節點的左右子樹的深度差不能超過1,那麼分別求出每個子樹的
左右子樹的深度,求差即可判斷:

public boolean isBalanced(TreeNode root){
    if(root==null) return true;
    //分別判斷左子樹和右子樹是不是平衡樹,然後判斷整棵樹是不是平衡樹
    return isBalanced(root.left)&&isBalanced(root.right)&&(Math.abs(maxDepth(root.left)-maxDepth(root.right))<2);
}

上面程式碼計算判斷每一個節點開始的二叉樹都會判斷他的所有子樹是不是平衡二叉樹,進行了大量的重複計算,那如何省去這些重複的計算呢?

答案是從葉結點開始計算,判斷每一個葉結點為根的子樹,是平衡樹則返回他的樹高,如果不是則返回-1,然後對其父父節點為根的子樹,使用返回的樹高進行計算。

    public boolean isBalanced2(TreeNode root){
        return recur(root)!=-1;
    }

    public int recur(TreeNode root){
        if (root==null) return 0;
        int left=recur(root.left);
        if(left==-1) return -1;
        int right=recur(root.right);
        if(right==-1) return -1;
        return Math.abs(left-right)>1?-1:Math.max(left,right)+1;
    }

二叉樹的子結構

劍指offer第26題:

輸入兩棵二叉樹A和B,判斷B是不是A的子結構。(約定空樹不是任意一個樹的子結構),B是A的子結構, 即 A中有出現和B相同的結構和節點值。

例如:

給定的樹 A:

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

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

這個題我第一反應,要用A樹種的每一個節點作為A的某棵子樹的根節點和B進行比較。
所以我覺得需要用個佇列或者棧或者列表將遍歷整個樹得到的元素儲存起來,再一一判斷。
但是我已經遍歷樂一遍了,在遍歷的時候遍歷到當前節點就對以當前節點為根的子樹進行判斷,不好嗎?

public boolean isSubStructure(TreeNode A, TreeNode B){
    if(A==null || B==null) return false;
    return help(A,B)||isSubStructure(A.left,B)||isSubStructure(A.right,B);
}

//從A的根和B的跟開始判斷是不是一樣的結構
//這句話不知道怎麼描述
public boolean help(TreeNode A, TreeNode B){
    if(B==null) reurn false;
    if(A==null || A.val!=B.val) return false;
    return help(A.left,B.left)&&help(A.right,B.right);
}

二叉樹的重建

二叉樹的重建,嗯。。。想起來挺簡單,做起來挺不簡單的。。。

劍指offer第7題,Leetcode第105題:

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。

例如:

前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]

返回如下二叉樹

    3
   / \
  9  20
    /  \
   15   7

怎麼說,剛看到這個題目一臉茫然,好像大學學過,但是呢,,,嗯學的慘不忍睹。。。

  • 首先:二叉樹不管前序,中序還是後序遍歷,得到的元素的個數,一定是一樣的
  • 其次:前序遍歷的第一個節點一定是整個樹的根
  • 第三:中序遍歷中,排在根節點前面的一定是左子樹的節點,後面的一定是右子樹的節點
  • 第四:如果根節點存在左子樹,那麼前序遍歷中,根節點後面跟的一定是左子樹的值,數量和中序遍歷中左子樹的個數一樣
  • 最後,還有一句保證裡面沒有重複元素來保證我們在中序遍歷中查詢根節點的時候是唯一的。

至此,可以將中序便利的陣列分為三部分,根節點,左子樹和右子樹,對於左右子樹,他們又是一個完成的樹結構,重複以上方法即可。

什麼意思呢?對於上面的題目:

前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]

根據前序遍歷可以得到根節點是3 
將中序遍歷的結果分為3部分 左子樹[9],根節點[3],右子樹[15,20,7]
        3
       / \
      9  [15,20,7]

在前序遍歷中對應的[15,20,7]的遍歷結果是[20,15,7]
重複上述過程則有

    3
   / \
  9  20
    /  \
   15   7

上程式碼:

public TreeNode buildTree(int[] preorder, int[] inorder) {
    if(preorder==null||preorder.size()==0) return null;
    int len=preorder.size();
    Map<Integer,Integer> indexMap=new HashMap<>();
    for(int i=0;i<len;i++){
        indexMap.put(inorder[i],i);
    }
    
}

public TreeNode buildTree(int[] preorder, int preStrat,int preEnd,
                          int[] inorder, int inStart, int inEnd,
                          Map<Integer,Integer> indexMap){
    if(preStart>preend) return null;
    int rootVal=preorder[preStart];
    TreeNode root =new TreeNode(rootVal);
    int rootIndex=indexMap.get(rootVal);
    int leftNodes=rootIndex-instart;
    int rigthNodes=inEnd-rootIndex;
    root.left=buildTree(preorder,preStrat+1,preStrat+leftNodes,
                        inorder,inStart,rootIndex-1,
                        indexMap);
    root.right=buildTree(preorder,preEnd-rightNodes+1,preEnd,
                         inorder,rootIndex+1,inEnd,
                         indexMap);
    return root;
    
}

總結

對於樹的各種操作分治思想是一個很奇妙的思想,要判斷一整棵樹,可以先判斷左右子樹,依次遞迴,注意遞迴退出的邊界條件。

參考資料

  • leetcode對應題號以及解析

相關文章