← 題庫 / Archive
2026-08-02 TI150 Medium ArrayHash TableDivide and ConquerTreeBinary Tree

105. Construct Binary Tree from Preorder and Inorder Traversal

題目 / Problem

中文:給你兩個整數陣列 preorderinorderpreorder 是某棵二元樹的「前序遍歷」結果,inorder 是同一棵樹的「中序遍歷」結果。請你根據這兩個陣列,重建並回傳這棵二元樹。

English: You are given two integer arrays preorder and inorder, where preorder is the preorder traversal of a binary tree and inorder is the inorder traversal of the same tree. Construct and return that binary tree.

Constraints / 限制 - 1 <= preorder.length <= 3000 - inorder.length == preorder.length - -3000 <= preorder[i], inorder[i] <= 3000 - preorderinorder 中的值都是唯一的(no duplicates)。 - inorder 中每個值都出現在 preorder 裡(they contain the same set of values)。 - 保證 preorder 是合法的前序遍歷,inorder 是合法的中序遍歷。

Worked Example / 範例

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

這對應下面這棵樹 / This corresponds to the tree:

        3
       / \
      9   20
         /  \
        15   7

名詞解釋 / Glossary

  • 二元樹 / Binary Tree:每個節點最多有兩個子節點(左子節點、右子節點)的樹狀資料結構。A tree where each node has at most two children: a left child and a right child.
  • 節點 / Node:樹的基本單位,這裡每個節點存一個整數值 val,以及指向左右子樹的指標 leftright。The basic unit of a tree; here each node stores an integer val and two pointers left and right.
  • 前序遍歷 / Preorder Traversal:走訪順序是「根 → 左子樹 → 右子樹」。所以 preorder第一個元素一定是整棵樹的根。Visit order is root → left subtree → right subtree, so the first element of preorder is always the root.
  • 中序遍歷 / Inorder Traversal:走訪順序是「左子樹 → 根 → 右子樹」。因此在 inorder 中,根節點左邊全是左子樹的值,右邊全是右子樹的值。Visit order is left subtree → root → right subtree; so in inorder everything left of the root belongs to the left subtree and everything right belongs to the right subtree.
  • 遞迴 / Recursion:函式呼叫自己來解決更小的子問題。這裡我們用遞迴分別建立左子樹和右子樹。A function calling itself to solve smaller sub-problems; we use it to build the left and right subtrees.
  • 分治法 / Divide and Conquer:把大問題拆成獨立的小問題,各自解完再組合。Split a big problem into independent smaller ones, solve each, then combine.
  • 雜湊表 / Hash Map:一種能用「值」在 O(1) 平均時間查到對應資訊的資料結構。這裡用來記錄「某個值在 inorder 中的位置」,避免每次都線性搜尋。A structure giving average O(1) lookup; here it maps each value to its index in inorder so we don't scan repeatedly.

思路

我們先想最直接的做法:前序遍歷的第一個元素 preorder[0] 一定是整棵樹的根,因為前序是「根→左→右」。找到根之後,我們去中序陣列 inorder 裡找到這個根的位置。由於中序是「左→根→右」,根左邊的所有元素就是左子樹的中序,根右邊的所有元素就是右子樹的中序。假設左邊有 k 個元素,那麼左子樹就有 k 個節點;回到 preorder,根之後緊接著的 k 個元素就是左子樹的前序,剩下的就是右子樹的前序。這樣我們就把問題分成兩個一模一樣、但更小的子問題:用「左子樹的前序+中序」建左子樹,用「右子樹的前序+中序」建右子樹,然後把它們接到根上。這就是遞迴(分治)。基礎情況是:當某個區間為空時,回傳空節點(NULL)。若每次都用線性掃描去 inorder 裡找根的位置,最壞情況(例如樹退化成一條鏈)會是 O(n²)。優化的關鍵是:先用一個雜湊表把「每個值 → 它在 inorder 的索引」記下來,之後每次找根位置都是 O(1),總複雜度就降到 O(n)。實作上,我們用索引範圍(preStart..preEndinStart..inEnd)來代表當前正在處理的子陣列,避免真的去複製陣列,節省空間與時間。

The most direct idea starts from a key fact: preorder[0] is always the root of the whole tree, because preorder visits root first. Once we know the root value, we locate it inside inorder. Because inorder is left → root → right, every element to the left of the root in inorder forms the left subtree and every element to the right forms the right subtree. If there are k elements on the left, the left subtree has exactly k nodes — so back in preorder, the k elements right after the root are the left subtree's preorder, and the rest are the right subtree's preorder. Now the problem splits into two identical but smaller problems: build the left subtree from its (preorder, inorder) slice, build the right subtree from its slice, then attach both to the root. That is recursion / divide and conquer, with the base case "empty range → return NULL". If we find the root's position by scanning inorder linearly every time, a skewed tree degrades to O(n²). The optimization: precompute a hash map from value → index in inorder, making each root lookup O(1) and the whole algorithm O(n). To avoid copying arrays, we pass around index ranges (preStart..preEnd, inStart..inEnd) that describe the current sub-arrays.

逐步走查 / Walkthrough

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

First build the index map from inorder / 先建中序索引表: {9:0, 3:1, 15:2, 20:3, 7:4}.

We recurse with build(preStart, preEnd, inStart, inEnd). Below k = number of nodes in the left subtree = rootIndexInInorder - inStart.

Step preorder range inorder range root = pre[preStart] root idx in inorder left size k 動作 / Action
1 pre[0..4]=[3,9,20,15,7] in[0..4]=[9,3,15,20,7] 3 1 1 root=3;左子樹用 pre[1..1],in[0..0];右子樹用 pre[2..4],in[2..4]
2 (left of 3) pre[1..1]=[9] in[0..0]=[9] 9 0 0 root=9;左右皆空 → 葉節點 / leaf
3 (right of 3) pre[2..4]=[20,15,7] in[2..4]=[15,20,7] 20 3 1 root=20;左子樹 pre[3..3],in[2..2];右子樹 pre[4..4],in[4..4]
4 (left of 20) pre[3..3]=[15] in[2..2]=[15] 15 2 0 root=15;左右皆空 → 葉 / leaf
5 (right of 20) pre[4..4]=[7] in[4..4]=[7] 7 4 0 root=7;左右皆空 → 葉 / leaf

Assembled tree / 組合結果:

        3
       / \
      9   20
         /  \
        15   7

輸出 [3,9,20,null,null,15,7],正確 / matches expected output.

Solution — C

/*
 * 演算法 / Algorithm:
 * 前序第一個元素是根;在中序中找到根的位置,左邊是左子樹、右邊是右子樹。
 * The first preorder element is the root; its position in inorder splits
 * inorder into left/right subtrees. Recurse on index ranges (no array copying).
 * 用雜湊表把「值 → 中序索引」預先記下,讓每次找根為 O(1),總體 O(n)。
 * A value→inorder-index hash map makes each root lookup O(1), giving O(n) total.
 */

// LeetCode 提供的樹節點定義(此處僅為說明,實際由平台給出)
// LeetCode-provided node struct (shown for reference):
struct TreeNode {
    int val;                 // 節點存的整數 / the integer stored in this node
    struct TreeNode *left;   // 指向左子節點的指標 / pointer to the left child
    struct TreeNode *right;  // 指向右子節點的指標 / pointer to the right child
};

#include <stdlib.h>  // 需要 malloc / needed for malloc

// 值的範圍是 -3000..3000,共 6001 個可能值。用陣列當雜湊表最簡單快速。
// Values range -3000..3000 (6001 possibilities); a plain array is the simplest, fastest map.
// 索引 = 值 + 3000(把負數平移成非負)/ index = value + 3000 (shift negatives into [0,6000]).
#define OFFSET 3000                     // 平移量 / the shift amount
#define MAP_SIZE 6001                   // 陣列大小 / size of the lookup array

// 遞迴輔助函式:用給定的索引範圍建一棵子樹。
// Recursive helper: build one subtree from the given index ranges.
// idxMap[v+OFFSET] = v 在 inorder 中的位置 / position of value v inside inorder.
static struct TreeNode* build(int* preorder, int preStart, int preEnd,
                              int inStart, int inEnd, int* idxMap) {
    // 範圍為空表示沒有節點,回傳 NULL(空子樹)。
    // An empty range means no node here → return NULL (empty subtree).
    if (preStart > preEnd) {
        return NULL;
    }

    // 前序的第一個元素就是這棵(子)樹的根值。
    // The first element of this preorder slice is this subtree's root value.
    int rootVal = preorder[preStart];

    // 配置一個新節點;malloc 向系統要一塊記憶體,回傳指向它的指標。
    // Allocate a new node; malloc requests memory and returns a pointer to it.
    struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    root->val = rootVal;    // 設定節點值 / set the node's value
    root->left = NULL;      // 先設空,稍後可能覆寫 / default empty, may be overwritten
    root->right = NULL;     // 先設空 / default empty

    // O(1) 從雜湊表查出根在中序中的位置。
    // O(1) lookup of the root's position in inorder via the map.
    int inRoot = idxMap[rootVal + OFFSET];

    // 中序中根左邊的元素個數 = 左子樹的節點數。
    // Number of elements left of the root in inorder = size of the left subtree.
    int leftSize = inRoot - inStart;

    // 建左子樹:前序取根之後的 leftSize 個元素;中序取根左邊那段。
    // Build left subtree: preorder = the leftSize elements after root; inorder = left part.
    root->left = build(preorder,
                       preStart + 1, preStart + leftSize,  // 前序左子樹範圍 / preorder range
                       inStart, inRoot - 1,                // 中序左子樹範圍 / inorder range
                       idxMap);

    // 建右子樹:前序取剩下的元素;中序取根右邊那段。
    // Build right subtree: preorder = remaining elements; inorder = right part.
    root->right = build(preorder,
                        preStart + leftSize + 1, preEnd,   // 前序右子樹範圍 / preorder range
                        inRoot + 1, inEnd,                 // 中序右子樹範圍 / inorder range
                        idxMap);

    return root;  // 回傳建好的(子)樹根 / return the built subtree root
}

// LeetCode 入口函式;簽名固定。preorderSize、inorderSize 是陣列長度。
// LeetCode entry function; fixed signature. preorderSize/inorderSize are array lengths.
struct TreeNode* buildTree(int* preorder, int preorderSize,
                           int* inorder, int inorderSize) {
    // 空輸入直接回傳空樹(依約束不會發生,但保險起見)。
    // Empty input → empty tree (won't happen per constraints, but safe to guard).
    if (preorderSize == 0) {
        return NULL;
    }

    // 建立「值 → 中序索引」的雜湊表。
    // Build the value→inorder-index map.
    // 用 malloc 配置陣列;每個 int 存一個索引 / allocate an int array to hold indices.
    int* idxMap = (int*)malloc(sizeof(int) * MAP_SIZE);

    // 掃一遍 inorder,把每個值的位置記進 idxMap。
    // Scan inorder once, recording each value's position into idxMap.
    for (int i = 0; i < inorderSize; i++) {
        // inorder[i] + OFFSET 把值平移成合法陣列索引 / shift value to a valid index.
        idxMap[inorder[i] + OFFSET] = i;
    }

    // 從整個陣列範圍開始遞迴建樹。
    // Start recursion over the full index ranges.
    struct TreeNode* root = build(preorder, 0, preorderSize - 1,
                                  0, inorderSize - 1, idxMap);

    // 釋放雜湊表記憶體,避免記憶體洩漏。
    // Free the map memory to avoid a memory leak.
    free(idxMap);

    return root;  // 回傳整棵樹的根 / return the whole tree's root
}

Solution — C++

/*
 * 演算法 / Algorithm:
 * preorder[0] 是根;在 inorder 找到它,左邊為左子樹、右邊為右子樹,遞迴建樹。
 * preorder[0] is the root; locate it in inorder to split into left/right subtrees, recurse.
 * 用 unordered_map 記「值→中序索引」讓查找 O(1),總複雜度 O(n)。
 * An unordered_map (value→inorder index) makes lookups O(1), giving O(n) overall.
 */

#include <vector>
#include <unordered_map>
using namespace std;

// LeetCode 的節點定義(此處僅供參考)/ LeetCode's node definition (for reference):
// struct TreeNode {
//     int val;
//     TreeNode *left;
//     TreeNode *right;
//     TreeNode() : val(0), left(nullptr), right(nullptr) {}
//     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
// };

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        // unordered_map 是雜湊表,平均 O(1) 查找 / hash map with average O(1) lookup.
        // key = 值, value = 它在 inorder 的索引 / key = value, value = its index in inorder.
        unordered_map<int, int> idxMap;

        // range-for 逐一走訪 inorder 的索引 / range-based loop over inorder indices.
        for (int i = 0; i < (int)inorder.size(); i++) {
            idxMap[inorder[i]] = i;  // 記錄每個值的位置 / record each value's position
        }

        // 從整個範圍開始遞迴建樹 / start recursion over the full ranges.
        return build(preorder, 0, (int)preorder.size() - 1,
                     0, (int)inorder.size() - 1, idxMap);
    }

private:
    // 遞迴輔助函式:用索引範圍建一棵子樹 / recursive helper building one subtree by ranges.
    // idxMap 以參考 (&) 傳入,避免複製整張表 / passed by reference (&) to avoid copying the map.
    TreeNode* build(const vector<int>& preorder, int preStart, int preEnd,
                    int inStart, int inEnd, unordered_map<int, int>& idxMap) {
        // 範圍為空 → 空子樹 / empty range → empty subtree.
        if (preStart > preEnd) {
            return nullptr;
        }

        // 前序第一個元素是根值 / first preorder element is the root value.
        int rootVal = preorder[preStart];

        // new 在堆積上建立節點並回傳指標 / new allocates a node on the heap, returns a pointer.
        TreeNode* root = new TreeNode(rootVal);

        // O(1) 查出根在中序的位置 / O(1) lookup of root's index in inorder.
        int inRoot = idxMap[rootVal];

        // 根左邊的元素個數 = 左子樹大小 / count left of root = size of left subtree.
        int leftSize = inRoot - inStart;

        // 建左子樹:前序取接下來 leftSize 個、中序取左段。
        // Build left subtree: next leftSize preorder elements; left inorder segment.
        root->left = build(preorder,
                           preStart + 1, preStart + leftSize,
                           inStart, inRoot - 1,
                           idxMap);

        // 建右子樹:前序取其餘元素、中序取右段。
        // Build right subtree: remaining preorder elements; right inorder segment.
        root->right = build(preorder,
                            preStart + leftSize + 1, preEnd,
                            inRoot + 1, inEnd,
                            idxMap);

        return root;  // 回傳這棵子樹的根 / return this subtree's root
    }
};

複雜度 / Complexity

  • Time: O(n)n 是節點數。建雜湊表掃一遍是 O(n);每個節點在遞迴中剛好被當作「根」處理一次,且每次找根位置是 O(1),所以總共是 O(n)。若不用雜湊表而每次線性掃 inorder,最壞會退化成 O(n²)。 / n is the number of nodes. Building the map is O(n); each node becomes a root exactly once and its lookup is O(1), so O(n) total. Without the map, linear scans would make it O(n²) in the worst (skewed) case.
  • Space: O(n) — 雜湊表存 n 個項目佔 O(n);遞迴呼叫堆疊最深為樹的高度,最壞(鏈狀樹)為 O(n),平衡時為 O(log n)。輸出的樹本身不計入額外空間。 / The map holds n entries → O(n); the recursion stack goes as deep as the tree height, O(n) worst case (skewed) and O(log n) when balanced. The output tree itself isn't counted as extra space.

Pitfalls & Edge Cases

  • 左子樹大小算錯 / Miscomputing leftSize:必須用 inRoot - inStart(相對於當前子區間的起點),不能用 inRoot(絕對索引)。用絕對索引在右子樹遞迴時會切錯範圍。Must use inRoot - inStart (relative to the current sub-range's start), not the absolute inRoot; the absolute value breaks the split once you recurse into right subtrees.
  • 前序範圍的邊界 / Preorder boundaries:左子樹是 preStart+1 .. preStart+leftSize,右子樹是 preStart+leftSize+1 .. preEnd。off-by-one 是這題最常見的錯誤。These +1/+leftSize offsets are the classic off-by-one traps in this problem.
  • 忘記回傳 NULL 的基礎情況 / Missing the NULL base casepreStart > preEnd 時必須回傳空,否則會無限遞迴或存取越界。Without the empty-range check the recursion never terminates and reads out of bounds.
  • 負值當索引 / Negative values as indices (C only):值可為負,C 版用陣列雜湊表時要加 OFFSET(+3000)把值平移成非負索引,否則會越界寫入。In the C array-map, values can be negative, so add OFFSET before indexing; forgetting it corrupts memory.
  • 重複值假設 / Relying on uniqueness:本演算法依賴「值唯一」才能在 inorder 中確定根的唯一位置。若有重複值這個方法就不成立——但題目已保證唯一。The approach depends on unique values to pin down the root's single position in inorder; guaranteed here.
  • 記憶體釋放 / Freeing memory (C):C 版用 malloc 建雜湊表後要 free,避免洩漏;節點本身由 LeetCode 負責回收,不要提前 free。Free the map you malloc, but don't free the tree nodes — LeetCode owns those.
  • C++ 傳參用參考 / Pass the map by reference in C++unordered_map<int,int>& idxMap& 避免每次遞迴複製整張表(否則會退化成 O(n²) 時間與空間)。The & avoids copying the whole map on every recursive call.