Skip to content

Commit 492aa89

Browse files
committed
feat: add LeetCode 112 Path Sum solution and update markdownlint configuration
1 parent 82cd0bb commit 492aa89

7 files changed

Lines changed: 4048 additions & 7 deletions

File tree

.markdownlint.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"MD002": true,
55
"MD003": { "style": "atx" },
66
"MD004": { "style": "consistent" },
7-
"MD007": { "indent": 2 },
7+
"MD007": { "indent": 4 },
88
"MD009": { "br_spaces": 2 },
99
"MD012": true,
1010
"MD013": {
@@ -20,6 +20,8 @@
2020
"MD024": { "siblings_only": true },
2121
"MD025": { "front_matter_title": "" },
2222
"MD026": { "punctuation": ".,;:!。,;:" },
23+
"MD027": false,
24+
"MD028": false,
2325
"MD029": false,
2426
"MD030": {
2527
"ul_single": 1,
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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

Comments
 (0)