アルゴリズム概要

💡 この問題を一言で言うと:「2種類の木の巡回記録(Inorder・Postorder)を手がかりに、元の二分木の形を復元する問題」

二分木を「巡回する順番のルール」が複数あり、Postorderの末尾要素は必ずその部分木のルート(根)になります。この性質とHashMapを組み合わせることで、配列コピーなしにO(n)で木を再構築できます。

⚠️ なぜ単純な方法では解けないのか

  • list.index() でルート位置を毎回探すと O(n) × n回 = O(n²) になりTLEになる(n=3000で最大900万回の操作)
  • 配列をスライスしてコピーしながら再帰すると O(n²) のメモリが必要になりMLEになる
  • Pythonのデフォルト再帰上限は1000。偏った木ではn=3000段の再帰が必要になるため sys.setrecursionlimit が必須
O(n)
時間計算量
O(n)
空間計算量
HashMap+再帰
アルゴリズム
≤ 3000
入力サイズ上限

入力

inorder   = [9, 3, 15, 20, 7]
postorder = [9, 15, 7, 20, 3]

出力(木の形)

      3
     / \
    9   20
       /  \
      15    7

なぜこれが正解か: postorderの末尾3がルート → inorderで3の左が[9](左部分木)、右が[15,20,7](右部分木)と特定できる。この操作を再帰的に繰り返すことで木全体を復元できる。

ステップバイステップ解説

▶ Play ボタンで自動再生、Prev/Next で手動操作できます。

Python 実装

📋 このコードの構造(先に全体像を把握しよう)

  1. 再帰上限引き上げ:完全偏り木でも安全に動作させるため sys.setrecursionlimit(10_000) を設定
  2. HashMap構築idx_map = {v: i for i, v in enumerate(inorder)} で O(1) 検索を可能にする
  3. ポインタ初期化post_idx = [len(postorder) - 1] で末尾からルートを消費するカーソルを設置
  4. 再帰ヘルパー dfs:終了条件チェック → ルート取得 → 右部分木を先に再帰 → 左部分木を再帰 → ノード返却
import sys
from typing import Optional, List

class Solution:
    def buildTree(
        self,
        inorder: List[int],
        postorder: List[int]
    ) -> Optional[TreeNode]:
        # Python のデフォルト再帰上限は 1000。
        # 完全偏り木では n=3000 段の再帰が必要になるため上限を引き上げる。
        sys.setrecursionlimit(10_000)

        # dict 内包表記で「値 → inorder のインデックス」を O(n) で1回だけ構築。
        # list.index() を毎回呼ぶと O(n²) になるため、事前構築で O(1) 検索を実現。
        idx_map: dict[int, int] = {v: i for i, v in enumerate(inorder)}

        # post_idx をリストで包む慣用句。
        # 内側関数から外側の int を書き換えるには nonlocal が必要だが、
        # リストに包むことで「リストの中身を変える」操作になり nonlocal 不要になる。
        post_idx: List[int] = [len(postorder) - 1]

        def dfs(left: int, right: int) -> Optional[TreeNode]:
            # 終了条件:左端が右端を超えた = この範囲に要素がない = 部分木なし
            if left > right:
                return None

            # postorder の末尾から現在の部分木のルートを取り出す。
            # list のインデックスアクセスは O(1)。カーソルを1つ前に進める。
            val: int = postorder[post_idx[0]]
            post_idx[0] -= 1

            # ルートノードを作成する。
            node = TreeNode(val)

            # dict から O(1) でルートの inorder 上の位置を取得する。
            # この位置より「左側」が左部分木、「右側」が右部分木になる。
            mid: int = idx_map[val]

            # ★重要★ 右部分木を先に再帰する理由:
            # postorder を末尾から逆順に消費すると「ルート→右→左」の順になる。
            # つまり次の pop は「右部分木のルート」を指している。
            # 左を先にすると消費順序がずれて誤った木になってしまう。
            node.right = dfs(mid + 1, right)  # 右:mid+1 〜 right
            node.left  = dfs(left, mid - 1)   # 左:left 〜 mid-1

            return node

        # inorder の全範囲(0 〜 n-1)を対象に木を構築して返す
        return dfs(0, len(inorder) - 1)

▶ 入力例 inorder=[9,3,15,20,7], postorder=[9,15,7,20,3] での動作トレース

事前準備:
  idx_map   = { 9:0, 3:1, 15:2, 20:3, 7:4 }
  post_idx  = [4]  ← postorder の末尾インデックス

dfs(0, 4)  ← inorder 全体の範囲
  val = postorder[4] = 3    post_idx: [4]→[3]
  mid = idx_map[3] = 1
  node = TreeNode(3)
  ├─ node.right = dfs(2, 4)
  │    val = postorder[3] = 20   post_idx: [3]→[2]
  │    mid = idx_map[20] = 3
  │    node = TreeNode(20)
  │    ├─ node.right = dfs(4, 4)
  │    │    val = postorder[2] = 7    post_idx: [2]→[1]
  │    │    node = TreeNode(7) ← 葉ノード(左右None)
  │    │    return TreeNode(7) ✅
  │    └─ node.left = dfs(2, 2)
  │         val = postorder[1] = 15   post_idx: [1]→[0]
  │         node = TreeNode(15) ← 葉ノード
  │         return TreeNode(15) ✅
  │    return TreeNode(20) ✅
  └─ node.left = dfs(0, 0)
       val = postorder[0] = 9    post_idx: [0]→[-1]
       node = TreeNode(9) ← 葉ノード
       return TreeNode(9) ✅

最終結果:
      3
     / \
    9   20
       /  \
      15    7   ✅

処理フローチャート

🗺️ フローチャートの読み方

楕円(緑)= 開始・終了
四角(青)= 処理ステップ
ひし形(黄)= 条件分岐
緑=はい 赤=いいえ
buildTree 開始 inorder が空? len(inorder) == 0 はい None いいえ ① idx_map を構築 {v: i for i, v in enumerate(inorder)} ② post_idx を末尾インデックスに設定 post_idx = [len(postorder) - 1] dfs ヘルパー関数 ③ dfs(0, n-1) 呼び出し inorder の全範囲を対象に再帰開始 ④ left > right ? 部分木が空の場合 はい None いいえ ⑤ postorder 末尾からルートを取得 val = postorder[post_idx[0]]; post_idx[0] -= 1 ⑥ ノード作成 & mid を O(1) で検索 node = TreeNode(val) mid = idx_map[val] # O(1) ⑦ 右部分木を先に再帰 ★重要★ node.right = dfs(mid + 1, right) ⑧ 左部分木を再帰 node.left = dfs(left, mid - 1) ⑨ 子ノードを設定して返す return node buildTree 完了・木を返す

🔎 入力例 inorder=[9,3,15,20,7], postorder=[9,15,7,20,3] でのフロー追跡

  1. 「buildTree 開始」→ inorder=[9,3,15,20,7] を受け取る(空でない → いいえ経路へ)
  2. 「idx_map を構築」→ {9:0, 3:1, 15:2, 20:3, 7:4} を O(n)で生成
  3. 「post_idx を設定」→ post_idx=[4](末尾インデックス)
  4. 「dfs(0, 4) 呼び出し」→ left=0, right=4、終了条件チェック: 0 ≤ 4 → いいえ経路へ
  5. 「ルートを取得」→ val=postorder[4]=3、post_idx=[3]
  6. 「ノード作成・mid検索」→ node=TreeNode(3)mid=idx_map[3]=1
  7. 「右部分木を先に再帰」→ dfs(2, 4) でルート20の部分木を構築
  8. 「左部分木を再帰」→ dfs(0, 0) でルート9の葉ノードを構築
  9. 「完了・木を返す」→ ルート TreeNode(3) を返す ✅

⚡ なぜ「右を先に再帰する」のか?

Postorderは「左→右→自分」の順なので、末尾から逆順に取り出すと「自分→右→左」の順になります。post_idxのカーソルを進めた直後に来る次の末尾要素は常に「右部分木のルート」です。左を先に再帰してしまうとカーソルがずれ、誤ったノードをルートにしてしまいます。

計算量分析

📖 Big-O 記法の読み方(入力サイズ n が大きくなるにつれて処理時間がどう増えるかの目安)

O(1)
常に一定
例:dict の直接引き
O(n)
入力に比例
例:リストを1回走査
O(n log n)
n より少し多い
例:ソートアルゴリズム
O(n²)
入力の2乗
例:二重ループ総当たり
種別 計算量 内訳
時間計算量 O(n) idx_map 構築 O(n) + 各ノードを1回だけ処理 O(n) = O(n)
空間計算量 O(n) idx_map O(n) + 再帰スタック O(h)(h=木の高さ、最悪 O(n))+ 出力ノード O(n)
⚠️ 比較:ナイーブ版 O(n²) list.index() を毎回呼ぶと O(n) × n回 = O(n²)。n=3000で最大900万操作。

🔍 なぜこの計算量になるのか

時間O(n)の理由idx_mapの構築はenumerate()で1回だけ走査するのでO(n)。dfs()はn個のノードそれぞれを1回だけ処理し、各処理内のdict検索とpop()はO(1)なので合計O(n)。全体でO(n)+O(n)=O(n)。

空間O(n)の理由idx_mapにn個のエントリを格納するのでO(n)。再帰スタックは木の高さh分だけ積まれるが、完全に偏った木では h=n になるので最悪O(n)。出力の木もn個のノードを作成するのでO(n)。

📖 用語集

このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。

後順走査(Postorder Traversal)
「左の子 → 右の子 → 自分(ルート)」の順でノードを訪れる走査方法。 配列の末尾要素が必ずその部分木のルートになるという性質がこのアルゴリズムの核心。 例:ルート=3の木では postorder=[9, 15, 7, 20, 3] のように末尾に3が来る。
中順走査(Inorder Traversal)
「左の子 → 自分(ルート) → 右の子」の順でノードを訪れる走査方法。 ルートのインデックスが分かると、そのインデックスの左側が左部分木・右側が右部分木という分割ができる。 例:inorder=[9, 3, 15, 20, 7]でルート=3なら、左部分木=[9]、右部分木=[15,20,7]。
dict(ハッシュテーブル / 辞書)
「キー → 値」の対応を O(1)(=入力サイズに関わらず常に一定の時間)で検索できるデータ構造。 図書館の索引カードのようなもので、タイトル(キー)から棚番号(値)を瞬時に引ける。 このアルゴリズムでは idx_map = {v: i for i, v in enumerate(inorder)} で 「値 → inorderでの位置番号」を記録し、ルートの位置を O(n)→O(1)に短縮している。
再帰(Recursion)
関数が自分自身を呼び出すことで問題を分割して解く手法。 「大きな問題 = 小さな問題 × 2 + 少しの処理」の構造を持つ木の問題に適している。 必ず終了条件(再帰の底)が必要で、このコードでは if left > right: return None がそれにあたる。
再帰スタック(Call Stack)
再帰呼び出しが積み重なるメモリ領域。お皿の積み重ねと同じで、最後に呼んだ関数が最初に終わる(LIFO)。 木の高さ h 段分だけ積まれるため、完全偏り木(全ノードが一方向に連なる)ではh=nになり、デフォルト上限1000を超える可能性がある。 sys.setrecursionlimit(10_000) で上限を引き上げて対処する。
dict 内包表記
{k: v for ...} の形で dict を1行で作る Python の書き方。 通常の for ループより CPython(=最も広く使われる Python の実装)内部で最適化された命令を使うため高速。 {v: i for i, v in enumerate(inorder)} は 「inorder の値 → そのインデックス」を O(n) で一括構築する。
TLE(Time Limit Exceeded)
LeetCode などの競技プログラミングサイトで、処理時間が制限を超えたときに表示されるエラー。 list.index() を毎回呼ぶナイーブな実装は O(n²) になり、 n=3000 で最大900万回の操作が必要になるため TLE になる可能性がある。
ルート / 葉ノード(Root / Leaf Node)
ルート(根):木の最上位ノード。この問題では postorder の末尾がルートになる。 葉ノード:左右の子を持たない末端ノード。再帰の終了時に left == right になると葉ノードが作られる。
部分木(Subtree)
ある木のノードを根(ルート)とした木の一部分のこと。 このアルゴリズムは「全体 = 左部分木 + ルート + 右部分木」という性質を利用して再帰的に問題を分割している。 dfs(left, right) の引数 left/right は「inorder 上でこの部分木が占める範囲のインデックス」を表す。