アルゴリズム概要

💡 この問題を一言で言うと:「二分木の根から一番遠い葉まで何段あるかを数える問題」です。

二分木(=各ノードが最大2つの子「左・右」を持つ木構造データ)の根(root)から、 一番遠い葉ノード(=子を持たない末端ノード)までのノード数を返します。 すべてのノードを1回ずつ訪問しなければならないため、時間計算量の理論限界はO(n)です。

⚠️ なぜ単純な方法では解けないのか

  • 木の深さを知るには左右両方の部分木をすべて探索しないと「どちらが深いか」が分からない
  • CPython のデフォルト再帰深度制限(約1000)があるため、一本道の木では単純な再帰が失敗する
O(n)
時間計算量
O(h)
空間計算量(DFS)
O(w)
空間計算量(BFS)
0〜10,000
ノード数

入力例 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反復版の動きを追います。

Python 実装

2つの実装パターンを提供します。 業務開発版(BFS)は再帰深度制限を回避するため本番環境に適しており、 競技プログラミング版(再帰DFS)は最もシンプルでコードが短い実装です。

📋 業務開発版(BFS)のコード構造

  1. root が None(空の木)の場合はすぐ 0 を返す(エッジケース処理)
  2. deque([root]) でキューを初期化し、depth = 0 を設定する
  3. while queue: でキューが空になるまでループする
  4. level_size = len(queue) で現在レベルのノード数を事前記録する
  5. level_size 回 popleft() を行い、左右の子が存在すればキューへ追加する
  6. 1レベル処理完了ごとに depth += 1 して最終的に depth を返す
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 ✅

処理フローチャート

🗺️ フローチャートの読み方

楕円(緑)= 開始・終了
四角(青)= 処理ステップ
ひし形(黄)= 条件分岐
緑=はい 赤=いいえ 紫=ループ
開始: maxDepth(root) root is None ? (空の木チェック) はい return 0 いいえ queue = deque([root]) depth = 0 queue が空? (while ループ判定) はい return depth いいえ level_size = len(queue) level_size 回: node = queue.popleft() 左右の子が存在すれば queue.append() depth += 1 ループ継続

🔎 入力例1 [3,9,20,null,null,15,7] でのフロー追跡

  1. 「開始」→ root=Node(3) を受け取る
  2. 「root is None?」→ Node(3) は None ではない → いいえ の経路へ
  3. 「キュー初期化」→ queue=deque([Node(3)]), depth=0
  4. 「queue が空?」→ [Node(3)] は空でない → いいえ
  5. level_size=1 → Node(3) をpopleft → 子 Node(9),Node(20) をappend → depth=1
  6. 「queue が空?」→ [Node(9),Node(20)] は空でない → level_size=2 → 処理 → depth=2
  7. 「queue が空?」→ [Node(15),Node(7)] は空でない → level_size=2 → 処理 → depth=3
  8. 「queue が空?」→ [] は空 → はい → 「return depth」→ 3 を返す ✅

計算量分析

📖 Big-O 記法の読み方(n が大きくなるにつれて処理時間がどう増えるかの目安)

O(1)
常に一定
例:辞書の直接引き
O(n)
入力に比例
例:リストを1回走査
O(log n)
半分ずつ絞る
例:二分探索
O(n²)
入力の2乗
例:二重ループ
実装 時間計算量 空間計算量 空間の詳細
業務版(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) です。

📖 用語集

このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。

BFS(幅優先探索)
Breadth-First Search の略。木やグラフを「同じ深さ(レベル)のノードを全部見てから次の深さへ」進む方式で探索する手法。階数が同じ場所を先に全部確認してから次の階へ進むエレベーターのイメージ。Pythonでは collections.deque を使って実装する。
DFS(深さ優先探索)
Depth-First Search の略。木の一本道を葉(末端)まで進み切ってから戻り、次の道を探す方式。迷路で「行き止まりまで進んでから引き返す」探索法に似ている。再帰(関数が自分自身を呼び出す仕組み)と相性が良い。
O(n) 記法(ビッグオー記法)
アルゴリズムの速さやメモリ使用量が「入力の大きさ n が増えるにつれてどう増えるか」を表す記法。O(n) は「n が2倍になると処理も約2倍になる」こと、O(1) は「n に関わらず常に一定」を意味する。
コールスタック
関数が呼び出されるたびに「この関数に戻ってくる場所」の記録が積み上がる高速なメモリ領域。お皿の積み重ねのように、最後に置いたものが最初に取り出される(LIFO)。再帰が深くなりすぎるとこれが溢れて RecursionError が発生する。
collections.deque(両端キュー)
前後どちらからも O(1) で要素を追加・削除できるデータ構造。「両端開きの箱」のイメージ。list.pop(0)(先頭削除)は全要素をずらすため O(n) かかるが、deque.popleft() は O(1) で済む。BFS の実装には必ず deque を使う。
再帰(Recursion)
関数が自分自身を呼び出す手法。「問題を同じ形の小さな問題に分割して解く」のに最適。必ず「ベースケース(終了条件)」が必要で、ないと無限に呼び出しが続いてエラーになる。木の探索と非常に相性が良い。
二分木(Binary Tree)
各ノード(節)が最大2つの子(左・右)を持つ木構造のデータ形式。家系図のように根(root)を頂点として下に枝分かれしていく。子を持たないノードを「葉(leaf)」と呼ぶ。
Optional[T](型ヒント)
「T 型またはNone」のどちらかであることを表すPythonの型ヒント。Optional[TreeNode] と書くと、pylance(型チェッカー)が「None かもしれない」ことを理解し、None チェックなしに root.left へアクセスすると警告してくれる。
ベースケース(Base Case)
再帰の「終了条件」。これ以上小さく分割できない最小の状態を定義する。この問題では root is None がベースケースで、「存在しないノードの深さは0」を意味する。ベースケースがないと再帰が無限に続いてエラーになる。