面試題 07. 重建二叉樹
前中序構建
要根據二叉樹的前序遍歷和中序遍歷結果來構建二叉樹,我們可以利用以下性質:
- 前序遍歷的第一個元素總是當前樹的根節點。
- 中序遍歷中,根節點將二叉樹分為左子樹和右子樹。
思路
- 根據前序遍歷的第一個元素確定根節點。
- 在中序遍歷中找到根節點位置,這樣可以確定左子樹和右子樹的元素。
- 遞迴地對左子樹和右子樹執行同樣的操作,構建二叉樹。
遞迴構建的步驟:
- 在前序遍歷中,根節點是第一個元素。
- 找到這個根節點在中序遍歷中的位置,左側的所有元素屬於左子樹,右側的所有元素屬於右子樹。
- 根據這個分割,遞迴構建左子樹和右子樹。
C++ 實現
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
// 定義二叉樹節點
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class Solution {
public:
// 雜湊表用於快速查詢中序遍歷中根節點的位置
unordered_map<int, int> inorder_map;
// 構建二叉樹函式
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
// 將中序遍歷的值和它們對應的下標存入雜湊表,便於快速查詢根節點位置
for (int i = 0; i < inorder.size(); ++i) {
inorder_map[inorder[i]] = i;
}
return build(preorder, 0, preorder.size() - 1, 0, inorder.size() - 1);
}
private:
TreeNode* build(const vector<int>& preorder, int preStart, int preEnd, int inStart, int inEnd) {
// 如果已經沒有節點需要構建,返回空
if (preStart > preEnd || inStart > inEnd) {
return nullptr;
}
// 前序遍歷的第一個節點是當前樹的根節點
int rootVal = preorder[preStart];
TreeNode* root = new TreeNode(rootVal);
// 在中序遍歷中找到根節點的位置
int inRoot = inorder_map[rootVal];
// 左子樹的節點數目
int leftTreeSize = inRoot - inStart;
// 遞迴構建左子樹和右子樹
root->left = build(preorder, preStart + 1, preStart + leftTreeSize, inStart, inRoot - 1);
root->right = build(preorder, preStart + leftTreeSize + 1, preEnd, inRoot + 1, inEnd);
return root;
}
};
// 測試函式
void printTree(TreeNode* root) {
if (root == nullptr) {
return;
}
cout << root->val << " "; // 列印根節點
printTree(root->left); // 遞迴列印左子樹
printTree(root->right); // 遞迴列印右子樹
}
int main() {
vector<int> preorder = {3, 9, 20, 15, 7}; // 前序遍歷
vector<int> inorder = {9, 3, 15, 20, 7}; // 中序遍歷
Solution sol;
TreeNode* root = sol.buildTree(preorder, inorder);
// 列印構建後的二叉樹,前序遍歷方式
printTree(root); // 輸出: 3 9 20 15 7
return 0;
}
程式碼解析
-
雜湊表最佳化:我們使用
unordered_map
將中序遍歷的每個值與它在陣列中的位置進行對映,以便快速查詢根節點在中序遍歷中的位置,時間複雜度從 O(n) 降到 O(1)。 -
遞迴函式
build
:preStart
和preEnd
分別表示前序遍歷中當前子樹的起始和結束位置。inStart
和inEnd
表示中序遍歷中當前子樹的起始和結束位置。- 每次從前序遍歷中獲取根節點,然後在中序遍歷中找到這個根節點的位置,從而將左右子樹分割開。
- 遞迴地構建左右子樹。
-
時間複雜度:O(n),因為每個節點都需要訪問一次。
-
空間複雜度:O(n),用於儲存雜湊表和遞迴呼叫棧。
測試輸出
對於輸入的前序遍歷 [3, 9, 20, 15, 7]
和中序遍歷 [9, 3, 15, 20, 7]
,構建出的二叉樹結構如下:
3
/ \
9 20
/ \
15 7
輸出的前序遍歷為:3 9 20 15 7
,與原始前序遍歷一致,說明二叉樹構建正確。
中後序構建
使用中序遍歷和後序遍歷結果構建二叉樹的過程與前序和中序構建類似,不過需要注意:
- 在後序遍歷中,最後一個元素是當前樹的根節點。
- 在中序遍歷中,根節點將二叉樹分為左子樹和右子樹。
- 遞迴地對左子樹和右子樹執行同樣的操作,構建二叉樹。
構建步驟
- 後序遍歷的最後一個元素即為當前樹的根節點。
- 在中序遍歷中找到根節點的位置,從而將左子樹和右子樹分開。
- 遞迴地構建左右子樹。
C++ 實現
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
// 二叉樹節點的定義
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class Solution {
public:
unordered_map<int, int> inorder_map; // 雜湊表儲存中序遍歷的索引,方便查詢
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
// 儲存中序遍歷值對應的索引
for (int i = 0; i < inorder.size(); ++i) {
inorder_map[inorder[i]] = i;
}
return build(inorder, postorder, 0, inorder.size() - 1, 0, postorder.size() - 1);
}
private:
TreeNode* build(const vector<int>& inorder, const vector<int>& postorder, int inStart, int inEnd, int postStart, int postEnd) {
// 遞迴終止條件
if (inStart > inEnd || postStart > postEnd) {
return nullptr;
}
// 後序遍歷的最後一個元素是根節點
int rootVal = postorder[postEnd];
TreeNode* root = new TreeNode(rootVal);
// 在中序遍歷中找到根節點的位置
int inRoot = inorder_map[rootVal];
int leftTreeSize = inRoot - inStart; // 左子樹的節點數目
// 遞迴構建左子樹和右子樹
root->left = build(inorder, postorder, inStart, inRoot - 1, postStart, postStart + leftTreeSize - 1);
root->right = build(inorder, postorder, inRoot + 1, inEnd, postStart + leftTreeSize, postEnd - 1);
return root;
}
};
// 列印樹的前序遍歷
void printPreOrder(TreeNode* root) {
if (root == nullptr) return;
cout << root->val << " ";
printPreOrder(root->left);
printPreOrder(root->right);
}
int main() {
vector<int> inorder = {9, 3, 15, 20, 7}; // 中序遍歷
vector<int> postorder = {9, 15, 7, 20, 3}; // 後序遍歷
Solution sol;
TreeNode* root = sol.buildTree(inorder, postorder);
// 輸出前序遍歷的結果,以檢查樹是否構建正確
printPreOrder(root); // 輸出: 3 9 20 15 7
return 0;
}
程式碼解析
-
構建雜湊表:
- 使用
unordered_map
儲存中序遍歷中每個值的索引,方便快速找到根節點在中序遍歷中的位置。
- 使用
-
遞迴構建函式
build
:inStart
和inEnd
表示當前子樹在中序遍歷中的範圍。postStart
和postEnd
表示當前子樹在後序遍歷中的範圍。- 從後序遍歷的最後一個元素獲取根節點,並在中序遍歷中找到該節點的位置,進而劃分左右子樹。
- 遞迴呼叫
build
分別構建左右子樹。
-
測試:
- 構建的樹結構:
3 / \ 9 20 / \ 15 7
- 使用前序遍歷列印輸出:
3 9 20 15 7
,驗證構建的樹結構正確。
- 構建的樹結構:
複雜度分析
- 時間複雜度:O(n),遍歷所有節點,並在雜湊表中查詢根節點索引的操作是 O(1)。
- 空間複雜度:O(n),用於儲存雜湊表和遞迴呼叫棧。
層序編列
1
/ \
2 3
\ / \
4 5 6
面試題 09. 用兩個棧實現佇列
這個問題可以用兩個棧來實現,用一個棧儲存歸還的書籍(push 操作),而借出書籍時(pop 操作)可以透過另一個棧來保證借出的是最早歸還的書。具體思路如下:
- 使用兩個棧
stack1
和stack2
。stack1
用於存放歸還的書籍,stack2
用於按順序借出書籍。 push
操作:直接將書籍編號壓入stack1
。pop
操作:如果stack2
為空,則將stack1
中的所有元素依次彈出並壓入stack2
,這樣最早歸還的書會在stack2
的頂部。然後從stack2
彈出頂部的元素,返回該書籍編號。
在實現中,這種方式保證了最早歸還的書優先借出。
以下是 C++ 程式碼:
#include <stack>
class BookQueue {
private:
std::stack<int> stack1; // 用於歸還書籍
std::stack<int> stack2; // 用於借出書籍
public:
BookQueue() {}
void push(int bookID) {
stack1.push(bookID);
}
int pop() {
if (stack2.empty()) {
while (!stack1.empty()) {
stack2.push(stack1.top());
stack1.pop();
}
}
if (stack2.empty()) {
return -1; // 沒有書可以借出
}
int bookID = stack2.top();
stack2.pop();
return bookID;
}
};
示例
BookQueue bookQueue;
bookQueue.push(1); // 書佇列為 [1]
bookQueue.push(2); // 書佇列為 [1, 2]
std::cout << bookQueue.pop(); // 返回 1,書佇列變為 [2]
解釋
push
操作只涉及stack1
,時間複雜度為 (O(1))。pop
操作在stack2
不為空時為 (O(1));如果stack2
為空時,則需要將stack1
的元素移動到stack2
中,平均時間複雜度仍為 (O(1))。
這樣,我們就可以實現按照順序的借書和還書功能了。