アルゴリズム概要

💡 この問題を一言で言うと:「2本の二分木が、形も値もまったく同じかどうかを確認する問題」

二分木(=各ノードが左と右に高々1つずつ子を持つ木構造)が2本与えられます。 「同じ木」とは、すべての対応するノードが同じ値を持ち、かつ木の形(どこに子がいるか)も完全に一致することです。 単純に見えますが、「木の形の比較」という点で少し考える必要があります。

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

  • null(何もない)の扱いが難しい:片方の木にはノードがあり、もう片方には何もない(null)という場合を正確に区別しなければならない
  • 全ノードを調べる必要がある:根(ルート)の値が同じでも、葉(末端)の値や位置が違えば「異なる木」になる。部分的な確認では不十分
O(n)
時間計算量
O(h)
空間計算量
再帰DFS
アルゴリズム
Easy
難易度

例1 → true

p: [1,2,3]    q: [1,2,3]
    1              1
   / \            / \
  2   3          2   3

形も値もすべて同じ → true ✅

例2 → false

p: [1,2]      q: [1,null,2]
    1              1
   /                \
  2                  2

値は同じでも位置(左 vs 右)が違う → false ❌

例3 → false

p: [1,2,1]    q: [1,1,2]
    1              1
   / \            / \
  2   1          1   2

左右の値が入れ替わっている → false ❌

📌 制約

  • 両方の木のノード数は 0 以上 100 以下
  • -10⁴ ≤ Node.val ≤ 10⁴

ステップバイステップ解説

再帰DFSがどのように動くかを4つのステップで確認しましょう。 ▶ Play ボタンで自動的に進めることもできます。

Python 実装

📋 このコードの構造(先に全体像を把握しよう)

  1. 両方が None かを確認 → 同じ葉の先端なら True
  2. 片方だけ None かを確認 → 構造が違うので False
  3. 両方の値(p.val != q.val)が違うなら False
  4. 左の子木・右の子木を再帰で比較し、両方一致なら True
class Solution(object):
    def isSameTree(self, p, q):
        """
        :type p: Optional[TreeNode]
        :type q: Optional[TreeNode]
        :rtype: bool
        """
        # ── ① 両方 None のとき ──────────────────────────
        # 葉ノードのさらに下(何もない場所)に両方とも到達した。
        # 「どちらにも子がない」=構造が一致している → True
        # `is None` を使うのが Pythonic(Pythonらしい慣用的な書き方)
        if p is None and q is None:
            return True

        # ── ② 片方だけ None のとき ──────────────────────
        # ①で「両方 None」はすでに return 済み。
        # ここに来るのは「どちらか一方だけ None」の場合のみ。
        # 片方にノードがあり、片方にない = 構造が違う → False
        if p is None or q is None:
            return False

        # ── ③ 値の比較 ───────────────────────────────────
        # ①②を通過した時点で p も q も None でないことが確定。
        # pylance もここでは p・q を TreeNode として認識する
        # (型の絞り込み = Type Narrowing と呼ばれる仕組み)。
        if p.val != q.val:
            return False

        # ── ④ 左右の子木を再帰で比較 ────────────────────
        # 根の値が一致したので、次は左・右の子木を同じ手順で比較する。
        # `and` の短絡評価(左が False なら右は実行しない)で
        # 不一致が見つかった時点で即座に False を返せる。
        return (
            self.isSameTree(p.left, q.left)
            and self.isSameTree(p.right, q.right)
        )

▶ 入力例 p=[1,2] / q=[1,null,2] での動作トレース

呼び出し①: isSameTree(Node(1), Node(1))
  → ① 両方非 None → パス
  → ② どちらも非 None → パス
  → ③ 1 == 1 → パス(値が等しいので続ける)
  → ④ 左の子を比較するために再帰呼び出し

呼び出し②: isSameTree(Node(2), None)   ← p.left=Node(2), q.left=None
  → ① p は非 None → パス(両方 None ではない)
  → ② p は非 None だが q は None → return False ← ここで終了!

呼び出し①に戻る:
  → isSameTree(p.left, q.left) = False
  → and の短絡評価:False and ... → 右辺の再帰は実行されない
  → return False

最終結果: False ✅

処理フローチャート

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

楕円(緑)= 開始・終了
四角(青)= 処理ステップ
ひし形(黄)= 条件分岐
緑=はい 赤=いいえ
開始: isSameTree(p, q) p と q は どちらも None? はい True いいえ どちらか一方だけ None? はい False いいえ p.val ≠ q.val (値が違う)? はい False いいえ isSameTree(p.left, q.left) を再帰呼び出し 左の結果は True? いいえ False はい isSameTree(p.right, q.right) を再帰呼び出し 右の結果は True? いいえ False はい 終了: True を返す 再帰ループ(子ノードへ)

🔎 入力例 p=[1,2,3] / q=[1,2,3] でのフロー追跡

  1. 「開始」ノード → isSameTree(Node(1), Node(1)) を受け取る
  2. 「どちらも None?」ノード → 両方非 None → いいえ の経路へ
  3. 「片方だけ None?」ノード → どちらも非 None → いいえ の経路へ
  4. 「p.val ≠ q.val?」ノード → 1 == 1 → いいえ の経路へ(値が等しいので続ける)
  5. 「左の子木を再帰比較」→ isSameTree(Node(2), Node(2)) を再帰呼び出し(さらに深く潜る)
  6. 「左の結果 True?」→ 左の子木も一致 → はい の経路へ
  7. 「右の子木を再帰比較」→ isSameTree(Node(3), Node(3)) を再帰呼び出し
  8. 「右の結果 True?」→ 右の子木も一致 → はい の経路へ
  9. 「終了」ノード → True を返す ✅

計算量分析

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

O(1)
常に一定
例:辞書の直接引き
O(n)
入力に比例
例:リストを1回走査
O(n log n)
n より少し多い
例:ソートアルゴリズム
O(n²)
入力の2乗
例:二重ループ総当たり
観点 計算量 条件
時間計算量 O(n) n = 2つの木の総ノード数。全ノードを最大1回訪問
空間計算量(平均) O(log n) 平衡二分木の場合(高さ h ≈ log₂ n)
空間計算量(最悪) O(n) 一本道の木(高さ = ノード数)の場合
本問題での実際 O(100) ノード数 ≤ 100 の制約により事実上定数

🔍 なぜこの計算量になるのか

時間計算量 O(n):「2つの木が同じかどうか」を確認するには、すべてのノードを少なくとも1回は調べなければなりません。再帰DFSは各ノードをちょうど1回だけ訪問するため、n ノードに対して O(n) の操作で済みます。
空間計算量 O(h):再帰呼び出しはコールスタック(=関数呼び出しの積み重ね)にメモリを使います。一番深くまで潜ったとき(葉ノードに到達したとき)の積み重ねの深さが木の高さ h なので、O(h) のメモリが必要です。追加のデータ構造(リストやキューなど)は一切使わないため、スタック以外のメモリは O(1) です。

📊 アプローチ別比較

アプローチ 時間 空間 特徴
✅ 再帰DFS(採用) O(n) O(h) コードが最もシンプル。問題の定義と1対1対応
反復DFS(スタック) O(n) O(h) list をスタック代わりに使用。再帰を使わない
反復BFS(キュー) O(n) O(n) deque 使用。常に O(n) メモリを消費して不利

📖 用語集

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

None(ナン)
Pythonで「何もない」を表す特別な値。他の言語の null に相当します。 二分木では、子がいないノードの leftrightNone になります。 比較する際は == None ではなく is None を使うのが Pythonic です。
DFS(深さ優先探索 / Depth-First Search)
木やグラフを「根から葉まで深く潜ってから戻る」順番で探索する手法。 迷路を解くとき「行き止まりに当たるまでまっすぐ進み、行き止まりになったら戻って別の道を試す」のと同じ考え方です。 今回の問題では再帰関数が自動的に DFS の順序でノードを訪問します。
コールスタック(Call Stack)
関数を呼び出すたびに「呼び出し情報」を積み上げていくメモリ領域。 お皿の山積みに例えると、新しい関数呼び出しのたびにお皿を1枚重ね、関数が終了するとお皿を1枚取り除きます。 再帰が深くなるほどお皿が積み重なり、メモリを消費します。これが空間計算量 O(h) の理由です。
再帰(Recursion)
関数が自分自身を呼び出す仕組み。「木の比較」のように「同じ問題が小さいサイズで繰り返される」構造に特に適しています。 必ず「基底条件(これ以上深く行かない条件)」を設定しないと無限ループになるので注意が必要です。 今回の基底条件は p is None and q is None のときに True を返す部分です。
短絡評価(Short-Circuit Evaluation)
A and B の A が False なら B を評価しない・ A or B の A が True なら B を評価しない仕組み。 今回のコードでは isSameTree(p.left, q.left) and isSameTree(p.right, q.right) において、 左の子木が一致しなければ右の再帰は実行されません。不必要な処理を省いて効率化できます。
二分木(Binary Tree)
各ノード(節点)が高々2つの子(左の子・右の子)を持つ木構造のこと。 家系図に例えると、親が最大2人の子を持てる構造です。 今回の問題の TreeNode クラスはこの構造を leftright の2つの参照で表現しています。
平衡二分木(Balanced Binary Tree)
左右の子木の高さの差が小さい、バランスの取れた二分木のこと。 n 個のノードを持つ平衡二分木の高さは約 log₂ n になります。 例えば 1000 ノードなら高さは約 10 です。 逆に「一本道」の木(チェーン状)は高さが n になり、最悪ケースの空間計算量 O(n) に相当します。
Pythonic(パイソニック)
Pythonらしい、慣用的な書き方のこと。Pythonコミュニティが「この書き方が自然で読みやすい」と考えるスタイルを指します。 例えば x == None より x is Nonelen(lst) == 0 より not lst が Pythonic とされています。