再帰 DFS & 反復 BFS による O(n) 実装
💡 一言で言うと:「木の根から一番遠い葉まで何段あるかを数える問題」です。
💡 この問題を一言で言うと:「二分木の根から一番遠い葉まで何段あるかを数える問題」です。
二分木(=各ノードが最大2つの子「左・右」を持つ木構造データ)の根(root)から、 一番遠い葉ノード(=子を持たない末端ノード)までのノード数を返します。 すべてのノードを1回ずつ訪問しなければならないため、時間計算量の理論限界はO(n)です。
⚠️ なぜ単純な方法では解けないのか
入力例 1
3 ← 深さ 1
/ \
9 20 ← 深さ 2
/ \
15 7 ← 深さ 3 (葉)
出力: 3
深さ3まで葉ノードが存在するため、最大深さ=3
入力例 2
1 ← 深さ 1
\
2 ← 深さ 2 (葉)
出力: 2
右の子だけが存在し、葉は深さ2にあるため、最大深さ=2
入力例1(root=[3,9,20,null,null,15,7])を使って、BFS反復版の動きを追います。
2つの実装パターンを提供します。 業務開発版(BFS)は再帰深度制限を回避するため本番環境に適しており、 競技プログラミング版(再帰DFS)は最もシンプルでコードが短い実装です。
📋 業務開発版(BFS)のコード構造
from __future__ import annotations
from typing import Optional
from collections import deque
# ════════════════════════════════════════════════
# 業務開発版:反復 BFS(CPython 再帰制限を回避)
# ════════════════════════════════════════════════
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
# エッジケース:空の木(ノードが1つもない)→ 深さ 0
# 後続の deque 処理に None を入れないための早期リターン
if root is None:
return 0
# deque を使う理由:
# list.pop(0) は先頭削除が O(n) だが
# deque.popleft() は O(1) で済む
queue: deque[TreeNode] = deque([root])
depth: int = 0 # 処理したレベルの数 = 深さ
while queue:
# この時点の len(queue) = 今のレベルのノード数
# ループ前に固定することで「次のレベルのノードが
# append されても影響を受けない」ようにする
level_size: int = len(queue)
for _ in range(level_size):
node: TreeNode = queue.popleft() # O(1)
# 左・右の子が存在すれば次のレベルとしてキューへ
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
# 今のレベルを全部処理し終えた = 1段降りた
depth += 1
return depth
# ════════════════════════════════════════════════
# 競技プログラミング版:再帰 DFS(最もシンプル)
# ════════════════════════════════════════════════
class Solution2:
def maxDepth(self, root: Optional[TreeNode]) -> int:
# ベースケース:None = 存在しないノードの深さは 0
# "is None" を使う理由:
# 通常のノードオブジェクトはvalの値にかかわらずtruthyですが、
# ノードが存在しない(None)ことを真偽値ではなく明示的に判定するためです。
if root is None:
return 0
# max() は C実装の組み込み関数なので if文より高速
# 「左の深さ」と「右の深さ」の大きい方 + 現在ノード分(+1)
return 1 + max(
self.maxDepth(root.left),
self.maxDepth(root.right),
)
▶ 入力例1 [3,9,20,null,null,15,7] での動作トレース(BFS版)
初期状態: queue=deque([Node(3)]), depth=0
【レベル1】level_size=1
popleft() → Node(3)
left=Node(9) → append → queue=[Node(9)]
right=Node(20) → append → queue=[Node(9),Node(20)]
depth=1
【レベル2】level_size=2
popleft() → Node(9)
left=None, right=None → 追加なし
popleft() → Node(20)
left=Node(15) → append → queue=[Node(15)]
right=Node(7) → append → queue=[Node(15),Node(7)]
depth=2
【レベル3】level_size=2
popleft() → Node(15) → 子なし
popleft() → Node(7) → 子なし
depth=3
queue=deque([]) → 空 → ループ終了
return 3 ✅
🗺️ フローチャートの読み方
🔎 入力例1 [3,9,20,null,null,15,7] でのフロー追跡
📖 Big-O 記法の読み方(n が大きくなるにつれて処理時間がどう増えるかの目安)
| 実装 | 時間計算量 | 空間計算量 | 空間の詳細 |
|---|---|---|---|
| 業務版(BFS) | O(n) | O(w) | w = 木の最大幅。完全二分木では最下段≈n/2 |
| 競技版(再帰DFS) | O(n) | O(h) | h = 木の高さ。平衡木でO(log n)、一本道でO(n) |
🔍 なぜこの計算量になるのか
時間計算量 O(n):「木の最大深さ」を求めるには、全ノードを少なくとも1回は訪問しなければなりません(どの葉が一番深いか分からないため)。BFS版・DFS版どちらも全n個のノードを正確に1回ずつ処理するので
O(n) です。
空間計算量(BFS)O(w):キューには「現在のレベルのノード」と「次のレベルのノード」が同時に入ります。同じ深さのノード数の最大値
w が最大キューサイズになります。
空間計算量(DFS)O(h):再帰呼び出しのたびにコールスタックに1段積まれます。最大で「木の高さ
h」段まで積まれるため O(h) です。
このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。
collections.deque
を使って実装する。
RecursionError
が発生する。
list.pop(0)(先頭削除)は全要素をずらすため O(n) かかるが、deque.popleft()
は O(1) で済む。BFS の実装には必ず deque を使う。
Optional[TreeNode]
と書くと、pylance(型チェッカー)が「None
かもしれない」ことを理解し、None チェックなしに
root.left
へアクセスすると警告してくれる。
root is None
がベースケースで、「存在しないノードの深さは0」を意味する。ベースケースがないと再帰が無限に続いてエラーになる。