|
| 1 | +# Binary Tree Level Order Traversal - 木を階層ごとにグループ化する |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +## 目次(Table of Contents) |
| 6 | + |
| 7 | +- [1. Overview](#overview) |
| 8 | +- [2. Algorithm](#algorithm) |
| 9 | +- [3. Complexity](#complexity) |
| 10 | +- [4. Implementation](#implementation) |
| 11 | +- [5. Optimization](#optimization) |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +<h2 id="overview">1. Overview</h2> |
| 16 | + |
| 17 | +> 💡 **この問題は一言で言うと「木を上から下へ、同じ高さのノードをまとめてグループ化する問題」です。** |
| 18 | +
|
| 19 | +与えられた二分木(=各ノードが最大2つの子を持つ木構造のデータ)のノードを、 |
| 20 | +**深さ(根からの距離)が同じものをひとつの配列にまとめ**、深さ順に並べた2次元配列を返します。 |
| 21 | + |
| 22 | +``` |
| 23 | + 3 ← 深さ0 → [3] |
| 24 | + / \ |
| 25 | + 9 20 ← 深さ1 → [9, 20] |
| 26 | + / \ |
| 27 | + 15 7 ← 深さ2 → [15, 7] |
| 28 | +
|
| 29 | +出力: [[3], [9, 20], [15, 7]] |
| 30 | +``` |
| 31 | + |
| 32 | +**なぜこの問題が難しいのか:** |
| 33 | +木構造を「縦(深さ方向)」に探索するのは直感的ですが、この問題は「横(同じ深さ)」の単位でまとめる必要があります。 |
| 34 | +そのため、**同じ深さにあるノードをすべて処理し終えてから次の深さへ進む**BFS(幅優先探索)という手法を使う必要があり、 |
| 35 | +その実現に `collections.deque` というデータ構造が鍵を握ります。 |
| 36 | + |
| 37 | +**制約:** |
| 38 | + |
| 39 | +| 項目 | 範囲 | |
| 40 | +| ---------- | ------------------------------------------------ | |
| 41 | +| ノード数 | 0 以上 2000 以下 | |
| 42 | +| ノードの値 | -1000 以上 1000 以下(**0 が含まれる点に注意**) | |
| 43 | + |
| 44 | +> 📖 **この章で登場した用語** |
| 45 | +> |
| 46 | +> - **二分木**:各ノードが左の子・右の子の最大2つを持つ木構造のデータ |
| 47 | +> - **深さ**:根ノードからそのノードまでの辺の数。根の深さは 0 |
| 48 | +> - **BFS(幅優先探索)**:グラフや木を「横方向に広がりながら」探索する方法 |
| 49 | +> - **制約**:入力として与えられる値の範囲や条件のこと |
| 50 | +
|
| 51 | +--- |
| 52 | + |
| 53 | +<h2 id="algorithm">2. Algorithm</h2> |
| 54 | + |
| 55 | +> 💡 **TL;DR(Too Long; Didn't Read)** とは「長くて読めない人向けの要約」という意味です。 |
| 56 | +> ここではアルゴリズム全体の戦略をざっくり把握するための章です。 |
| 57 | +
|
| 58 | +- **手法:BFS(幅優先探索)** |
| 59 | + 木を「同じ深さのノードをすべて処理してから次の深さへ進む」順番で探索する。 |
| 60 | + これが今回の「階層ごとのグループ化」と自然に一致するため選ぶ。 |
| 61 | + |
| 62 | +- **データ構造:`collections.deque`(両端キュー)** |
| 63 | + 先頭からの取り出しが O(1) で行えるため、キュー(行列)として最適。 |
| 64 | + `list.pop(0)` を使うと先頭取り出しが O(n) になり全体が O(n²) へ悪化するため使わない。 |
| 65 | + |
| 66 | +- **核心テクニック:`level_size = len(queue)` をループ前に固定する** |
| 67 | + ループ中にキューへの追加・取り出しが同時に起きるため、「今の階のサイズ」を先に変数へ保存しないとズレが生じる。 |
| 68 | + |
| 69 | +### 図解 |
| 70 | + |
| 71 | +```mermaid |
| 72 | +flowchart TD |
| 73 | + Start[Start levelOrder root] --> IsNone{root is None} |
| 74 | + IsNone -- Yes --> RetEmpty[Return empty list] |
| 75 | + IsNone -- No --> Init[Init result and queue with root] |
| 76 | + Init --> WhileLoop{queue is not empty} |
| 77 | + WhileLoop -- No --> RetResult[Return result] |
| 78 | + WhileLoop -- Yes --> FixSize[level_size = len queue] |
| 79 | + FixSize --> InitVals[vals = empty list] |
| 80 | + InitVals --> ForLoop{i less than level_size} |
| 81 | + ForLoop -- No --> AppendLevel[result.append vals] |
| 82 | + AppendLevel --> WhileLoop |
| 83 | + ForLoop -- Yes --> PopNode[node = queue.popleft] |
| 84 | + PopNode --> AppendVal[vals.append node.val] |
| 85 | + AppendVal --> CheckLeft{node.left exists} |
| 86 | + CheckLeft -- Yes --> PushLeft[queue.append node.left] |
| 87 | + CheckLeft -- No --> CheckRight{node.right exists} |
| 88 | + PushLeft --> CheckRight |
| 89 | + CheckRight -- Yes --> PushRight[queue.append node.right] |
| 90 | + CheckRight -- No --> NextI[i plus 1] |
| 91 | + PushRight --> NextI |
| 92 | + NextI --> ForLoop |
| 93 | +``` |
| 94 | + |
| 95 | +### 正しさのスケッチ |
| 96 | + |
| 97 | +1. **不変条件**:「while ループの各反復の開始時点で、`queue` には現在の階のノードだけが入っている」 |
| 98 | + - `level_size` 回だけ `popleft()` することで今の階を全部処理し、その間に追加された子は「次の階」として末尾に積まれる。 |
| 99 | +2. **網羅性**:ルートから始まり、全ノードの左右の子をキューに積むため、存在する全ノードがちょうど1回処理される。 |
| 100 | +3. **基底条件**:`root is None` なら `[]` を返す。`while queue` で全ノード処理後に終了する。 |
| 101 | + |
| 102 | +--- |
| 103 | + |
| 104 | +<h2 id="complexity">3. Complexity</h2> |
| 105 | + |
| 106 | +| 項目 | 値 | 理由 | |
| 107 | +| -------------- | ---- | ---------------------------------------------------------------------------- | |
| 108 | +| **時間計算量** | O(n) | 各ノードをキューへの追加・取り出しでちょうど1回ずつ処理する(各操作 O(1)) | |
| 109 | +| **空間計算量** | O(n) | `result` 配列と、最大で最下層のノード数(最大 n/2 個)を保持するキューのため | |
| 110 | + |
| 111 | +--- |
| 112 | + |
| 113 | +<h2 id="implementation">4. Implementation</h2> |
| 114 | + |
| 115 | +### Python 実装(業務開発版) |
| 116 | + |
| 117 | +```python |
| 118 | +from collections import deque |
| 119 | +from typing import Optional |
| 120 | + |
| 121 | +class Solution: |
| 122 | + def levelOrder(self, root: Optional[TreeNode]) -> list[list[int]]: |
| 123 | + if root is None: |
| 124 | + return [] |
| 125 | + |
| 126 | + result: list[list[int]] = [] |
| 127 | + queue: deque[TreeNode] = deque([root]) |
| 128 | + |
| 129 | + while queue: |
| 130 | + level_size: int = len(queue) |
| 131 | + level_values: list[int] = [] |
| 132 | + |
| 133 | + for _ in range(level_size): |
| 134 | + node: TreeNode = queue.popleft() |
| 135 | + level_values.append(node.val) |
| 136 | + |
| 137 | + if node.left is not None: |
| 138 | + queue.append(node.left) |
| 139 | + if node.right is not None: |
| 140 | + queue.append(node.right) |
| 141 | + |
| 142 | + result.append(level_values) |
| 143 | + |
| 144 | + return result |
| 145 | +``` |
| 146 | + |
| 147 | +### エッジケースと検証観点 |
| 148 | + |
| 149 | +| ケース | 入力 | 期待出力 | 対処箇所 | |
| 150 | +| ------------------ | ------------- | --------------- | ---------------------------- | |
| 151 | +| 空ツリー | `root = None` | `[]` | `if root is None: return []` | |
| 152 | +| `val = 0` のノード | `[0]` | `[[0]]` | `vals.append(node.val)` | |
| 153 | +| 偏った木 (左のみ) | `1→2→3` | `[[1],[2],[3]]` | `level_size` の固定 | |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +<h2 id="optimization">5. Optimization</h2> |
| 158 | + |
| 159 | +### CPython 最適化ポイント |
| 160 | + |
| 161 | +1. **`list.pop(0)` → `deque.popleft()`**: |
| 162 | + `list.pop(0)` は O(n) の要素シフトが発生し全体で O(n²) になるが、`deque.popleft()` は O(1) で動作する。 |
| 163 | +2. **`or` トリックの回避**: |
| 164 | + `0 or extend(...)` は `val=0`(falsy)のときに右辺を評価してしまい、戻り値 `None` がリストに混入するため、安全な `append()` を使用する。 |
| 165 | + |
| 166 | +### FAQ |
| 167 | + |
| 168 | +- **Q: なぜ `level_size` を先に保存するのですか?** |
| 169 | + - **A**: ループ内で `append()` が行われるため、`len(queue)` が動的に増えてしまい、次の階のノードまで今の階として処理してしまうのを防ぐためです。 |
| 170 | +- **Q: DFS でも解けますか?** |
| 171 | + - **A**: はい。ただし「今どの深さにいるか」を引数で持ち回る必要があり、この問題には BFS の方が直感的で自然です。 |
0 commit comments