二分木の pre-order 走査

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

二分木の pre-order 走査

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

  • 同じ木を 3 種類の走査で回すと違う順が出る
  • pre-order は実用上、次のような場面で重宝します
  • In-order は BST で昇順に出すのに使い、pre-order は 構造のコピー に強い、というのが大まかな住み分けです

二分木の pre-order 走査 とは

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

前のレッスンで in-order (左 → 根 → 右) を実装しました。今度は pre-order (根 → 左 → 右) を扱います。違いは「根を訪問するタイミング」だけです。木の構造を理解する上で、3 つの走査の違いを体で覚えるのが重要です。

pre-order = 根 → 左 → 右。根を最初に出す ので、木のコピーやシリアライズに向いている。

pre-order の使いどころ

pre-order は実用上、次のような場面で重宝します。

  • 木の コピー (新しいノードを上から作る必要があるので、根を先に処理したい)
  • 木の シリアライズ ([1, 2, null, null, 3] のような形式で書き出す)
  • 数式の 前置記法 (+ a b のような表記)
  • ファイルシステムの ディレクトリ出力 (親フォルダ名 → 子フォルダ名 の順)

In-order は BST で昇順に出すのに使い、pre-order は 構造のコピー に強い、というのが大まかな住み分けです。

例で理解する

[1, 2, 3, null, 4] の木を pre-order で走査するとどうなるでしょうか。

diagram (will load when visible)

手順は次のとおりです。

  • 1 を訪問
  • 左部分木へ降りる → 根 2 を訪問 → 左 null (skip) → 右 4 を訪問
  • 右部分木へ降りる → 根 3 を訪問 (子なし)

結果は [1, 2, 4, 3] となります。1 が最初に出ているのが in-order との大きな違いです。

同じ木を 3 種類の走査で回すと違う順が出る。順を見るだけで「どの走査か」判別できるようになると一人前。

Python での実装

Python

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

in-order との違いは result.append の位置 だけです。最初に呼ぶと pre-order、左の後に呼ぶと in-order、右の後に呼ぶと post-order になります。再帰呼び出しの「外側で根を処理」しているので 再帰下降パーサーparse_expression のような構造とも相性が良いです。

JavaScript での実装

JavaScript

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

反復版もある

再帰の深さを避けたい場合は スタックを使った反復版 がよく出てきます。in-order よりも実装がシンプルです。

Python

def preorder_iter(tree): if not tree or tree[0] is None: return [] result, stack = [], [0] while stack: i = stack.pop() if i >= len(tree) or tree[i] is None: continue result.append(tree[i]) # 右を先に push (LIFO なので最後に右が処理される) stack.append(2 * i + 2) stack.append(2 * i + 1) return result

stack.pop() は LIFO なので 右を先に push して 左を後に push することで、左が先に取り出されます。これは面接でもよく聞かれる定番テクニックです。

よくある間違い

1 つ目は 走査順を間違えるvisit(2*i+1) (左) の前に result.append を置かないと、それは in-order や post-order になってしまいます。2 つ目は 反復版の左右の push 順を逆にする こと。stack は LIFO なので「最後に取り出したい方を先に push」が鉄則です。3 つ目は null を結果に入れてしまうnull は出力しないのが pre-order の暗黙の約束です。

やってみよう

  • [1, 2, 3] を pre-order で走査して、結果が [1, 2, 3] になることを確認する (たまたま BFS 順と一致する)。
  • [1, null, 2, null, null, null, 3] (右に偏った木) を pre-order すると [1, 2, 3] になる。
  • 反復版の解答も書いてみて、再帰版と結果が一致することを確認する。

3 種類の走査は 根を出すタイミングが違うだけ。これに気づくと木の問題が一気に楽になる。

よくある質問

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> のように出現数を値に持たせる方が探索効率が上がります。

次のレッスン

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

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

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

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

入出力例

test-cases.txt

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

ヒント

main.py
main.py
学習モード

メモ

二分木の pre-order 走査

⌘S で保存