跳轉至

12.3   構建二元樹問題

Question

給定一棵二元樹的前序走訪 preorder 和中序走訪 inorder ,請從中構建二元樹,返回二元樹的根節點。假設二元樹中沒有值重複的節點(如圖 12-5 所示)。

構建二元樹的示例資料

圖 12-5   構建二元樹的示例資料

1.   判斷是否為分治問題

原問題定義為從 preorderinorder 構建二元樹,是一個典型的分治問題。

  • 問題可以分解:從分治的角度切入,我們可以將原問題劃分為兩個子問題:構建左子樹、構建右子樹,加上一步操作:初始化根節點。而對於每棵子樹(子問題),我們仍然可以複用以上劃分方法,將其劃分為更小的子樹(子問題),直至達到最小子問題(空子樹)時終止。
  • 子問題是獨立的:左子樹和右子樹是相互獨立的,它們之間沒有交集。在構建左子樹時,我們只需關注中序走訪和前序走訪中與左子樹對應的部分。右子樹同理。
  • 子問題的解可以合併:一旦得到了左子樹和右子樹(子問題的解),我們就可以將它們連結到根節點上,得到原問題的解。

2.   如何劃分子樹

根據以上分析,這道題可以使用分治來求解,但如何透過前序走訪 preorder 和中序走訪 inorder 來劃分左子樹和右子樹呢

根據定義,preorderinorder 都可以劃分為三個部分。

  • 前序走訪:[ 根節點 | 左子樹 | 右子樹 ] ,例如圖 12-5 的樹對應 [ 3 | 9 | 2 1 7 ]
  • 中序走訪:[ 左子樹 | 根節點 | 右子樹 ] ,例如圖 12-5 的樹對應 [ 9 | 3 | 1 2 7 ]

以上圖資料為例,我們可以透過圖 12-6 所示的步驟得到劃分結果。

  1. 前序走訪的首元素 3 是根節點的值。
  2. 查詢根節點 3 在 inorder 中的索引,利用該索引可將 inorder 劃分為 [ 9 | 3 | 1 2 7 ]
  3. 根據 inorder 的劃分結果,易得左子樹和右子樹的節點數量分別為 1 和 3 ,從而可將 preorder 劃分為 [ 3 | 9 | 2 1 7 ]

在前序走訪和中序走訪中劃分子樹

圖 12-6   在前序走訪和中序走訪中劃分子樹

3.   基於變數描述子樹區間

根據以上劃分方法,我們已經得到根節點、左子樹、右子樹在 preorderinorder 中的索引區間。而為了描述這些索引區間,我們需要藉助幾個指標變數。

  • 將當前樹的根節點在 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 中元素到索引的對映:

build_tree.py
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
build_tree.cpp
/* 構建二元樹:分治 */
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;
}
build_tree.java
/* 構建二元樹:分治 */
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;
}
build_tree.cs
/* 構建二元樹:分治 */
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;
}
build_tree.go
/* 構建二元樹:分治 */
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
}
build_tree.swift
/* 構建二元樹:分治 */
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)
}
build_tree.js
/* 構建二元樹:分治 */
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;
}
build_tree.ts
/* 構建二元樹:分治 */
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;
}
build_tree.dart
/* 構建二元樹:分治 */
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;
}
build_tree.rs
/* 構建二元樹:分治 */
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
}
build_tree.c
/* 構建二元樹:分治 */
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;
}
build_tree.kt
/* 構建二元樹:分治 */
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
}
build_tree.rb
### 構建二元樹:分治 ###
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
build_tree.zig
[class]{}-[func]{dfs}

[class]{}-[func]{buildTree}
視覺化執行

圖 12-8 展示了構建二元樹的遞迴過程,各個節點是在向下“遞”的過程中建立的,而各條邊(引用)是在向上“迴”的過程中建立的。

構建二元樹的遞迴過程

built_tree_step2

built_tree_step3

built_tree_step4

built_tree_step5

built_tree_step6

built_tree_step7

built_tree_step8

built_tree_step9

圖 12-8   構建二元樹的遞迴過程

每個遞迴函式內的前序走訪 preorder 和中序走訪 inorder 的劃分結果如圖 12-9 所示。

每個遞迴函式中的劃分結果

圖 12-9   每個遞迴函式中的劃分結果

設樹的節點數量為 \(n\) ,初始化每一個節點(執行一個遞迴函式 dfs() )使用 \(O(1)\) 時間。因此總體時間複雜度為 \(O(n)\)

雜湊表儲存 inorder 元素到索引的對映,空間複雜度為 \(O(n)\) 。在最差情況下,即二元樹退化為鏈結串列時,遞迴深度達到 \(n\) ,使用 \(O(n)\) 的堆疊幀空間。因此總體空間複雜度為 \(O(n)\)