|
| 1 | +## 1. 問題分析結果 |
| 2 | + |
| 3 | +> 💡 **初学者向け補足**:この問題を一言で言うと「2本の二分木が、形も値もまったく同じかどうかをPythonの再帰で確認する問題」です。 |
| 4 | +> |
| 5 | +> **Pythonで解く際に特に気をつけるべきCPython特有の注意点**:LeetCodeが提供する `TreeNode` は通常のPythonクラスです。TypeScriptやRustのような複雑なラッパー型は不要で、`Optional[TreeNode]` という型ヒントで「ノードまたは `None`」を表現できます。Python の再帰はデフォルトで深さ1000までしか許可されていませんが(`sys.setrecursionlimit` で変更可)、本問題の制約はノード数100以下なので問題ありません。また `None` の比較は `is None` を使うのが Pythonic(=Pythonらしい書き方)です。 |
| 6 | +
|
| 7 | +#### 競技プログラミング視点 |
| 8 | + |
| 9 | +- **制約分析**:ノード数 ≦ 100 → O(n) で十分。再帰深度も最大100なのでスタックオーバーフローの心配なし |
| 10 | +- **最速手法**:再帰DFS(深さ優先探索)。Pythonの関数呼び出しオーバーヘッドはあるが、n≦100の制約では無視できる |
| 11 | +- **メモリ最小化**:再帰スタックのみ使用。追加のデータ構造(`deque` や `list`)は不要 |
| 12 | + |
| 13 | +#### 業務開発視点 |
| 14 | + |
| 15 | +- **型安全設計**:`Optional[TreeNode]` を引数・戻り値に明示し、pylanceが `None` を渡した場合の型エラーを検出できるようにする |
| 16 | +- **エラーハンドリング**:制約上 `val` は必ず `int` なので型エラーは現実的ではないが、`None` アクセスを `if` で防ぐことが重要 |
| 17 | +- **可読性**:`if p is None and q is None` のように意図が明確な条件式を使う |
| 18 | + |
| 19 | +#### Python特有分析 |
| 20 | + |
| 21 | +- **データ構造選択**:追加のデータ構造不要。再帰コールスタックのみ |
| 22 | +- **標準ライブラリ活用度**:今回は `typing.Optional` のみ使用。シンプルな問題ほどPython組み込みの強みが活きる |
| 23 | +- **CPython最適化度**:再帰は Pure Python だが、n≦100 の制約では実用上問題なし |
| 24 | + |
| 25 | +> 📖 **このセクションで登場した用語** |
| 26 | +> |
| 27 | +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、組み込み関数の多くがC実装のため高速 |
| 28 | +> - **`Optional[T]`**:「T または None のどちらか」を表す型ヒント。`Optional[TreeNode]` は「TreeNodeかNoneか」 |
| 29 | +> - **Pythonic**:Pythonらしい、慣用的な書き方のこと。`is None` は `== None` より意図が明確でPythonicとされる |
| 30 | +> - **再帰深度制限**:CPythonはデフォルトで関数の入れ子呼び出しを1000回までに制限している。`sys.getrecursionlimit()` で確認できる |
| 31 | +
|
| 32 | +--- |
| 33 | + |
| 34 | +## 2. 採用アルゴリズムと根拠 |
| 35 | + |
| 36 | +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。「Python的にどの書き方が速いか・読みやすいか」という観点で比較します。C実装の組み込み関数を使えるかどうかも重要な判断基準です。 |
| 37 | +
|
| 38 | +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | |
| 39 | +| -------------------- | ---------- | ---------- | ---------------- | ------ | ------------------------------- | ----------------------------- | -------------------------------- | |
| 40 | +| **再帰DFS** | O(n) | O(h) | 低 | ★★★ | `typing` のみ | 不適(Pure Python再帰) | 問題構造と直接対応、最もシンプル | |
| 41 | +| **反復DFS(stack)** | O(n) | O(h) | 中 | ★★☆ | `collections` 不要・`list` のみ | 適(list操作はC実装) | `append`/`pop` は O(1) | |
| 42 | +| **反復BFS(deque)** | O(n) | O(n) | 中 | ★★☆ | `collections.deque` | 適(`deque.popleft` が O(1)) | 常に O(n) メモリを消費 | |
| 43 | + |
| 44 | +**選択理由**:再帰DFSを採用します。`list` の `pop()` はC実装で速いですが、今回は n≦100 の制約なので速度差は無意味です。「2つの木が同じ ⟺ 根の値が同じ かつ 左の子木が同じ かつ 右の子木が同じ」という問題の定義がそのまま再帰の構造に対応しており、コードの意図が最も明確に伝わるためです。 |
| 45 | + |
| 46 | +**Python最適化戦略**:`and` の短絡評価(=左辺が `False` なら右辺を評価しない仕組み)を活用して、不一致が見つかった時点で即座に `False` を返します。 |
| 47 | + |
| 48 | +> 📖 **このセクションで登場した用語** |
| 49 | +> |
| 50 | +> - **短絡評価**:`A and B` の A が `False` なら B を評価しない仕組み。`False and 重い処理()` は重い処理を実行しない |
| 51 | +> - **`deque.popleft()`**:`deque`(両端キュー)の先頭要素を O(1) で取り出す操作。`list.pop(0)` は O(n) なので大きなデータには不向き |
| 52 | +> - **Pure Python**:C言語ではなくPythonで書かれたコード。CPythonの組み込み関数より遅い傾向がある |
| 53 | +
|
| 54 | +--- |
| 55 | + |
| 56 | +## 3. 実装パターン |
| 57 | + |
| 58 | +> コードの骨格: |
| 59 | +> |
| 60 | +> 1. 両方が `None` なら → 同じ(`True`) |
| 61 | +> 2. 片方だけ `None` なら → 違う(`False`) |
| 62 | +> 3. 値が違うなら → 違う(`False`) |
| 63 | +> 4. 値が同じなら → 左の子木・右の子木を再帰で比較 |
| 64 | +
|
| 65 | +--- |
| 66 | + |
| 67 | +【業務開発版を使う場面】 |
| 68 | +チームで長期間メンテナンスするプロダクションコードに向きます。型ヒントを丁寧に書き、pylanceが `None` の不正アクセスを実行前に検出できる構造にしています。コメントや docstring で意図を伝えることを優先します。 |
| 69 | + |
| 70 | +```python |
| 71 | +from typing import Optional |
| 72 | + |
| 73 | + |
| 74 | +class Solution: |
| 75 | + def isSameTree( |
| 76 | + self, |
| 77 | + p: Optional["TreeNode"], |
| 78 | + q: Optional["TreeNode"], |
| 79 | + ) -> bool: |
| 80 | + """ |
| 81 | + 2つの二分木が構造・値ともに完全に一致するか判定する(業務開発版) |
| 82 | +
|
| 83 | + Args: |
| 84 | + p: 比較元の木のルートノード(または None) |
| 85 | + q: 比較先の木のルートノード(または None) |
| 86 | +
|
| 87 | + Returns: |
| 88 | + 2つの木が同一なら True、異なれば False |
| 89 | +
|
| 90 | + Complexity: |
| 91 | + Time: O(n) n = 総ノード数(全ノードを最大1回訪問) |
| 92 | + Space: O(h) h = 木の高さ(再帰コールスタックの深さ) |
| 93 | + 平均 O(log n)、最悪(一本道の木)O(n) |
| 94 | + """ |
| 95 | + # ── ① 両方 None のとき ────────────────────────────────── |
| 96 | + # 両方が None ということは「どちらにも子が存在しない」 |
| 97 | + # = 葉ノードの先端に到達し、構造が一致している → True |
| 98 | + # `is None` を使うのが Pythonic。`== None` は非推奨(pylance 警告あり) |
| 99 | + if p is None and q is None: |
| 100 | + return True |
| 101 | + |
| 102 | + # ── ② 片方だけ None のとき ────────────────────────────── |
| 103 | + # ①で「両方 None」は処理済みなので、ここは「どちらか一方だけ None」の場合 |
| 104 | + # 構造が異なる → False |
| 105 | + if p is None or q is None: |
| 106 | + return False |
| 107 | + |
| 108 | + # ── ③ 値の比較 ─────────────────────────────────────────── |
| 109 | + # ここに到達した時点で p も q も None でないことが確定している。 |
| 110 | + # pylance もここでは p・q を TreeNode として認識する(型の絞り込み)。 |
| 111 | + # 値が違えば木の内容が異なる → False |
| 112 | + if p.val != q.val: |
| 113 | + return False |
| 114 | + |
| 115 | + # ── ④ 左右の子木を再帰で比較 ──────────────────────────── |
| 116 | + # 「p と q が同じ木」 ⟺ |
| 117 | + # 「根の値が同じ」かつ「左の子木が同じ」かつ「右の子木が同じ」 |
| 118 | + # `and` の短絡評価により、左が False なら右の再帰は実行されない。 |
| 119 | + # 不一致が見つかった時点で即座に False を返せるので効率的。 |
| 120 | + left_same: bool = self.isSameTree(p.left, q.left) |
| 121 | + right_same: bool = self.isSameTree(p.right, q.right) |
| 122 | + |
| 123 | + return left_same and right_same |
| 124 | +``` |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +【競技プログラミング版を使う場面】 |
| 129 | +LeetCodeなど制限時間内に正解を出すことが目的のコードに向きます。エラーハンドリングや丁寧なコメントを省き、1つの `return` 文で処理全体を表現します。 |
| 130 | + |
| 131 | +```python |
| 132 | +from typing import Optional |
| 133 | + |
| 134 | + |
| 135 | +class Solution: |
| 136 | + def isSameTree( |
| 137 | + self, |
| 138 | + p: Optional["TreeNode"], |
| 139 | + q: Optional["TreeNode"], |
| 140 | + ) -> bool: |
| 141 | + # 両方 None → True、片方だけ None → False を1行で処理。 |
| 142 | + # `p and q` は p が None(= Falsy)なら False を返す短絡評価。 |
| 143 | + # `not (p or q)` は「どちらも None」のとき True になる。 |
| 144 | + if not p and not q: |
| 145 | + return True |
| 146 | + # 片方だけ None、または値が違う → False |
| 147 | + if not p or not q or p.val != q.val: |
| 148 | + return False |
| 149 | + # 左右の子木を再帰で比較。and の短絡評価で早期終了。 |
| 150 | + return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) |
| 151 | +``` |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +> 💡 **コードの動作トレース**(Example 2: `p=[1,2]`, `q=[1,null,2]`) |
| 156 | +> |
| 157 | +> ``` |
| 158 | +> 呼び出し①: isSameTree(p=Node(1), q=Node(1)) |
| 159 | +> → ① p,q ともに非 None → パス |
| 160 | +> → ② どちらも非 None → パス |
| 161 | +> → ③ 1 == 1 → パス |
| 162 | +> → ④ left_same = isSameTree(p.left, q.left) を呼び出す |
| 163 | +> |
| 164 | +> 呼び出し②: isSameTree(p=Node(2), q=None) |
| 165 | +> → ① p は非 None → パス |
| 166 | +> → ② q は None → return False ← ここで終了! |
| 167 | +> |
| 168 | +> 呼び出し①に戻る: |
| 169 | +> → left_same = False |
| 170 | +> → False and ... → and の短絡評価で right_same の再帰は実行されない |
| 171 | +> → return False |
| 172 | +> |
| 173 | +> 全体の結果: False |
| 174 | +> ``` |
| 175 | +
|
| 176 | +--- |
| 177 | +
|
| 178 | +> 📖 **このセクションで登場した用語** |
| 179 | +> |
| 180 | +> - **型の絞り込み(Type Narrowing)**:`if p is None: return` の後ではpylanceが「pはNoneではない」と自動的に判断してくれる仕組み |
| 181 | +> - **Falsy**:`bool(x)` が `False` になる値。Pythonでは `None`・`0`・空リスト `[]`・空文字 `""` などが該当する |
| 182 | +> - **短絡評価**:`A and B` の A が `False` なら B を評価しない・`A or B` の A が `True` なら B を評価しない仕組み |
| 183 | +> - **`Optional["TreeNode"]`**:文字列で型名を書く「前方参照」。クラス定義より前に型名を使いたいときに `"TreeNode"` と文字列にする |
| 184 | +
|
| 185 | +--- |
| 186 | +
|
| 187 | +## 4. 検証 |
| 188 | +
|
| 189 | +> エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。 |
| 190 | +
|
| 191 | +| ケース | p | q | 期待出力 | 確認ポイント | |
| 192 | +| ---------------- | --------- | ------------ | -------- | ------------------------- | |
| 193 | +| 両方空 | `None` | `None` | `True` | ① の `None and None` 処理 | |
| 194 | +| 片方空 | `[1]` | `None` | `False` | ② の片側 `None` 処理 | |
| 195 | +| 構造同一・値同一 | `[1,2,3]` | `[1,2,3]` | `True` | 全ノード比較の正常系 | |
| 196 | +| 構造異なる | `[1,2]` | `[1,null,2]` | `False` | 片方だけ子がある場合 | |
| 197 | +| 値異なる | `[1,2,1]` | `[1,1,2]` | `False` | ③ の値比較(左右反転) | |
| 198 | +| 1ノードのみ同値 | `[1]` | `[1]` | `True` | 葉ノード単体の比較 | |
| 199 | +| 1ノードのみ異値 | `[1]` | `[2]` | `False` | 根だけで即 `False` | |
| 200 | +
|
| 201 | +> 📖 **このセクションで登場した用語** |
| 202 | +> |
| 203 | +> - **エッジケース**:空の入力・要素1つ・最大サイズ入力など、境界的な条件のこと |
| 204 | +> - **正常系**:想定通りの入力で期待通りの出力が得られることを確認するテスト |
| 205 | +> - **前方参照**:クラスや関数の定義より前に、その型名を文字列 `"ClassName"` で書くPythonの慣用的な書き方。Python 3.10以降は `from __future__ import annotations` で省略できる |
| 206 | +
|
| 207 | +```python |
| 208 | +# Runtime 0 ms |
| 209 | +# Beats 100.00% |
| 210 | +# Memory 12.55 MB |
| 211 | +# Beats 47.63% |
| 212 | +class Solution(object): |
| 213 | + def isSameTree(self, p, q): |
| 214 | + """ |
| 215 | + :type p: Optional[TreeNode] |
| 216 | + :type q: Optional[TreeNode] |
| 217 | + :rtype: bool |
| 218 | + """ |
| 219 | + # ── ① 両方 None のとき ────────────────────────────────── |
| 220 | + # 葉ノードの先端に到達し、構造が一致している → True |
| 221 | + # `is None` を使うのが Pythonic(`== None` より意図が明確) |
| 222 | + if p is None and q is None: |
| 223 | + return True |
| 224 | +
|
| 225 | + # ── ② 片方だけ None のとき ────────────────────────────── |
| 226 | + # ①で「両方 None」は処理済みなので、ここは「どちらか一方だけ None」 |
| 227 | + # 構造が異なる → False |
| 228 | + if p is None or q is None: |
| 229 | + return False |
| 230 | +
|
| 231 | + # ── ③ 値の比較 ─────────────────────────────────────────── |
| 232 | + # 両方 None でないことが確定しているので .val に安全にアクセスできる |
| 233 | + # 値が違えば木の内容が異なる → False |
| 234 | + if p.val != q.val: |
| 235 | + return False |
| 236 | +
|
| 237 | + # ── ④ 左右の子木を再帰で比較 ──────────────────────────── |
| 238 | + # `and` の短絡評価により、左が False なら右の再帰は実行されない |
| 239 | + # 不一致が見つかった時点で即座に False を返せるので効率的 |
| 240 | + return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) |
| 241 | +``` |
0 commit comments