程式設計熊講解LeetCode演算法《二叉樹》

公眾號【程式設計熊】 發表於 2021-08-14
演算法 LeetCode

大家好,我是程式設計熊。

往期我們一起學習了《線性表》相關知識。

本期我們一起學習二叉樹,二叉樹的問題,大多以遞迴為基礎,根據題目的要求,在遞迴過程中記錄關鍵資訊,進而解決問題。

如果還未學習遞迴的同學,程式設計熊後續會講解遞迴,建議學習遞迴後再來做二叉樹相關題目,但並不影響學習二叉樹基礎知識部分。

本文將從以下幾個方面展開,學習完可以解決面試常見的二叉樹問題。

程式設計熊講解LeetCode演算法《二叉樹》

二叉樹概述和定義

顧名思義,二叉樹的每個節點最多有兩個子節點,下圖展示了常見的二叉樹。

程式設計熊講解LeetCode演算法《二叉樹》

二叉樹的定義方式

二叉樹是由許多節點組成,節點有資料域、指標域,節點之間通過指標連結,形成一棵樹。

二叉樹節點的定義方式(C++程式碼):

struct TreeNode {
  	// 節點資料
    int val;
    // 指向左兒子的指標
    TreeNode *left;
    // 指向右兒子的指標
    TreeNode *right;
    // 建構函式
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

二叉樹儲存方式

二叉樹常見的儲存方式有:

  1. 鏈式儲存: 連結串列的思想,通過指標定位。
  2. 陣列儲存: 陣列的下標也是一個索引,可以定位到節點,陣列儲存的資料即為節點資料。

下圖是鏈式儲存的示意圖。

程式設計熊講解LeetCode演算法《二叉樹》

下圖是陣列儲存的示意圖。

程式設計熊講解LeetCode演算法《二叉樹》

二叉樹分類

二叉樹根據節點的分佈位置、節點數值的排列方式,分為以下幾種。

  • 滿二叉樹
  • 完全二叉樹
  • 平衡二叉樹
  • 二叉搜尋樹

下面,我將從分別討論以上幾種二叉樹特點。

滿二叉樹

滿二叉樹的特點有:

  1. 除最後一層無子節點外,其餘每層的節點都有兩個子節點。

下圖是一個滿二叉樹。

程式設計熊講解LeetCode演算法《二叉樹》

完全二叉樹

完全二叉樹的特點有:

  1. 至多隻有最後一的沒有被填充滿,其餘每層都被填充滿。
  2. 如果最後一層沒有填充滿,那麼所有的節點都在最後一層左邊的位置上。

下圖演示了一個完全二叉樹。

程式設計熊講解LeetCode演算法《二叉樹》

平衡二叉樹

平衡二叉樹的特點有:

  1. 空樹 或 左右兩個子樹的高度差 的絕對值不超過1。
  2. 左右兩個子樹都是一個平衡二叉樹。

下圖演示了一個平衡二叉樹。

程式設計熊講解LeetCode演算法《二叉樹》

二叉搜尋樹

二叉搜尋樹的特點有:

  1. 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值。
  2. 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值

下圖演示了一個二叉搜尋樹。

程式設計熊講解LeetCode演算法《二叉樹》

二叉樹遍歷方式

二叉樹遍歷方式根據遍歷節點的先後順序,分為以下幾種。

  • 前序遍歷
  • 中序遍歷
  • 後序遍歷

前序遍歷

前序遍歷節點的順序是:根、左、右。

下面是前序遍歷的虛擬碼。

// 虛擬碼
// root left right

// 遍歷根
dfs(root);

// 遍歷左節點
dfs(left);

// 遍歷右節點
dfs(right);

中序遍歷

中序遍歷節點的順序是:左、根、右。

下面是中序遍歷的虛擬碼。

// 虛擬碼
// 順序: left root right

// 遍歷左節點
dfs(left);

// 遍歷根
dfs(root);

// 遍歷右節點
dfs(right);

後序遍歷

後序遍歷節點的順序是:左、右、根。

下面是後序遍歷的虛擬碼。

// 虛擬碼
// 順序: left right root

// 遍歷左節點
dfs(left);

// 遍歷右節點
dfs(right);

// 遍歷根
dfs(root);

舉例分析

下圖以一棵樹,分別演示了前序、中序、後續遍歷的結果。

程式設計熊講解LeetCode演算法《二叉樹》

例題

LeetCode 104. 二叉樹的最大深度

題意

給定一個二叉樹,找出其最大深度。

二叉樹的深度為根節點到最遠葉子節點的最長路徑上的節點數。

說明: 葉子節點是指沒有子節點的節點。

示例

給定二叉樹 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

題解

從根節點遞迴遍歷每個節點,同時記錄每個節點的深度。

每個點的深度等於父節點深度+1,根節點的深度設為1。

下圖為求二叉樹的最大深度的示意圖。

程式設計熊講解LeetCode演算法《二叉樹》

程式碼

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) 
          return 0;
        return 1 +  max(maxDepth(root->left), maxDepth(root->right));
    }
};

LeetCode 110. 平衡二叉樹

題意

給定一個二叉樹,判斷它是否是高度平衡的二叉樹。

本題中,一棵高度平衡二叉樹定義為:一個二叉樹每個節點 的左右兩個子樹的高度差的絕對值不超過 1 。

示例

程式設計熊講解LeetCode演算法《二叉樹》

輸入:root = [3,9,20,null,null,15,7]
輸出:true

題解

遞迴記錄每個節點左右子樹的高度差,平衡二叉樹定義: 左右兩個子樹的高度差的絕對值不超過1。若超過則不是平衡二叉樹。

程式碼

class Solution {
public:
    bool isBalanced(TreeNode* root) {
        return dfs(root) >= 0;
    }
                    
    int dfs(TreeNode* root) {
        if (root == nullptr) {
            return 0;
        }
        // 記錄左子樹的高度
        int left_height = dfs(root->left);
      	
      	// 記錄右子樹的高度
        int right_height = dfs(root->right);
        
      	// 根據左右子樹深度,判斷是否滿足平衡二叉樹條件
        if (abs(left_height - right_height) > 1 || left_height == -1 || right_height == -1) {
            return -1;
        } 
        
      	// 返回當前節點的子樹的最大深度
        return 1 + max(left_height, right_height);
    }
};

LeetCode 144. 二叉樹的前序遍歷

題意

給你二叉樹的根節點 root ,返回它節點值的 前序 遍歷。

示例

程式設計熊講解LeetCode演算法《二叉樹》

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

題解

根據前序遍歷定義的遍歷順序,即根、左、右的順序遍歷二叉樹。

程式碼

class Solution {
public:
    vector<int> preorderTraversal(TreeNode *root) {
        vector<int> ans;
        dfs(root, ans);
        return ans;
    }
                    
    void dfs(TreeNode *root, vector<int> &ans) {
        if (root == nullptr) {
            return;
        }
        
        ans.push_back(root->val);
        
        dfs(root->left, ans);
        dfs(root->right, ans);
    }
};

LeetCode 105. 從前序與中序遍歷序列構造二叉樹

題意

給定一棵樹的前序遍歷 preorder 與中序遍歷 inorder。請構造二叉樹並返回其根節點。

示例

程式設計熊講解LeetCode演算法《二叉樹》

Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]

題解

前序遍歷的順序是: 根、左、右。因此前序遍歷陣列的第一個節點就是根,因此前序序列可以快速定位根。

中序遍歷的順序是: 左、根、右。根據前序序列找到根,可以將左子樹和右子樹 分割 開,同時可以知道左子樹的節點數量和右子樹的節點數量。

下圖是思路示意圖。

程式設計熊講解LeetCode演算法《二叉樹》

因此我們可以利用前序遍歷快速定位根,再利用中序遍歷將左子樹和右子樹 分割 開,並知道左右子樹的節點數量。

程式碼實現上,可以通過遞迴不斷的劃分左右子樹,左右子樹分別返回其根節點,就是根節點的左兒子、右兒子,細節可以看下面程式碼,很容易理解。

程式碼

// 程式碼來源於下面連結, 根據自己偏好, 進行了修改。
// https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solution/cong-qian-xu-yu-zhong-xu-bian-li-xu-lie-gou-zao-9/
struct TreeNode {
     int val;
     TreeNode *left;
     TreeNode *right;
     TreeNode() : val(0), left(nullptr), right(nullptr) {}
     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

class Solution {
public:
    unordered_map<int, int> index;
    TreeNode* myBuildTree(vector<int>& preorder, vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return nullptr;
        }

        // 前序遍歷中的第一個節點就是根節點
        int preorder_root = preorder_left;
        // 在中序遍歷中定位根節點
        int inorder_root = index[preorder[preorder_root]];

        // 先把根節點建立出來
        TreeNode* root = new TreeNode(preorder[preorder_root]);
        // 得到左子樹中的節點數目
        int size_left_subtree = inorder_root - inorder_left;
        // 遞迴地構造左子樹,並連線到根節點
        // 先序遍歷中「從 左邊界+1 開始的 size_left_subtree」個元素就對應了中序遍歷中「從 左邊界 開始到 根節點定位-1」的元素
        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 遞迴地構造右子樹,並連線到根節點
        // 先序遍歷中「從 左邊界+1+左子樹節點數目 開始到 右邊界」的元素就對應了中序遍歷中「從 根節點定位+1 到 右邊界」的元素
        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int size = preorder.size();
        // 構造雜湊對映,快速定位中序遍歷的根節點
        for (int i = 0; i <= size - 1; ++i) {
            index[inorder[i]] = i;
        }
        return myBuildTree(preorder, inorder, 0, size - 1, 0, size - 1);
    }
};

習題推薦

  1. LeetCode 98. 驗證二叉搜尋樹
  2. LeetCode 637. 二叉樹的層平均值
  3. LeetCode 112. 路徑總和
  4. LeetCode 543. 二叉樹的直徑
  5. LeetCode 106. 從中序與後序遍歷序列構造二叉樹

我是程式設計熊,我們下期見。