BFS(幅優先探索)による O(n) 実装
💡 一言で言うと:「根ノードから最も近い葉ノードまでの最短経路のノード数を求める問題」
💡 この問題を一言で言うと:「根ノードから最も近い葉ノードまでの最短経路のノード数を返す問題」
「最小深さ」とは、木の根(一番上)から出発して、最初に到達できる葉(子を持たないノード)までのノード数のことです。深さは根を1として数えます。葉ノードとは、左の子も右の子もどちらも存在しないノードのことです。
⚠️ なぜ単純な方法では解けないのか
例1: 通常の二分木
入力: root = [3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
出力: 2
根(3)→左の子(9) の経路が長さ2で最短。ノード9は左も右も子がない「葉」のため、深さ2が答えになります。
例2: 一方向にだけ伸びる木
入力: root = [2,null,3,null,4,null,5,null,6]
2
\
3
\
4
\
5
\
6 ← 唯一の葉
出力: 5
右方向にのみ子があり、唯一の葉はノード6。葉までの距離が5なのでその深さが答えです。
📋 このコードの構造(先に全体像を把握しよう)
from typing import Optional
from collections import deque
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
# ─────────────────────────────────────
# エッジケース: 空の木 → 深さ 0 を返す
# 根がなければ「葉までの道」自体が存在しない
# ─────────────────────────────────────
if root is None:
return 0
# ─────────────────────────────────────
# BFS キューを初期化する
# deque の popleft() は O(1) ← list より効率的
# タプル (ノード, 現在の深さ) でペアを管理する
# ─────────────────────────────────────
queue: deque = deque()
queue.append((root, 1)) # 根ノードは深さ 1 からスタート
while queue:
# popleft() = FIFO(先入れ先出し)→ 浅い順に処理
node, depth = queue.popleft()
# ─────────────────────────────────
# 葉ノード判定: 左も右も子がない = 葉
# BFS は浅い順なので、最初の葉 = 最小深さ
# ─────────────────────────────────
if node.left is None and node.right is None:
return depth # 最小深さ確定!
# 子ノードは存在する場合のみキューに追加
if node.left is not None:
queue.append((node.left, depth + 1))
if node.right is not None:
queue.append((node.right, depth + 1))
return 0 # 有効な木なら通常ここに到達しない
▶ 入力例 [3,9,20,null,null,15,7] での動作トレース
初期状態: queue = [(node=3, depth=1)] 反復 1: pop → (node=3, depth=1) 左の子: 9 あり, 右の子: 20 あり → 葉ではない → queue に (node=9, depth=2) と (node=20, depth=2) を追加 → queue = [(node=9, depth=2), (node=20, depth=2)] 反復 2: pop → (node=9, depth=2) 左の子: None, 右の子: None → 🌿 葉ノード発見! → return 2 ✅ 答え: 2(探索終了) ※ node=20 はキューに残ったままですが、葉を見つけた時点で即 return するためこれ以上処理は行われません。
🗺️ フローチャートの読み方
🔎 入力例 [3,9,20,null,null,15,7] でのフロー追跡
フローの要点:
BFS
はキューを使って「浅いノードから順番に」処理します。ループ(紫の矢印)は次のノードを処理するためにキューへ戻ることを表しています。右側の緑の線(サイドライン)は「条件を満たしたので早期リターン」する経路です。root is None
のときは 0 を、葉ノードを見つけたときはそのときの
depth
を即座に返します。
📖 Big-O 記法の読み方(入力サイズ n が大きくなると処理時間がどう変わるか)
| 種別 | 記法 | 変数の意味 | ケース |
|---|---|---|---|
| 時間計算量 |
O(n)
|
n = 木のノード総数 | 最悪ケース(全ノード訪問) |
| 空間計算量 |
O(w)
|
w = 木の最大幅(1段のノード最大数) | 完全二分木では O(n/2) ≈ O(n) |
🔍 なぜこの計算量になるのか
時間 O(n):最悪のケースでは、葉が最後の1個で木の最深部にある状況です(例2のような一方向に伸びる木)。この場合、全
n ノードをキューから1回ずつ取り出して処理するため、処理回数は n
に比例します。ただし最良のケースは根ノード自体が葉(n=1)のときで、即座に
O(1) で返ります。
空間 O(w):キューには「同じ深さのノード」が同時に蓄積されます。最もノードが密集する段(=
木の最大幅 w)のとき、キューのサイズが最大になります。完全二分木の最下段では
w ≈ n/2 となり、事実上 O(n) と等価です。
このページで登場した専門用語をまとめました。分からない言葉が出てきたら参照してください。
collections.deque
が提供します。先頭と末尾の両端から O(1)
の定数時間で要素の追加・取り出しができます。通常の
list の
pop(0)
は O(n) のコストがかかるため、BFS には deque の使用が必須です。
node.left is None and node.right is None
です。注意点:左右どちらか一方の子しか持たないノードは葉ではありません。
append()
で末尾に追加し、popleft()
で先頭から取り出します。
TreeNode
クラスが使われ、val(値)・left(左の子)・right(右の子)の3つの属性を持ちます。