Skip to content

Commit e868f02

Browse files
committed
fix(Trees): fix zero val bug in Python solution and add READMEs for 102. Binary Tree Level Order Traversal
1 parent 2d504a5 commit e868f02

6 files changed

Lines changed: 3788 additions & 5 deletions

File tree

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
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 個 |

Algorithm/BinaryTree/claude sonnet 4.6 extended/102. Binary Tree Level Order Traversal/Binary_Tree_Level_Order_Traversal_Typescript.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
> BFS は「同じ階のすべての部屋を開けてから、次の階に進むエレベーター」のようなものです。
7070
> 木でいえば「同じ深さのノードをすべて処理してから、次の深さへ進む」動き方です。
7171
> これを実現するのが **キュー(=先に入れたものを先に出す「行列」のデータ構造)** です。
72-
7372
> 📖 **このセクションで登場した用語**
7473
>
7574
> - **BFS(幅優先探索)**:グラフや木を「横方向に広がりながら」探索する方法

0 commit comments

Comments
 (0)