|
| 1 | +> 🎯 **[algo-beginner スキル発火]** |
| 2 | +> 言語/カテゴリ: TypeScript |
| 3 | +> 適用ルールセット: 共通5ルール + TS固有5ルール |
| 4 | +> 参照ファイル: references/common.md + references/typescript.md |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +# 1. 問題の分析 |
| 9 | + |
| 10 | +> 💡 **この問題は一言で言うと**「二分木(=各ノードが最大2つの子を持つ木構造)を階層ごとに読み取り、偶数階層は左→右、奇数階層は右→左と**ジグザグ**に読む問題」です。 |
| 11 | +
|
| 12 | +## 競技プログラミング視点での分析 |
| 13 | + |
| 14 | +通常のレベルオーダー(幅優先探索=BFS)に「偶数階層か奇数階層かで読む向きを反転する」処理を追加します。 |
| 15 | + |
| 16 | +- 全ノードを一度だけ訪問すれば解けるため、時間計算量(=処理にかかる手間の目安)は **O(n)** が目標 |
| 17 | +- キューに保持するノード数は「最も幅が広い階層のノード数」が上限 → 完全二分木なら最大 `n/2` ノード |
| 18 | + |
| 19 | +## 業務開発視点での分析 |
| 20 | + |
| 21 | +- `TreeNode | null` という **Union型(=複数の型のどちらかを表す型)** を通じてnull安全性を確保 |
| 22 | +- `root` が `null`(空ツリー)のケースは最初にガードして早期リターンする |
| 23 | +- 各階層の結果を `number[][]` に格納するため、型が明確で保守しやすい |
| 24 | + |
| 25 | +## TypeScript特有の考慮点 |
| 26 | + |
| 27 | +- LeetCode環境では `TreeNode` クラスが外部から定義されているため、ジェネリクスは使わず `number` 固定で問題なし |
| 28 | +- `readonly` や `const assertion` より**可読性を最優先**にした実装が現場にもマッチする |
| 29 | + |
| 30 | +> 📖 **このセクションで登場した用語** |
| 31 | +> |
| 32 | +> - **BFS(幅優先探索)**:木やグラフを「階層ごと」に左から右へ探索する方法。キュー(行列)を使う |
| 33 | +> - **Union型**:`A | B` のように「AかBのどちらかの型」を表すTypeScript独自の型表現 |
| 34 | +> - **null安全性**:`null` や `undefined` への予期せぬアクセスでクラッシュしないように守る仕組み |
| 35 | +
|
| 36 | +--- |
| 37 | + |
| 38 | +# 2. アルゴリズムアプローチ比較 |
| 39 | + |
| 40 | +> 💡 同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べ、最適なものを選びます。 |
| 41 | +
|
| 42 | +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | |
| 43 | +| ------------------------------------------------ | ---------- | ----------- | ------------ | -------- | ------ | ------------------------------------- | |
| 44 | +| **BFS + 偶奇で reverse** | O(n) | O(n) | 低 | 高 | ⭐高 | 最もシンプル・直感的 | |
| 45 | +| BFS + 両端キュー(deque)で先頭/末尾交互挿入 | O(n) | O(n) | 中 | 高 | 中 | JS標準にdequeがなく実装が煩雑 | |
| 46 | +| DFS(深さ優先探索)+ 各階層にpush | O(n) | O(n) + O(h) | 中 | 高 | 中 | 再帰スタックが木の高さ h 分追加される | |
| 47 | +| ブルートフォース(全ノードを配列に格納して整理) | O(n²) | O(n) | 低 | 高 | 低 | reverseが各階層で走り非効率 | |
| 48 | + |
| 49 | +> 💡 **Big-O記法の読み方** |
| 50 | +> |
| 51 | +> - `O(n)`:ノード数が2倍になると処理も約2倍(一番無駄がない) |
| 52 | +> - `O(h)`:木の高さ h 分のスタック消費(最悪 O(n)、バランスが取れていれば O(log n)) |
| 53 | +> 📖 **このセクションで登場した用語** |
| 54 | +> - **時間計算量**:ノード数に対して処理の手間がどう増えるかの目安 |
| 55 | +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 |
| 56 | +> - **deque(両端キュー)**:先頭・末尾どちらからでも追加・取り出しができるデータ構造 |
| 57 | +
|
| 58 | +--- |
| 59 | + |
| 60 | +# 3. 選択したアルゴリズムと理由 |
| 61 | + |
| 62 | +- **選択したアプローチ**: **BFS + 偶数階層はそのまま / 奇数階層は reverse** |
| 63 | + |
| 64 | +| 観点 | 理由 | |
| 65 | +| ----------------------- | -------------------------------------------------------------------------------------- | |
| 66 | +| 計算量 | O(n) で全ノードを一度だけ処理。反転は各階層のサイズに応じた O(k) なのでトータルは O(n) | |
| 67 | +| 型安全性 | キューの型を `TreeNode[]` で明示でき、null チェックも自然に書ける | |
| 68 | +| 可読性 | BFS の骨格はシンプルなので「偶奇で方向を切り替える」意図がコードから一目で読み取れる | |
| 69 | +| dequeを選ばなかった理由 | JavaScriptには標準の deque がなく、配列の `unshift` は O(n) コストがかかるため不利 | |
| 70 | +| DFSを選ばなかった理由 | 再帰の深さが木の高さ分スタックを消費するため、偏った木(線形に伸びた木)では危険 | |
| 71 | + |
| 72 | +> 📖 **このセクションで登場した用語** |
| 73 | +> |
| 74 | +> - **BFS(幅優先探索)**:キューを使って「今いる階層を全部処理してから次の階層へ」進む方法 |
| 75 | +> - **reverse**:配列の要素順を逆にするメソッド |
| 76 | +> - **unshift**:配列の先頭に要素を追加するメソッド。末尾追加(push)と違い O(n) のコストがかかる |
| 77 | +
|
| 78 | +--- |
| 79 | + |
| 80 | +# 4. 実装コード |
| 81 | + |
| 82 | +> 💡 **コードの大まかな構造(骨格)** |
| 83 | +> |
| 84 | +> 1. `root` が `null` なら空配列を即リターン(ガード節) |
| 85 | +> 2. キュー(=行列)に `root` を入れて BFS 開始 |
| 86 | +> 3. 各階層のノードを全て取り出しながら値を収集し、子ノードをキューへ追加 |
| 87 | +> 4. 奇数階層(1, 3, 5…)なら収集した値を逆順にして結果に追加 |
| 88 | +> 5. 全階層を処理し終えた結果配列を返す |
| 89 | +
|
| 90 | +```typescript |
| 91 | +function zigzagLevelOrder(root: TreeNode | null): number[][] { |
| 92 | + // ── ガード節 ──────────────────────────────────────────────── |
| 93 | + // root が null(空ツリー)なら即座に空配列を返す。 |
| 94 | + // 後続の処理でノードへアクセスして null 参照エラーが起きるのを防ぐため。 |
| 95 | + if (root === null) return []; |
| 96 | + |
| 97 | + // ── 結果格納用の配列 ───────────────────────────────────────── |
| 98 | + // 各階層の値の配列を順に格納していく。最終的にこれを返す。 |
| 99 | + const result: number[][] = []; |
| 100 | + |
| 101 | + // ── キュー(待ち行列)の初期化 ─────────────────────────────── |
| 102 | + // キューとは「先に入れたものが先に出る(FIFO)」データ構造。 |
| 103 | + // レジの行列と同じイメージで、最初にルートノードを並ばせる。 |
| 104 | + const queue: TreeNode[] = [root]; |
| 105 | + |
| 106 | + // ── BFS メインループ ───────────────────────────────────────── |
| 107 | + // キューが空になるまで繰り返す。 |
| 108 | + // 「キューが空 = 未処理のノードがなくなった」ことを意味する。 |
| 109 | + while (queue.length > 0) { |
| 110 | + // 現在の階層にいるノード数を確定させる。 |
| 111 | + // ループ中にキューへ子ノードを追加していくため、 |
| 112 | + // 「今の階層のノード数」をループ開始時点で固定しておく必要がある。 |
| 113 | + const levelSize: number = queue.length; |
| 114 | + |
| 115 | + // この階層のノード値を格納する一時配列。 |
| 116 | + // 後で偶奇に応じて逆順にするため、先に全値を収集する。 |
| 117 | + const levelValues: number[] = []; |
| 118 | + |
| 119 | + // ── 現在の階層を全て処理する ─────────────────────────────── |
| 120 | + for (let i = 0; i < levelSize; i++) { |
| 121 | + // キューの先頭からノードを取り出す。 |
| 122 | + // shift() は配列の先頭を取り出す操作(BFS の「先入れ先出し」を実現)。 |
| 123 | + // ここで取り出すのは必ず TreeNode(null が入ることはない) |
| 124 | + // のでアサーション(!)で TypeScript に伝える。 |
| 125 | + const node = queue.shift()!; |
| 126 | + |
| 127 | + // 現在ノードの値を収集する。 |
| 128 | + // ジグザグ処理は後でまとめて行うため、ここでは単純に追加。 |
| 129 | + levelValues.push(node.val); |
| 130 | + |
| 131 | + // 左の子ノードが存在すれば次の階層用にキューへ追加する。 |
| 132 | + // null チェックをしてから追加することで、 |
| 133 | + // null をキューに入れて後続処理がクラッシュするのを防ぐ。 |
| 134 | + if (node.left !== null) queue.push(node.left); |
| 135 | + |
| 136 | + // 右の子ノードも同様にキューへ追加する。 |
| 137 | + if (node.right !== null) queue.push(node.right); |
| 138 | + } |
| 139 | + |
| 140 | + // ── ジグザグ処理(偶奇による方向切り替え)────────────────── |
| 141 | + // result.length は「今まで完了した階層数」と等しい。 |
| 142 | + // - result.length が偶数(0, 2, 4…)→ 左→右(そのまま) |
| 143 | + // - result.length が奇数(1, 3, 5…)→ 右→左(逆順) |
| 144 | + // reverse() は配列を破壊的に逆順にする。levelValues は |
| 145 | + // この後使わないので破壊的操作で問題ない。 |
| 146 | + if (result.length % 2 === 1) { |
| 147 | + levelValues.reverse(); |
| 148 | + } |
| 149 | + |
| 150 | + // 処理済み階層の値を最終結果に追加する。 |
| 151 | + result.push(levelValues); |
| 152 | + } |
| 153 | + |
| 154 | + // 全階層を処理した結果を返す。 |
| 155 | + return result; |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +# 5. 動作トレース |
| 162 | + |
| 163 | +入力: `root = [3, 9, 20, null, null, 15, 7]` |
| 164 | + |
| 165 | +``` |
| 166 | +【ツリーの形状】 |
| 167 | + 3 ← 階層 0(偶数 → 左→右) |
| 168 | + / \ |
| 169 | + 9 20 ← 階層 1(奇数 → 右→左) |
| 170 | + / \ |
| 171 | + 15 7 ← 階層 2(偶数 → 左→右) |
| 172 | +
|
| 173 | +┌─────────────────────────────────────────────────────────┐ |
| 174 | +│ Step 0: 初期状態 │ |
| 175 | +│ queue = [Node(3)] │ |
| 176 | +│ result = [] │ |
| 177 | +└─────────────────────────────────────────────────────────┘ |
| 178 | +
|
| 179 | +┌─────────────────────────────────────────────────────────┐ |
| 180 | +│ Step 1: 階層 0 を処理(result.length=0 → 偶数 → そのまま)│ |
| 181 | +│ levelSize = 1 │ |
| 182 | +│ 取り出し : Node(3) → levelValues = [3] │ |
| 183 | +│ 子を追加 : queue = [Node(9), Node(20)] │ |
| 184 | +│ 偶数階層 : reverse しない → [3] │ |
| 185 | +│ result = [[3]] │ |
| 186 | +└─────────────────────────────────────────────────────────┘ |
| 187 | +
|
| 188 | +┌─────────────────────────────────────────────────────────┐ |
| 189 | +│ Step 2: 階層 1 を処理(result.length=1 → 奇数 → 逆順) │ |
| 190 | +│ levelSize = 2 │ |
| 191 | +│ 取り出し : Node(9) → levelValues = [9] │ |
| 192 | +│ Node(20) → levelValues = [9, 20] │ |
| 193 | +│ 子を追加 : Node(9) の子は null / Node(20) の子追加 │ |
| 194 | +│ queue = [Node(15), Node(7)] │ |
| 195 | +│ 奇数階層 : reverse → [20, 9] │ |
| 196 | +│ result = [[3], [20, 9]] │ |
| 197 | +└─────────────────────────────────────────────────────────┘ |
| 198 | +
|
| 199 | +┌─────────────────────────────────────────────────────────┐ |
| 200 | +│ Step 3: 階層 2 を処理(result.length=2 → 偶数 → そのまま)│ |
| 201 | +│ levelSize = 2 │ |
| 202 | +│ 取り出し : Node(15) → levelValues = [15] │ |
| 203 | +│ Node(7) → levelValues = [15, 7] │ |
| 204 | +│ 子を追加 : 子は全て null → queue = [] │ |
| 205 | +│ 偶数階層 : reverse しない → [15, 7] │ |
| 206 | +│ result = [[3], [20, 9], [15, 7]] │ |
| 207 | +└─────────────────────────────────────────────────────────┘ |
| 208 | +
|
| 209 | +✅ queue が空になったのでループ終了 |
| 210 | +🎉 最終出力: [[3], [20, 9], [15, 7]] |
| 211 | +``` |
| 212 | + |
| 213 | +--- |
| 214 | + |
| 215 | +# LeetCode 提出コード(最終版) |
| 216 | + |
| 217 | +```typescript |
| 218 | +// Runtime 0 ms |
| 219 | +// Beats 100.00% |
| 220 | +// Memory 58.12 MB |
| 221 | +// Beats 22.08% |
| 222 | +function zigzagLevelOrder(root: TreeNode | null): number[][] { |
| 223 | + if (root === null) return []; |
| 224 | + |
| 225 | + const result: number[][] = []; |
| 226 | + const queue: TreeNode[] = [root]; |
| 227 | + |
| 228 | + while (queue.length > 0) { |
| 229 | + const levelSize: number = queue.length; |
| 230 | + const levelValues: number[] = []; |
| 231 | + |
| 232 | + for (let i = 0; i < levelSize; i++) { |
| 233 | + const node = queue.shift()!; |
| 234 | + levelValues.push(node.val); |
| 235 | + if (node.left !== null) queue.push(node.left); |
| 236 | + if (node.right !== null) queue.push(node.right); |
| 237 | + } |
| 238 | + |
| 239 | + if (result.length % 2 === 1) levelValues.reverse(); |
| 240 | + result.push(levelValues); |
| 241 | + } |
| 242 | + |
| 243 | + return result; |
| 244 | +} |
| 245 | +``` |
| 246 | + |
| 247 | +--- |
| 248 | + |
| 249 | +# TypeScript固有の最適化観点 |
| 250 | + |
| 251 | +### `shift()!` の Non-null アサーション(`!`)について |
| 252 | + |
| 253 | +TypeScriptは `Array.shift()` の戻り値型を `T | undefined` と判断します。しかし今回は `levelSize = queue.length` でサイズを固定し、`levelSize` 回だけループしているため、**ループ内で `shift()` が `undefined` を返すことは構造上ありえません**。そのため `!` アサーション(=「私が保証するのでundefinedチェックは不要」と伝える記号)を使うことで型エラーを回避しています。 |
| 254 | + |
| 255 | +```typescript |
| 256 | +// TypeScript がエラーを出す(T | undefined を node: TreeNode に代入できない) |
| 257 | +const node: TreeNode = queue.shift(); // ❌ |
| 258 | + |
| 259 | +// ! で「undefined にならないことを私が保証する」と伝える |
| 260 | +const node = queue.shift()!; // ✅ |
| 261 | +``` |
| 262 | + |
| 263 | +### なぜ `const` で `queue` を宣言するか |
| 264 | + |
| 265 | +`const` は「変数自体の再代入禁止」であり、配列の中身の変更(`push` / `shift`)は許されます。これにより「`queue` という名前が別の配列に差し替えられる」バグを防ぎつつ、BFSの操作は問題なく行えます。 |
| 266 | + |
| 267 | +--- |
| 268 | + |
| 269 | +> 📖 **このセクションで登場した用語** |
| 270 | +> |
| 271 | +> - **BFS(幅優先探索)**:キューを使って階層ごとに探索する方法。パン屋のレジ行列のように「先に来た人が先に処理される」 |
| 272 | +> - **キュー(Queue)**:先入れ先出し(FIFO)のデータ構造。`push` で末尾追加、`shift` で先頭取り出し |
| 273 | +> - **ガード節(早期リターン)**:関数の冒頭でエラーや特殊ケースをチェックし、すぐ `return` する書き方。後続のコードをシンプルに保てる |
| 274 | +> - **Non-null アサーション(`!`)**:TypeScriptに「この値は絶対に null/undefined ではない」と伝える記号。使いすぎると危険なので、「構造上あり得ない」と証明できる場合のみ使う |
| 275 | +> - **reverse()**:配列を破壊的に(元の配列を直接変えて)逆順にするメソッド |
| 276 | +> - **shift()**:配列の先頭要素を取り出すメソッド。取り出した要素は配列から削除される |
0 commit comments