|
| 1 | +> 🎯 **[algo-beginner スキル発火]** |
| 2 | +> 言語/カテゴリ: TypeScript |
| 3 | +> 適用ルールセット: 共通5ルール + TS固有5ルール |
| 4 | +> 参照ファイル: references/common.md + references/typescript.md |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +# Path Sum(パスの合計)— LeetCode 112 |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## 1. 問題の分析 |
| 13 | + |
| 14 | +> 💡 **この問題は一言で言うと:** |
| 15 | +> 「木の根(てっぺん)から葉(末端)まで下りる道の数値の合計が、目標値と一致するルートが存在するか?」を調べる問題です。 |
| 16 | +
|
| 17 | +### 競技プログラミング視点での分析 |
| 18 | + |
| 19 | +木(ツリー)構造の問題では、**全ノード(節点)を一度ずつ訪れるだけで答えが出る**ため、最低でも O(n) の時間が必要です。各ノードで「今の合計が targetSum に達したか」をチェックしながら下に進めばよく、余計なメモリを使わずに解けます。 |
| 20 | + |
| 21 | +- 実行速度最優先 → **再帰DFS(深さ優先探索)**。関数呼び出しスタックのみを利用し、追加の配列不要 |
| 22 | +- メモリ最小化 → 木の高さ分のスタックしか使わない。最悪 O(n)(一直線の木)、均衡木なら O(log n) |
| 23 | + |
| 24 | +### 業務開発視点での分析 |
| 25 | + |
| 26 | +- `TreeNode | null` という**Union型(複数の型を `|` でつなげた型)** を正確に扱う必要がある |
| 27 | +- `null` チェックを忘れると実行時にクラッシュするため、型ガードで防御する |
| 28 | +- 再帰関数はテストしやすく、ロジックが短く読みやすい → 保守性◎ |
| 29 | + |
| 30 | +### TypeScript特有の考慮点 |
| 31 | + |
| 32 | +- `root: TreeNode | null` を受け取る関数では、最初に `null` チェック(型ガード)を入れることで TypeScript コンパイラが以降の処理で `TreeNode` 型として扱えるようになる |
| 33 | +- 今回は LeetCode 提供の `TreeNode` クラスを使うため、ジェネリクスは不要(型が固定されているため) |
| 34 | +- `boolean` の戻り値型を明示することで、誤って数値や `undefined` を返すミスをコンパイル時に防げる |
| 35 | + |
| 36 | +> 📖 **このセクションで登場した用語** |
| 37 | +> |
| 38 | +> - **DFS(深さ優先探索)**:木を「できるだけ深く」潜ってから引き返す探索方法。迷路を一本道ずつ試すイメージ |
| 39 | +> - **Union型**:`A | B` のように「AまたはB」を表す型。`TreeNode | null` は「ノードかnullのどちらか」を意味する |
| 40 | +> - **型ガード**:`if (root === null)` のように実行時に型を絞り込む仕組み。以降のコードで型を安全に使えるようにする |
| 41 | +> - **コンパイル時**:TypeScriptのコードをJavaScriptに変換する段階。ここでエラーを検出できると実行前にバグを防げる |
| 42 | +
|
| 43 | +--- |
| 44 | + |
| 45 | +## 2. アルゴリズムアプローチ比較 |
| 46 | + |
| 47 | +> 💡 同じ問題でも複数の解き方があります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。「O(n)」は「ノードを全部一度ずつ見る」という意味です。 |
| 48 | +
|
| 49 | +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | |
| 50 | +| --------------------------------- | ---------- | ---------- | ------------ | -------- | ------ | --------------------------------------- | |
| 51 | +| ① 再帰DFS(残りの合計を減らす) | O(n) | O(h) | 低 | 高 | 高 | hは木の高さ。均衡木でO(log n)、最悪O(n) | |
| 52 | +| ② 反復DFS(スタックを自前で管理) | O(n) | O(h) | 中 | 高 | 中 | スタックオーバーフローの心配なし | |
| 53 | +| ③ BFS(幅優先探索、キューを使う) | O(n) | O(w) | 中 | 高 | 低 | wは木の最大幅。パスの復元には不向き | |
| 54 | + |
| 55 | +> 💡 **Big-O記法の読み方**(初学者向け) |
| 56 | +> |
| 57 | +> - `O(n)`:ノード数が2倍になると処理も約2倍(全ノードを一度見る) |
| 58 | +> - `O(h)`:木の高さ分のメモリだけ使う(hはheightの略) |
| 59 | +> - `O(log n)`:均衡した木では高さ ≈ log₂(n) なので、1000ノードでも高さは約10 |
| 60 | +
|
| 61 | +> 📖 **このセクションで登場した用語** |
| 62 | +> |
| 63 | +> - **再帰(Recursion)**:関数が自分自身を呼び出す仕組み。「根→左の子→さらに左…」と自動的に深く潜れる |
| 64 | +> - **スタック(Stack)**:後から積んだものを先に取り出す構造(皿の積み重ねイメージ)。再帰の内部実装でも使われている |
| 65 | +> - **BFS(幅優先探索)**:木を「同じ深さ」の階層ごとに横向きに探索する方法 |
| 66 | +
|
| 67 | +--- |
| 68 | + |
| 69 | +## 3. 選択したアルゴリズムと理由 |
| 70 | + |
| 71 | +- **選択したアプローチ**: ① 再帰DFS(`targetSum` を減らしながら下に進む方法) |
| 72 | + |
| 73 | +- **理由**: |
| 74 | + - BFS(方法③)を選ばない理由 → パスの「合計」を追跡するのに、幅広く横に探索するBFSより、根から葉まで縦に深く追うDFSの方が直感的で合う |
| 75 | + - 反復DFS(方法②)を選ばない理由 → 制約がノード数5000以下のため、再帰の深さ上限に引っかかるリスクが極めて低い。実装もシンプルな再帰の方が読みやすい |
| 76 | + - **再帰DFS(方法①)を選ぶ理由** → コードが最も短く、「左右それぞれの子を同じルールで調べる」という木の性質(再帰的構造)を自然に表現できる |
| 77 | + |
| 78 | +- **TypeScript特有の最適化ポイント**: |
| 79 | + - `root === null` の null チェックで型ガードを使い、コンパイラに「この行以降は `TreeNode` 型が確定」と教える |
| 80 | + - 戻り値 `boolean` を明示することで、誤って `void` や `undefined` を返すコードをコンパイル時にブロック |
| 81 | + |
| 82 | +> 📖 **このセクションで登場した用語** |
| 83 | +> |
| 84 | +> - **再帰的構造**:木が「根+左の小さな木+右の小さな木」という同じ形の組み合わせでできていること。再帰関数と相性が良い |
| 85 | +> - **null チェック(null guard)**:`null` の可能性がある値を使う前に `=== null` で確認する処理 |
| 86 | +
|
| 87 | +--- |
| 88 | + |
| 89 | +## 4. 実装コード |
| 90 | + |
| 91 | +> 💡 **コード全体の骨格(構造の概要)** |
| 92 | +> |
| 93 | +> 1. `root` が `null` なら木が空(または葉を超えた)→ `false` を返す |
| 94 | +> 2. 今いるノードが **葉(子が両方 null)** なら、残り合計が0かどうかを確認して答えを返す |
| 95 | +> 3. 葉でなければ、左の子・右の子のどちらかで条件を満たすパスがあるか再帰的に確認する |
| 96 | +
|
| 97 | +```typescript |
| 98 | +// Runtime 0 ms |
| 99 | +// Beats 100.00% |
| 100 | +// Memory 59.53 MB |
| 101 | +// Beats 41.61% |
| 102 | +/** |
| 103 | + * 二分木の根から葉までのパスの合計が targetSum と等しいパスが存在するか判定する |
| 104 | + * @param root - 二分木の根ノード(null の場合は空の木) |
| 105 | + * @param targetSum - 目標とする合計値 |
| 106 | + * @returns パスが存在すれば true、存在しなければ false |
| 107 | + * @complexity Time: O(n), Space: O(h) n=ノード数, h=木の高さ |
| 108 | + */ |
| 109 | +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { |
| 110 | + // --- ベースケース①:root が null の場合 --- |
| 111 | + // 木が空、または葉を超えて「存在しないノード」に来た場合。 |
| 112 | + // このルートにはパスが存在しないので false を返す。 |
| 113 | + if (root === null) { |
| 114 | + return false; |
| 115 | + // ↑ TypeScriptの型ガード:この行以降 root は TreeNode 型として確定する |
| 116 | + } |
| 117 | + |
| 118 | + // --- ベースケース②:現在のノードが「葉(leaf)」かどうかを確認 --- |
| 119 | + // 葉とは「左の子も右の子も存在しないノード」のこと。 |
| 120 | + // 根から葉まで来たということは、パスの終点に到達したことを意味する。 |
| 121 | + const isLeaf: boolean = root.left === null && root.right === null; |
| 122 | + |
| 123 | + if (isLeaf) { |
| 124 | + // 葉に到達したとき、残りの目標値(targetSum)がちょうど現在のノードの値と等しければ |
| 125 | + // 「このパスの合計 = 最初の targetSum」が成立する。 |
| 126 | + // *ここで root.val を引いた結果が 0 になるか、直接比較するかは好みだが |
| 127 | + // 「今のノードの値で最後の差し引きが済む」と考えると分かりやすい。 |
| 128 | + return root.val === targetSum; |
| 129 | + } |
| 130 | + |
| 131 | + // --- 再帰ステップ:左の子・右の子へ潜る --- |
| 132 | + // 現在のノードの値 (root.val) を targetSum から引くことで |
| 133 | + // 「残りあとどれだけ合計が必要か」を次の階層に渡す。 |
| 134 | + // 例:targetSum=22, root.val=5 → 次の階層では targetSum=17 を目指す |
| 135 | + const remaining: number = targetSum - root.val; |
| 136 | + |
| 137 | + // 左の子ツリーか、右の子ツリーのどちらか一方でも条件を満たすパスがあれば true。 |
| 138 | + // || (論理和)なので、どちらかが true なら即座に true を返す(短絡評価)。 |
| 139 | + return hasPathSum(root.left, remaining) || hasPathSum(root.right, remaining); |
| 140 | +} |
| 141 | +``` |
| 142 | + |
| 143 | +--- |
| 144 | + |
| 145 | +### 💡 動作トレース(Example 1 での変数変化) |
| 146 | + |
| 147 | +``` |
| 148 | +入力: root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 |
| 149 | +
|
| 150 | +木の形(図解): |
| 151 | + 5 |
| 152 | + / \ |
| 153 | + 4 8 |
| 154 | + / / \ |
| 155 | + 11 13 4 |
| 156 | + / \ \ |
| 157 | + 7 2 1 |
| 158 | +
|
| 159 | +───────────────────────────────────────────────────── |
| 160 | +Step 1: hasPathSum(5, 22) |
| 161 | + → root=5, isLeaf=false |
| 162 | + → remaining = 22 - 5 = 17 |
| 163 | + → 左(4, 17) と 右(8, 17) を調べる |
| 164 | +
|
| 165 | +Step 2: hasPathSum(4, 17) ← 左の子を先に調べる |
| 166 | + → root=4, isLeaf=false |
| 167 | + → remaining = 17 - 4 = 13 |
| 168 | + → 左(11, 13) と 右(null, 13) を調べる |
| 169 | +
|
| 170 | +Step 3: hasPathSum(11, 13) |
| 171 | + → root=11, isLeaf=false |
| 172 | + → remaining = 13 - 11 = 2 |
| 173 | + → 左(7, 2) と 右(2, 2) を調べる |
| 174 | +
|
| 175 | +Step 4: hasPathSum(7, 2) |
| 176 | + → root=7, isLeaf=true(子が両方null) |
| 177 | + → 7 === 2 ? → false ❌ |
| 178 | +
|
| 179 | +Step 5: hasPathSum(2, 2) |
| 180 | + → root=2, isLeaf=true(子が両方null) |
| 181 | + → 2 === 2 ? → true ✅ ← ここで条件達成! |
| 182 | +
|
| 183 | +Step 6: Step 5 の true が || で Step 3 に伝わる → true |
| 184 | +Step 7: Step 3 の true が || で Step 2 に伝わる → true |
| 185 | +Step 8: Step 2 の true が || で Step 1 に伝わる → true |
| 186 | +───────────────────────────────────────────────────── |
| 187 | +出力: true |
| 188 | +パス: 5 → 4 → 11 → 2 合計 = 22 ✅ |
| 189 | +``` |
| 190 | + |
| 191 | +``` |
| 192 | +入力: root = [1,2,3], targetSum = 5 |
| 193 | +
|
| 194 | +木の形: |
| 195 | + 1 |
| 196 | + / \ |
| 197 | + 2 3 |
| 198 | +
|
| 199 | +Step 1: hasPathSum(1, 5) |
| 200 | + → remaining = 5 - 1 = 4 |
| 201 | + → 左(2, 4) と 右(3, 4) を調べる |
| 202 | +
|
| 203 | +Step 2: hasPathSum(2, 4) |
| 204 | + → root=2, isLeaf=true → 2 === 4 ? → false ❌ |
| 205 | +
|
| 206 | +Step 3: hasPathSum(3, 4) |
| 207 | + → root=3, isLeaf=true → 3 === 4 ? → false ❌ |
| 208 | +
|
| 209 | +Step 4: false || false → false |
| 210 | +───────────────────────────────────────────────────── |
| 211 | +出力: false ✅ |
| 212 | +``` |
| 213 | + |
| 214 | +``` |
| 215 | +入力: root = [], targetSum = 0 (空の木) |
| 216 | +
|
| 217 | +Step 1: hasPathSum(null, 0) |
| 218 | + → root === null → 即 false を返す |
| 219 | +───────────────────────────────────────────────────── |
| 220 | +出力: false ✅ |
| 221 | +``` |
| 222 | + |
| 223 | +> 📖 **このセクションで登場した用語** |
| 224 | +> |
| 225 | +> - **ベースケース(Base Case)**:再帰関数が「これ以上潜らなくていい」と判断して結果を返す条件。ここでは「null に到達」と「葉に到達」の2つ |
| 226 | +> - **葉(Leaf Node)**:子ノードが1つも存在しない末端のノード。パスの終点となる |
| 227 | +> - **短絡評価(Short-circuit Evaluation)**:`A || B` で A が true なら B を評価せず即 true を返す仕組み。無駄な処理を省ける |
| 228 | +> - **再帰ステップ(Recursive Step)**:関数が自分自身を呼び出す部分。今回は左右の子ノードに対して同じ処理を繰り返す |
| 229 | +> - **remaining**:「残り目標合計」。現在のノードの値を差し引いた後、次の階層に渡す値 |
| 230 | +
|
| 231 | +--- |
| 232 | + |
| 233 | +## TypeScript固有の最適化観点まとめ |
| 234 | + |
| 235 | +### 型安全性の活用 |
| 236 | + |
| 237 | +```typescript |
| 238 | +// ❌ JavaScriptだと null チェックを忘れても実行時まで気づけない |
| 239 | +function hasPathSumJS(root, targetSum) { |
| 240 | + return root.val === targetSum; // null.val → クラッシュ! |
| 241 | +} |
| 242 | + |
| 243 | +// ✅ TypeScriptなら root: TreeNode | null と宣言するだけで |
| 244 | +// コンパイラが「null かもしれないのに使っている」と警告してくれる |
| 245 | +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { |
| 246 | + if (root === null) return false; // ← 型ガードでここ以降は TreeNode 確定 |
| 247 | + // この行では root.val に安全にアクセスできる |
| 248 | + return root.val === targetSum; |
| 249 | +} |
| 250 | +``` |
| 251 | + |
| 252 | +**JavaScriptにはない理由**: JavaScriptは実行するまで型のミスに気づけません。TypeScriptの `TreeNode | null` という型宣言により、コンパイル時(コードをJavaScriptに変換する段階)に「null の可能性があるのに使っている」とエラーを出してくれます。 |
| 253 | + |
| 254 | +### readonly 修飾子について |
| 255 | + |
| 256 | +今回は LeetCode が `TreeNode` クラスを提供するため自分で定義しませんが、業務コードとして自分で定義するなら以下のように `readonly` をつけることで意図しない書き換えを防げます: |
| 257 | + |
| 258 | +```typescript |
| 259 | +// 業務開発版:ノードの値が外から書き換えられないように readonly で守る |
| 260 | +class SafeTreeNode { |
| 261 | + readonly val: number; // val を書き換え不可にする(JavaScriptにはない保護) |
| 262 | + readonly left: SafeTreeNode | null; |
| 263 | + readonly right: SafeTreeNode | null; |
| 264 | + |
| 265 | + constructor(val = 0, left: SafeTreeNode | null = null, right: SafeTreeNode | null = null) { |
| 266 | + this.val = val; |
| 267 | + this.left = left; |
| 268 | + this.right = right; |
| 269 | + } |
| 270 | +} |
| 271 | +``` |
| 272 | + |
| 273 | +> 📖 **このセクションで登場した用語** |
| 274 | +> |
| 275 | +> - **readonly**:変数の値を変更できないようにする修飾子。JavaScriptにはなく、TypeScript独自の機能。意図せぬ書き換えをコンパイル時(実行前)に防げる |
| 276 | +> - **型ガード(Type Guard)**:`if (root === null)` のように実行時に型を絞り込む処理。TypeScriptコンパイラはこれを認識し、以降のブロックで型を自動的に絞り込んでくれる(型推論の一種) |
| 277 | +> - **コンパイル時エラー**:TypeScriptをJavaScriptに変換する段階で検出するエラー。実行前に気づけるため、実行時クラッシュより安全 |
0 commit comments