BFS + collections.deque による O(n) 実装
💡 一言で言うと:「木を上から下へ、同じ高さのノードをまとめてグループ化する問題」
💡 この問題を一言で言うと:
「木の同じ深さ(階層)にあるノードを全部集めて1つのリストにまとめ、それを深さ順に並べた2次元配列を返す問題」です。
左から右の順番でノードを集める必要があります。
⚠️ なぜ単純な方法では解けないのか
list.pop(0)
を使うと先頭削除が O(n) になり全体が O(n²) へ悪化する
0
のとき、or
トリックは
0(falsy)と誤判定して壊れる
📥 入出力例
入力(ツリー)
3
/ \
9 20
/ \
15 7
出力と理由
[[3],[9,20],[15,7]] 深さ0 → [3] 深さ1 → [9, 20] ← 左から右 深さ2 → [15, 7] ← 左から右
📋 制約
各ステップをクリックするか、▶ Play ボタンで自動再生できます。
📋 このコードの構造(先に全体像を把握しよう)
from collections import deque
from typing import Optional
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> list[list[int]]:
# 基底条件: root が None(空ツリー)なら即座に空リストを返す
# None チェックをしないと後続の node.val アクセスでクラッシュする
if root is None:
return []
result: list[list[int]] = []
# collections.deque をキューとして使う
# list.pop(0) は O(n) だが deque.popleft() は O(1)
# deque は C言語実装の双方向キューで先頭・末尾への操作が高速
queue: deque[TreeNode] = deque([root])
# キューが空になるまでループ(= 全ノードを処理し終えるまで)
while queue:
# ── BFS の核心:今の階のサイズをここで固定する ──
# ループ中に popleft()/append() で queue の長さが変化するため
# 先に変数へ保存しないと「今の階」の範囲がずれて Wrong Answer になる
level_size: int = len(queue)
level_values: list[int] = []
for _ in range(level_size):
# popleft() でキューの先頭ノードを O(1) で取り出す
node: TreeNode = queue.popleft()
# val を直接 append する(0 も正しく追加される)
# `or` トリックは val=0 のとき falsy と判定されて壊れるため使わない
level_values.append(node.val)
# 子が存在する場合のみキューへ追加(次の階の準備)
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
result.append(level_values)
return result
▶ 入力例 [3, 9, 20, null, null, 15, 7] での動作トレース
初期状態: queue = deque([Node(3)]), result = []
━━ ループ 1回目(深さ0) ━━
level_size = 1 ← ここで固定!
vals = []
i=0: popleft() → Node(3) queue = deque([])
append(3) → vals = [3]
left=Node(9) → queue = deque([Node(9)])
right=Node(20) → queue = deque([Node(9), Node(20)])
result = [[3]]
━━ ループ 2回目(深さ1) ━━
level_size = 2 ← ここで固定!
i=0: Node(9) → vals=[9] 子なし
i=1: Node(20) → vals=[9,20] left=Node(15), right=Node(7)
result = [[3], [9, 20]]
━━ ループ 3回目(深さ2) ━━
level_size = 2
i=0: Node(15) → vals=[15] 子なし
i=1: Node(7) → vals=[15,7] 子なし
result = [[3], [9, 20], [15, 7]]
queue が空 → ループ終了
戻り値: [[3], [9, 20], [15, 7]] ✅
🗺️ フローチャートの読み方(Mermaid 記法)
🔎 入力例 [3, 9, 20, null, null, 15, 7] でのフロー追跡
📖 Big-O 記法の読み方(n = ノード数)
| 種別 | 計算量 | 理由 |
|---|---|---|
| 時間計算量 | O(n) | 各ノードを popleft() で1回だけ処理(O(1)×n回) |
| 空間計算量 | O(n) | キューに最大で最下層のノード数(最大 n/2 個)が入る |
| 比較:list.pop(0) | O(n²) | 先頭削除のたびに残り全要素をシフト(O(n)×n回) |
🔍 なぜこの計算量になるのか
全ノード数を n とすると、各ノードはキューへの
append()(O(1))と
popleft()(O(1))を1回ずつ行うため、 合計操作回数は 2n =
O(n) です。 空間については、完全二分木の最下層に最大 n/2
個のノードが集まるため、 キューの最大サイズは O(n) となります。
これは出力配列 result のサイズも同様で、全ノードの値を格納するため O(n)
です。
このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。
collections.deque
のこと。 Doubly-Ended Queue(両端キュー)の略。前からも後ろからも O(1)
で出し入れできる「両端開きの箱」。 Python の
list
の先頭削除は O(n) だが、 deque の
popleft() は
O(1)。 C言語で実装されており高速。
if
の条件式で
False
相当と見なされる値。
0、None、[]、""
などが該当する。 ノードの値
val = 0 は
falsy なので、
0 or 式
という書き方は「0のとき右辺を実行する」という誤動作を引き起こす。
level_size = len(queue)
で 「今の階のノード数」を変数に保存するテクニック。 ループ中に子の追加で
queue の長さが変化するため、先に固定しないと
「今の階」と「次の階」の境界が混在して Wrong Answer になる。
deque
の先頭要素を O(1) で取り出すメソッド。
list.pop(0)
は残り全要素を前にシフトするため O(n) かかるが、
deque.popleft()
は双方向連結リストの先頭ポインタを1つ進めるだけなので O(1)。 n=2000
のとき、この差は 2000 回 vs 最大 4,000,000 回の操作差になる。