12.3 構建二元樹問題¶
Question
給定一棵二元樹的前序走訪 preorder
和中序走訪 inorder
,請從中構建二元樹,返回二元樹的根節點。假設二元樹中沒有值重複的節點(如圖 12-5 所示)。
圖 12-5 構建二元樹的示例資料
1. 判斷是否為分治問題¶
原問題定義為從 preorder
和 inorder
構建二元樹,是一個典型的分治問題。
- 問題可以分解:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
- 子問題是獨立的:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
- 子問題的解可以合併:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。
2. 如何劃分子樹¶
根據以上分析,這道題可以使用分治來求解,但如何透過前序走訪 preorder
和中序走訪 inorder
來劃分左子樹和右子樹呢?
根據定義,preorder
和 inorder
都可以劃分為三個部分。
- 前序走訪:
[ 根節點 | 左子樹 | 右子樹 ]
,例如圖 12-5 的樹對應[ 3 | 9 | 2 1 7 ]
。 - 中序走訪:
[ 左子樹 | 根節點 | 右子樹 ]
,例如圖 12-5 的樹對應[ 9 | 3 | 1 2 7 ]
。
以上圖資料為例,我們可以透過圖 12-6 所示的步驟得到劃分結果。
- 前序走訪的首元素 3 是根節點的值。
- 查詢根節點 3 在
inorder
中的索引,利用該索引可將inorder
劃分為[ 9 | 3 | 1 2 7 ]
。 - 根據
inorder
的劃分結果,易得左子樹和右子樹的節點數量分別為 1 和 3 ,從而可將preorder
劃分為[ 3 | 9 | 2 1 7 ]
。
圖 12-6 在前序走訪和中序走訪中劃分子樹
3. 基於變數描述子樹區間¶
根據以上劃分方法,我們已經得到根節點、左子樹、右子樹在 preorder
和 inorder
中的索引區間。而為了描述這些索引區間,我們需要藉助幾個指標變數。
- 將當前樹的根節點在
preorder
中的索引記為 \(i\) 。 - 將當前樹的根節點在
inorder
中的索引記為 \(m\) 。 - 將當前樹在
inorder
中的索引區間記為 \([l, r]\) 。
如表 12-1 所示,透過以上變數即可表示根節點在 preorder
中的索引,以及子樹在 inorder
中的索引區間。
表 12-1 根節點和子樹在前序走訪和中序走訪中的索引
根節點在 preorder 中的索引 |
子樹在 inorder 中的索引區間 |
|
---|---|---|
當前樹 | \(i\) | \([l, r]\) |
左子樹 | \(i + 1\) | \([l, m-1]\) |
右子樹 | \(i + 1 + (m - l)\) | \([m+1, r]\) |
請注意,右子樹根節點索引中的 \((m-l)\) 的含義是“左子樹的節點數量”,建議結合圖 12-7 理解。
圖 12-7 根節點和左右子樹的索引區間表示
4. 程式碼實現¶
為了提升查詢 \(m\) 的效率,我們藉助一個雜湊表 hmap
來儲存陣列 inorder
中元素到索引的對映:
def dfs(
preorder: list[int],
inorder_map: dict[int, int],
i: int,
l: int,
r: int,
) -> TreeNode | None:
"""構建二元樹:分治"""
# 子樹區間為空時終止
if r - l < 0:
return None
# 初始化根節點
root = TreeNode(preorder[i])
# 查詢 m ,從而劃分左右子樹
m = inorder_map[preorder[i]]
# 子問題:構建左子樹
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
# 子問題:構建右子樹
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
# 返回根節點
return root
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
"""構建二元樹"""
# 初始化雜湊表,儲存 inorder 元素到索引的對映
inorder_map = {val: i for i, val in enumerate(inorder)}
root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)
return root
/* 構建二元樹:分治 */
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return NULL;
// 初始化根節點
TreeNode *root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
return root;
}
/* 構建二元樹:分治 */
TreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return null;
// 初始化根節點
TreeNode root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode buildTree(int[] preorder, int[] inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Map<Integer, Integer> inorderMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderMap.put(inorder[i], i);
}
TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
/* 構建二元樹:分治 */
TreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {
// 子樹區間為空時終止
if (r - l < 0)
return null;
// 初始化根節點
TreeNode root = new(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode? BuildTree(int[] preorder, int[] inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Dictionary<int, int> inorderMap = [];
for (int i = 0; i < inorder.Length; i++) {
inorderMap.TryAdd(inorder[i], i);
}
TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);
return root;
}
/* 構建二元樹:分治 */
func dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {
// 子樹區間為空時終止
if r-l < 0 {
return nil
}
// 初始化根節點
root := NewTreeNode(preorder[i])
// 查詢 m ,從而劃分左右子樹
m := inorderMap[preorder[i]]
// 子問題:構建左子樹
root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)
// 子問題:構建右子樹
root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)
// 返回根節點
return root
}
/* 構建二元樹 */
func buildTree(preorder, inorder []int) *TreeNode {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
inorderMap := make(map[int]int, len(inorder))
for i := 0; i < len(inorder); i++ {
inorderMap[inorder[i]] = i
}
root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)
return root
}
/* 構建二元樹:分治 */
func dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {
// 子樹區間為空時終止
if r - l < 0 {
return nil
}
// 初始化根節點
let root = TreeNode(x: preorder[i])
// 查詢 m ,從而劃分左右子樹
let m = inorderMap[preorder[i]]!
// 子問題:構建左子樹
root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)
// 子問題:構建右子樹
root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)
// 返回根節點
return root
}
/* 構建二元樹 */
func buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)
}
/* 構建二元樹:分治 */
function dfs(preorder, inorderMap, i, l, r) {
// 子樹區間為空時終止
if (r - l < 0) return null;
// 初始化根節點
const root = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
const m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
function buildTree(preorder, inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = new Map();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
/* 構建二元樹:分治 */
function dfs(
preorder: number[],
inorderMap: Map<number, number>,
i: number,
l: number,
r: number
): TreeNode | null {
// 子樹區間為空時終止
if (r - l < 0) return null;
// 初始化根節點
const root: TreeNode = new TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
const m = inorderMap.get(preorder[i]);
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let inorderMap = new Map<number, number>();
for (let i = 0; i < inorder.length; i++) {
inorderMap.set(inorder[i], i);
}
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
/* 構建二元樹:分治 */
TreeNode? dfs(
List<int> preorder,
Map<int, int> inorderMap,
int i,
int l,
int r,
) {
// 子樹區間為空時終止
if (r - l < 0) {
return null;
}
// 初始化根節點
TreeNode? root = TreeNode(preorder[i]);
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]]!;
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode? buildTree(List<int> preorder, List<int> inorder) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
Map<int, int> inorderMap = {};
for (int i = 0; i < inorder.length; i++) {
inorderMap[inorder[i]] = i;
}
TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
return root;
}
/* 構建二元樹:分治 */
fn dfs(
preorder: &[i32],
inorder_map: &HashMap<i32, i32>,
i: i32,
l: i32,
r: i32,
) -> Option<Rc<RefCell<TreeNode>>> {
// 子樹區間為空時終止
if r - l < 0 {
return None;
}
// 初始化根節點
let root = TreeNode::new(preorder[i as usize]);
// 查詢 m ,從而劃分左右子樹
let m = inorder_map.get(&preorder[i as usize]).unwrap();
// 子問題:構建左子樹
root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);
// 子問題:構建右子樹
root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);
// 返回根節點
Some(root)
}
/* 構建二元樹 */
fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
let mut inorder_map: HashMap<i32, i32> = HashMap::new();
for i in 0..inorder.len() {
inorder_map.insert(inorder[i], i as i32);
}
let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);
root
}
/* 構建二元樹:分治 */
TreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {
// 子樹區間為空時終止
if (r - l < 0)
return NULL;
// 初始化根節點
TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));
root->val = preorder[i];
root->left = NULL;
root->right = NULL;
// 查詢 m ,從而劃分左右子樹
int m = inorderMap[preorder[i]];
// 子問題:構建左子樹
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);
// 子問題:構建右子樹
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);
// 返回根節點
return root;
}
/* 構建二元樹 */
TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);
for (int i = 0; i < inorderSize; i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);
free(inorderMap);
return root;
}
/* 構建二元樹:分治 */
fun dfs(
preorder: IntArray,
inorderMap: Map<Int?, Int?>,
i: Int,
l: Int,
r: Int
): TreeNode? {
// 子樹區間為空時終止
if (r - l < 0) return null
// 初始化根節點
val root = TreeNode(preorder[i])
// 查詢 m ,從而劃分左右子樹
val m = inorderMap[preorder[i]]!!
// 子問題:構建左子樹
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)
// 子問題:構建右子樹
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)
// 返回根節點
return root
}
/* 構建二元樹 */
fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
// 初始化雜湊表,儲存 inorder 元素到索引的對映
val inorderMap = HashMap<Int?, Int?>()
for (i in inorder.indices) {
inorderMap[inorder[i]] = i
}
val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)
return root
}
### 構建二元樹:分治 ###
def dfs(preorder, inorder_map, i, l, r)
# 子樹區間為空時終止
return if r - l < 0
# 初始化根節點
root = TreeNode.new(preorder[i])
# 查詢 m ,從而劃分左右子樹
m = inorder_map[preorder[i]]
# 子問題:構建左子樹
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
# 子問題:構建右子樹
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
# 返回根節點
root
end
### 構建二元樹 ###
def build_tree(preorder, inorder)
# 初始化雜湊表,儲存 inorder 元素到索引的對映
inorder_map = {}
inorder.each_with_index { |val, i| inorder_map[val] = i }
dfs(preorder, inorder_map, 0, 0, inorder.length - 1)
end
視覺化執行
圖 12-8 展示了構建二元樹的遞迴過程,各個節點是在向下“遞”的過程中建立的,而各條邊(引用)是在向上“迴”的過程中建立的。
圖 12-8 構建二元樹的遞迴過程
每個遞迴函式內的前序走訪 preorder
和中序走訪 inorder
的劃分結果如圖 12-9 所示。
圖 12-9 每個遞迴函式中的劃分結果
設樹的節點數量為 \(n\) ,初始化每一個節點(執行一個遞迴函式 dfs()
)使用 \(O(1)\) 時間。因此總體時間複雜度為 \(O(n)\) 。
雜湊表儲存 inorder
元素到索引的對映,空間複雜度為 \(O(n)\) 。在最差情況下,即二元樹退化為鏈結串列時,遞迴深度達到 \(n\) ,使用 \(O(n)\) 的堆疊幀空間。因此總體空間複雜度為 \(O(n)\) 。