二分木の post-order 走査

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

二分木の post-order 走査

このレッスンで分かること

  • in-order / pre-order / post-order の違いは「根を訪問する位置」だけ
  • 3 種類の走査の最後は post-order (左 → 右 → 根) です
  • post-order は、子の結果を集めてから親で集約する処理に向いています

二分木の post-order 走査 とは

二分木を配列表現で受け取り、post-order (左 → 右 → 根) で訪問した値の配列を返す関数を実装する。

3 種類の走査の最後は post-order (左 → 右 → 根) です。根を 最後 に訪問するのが特徴で、「子を全部処理してから親を処理する」流れになります。in-order / pre-order と並べて書くと、result.append の位置が違うだけだと気づくはずです。

post-order = 左 → 右 → 根。子を片付けてから親を処理 したい場面で使う (ノード削除、ファイルシステムの容量集計など)。

post-order の使いどころ

post-order は、子の結果を集めてから親で集約する処理に向いています。

  • 木の 解放 (delete) 子から先に消さないとメモリリークする
  • ディレクトリの 総容量計算 子フォルダのサイズを足し上げて親が確定する
  • 数式の 後置記法 (a b + のような逆ポーランド記法)
  • 関数呼び出しの 戻り値で集約する DP木 DP
  • HTML/XML の クローズタグ生成 (子要素を閉じてから親を閉じる)

要するに 「下から積み上げる」 性質を持つ問題には post-order が自然です。再帰の戻り値で何かを返すアルゴリズム (例えば「木の高さ」や「最大経路和」) は、必ず post-order の形になります。

例で理解する

[1, 2, 3, null, 4] の木を post-order で走査します。

diagram (will load when visible)
  • 左部分木 2 の post-order
    • null (skip)
    • 4 の post-order → [4]
    • 2[4, 2]
  • 右部分木 3 (子なし) → [3]
  • 最後に根 1 を追加 → [4, 2, 3, 1]

結果は [4, 2, 3, 1]1一番最後 に出ているのが post-order の典型です。逆に言えば、結果の 末尾が根 になっているかを見れば post-order だと判別できます。

in-order / pre-order / post-order の違いは「根を訪問する位置」だけ。3 つを並べて書き比べると一目瞭然になる。

Python での実装

Python

def postorder(tree): result = [] def visit(i): if i >= len(tree) or tree[i] is None: return visit(2 * i + 1) visit(2 * i + 2) result.append(tree[i]) visit(0) return result

再帰の 左 → 右 → 根 の順序がそのままコードに現れています。result.append最後 に置くのがポイントです。visit(2*i+1) が左、visit(2*i+2) が右、最後に result.append(tree[i]) で自分自身を訪問、という 3 ステップです。

JavaScript での実装

JavaScript

function postorder(tree) { const result = []; const visit = (i) => { if (i >= tree.length || tree[i] === null) return; visit(2 * i + 1); visit(2 * i + 2); result.push(tree[i]); }; visit(0); return result; }

post-order と DP の関係

次のレッスンで実装する「木の高さ」は post-order そのものです。

Python

def height(tree, i=0): if i >= len(tree) or tree[i] is None: return 0 return 1 + max(height(tree, 2*i+1), height(tree, 2*i+2))

左右の高さを再帰で取得してから親で max + 1 を返す、というのは典型的な post-order の流れです。「子の戻り値を見てから親を計算する」 タイプの再帰は、ほぼすべて post-order です。

後の章で出てくる「BST の妥当性チェック」「最大経路和」「最大独立集合 (House Robber III)」なども、すべて post-order の 木 DP で書けます。

木 DP の本体は post-order。「子の dp[child] から親の dp[node] を作る」流れが頭に染み込めば、難しい木問題も恐くなくなる。

よくある間違い

1 つ目は 根を真ん中に置いてしまう。それは in-order です。visit(left) → visit(right) → 根 の順序を厳守してください。2 つ目は 右の再帰を忘れる。左しか書かないと「左連鎖」だけ取って終わってしまいます。3 つ目は 空チェックを忘れる。再帰の基底ケースは「範囲外 or None」の両方が必要です。

やってみよう

  • [1, 2, 3] を post-order で走査すると [2, 3, 1]。in-order の [2, 1, 3] と比較する。
  • 完全二分木 [1, 2, 3, 4, 5, 6, 7] を post-order すると [4, 5, 2, 6, 7, 3, 1]
  • 反復版を書くと意外と難しい。「pre-order の 根 → 右 → 左」を逆順に並べる とトリッキーに解ける。挑戦してみよう。
  • ディレクトリ構造を post-order で走査して 総容量 を計算するスクリプトを書いてみる。

「ファイルシステムの容量を計算する」シチュエーションを想像すれば post-order の必然性が見えてくる。

よくある質問

Q. BST はなぜ高速ですか?

A. 平衡している場合、検索・挿入・削除がいずれも O(log n) です。各ノードで「左<自分<右」を満たすため、半分ずつ探索範囲が絞れます。ただし偏ると O(n) に劣化するため、実用では赤黒木・AVL 木のような自動平衡化された実装を使います。

Q. in-order 走査で何が得られますか?

A. BST を in-order で巡回するとソート済みの順序で要素が得られます。これが BST と他のツリーの大きな違いで、ソート済みデータの逐次処理に向いています。範囲検索(lo 以上 hi 以下)も簡潔に書けます。

Q. BST に重複値を入れるとどうなりますか?

A. 実装次第ですが、通常は「等しい場合は右に入れる」「カウントを持って同じノードに記録する」のいずれかです。重複が多いユースケースなら TreeMap<Key, Integer> のように出現数を値に持たせる方が探索効率が上がります。

次のレッスン

次は 二分木の幅優先走査 (BFS) で、二分木をレベル順(上から下、左から右)に訪問する幅優先走査を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. post-order 走査 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. post-order 走査 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. tree は BFS 順の配列で、null (Python では None) は欠損ノードを表す
  2. 再帰または反復で left -> right -> root の順に訪問する
  3. 戻り値は訪問順の値の配列。null は結果に含めない

入出力例

test-cases.txt

postorder([1,2,3,null,4])[4,2,3,1] postorder([1])[1] postorder([1,2,3])[2,3,1] postorder([1,null,2,null,null,null,3])[3,2,1] postorder([1,2,3,4,5,6,7])[4,5,2,6,7,3,1] postorder([5,3,8,1,4,null,9])[1,4,3,9,8,5]

ヒント

main.py
main.py
学習モード

メモ

二分木の post-order 走査

⌘S で保存