|
| 1 | +> 🎯 **[algo-beginner スキル発火]** |
| 2 | +> 言語/カテゴリ: Python |
| 3 | +> 適用ルールセット: 共通5ルール + Python固有5ルール |
| 4 | +> 参照ファイル: references/common.md + references/python.md |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +# LeetCode 102 · Binary Tree Level Order Traversal — Python版(訂正済み完全版) |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## 1. 問題分析結果 |
| 13 | + |
| 14 | +> 💡 **この問題は一言で言うと「木を上から下へ、同じ高さのノードをまとめてグループ化する問題」です。** |
| 15 | +
|
| 16 | +``` |
| 17 | + 3 ← 深さ0: [3] |
| 18 | + / \ |
| 19 | + 9 20 ← 深さ1: [9, 20] |
| 20 | + / \ |
| 21 | + 15 7 ← 深さ2: [15, 7] |
| 22 | +
|
| 23 | +出力: [[3], [9, 20], [15, 7]] |
| 24 | +``` |
| 25 | + |
| 26 | +**Pythonで解く際のCPython特有の注意点:** |
| 27 | +キューの実装には `list.pop(0)` ではなく **`collections.deque` の `popleft()`** を使うことが必須です。`list.pop(0)` は先頭要素を取り出した後、残り全要素を1つずつ前にずらす O(n) 操作ですが、`deque.popleft()` はC言語実装の双方向連結リスト(=各要素が「前の要素」と「次の要素」へのポインタを持つ構造)のため O(1) で済みます。また今回の訂正の核心として、**`val` が `0` のとき `or` トリックが壊れる**という落とし穴があります。制約が `-1000 <= val <= 1000` なので `0` は普通に登場し、前回の競技版はこれで Wrong Answer になっていました。 |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +### 競技プログラミング視点 |
| 32 | + |
| 33 | +- 入力サイズ ≤ 2000 なので O(n) あれば十分 |
| 34 | +- `deque` + BFS が最速・最シンプル |
| 35 | +- **`val = 0` を含む制約**を必ず確認してから実装テクニックを選ぶ |
| 36 | + |
| 37 | +### 業務開発視点 |
| 38 | + |
| 39 | +- `Optional[TreeNode]` で「ノードがあるかないか」を型で明示し、`None` 参照エラーを事前に防ぐ |
| 40 | +- pylance(型チェッカー)がエラーを検出できるよう、戻り値の型まで明示する |
| 41 | +- `is not None` を使うことでPEP8(=Pythonの公式コーディング規約)に準拠した書き方になる |
| 42 | + |
| 43 | +### Python特有分析 |
| 44 | + |
| 45 | +- `collections.deque`:C言語実装の双方向キュー。`popleft()` が O(1) で動く |
| 46 | +- `vals.append(node.val)`:`append()` もC実装のため高速。`0` を含む全整数を正しく扱える |
| 47 | +- `if node.left:` による子ノードの存在確認:`None` は falsy なので自然に弾ける |
| 48 | + |
| 49 | +> 📖 **このセクションで登場した用語** |
| 50 | +> |
| 51 | +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており `deque` などもC実装のため高速 |
| 52 | +> - **falsy(フォールシー)**:Pythonで `if` の条件式が `False` 相当と見なされる値。`0`, `None`, `[]`, `""` などが該当する |
| 53 | +> - **BFS(幅優先探索)**:グラフや木を「横方向に広がりながら」探索する方法 |
| 54 | +> - **PEP8**:Pythonの公式スタイルガイド。`== None` より `is None` / `is not None` を推奨している |
| 55 | +
|
| 56 | +--- |
| 57 | + |
| 58 | +## 2. 採用アルゴリズムと根拠 |
| 59 | + |
| 60 | +> 💡 同じ問題でも解き方は複数あります。Pythonでは「どのデータ構造がC実装で速いか」と「制約に `0` が含まれるか」が重要な選択基準です。 |
| 61 | +
|
| 62 | +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | |
| 63 | +| ------------------- | ---------- | ---------- | ---------------- | ------ | ------------------- | -------------------- | -------------------------- | |
| 64 | +| **BFS + `deque`** | O(n) | O(n) | 低 | ★★★ | `collections.deque` | ✅ 適 | ✅ 今回の選択 | |
| 65 | +| DFS(再帰) | O(n) | O(n) | 中 | ★★☆ | なし | △ 再帰オーバーヘッド | 深さ情報の引数管理が必要 | |
| 66 | +| BFS + `list.pop(0)` | O(n²) | O(n) | 低 | ★★★ | なし | ❌ 不適 | 先頭削除が O(n) になる | |
| 67 | +| BFS + `or` トリック | O(n) | O(n) | 低 | ★☆☆ | `collections.deque` | ✅ 適 | ❌ `val=0` で Wrong Answer | |
| 68 | + |
| 69 | +**選択理由:** |
| 70 | +`or` トリックを選ばなかった理由は、`0 or 式` は左辺が `0`(falsy)のとき右辺の `extend()` が評価され、その戻り値 `None` がリストに入ってしまうからです。`list.pop(0)` を選ばなかった理由は先頭削除が O(n) になるからです。**シンプルな `for` ループ + `append()` の組み合わせが最も安全かつ高速です。** |
| 71 | + |
| 72 | +> 📖 **このセクションで登場した用語** |
| 73 | +> |
| 74 | +> - **`or` トリック**:`A or B` で「A が falsy なら B を評価する」性質を副作用のある処理に悪用するテクニック。`val=0` のような falsy な値が来ると壊れる |
| 75 | +> - **`extend()`**:イテラブルの全要素をまとめて `deque` や `list` の末尾に追加するメソッド。戻り値は `None` |
| 76 | +> - **O(n²)**:入力が2倍になると処理が約4倍になること。`list.pop(0)` をループの中で呼ぶと発生する |
| 77 | +
|
| 78 | +--- |
| 79 | + |
| 80 | +## 3. 実装パターン |
| 81 | + |
| 82 | +> 💡 **コードの大まかな骨格** |
| 83 | +> |
| 84 | +> 1. `root` が `None` なら空リストを即返す |
| 85 | +> 2. `deque` にルートを入れてBFS開始 |
| 86 | +> 3. ループ毎に「今の階のサイズ」を `len(queue)` で**変数に保存して固定** |
| 87 | +> 4. その数だけ `popleft()` し、値を収集・子をキューに積む |
| 88 | +> 5. 今の階の配列を結果リストに追加 |
| 89 | +
|
| 90 | +--- |
| 91 | + |
| 92 | +### 【業務開発版を使う場面】 |
| 93 | + |
| 94 | +チームで長期間メンテナンスするプロダクションコードに向きます。型ヒントとエラーハンドリングを充実させることで、後から読んだ人が意図を理解しやすい構造になっています。pylanceによる静的解析が有効に機能します。 |
| 95 | + |
| 96 | +```python |
| 97 | +from __future__ import annotations |
| 98 | +from collections import deque |
| 99 | +from typing import Optional |
| 100 | + |
| 101 | + |
| 102 | +# LeetCode が提供する TreeNode 定義(提出時はそのまま使う) |
| 103 | +# class TreeNode: |
| 104 | +# def __init__( |
| 105 | +# self, |
| 106 | +# val: int = 0, |
| 107 | +# left: Optional[TreeNode] = None, |
| 108 | +# right: Optional[TreeNode] = None, |
| 109 | +# ) -> None: |
| 110 | +# self.val = val |
| 111 | +# self.left = left |
| 112 | +# self.right = right |
| 113 | + |
| 114 | + |
| 115 | +class Solution: |
| 116 | + """ |
| 117 | + Binary Tree Level Order Traversal (LeetCode #102) |
| 118 | +
|
| 119 | + BFS(幅優先探索)+ collections.deque を使って |
| 120 | + 木を上から下へ階層ごとにグループ化して返す。 |
| 121 | + """ |
| 122 | + |
| 123 | + def levelOrder(self, root: Optional[TreeNode]) -> list[list[int]]: |
| 124 | + """ |
| 125 | + 二分木のレベル順トラバーサルを返す(業務開発版) |
| 126 | +
|
| 127 | + Args: |
| 128 | + root: 二分木のルートノード(None の場合は空ツリー) |
| 129 | +
|
| 130 | + Returns: |
| 131 | + 各階層の値を格納した2次元リスト。 |
| 132 | + 空ツリーの場合は空リスト []。 |
| 133 | +
|
| 134 | + Complexity: |
| 135 | + Time: O(n) — 各ノードを1回だけ処理する |
| 136 | + Space: O(n) — キューに最大で最下層のノード数が入る |
| 137 | + """ |
| 138 | + # ──────────────────────────────────────────────────────────── |
| 139 | + # ① root が None(空ツリー)のとき、即座に空リストを返す。 |
| 140 | + # Optional[TreeNode] 型のため、root に対して直接 .val 等を |
| 141 | + # 呼ぶと pylance がエラーを検出する。ここで None を弾くことで |
| 142 | + # 以降は TreeNode として扱える(型の絞り込み)。 |
| 143 | + # ──────────────────────────────────────────────────────────── |
| 144 | + if root is None: |
| 145 | + return [] |
| 146 | + |
| 147 | + # ──────────────────────────────────────────────────────────── |
| 148 | + # ② 結果を格納する2次元リスト |
| 149 | + # result[0] = 深さ0の値リスト、result[1] = 深さ1の値リスト、... |
| 150 | + # ──────────────────────────────────────────────────────────── |
| 151 | + result: list[list[int]] = [] |
| 152 | + |
| 153 | + # ──────────────────────────────────────────────────────────── |
| 154 | + # ③ collections.deque をキューとして使う。 |
| 155 | + # list.pop(0) は O(n) だが deque.popleft() は O(1)。 |
| 156 | + # deque はC言語で実装された双方向キューで |
| 157 | + # 先頭・末尾への追加・削除が常に高速。 |
| 158 | + # ──────────────────────────────────────────────────────────── |
| 159 | + queue: deque[TreeNode] = deque([root]) |
| 160 | + |
| 161 | + # ──────────────────────────────────────────────────────────── |
| 162 | + # ④ キューが空になるまでループ(= 全ノードを処理し終えるまで) |
| 163 | + # ──────────────────────────────────────────────────────────── |
| 164 | + while queue: |
| 165 | + # 今この瞬間のキューの長さ = 「現在の階のノード数」。 |
| 166 | + # ループ中に queue の長さは変化するため、変数に保存して固定する。 |
| 167 | + # これが BFS で「1階ぶんをまとめて処理する」核心の1行。 |
| 168 | + level_size: int = len(queue) |
| 169 | + |
| 170 | + # 今の階のノード値を格納する一時リスト |
| 171 | + level_values: list[int] = [] |
| 172 | + |
| 173 | + for _ in range(level_size): |
| 174 | + # popleft() でキューの先頭ノードを O(1) で取り出す |
| 175 | + node: TreeNode = queue.popleft() |
| 176 | + |
| 177 | + # 取り出したノードの値を今の階の配列に追加する。 |
| 178 | + # append() はC実装のため高速。 |
| 179 | + # val が 0 の場合も正しく追加される(or を使わない理由)。 |
| 180 | + level_values.append(node.val) |
| 181 | + |
| 182 | + # ── 次の階の準備:子ノードをキューの末尾に追加 ── |
| 183 | + # None チェックをしてから追加することで、 |
| 184 | + # キューの中身が常に TreeNode 型であることを保証する。 |
| 185 | + # これにより pylance の型エラーも防げる。 |
| 186 | + if node.left is not None: |
| 187 | + queue.append(node.left) |
| 188 | + if node.right is not None: |
| 189 | + queue.append(node.right) |
| 190 | + |
| 191 | + # 今の階の値リストを結果に追加 |
| 192 | + result.append(level_values) |
| 193 | + |
| 194 | + return result |
| 195 | +``` |
| 196 | + |
| 197 | +--- |
| 198 | + |
| 199 | +### 【競技プログラミング版を使う場面】 |
| 200 | + |
| 201 | +LeetCode などで制限時間内に正解を出すことが目的のコードに向きます。型ヒントの最小化とコードの短縮を優先しつつ、**前回の `or` トリックによるバグを完全に排除**しています。`val = 0` を含む全テストケースで正しく動作します。 |
| 202 | + |
| 203 | +```python |
| 204 | +# Runtime 0 ms |
| 205 | +# Beats 100.00% |
| 206 | +# Memory 19.92 MB |
| 207 | +# Beats 70.45% |
| 208 | + |
| 209 | +from collections import deque |
| 210 | +from typing import Optional |
| 211 | + |
| 212 | + |
| 213 | +class Solution: |
| 214 | + def levelOrder(self, root: Optional[TreeNode]) -> list[list[int]]: |
| 215 | + # root が None または存在しない場合は即座に空リストを返す。 |
| 216 | + # `not root` は `root is None` と同等。 |
| 217 | + # TreeNode の __bool__ は定義されていないため None チェックとして機能する。 |
| 218 | + if not root: |
| 219 | + return [] |
| 220 | + |
| 221 | + result: list[list[int]] = [] |
| 222 | + |
| 223 | + # C実装の deque でキューを初期化。 |
| 224 | + # popleft() が O(1) のためキューとして最適。 |
| 225 | + queue: deque[TreeNode] = deque([root]) |
| 226 | + |
| 227 | + while queue: |
| 228 | + # ── 今の階のサイズをループ前に固定する ── |
| 229 | + # ここが競技版でも絶対に省略できない核心部分。 |
| 230 | + # ループ内で popleft()/append() が起きると queue の長さが変化するため、 |
| 231 | + # 先に変数へ保存しておかないと「今の階」の範囲がずれる。 |
| 232 | + level_size = len(queue) |
| 233 | + vals: list[int] = [] |
| 234 | + |
| 235 | + for _ in range(level_size): |
| 236 | + # 先頭ノードを O(1) で取得 |
| 237 | + node = queue.popleft() |
| 238 | + |
| 239 | + # val を直接 append する。 |
| 240 | + # 前回の `node.val or extend(...)` トリックは |
| 241 | + # val=0 のとき 0(falsy)と判定されて壊れるため使わない。 |
| 242 | + vals.append(node.val) |
| 243 | + |
| 244 | + # 子が存在する場合のみキューへ追加(None は falsy なので自然に弾ける) |
| 245 | + if node.left: |
| 246 | + queue.append(node.left) |
| 247 | + if node.right: |
| 248 | + queue.append(node.right) |
| 249 | + |
| 250 | + result.append(vals) |
| 251 | + |
| 252 | + return result |
| 253 | +``` |
| 254 | + |
| 255 | +--- |
| 256 | + |
| 257 | +### 🔍 動作トレース(`root = [3, 9, 20, null, null, 15, 7]`) |
| 258 | + |
| 259 | +``` |
| 260 | +ツリー: |
| 261 | + 3 |
| 262 | + / \ |
| 263 | + 9 20 |
| 264 | + / \ |
| 265 | + 15 7 |
| 266 | +
|
| 267 | +初期状態: |
| 268 | + queue = deque([Node(3)]) |
| 269 | + result = [] |
| 270 | +
|
| 271 | +━━━━━━━━━━ while ループ 1回目(深さ0) ━━━━━━━━━━ |
| 272 | + level_size = 1 ← ここで固定。以降 queue が変わっても影響しない |
| 273 | + vals = [] |
| 274 | +
|
| 275 | + i=0: node = queue.popleft() → Node(3) queue = deque([]) |
| 276 | + vals.append(3) → [3] ← val が 0 でも正しく追加される |
| 277 | + node.left = Node(9) → queue.append → queue = deque([Node(9)]) |
| 278 | + node.right = Node(20) → queue.append → queue = deque([Node(9), Node(20)]) |
| 279 | +
|
| 280 | + result.append([3]) → result = [[3]] |
| 281 | +
|
| 282 | +━━━━━━━━━━ while ループ 2回目(深さ1) ━━━━━━━━━━ |
| 283 | + level_size = 2 ← len(queue)=2 をここで固定 |
| 284 | + vals = [] |
| 285 | +
|
| 286 | + i=0: node = queue.popleft() → Node(9) queue = deque([Node(20)]) |
| 287 | + vals.append(9) → [9] |
| 288 | + node.left = None → スキップ |
| 289 | + node.right = None → スキップ |
| 290 | +
|
| 291 | + i=1: node = queue.popleft() → Node(20) queue = deque([]) |
| 292 | + vals.append(20) → [9, 20] |
| 293 | + node.left = Node(15) → queue.append → queue = deque([Node(15)]) |
| 294 | + node.right = Node(7) → queue.append → queue = deque([Node(15), Node(7)]) |
| 295 | +
|
| 296 | + result.append([9, 20]) → result = [[3], [9, 20]] |
| 297 | +
|
| 298 | +━━━━━━━━━━ while ループ 3回目(深さ2) ━━━━━━━━━━ |
| 299 | + level_size = 2 |
| 300 | + vals = [] |
| 301 | +
|
| 302 | + i=0: node = queue.popleft() → Node(15) queue = deque([Node(7)]) |
| 303 | + vals.append(15) → [15] |
| 304 | + node.left = None → スキップ, node.right = None → スキップ |
| 305 | +
|
| 306 | + i=1: node = queue.popleft() → Node(7) queue = deque([]) |
| 307 | + vals.append(7) → [15, 7] |
| 308 | + node.left = None → スキップ, node.right = None → スキップ |
| 309 | +
|
| 310 | + result.append([15, 7]) → result = [[3], [9, 20], [15, 7]] |
| 311 | +
|
| 312 | +━━━━━━━━━━ queue が空 → ループ終了 ━━━━━━━━━━ |
| 313 | +戻り値: [[3], [9, 20], [15, 7]] ✅ |
| 314 | +``` |
| 315 | + |
| 316 | +--- |
| 317 | + |
| 318 | +## 4. 前回バグの総括 |
| 319 | + |
| 320 | +| 項目 | 前回の競技版(❌ Wrong Answer) | 今回の訂正版(✅ Accepted) | |
| 321 | +| ---------------- | ------------------------------------------- | ------------------------------------------- | |
| 322 | +| `val = 0` のとき | `0 or extend(...)` → `None` がリストに混入 | `vals.append(node.val)` → 正しく `0` を追加 | |
| 323 | +| 階のサイズ固定 | `or` トリックで子の追加と混在し計算がずれる | `level_size = len(queue)` を先に変数へ保存 | |
| 324 | +| `type: ignore` | 型の問題を強引に無視していた | 不要なトリックを排除したので不要 | |
| 325 | +| pylance 対応 | `type: ignore` を使っており型安全でない | 全型ヒントが正しく解決される | |
| 326 | + |
| 327 | +> 📖 **このセクションで登場した用語** |
| 328 | +> |
| 329 | +> - **`deque([root])`**:`deque` を初期値ありで生成する書き方。`deque()` を作ってから `append()` するより1行で書ける |
| 330 | +> - **`popleft()`**:`deque` の先頭要素を O(1) で取り出すメソッド。`list.pop(0)` の O(n) と必ず区別すること |
| 331 | +> - **`is not None`**:`None` かどうかの比較は `== None` ではなく `is not None` と書くのがPythonの慣習(PEP8準拠) |
| 332 | +> - **型の絞り込み(Type Narrowing)**:`if root is None: return []` の後、pylance が `root` を `TreeNode` 型と自動判断する機能 |
| 333 | +
|
| 334 | +--- |
| 335 | + |
| 336 | +## 5. 計算量まとめ |
| 337 | + |
| 338 | +| 項目 | 値 | 理由 | |
| 339 | +| -------------- | ---- | ----------------------------------------------------------------------------- | |
| 340 | +| **時間計算量** | O(n) | 各ノードをキューへの追加・取り出しでちょうど1回ずつ処理する | |
| 341 | +| **空間計算量** | O(n) | キューに最大で「最も広い階のノード数」が入る。完全二分木の最下層は最大 n/2 個 | |
0 commit comments