HashMap 事前構築 + 再帰分割 による O(n) 実装
💡 この問題を一言で言うと:「2種類の木の巡回記録(Inorder・Postorder)を手がかりに、元の二分木の形を復元する問題」
二分木を「巡回する順番のルール」が複数あり、Postorderの末尾要素は必ずその部分木のルート(根)になります。この性質とHashMapを組み合わせることで、配列コピーなしにO(n)で木を再構築できます。
⚠️ なぜ単純な方法では解けないのか
list.index() でルート位置を毎回探すと O(n) × n回 = O(n²) になりTLEになる(n=3000で最大900万回の操作)sys.setrecursionlimit が必須入力
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 で手動操作できます。
📋 このコードの構造(先に全体像を把握しよう)
sys.setrecursionlimit(10_000) を設定idx_map = {v: i for i, v in enumerate(inorder)} で O(1) 検索を可能にするpost_idx = [len(postorder) - 1] で末尾からルートを消費するカーソルを設置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 ✅
🗺️ フローチャートの読み方
🔎 入力例 inorder=[9,3,15,20,7], postorder=[9,15,7,20,3] でのフロー追跡
{9:0, 3:1, 15:2, 20:3, 7:4} を O(n)で生成post_idx=[4](末尾インデックス)val=postorder[4]=3、post_idx=[3]node=TreeNode(3)、mid=idx_map[3]=1dfs(2, 4) でルート20の部分木を構築dfs(0, 0) でルート9の葉ノードを構築⚡ なぜ「右を先に再帰する」のか?
Postorderは「左→右→自分」の順なので、末尾から逆順に取り出すと「自分→右→左」の順になります。post_idxのカーソルを進めた直後に来る次の末尾要素は常に「右部分木のルート」です。左を先に再帰してしまうとカーソルがずれ、誤ったノードをルートにしてしまいます。
📖 Big-O 記法の読み方(入力サイズ n が大きくなるにつれて処理時間がどう増えるかの目安)
| 種別 | 計算量 | 内訳 |
|---|---|---|
| 時間計算量 | 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)。
このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。
idx_map = {v: i for i, v in enumerate(inorder)} で
「値 → inorderでの位置番号」を記録し、ルートの位置を O(n)→O(1)に短縮している。
if left > right: return None がそれにあたる。
sys.setrecursionlimit(10_000) で上限を引き上げて対処する。
{k: v for ...} の形で dict を1行で作る Python の書き方。
通常の for ループより CPython(=最も広く使われる Python の実装)内部で最適化された命令を使うため高速。
{v: i for i, v in enumerate(inorder)} は
「inorder の値 → そのインデックス」を O(n) で一括構築する。
list.index() を毎回呼ぶナイーブな実装は O(n²) になり、
n=3000 で最大900万回の操作が必要になるため TLE になる可能性がある。
left == right になると葉ノードが作られる。