二分木の幅優先走査 (BFS)

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

二分木の幅優先走査 (BFS)

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

  • ショートカット解は美味しいが、本質を学ぶならキューを使った BFS を書こう
  • Python では collections.deque を使うと popleft() が O(1) で動きます
  • このレッスンで扱うのは対極にある 幅優先走査 (BFS, Breadth-First Search) です

二分木の幅優先走査 とは

二分木を配列表現で受け取り、レベル順 (BFS) で訪問した値の配列を返す関数を実装する。本レッスンでは、二分木の幅優先走査 の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

ここまでの 3 つの走査 (in-order / pre-order / post-order) は、いずれも 再帰 で「深く潜ってから帰ってくる」スタイルでした。これらをまとめて 深さ優先走査 (DFS, Depth-First Search) と呼びます。

このレッスンで扱うのは対極にある 幅優先走査 (BFS, Breadth-First Search) です。BFS は「レベル (深さ) ごと に左から右へ順番に訪問する」走査で、level-order とも呼ばれます。

DFS は深く潜る、BFS は 同じ深さを横一列に 訪問する。最短経路問題はほぼ BFS が出番。

BFS のイメージ

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

diagram (will load when visible)

図のポイント (テキスト併記)

  • レベル 01、レベル 123、レベル 24 (null はスキップ)

レベル 01、レベル 123、レベル 24 (null はスキップ)。結果は [1, 2, 3, 4] です。

面白いことに、この木の場合は 入力の配列とほぼ同じ並び になります。これは入力配列が「BFS 順」で並んでいるためです。null を取り除いた配列 が答えになる、と言ってもよいでしょう。

入力配列は BFS 順なので、null を除いた配列がそのまま BFS 結果 になる。

キュー (queue) を使う実装

BFS は キュー (FIFO) を使って書くのが定石です。手順は次のとおりです。

  1. ルートのインデックス 0 をキューに入れる
  2. キューから 1 つ取り出す → 値を result に追加 → 左右の子を 存在すれば キューに入れる
  3. キューが空になるまで 2. を繰り返す

Python

from collections import deque def levelOrder(tree): if not tree or tree[0] is None: return [] result = [] queue = deque([0]) while queue: i = queue.popleft() if i >= len(tree) or tree[i] is None: continue result.append(tree[i]) queue.append(2 * i + 1) queue.append(2 * i + 2) return result

Python では collections.deque を使うと popleft() が O(1) で動きます。list.pop(0) だと O(n) かかるので避けてください。

JavaScript での実装

JavaScript

function levelOrder(tree) { if (tree.length === 0 || tree[0] === null) return []; const result = []; const queue = [0]; while (queue.length > 0) { const i = queue.shift(); if (i >= tree.length || tree[i] === null) continue; result.push(tree[i]); queue.push(2 * i + 1); queue.push(2 * i + 2); } return result; }

JS の Array.shift() は O(n) ですが、テスト規模なら問題ありません。本番では [head, ...] を使った deque 実装に置き換えるのが定石です。

もう 1 つの実装 (配列フィルタ)

入力が BFS 順の配列 で渡されている前提なら、わざわざキューを回さずとも null を除外するだけで答えになります。

Python

def levelOrder(tree): return [v for v in tree if v is not None]

この実装は null が子スロットを占有しない密な BFS 順 の入力にしか対応できません。キュー版はヒープ式インデックス (2 * i + 1 / 2 * i + 2) を使うため、null も固定スロットを占有する前提で動いており、両者は入力形式の前提が異なります。例えば [1, null, 3] のような内部に欠損ノードを含む木では、フィルタ版は [1, 3] を返しますが、キュー版も [1, 3] を返すように見えて、より深い木では食い違いが生じます。フィルタ版は一般解ではないため、面接の場では「キューを使った定石」を書いた方が評価されます。

ショートカット解は美味しいが、本質を学ぶならキューを使った BFS を書こう。

BFS の応用

BFS の真価は 最短経路 にあります。重みのない辺で「スタートから各ノードへの最短ステップ数」を求めたいなら、ほぼ無条件で BFS です。第 5 章のグラフでも再登場します。

他にも次のような応用があります。

  • 木のレベルごとに値を分ける ([[1], [2, 3], [4]] のような構造)
  • 二分木の右側のビュー (各レベルの一番右の値だけ取り出す)
  • ジグザグ走査 (レベルごとに左→右と右→左を交互に切り替える)

よくある間違い

1 つ目は スタックと混同する。DFS はスタック (LIFO)、BFS はキュー (FIFO) です。LIFO で書くと深さ優先になってしまいます。2 つ目は null の子をキューに入れて取り出すときに値を追加してしまう。取り出した後に None チェックを入れて continue するのが安全です。3 つ目は list.pop(0) の O(n)。Python は deque を使うのが鉄則です。

やってみよう

  • [1, 2, 3, 4, 5, 6, 7] を BFS で走査して [1, 2, 3, 4, 5, 6, 7] になることを確認する。
  • レベルごとに分けた配列 [[1], [2,3], [4,5,6,7]] を返す版を書いてみる。for _ in range(len(queue)) のテクニックを使う。
  • 「木の最大幅 (level に存在するノード数の最大)」を BFS で計算してみる。

BFS と DFS は CS の双璧。どちらも息をするように書けるようになると、グラフ問題の 8 割は片付く。

よくある質問

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

理解度チェック (30 秒)

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

この章のポイント

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

関連レッスン

要件

  1. tree は BFS 順の配列で、null (Python では None) は欠損ノードを表す
  2. キュー (FIFO) を使ってレベル順に訪問する
  3. 戻り値は訪問順の値の配列。null は結果に含めない

入出力例

test-cases.txt

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

ヒント

main.py
main.py
学習モード

メモ

二分木の幅優先走査 (BFS)

⌘S で保存