大家好,我是程式設計熊。
往期我們一起學習了《線性表》相關知識。
本期我們一起學習二叉樹,二叉樹的問題,大多以遞迴為基礎,根據題目的要求,在遞迴過程中記錄關鍵資訊,進而解決問題。
如果還未學習遞迴的同學,程式設計熊後續會講解遞迴,建議學習遞迴後再來做二叉樹相關題目,但並不影響學習二叉樹基礎知識部分。
本文將從以下幾個方面展開,學習完可以解決面試常見的二叉樹問題。
二叉樹概述和定義
顧名思義,二叉樹的每個節點最多有兩個子節點,下圖展示了常見的二叉樹。
二叉樹的定義方式
二叉樹是由許多節點組成,節點有資料域、指標域,節點之間通過指標連結,形成一棵樹。
二叉樹節點的定義方式(C++程式碼):
struct TreeNode {
// 節點資料
int val;
// 指向左兒子的指標
TreeNode *left;
// 指向右兒子的指標
TreeNode *right;
// 建構函式
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
二叉樹儲存方式
二叉樹常見的儲存方式有:
- 鏈式儲存: 連結串列的思想,通過指標定位。
- 陣列儲存: 陣列的下標也是一個索引,可以定位到節點,陣列儲存的資料即為節點資料。
下圖是鏈式儲存的示意圖。
下圖是陣列儲存的示意圖。
二叉樹分類
二叉樹根據節點的分佈位置、節點數值的排列方式,分為以下幾種。
- 滿二叉樹
- 完全二叉樹
- 平衡二叉樹
- 二叉搜尋樹
下面,我將從分別討論以上幾種二叉樹特點。
滿二叉樹
滿二叉樹的特點有:
- 除最後一層無子節點外,其餘每層的節點都有兩個子節點。
下圖是一個滿二叉樹。
完全二叉樹
完全二叉樹的特點有:
- 至多隻有最後一的沒有被填充滿,其餘每層都被填充滿。
- 如果最後一層沒有填充滿,那麼所有的節點都在最後一層左邊的位置上。
下圖演示了一個完全二叉樹。
平衡二叉樹
平衡二叉樹的特點有:
- 空樹 或 左右兩個子樹的高度差 的絕對值不超過1。
- 左右兩個子樹都是一個平衡二叉樹。
下圖演示了一個平衡二叉樹。
二叉搜尋樹
二叉搜尋樹的特點有:
- 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值。
- 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值
下圖演示了一個二叉搜尋樹。
二叉樹遍歷方式
二叉樹遍歷方式根據遍歷節點的先後順序,分為以下幾種。
- 前序遍歷
- 中序遍歷
- 後序遍歷
前序遍歷
前序遍歷節點的順序是:根、左、右。
下面是前序遍歷的虛擬碼。
// 虛擬碼
// 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 104. 二叉樹的最大深度
題意
給定一個二叉樹,找出其最大深度。
二叉樹的深度為根節點到最遠葉子節點的最長路徑上的節點數。
說明: 葉子節點是指沒有子節點的節點。
示例
給定二叉樹 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
題解
從根節點遞迴遍歷每個節點,同時記錄每個節點的深度。
每個點的深度等於父節點深度+1,根節點的深度設為1。
下圖為求二叉樹的最大深度的示意圖。
程式碼
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr)
return 0;
return 1 + max(maxDepth(root->left), maxDepth(root->right));
}
};
LeetCode 110. 平衡二叉樹
題意
給定一個二叉樹,判斷它是否是高度平衡的二叉樹。
本題中,一棵高度平衡二叉樹定義為:一個二叉樹每個節點 的左右兩個子樹的高度差的絕對值不超過 1 。
示例
輸入: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
,返回它節點值的 前序 遍歷。
示例
輸入: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
。請構造二叉樹並返回其根節點。
示例
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
題解
前序遍歷的順序是: 根、左、右。因此前序遍歷陣列的第一個節點就是根,因此前序序列可以快速定位根。
中序遍歷的順序是: 左、根、右。根據前序序列找到根,可以將左子樹和右子樹 分割 開,同時可以知道左子樹的節點數量和右子樹的節點數量。
下圖是思路示意圖。
因此我們可以利用前序遍歷快速定位根,再利用中序遍歷將左子樹和右子樹 分割 開,並知道左右子樹的節點數量。
程式碼實現上,可以通過遞迴不斷的劃分左右子樹,左右子樹分別返回其根節點,就是根節點的左兒子、右兒子,細節可以看下面程式碼,很容易理解。
程式碼
// 程式碼來源於下面連結, 根據自己偏好, 進行了修改。
// 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);
}
};
習題推薦
- LeetCode 98. 驗證二叉搜尋樹
- LeetCode 637. 二叉樹的層平均值
- LeetCode 112. 路徑總和
- LeetCode 543. 二叉樹的直徑
- LeetCode 106. 從中序與後序遍歷序列構造二叉樹
我是程式設計熊,我們下期見。