diff --git a/.markdownlint.json b/.markdownlint.json index 8df6452..d3f5138 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -4,7 +4,7 @@ "MD002": true, "MD003": { "style": "atx" }, "MD004": { "style": "consistent" }, - "MD007": { "indent": 2 }, + "MD007": { "indent": 4 }, "MD009": { "br_spaces": 2 }, "MD012": true, "MD013": { @@ -20,6 +20,8 @@ "MD024": { "siblings_only": true }, "MD025": { "front_matter_title": "" }, "MD026": { "punctuation": ".,;:!。,;:" }, + "MD027": false, + "MD028": false, "MD029": false, "MD030": { "ul_single": 1, diff --git a/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Python.md b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Python.md new file mode 100644 index 0000000..9a7ecb4 --- /dev/null +++ b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Python.md @@ -0,0 +1,367 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: Python (CPython 3.11.10) +> 適用ルールセット: 共通5ルール + Python固有ルール +> 参照ファイル: references/common.md + references/python.md + +--- + +# Path Sum(パスの合計)— LeetCode 112|Python版 + +--- + +## 1. 問題分析 + +> 💡 **この問題は一言で言うと:** +> 「木の根(てっぺん)から葉(末端)まで下りていくルートの数値の合計が、目標値と一致するパスが1本でも存在するか?」を調べる問題です。 + +### CPython特有の注意点(最初に押さえるべき点) + +Pythonの再帰(関数が自分自身を呼び出すこと)にはデフォルトで **最大再帰深度(=関数が何段まで入れ子になれるか)** が `sys.getrecursionlimit()` で約1000に制限されています。今回の制約は最大5000ノードの木なので、**一直線に伸びた最悪形状の木では再帰深度が5000になりうる**点に注意が必要です。競技版では問題ありませんが、業務版では `sys.setrecursionlimit()` の設定や、再帰を使わない反復(イテレーション)での実装を検討する価値があります。 + +--- + +### 競技プログラミング視点 + +- 制約分析:ノード数 `[0, 5000]`。全ノードを一度ずつ訪れる O(n) で十分間に合う +- 最速手法:再帰DFS。ただしCPythonのデフォルト再帰制限(約1000)では、5000ノードの偏った木で RecursionError が発生する。競技環境や平均的なケースでは最速だが、最悪ケースの深さを考慮するなら sys.setrecursionlimit() を調整するか、反復DFS(スタック使用)が安全。 +- メモリ最小化:追加のデータ構造を使わず、関数呼び出しスタックのみ利用。空間計算量 O(h)(hは木の高さ) + +### 業務開発視点 + +- `Optional[TreeNode]`(= `TreeNode` か `None` のどちらかを表す型ヒント)で `None` の可能性を明示し、pylanceに型エラーを検出させる +- `root is None` チェックを先頭に置き、以降のコードで `root.val` に安全にアクセスできるようにする +- 業務版では反復DFSで再帰深度問題を回避し、プロダクション環境での安全性を高める + +### Python特有の分析 + +- `TreeNode | None`(Python 3.10以降の union 記法)または `Optional[TreeNode]` で null 安全性を表現 +- 今回は追加の標準ライブラリ(`collections`, `heapq` など)は不要。木の構造そのものを再帰で辿るシンプルな問題 +- 業務版の反復DFSでは `collections.deque`(=前後どちらからでも出し入れできる「両端開きの箱」)を活用し、先頭操作を O(1) にする + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、組み込み関数の多くがC実装のため高速 +> - **再帰深度(Recursion Depth)**:関数が自分自身を呼び出す入れ子の深さ。Pythonはデフォルトで約1000まで +> - **DFS(深さ優先探索)**:木をできるだけ深く潜ってから引き返す探索方法。パス(根から葉への道)を追うのに適している +> - **O(h)**:木の高さ(height)に比例するメモリ使用量。均衡した木では O(log n)、一直線の木では O(n) + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 同じ問題でも複数の解き方があります。「速さ(時間計算量)」と「メモリ使用量(空間計算量)」、さらに「Pythonらしい書きやすさ」を比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ------------------- | ---------- | ---------- | ---------------- | ------ | ------------------- | -------------------- | ----------------------------- | +| ① 再帰DFS | O(n) | O(h) | 低 | ★★★ | なし | 適(シンプルで最速) | 競技版向け。再帰深度に注意 | +| ② 反復DFS(deque) | O(n) | O(h) | 中 | ★★☆ | `collections.deque` | 適(C実装deque) | 業務版向け。再帰深度問題なし | +| ③ BFS(幅優先探索) | O(n) | O(w) | 中 | ★☆☆ | `collections.deque` | 適 | wは最大幅。パス追跡には不向き | + +- **競技版に①再帰DFSを選ぶ理由**:コードが最も短く直感的。「木の各ノードに対して同じ処理を繰り返す」という再帰的性質をそのままコードで表現できる +- **業務版に②反復DFSを選ぶ理由**:最大5000ノードの一直線の木では再帰深度が5000に達し、Pythonのデフォルト再帰制限(約1000)を超える恐れがある。`deque` を使ったスタック管理で安全に回避できる + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **`collections.deque`**:前後どちらからでも O(1) で追加・取り出しできる「両端開きの箱」。`list` で先頭操作をすると O(n) かかるため、スタック・キューには `deque` が適している +> - **トレードオフ**:何かを得ると何かを失う関係。再帰は可読性が高いがスタック深度に限界があり、反復はその逆 + +--- + +## 3. 実装パターン + +> 💡 **コードの骨格(全体の構造)** +> +> 1. 入力検証(型チェック・制約確認) +> 2. 空の木(`root is None`)のエッジケース処理 +> 3. 各ノードで「残り目標値 = targetSum − 現在ノードの値」を計算しながら下に進む +> 4. 葉(子が両方 `None`)に到達したとき、残り目標値が 0 かどうかを確認して返す + +--- + +### 【業務開発版を使う場面】 + +チームで長期間メンテナンスするプロダクションコードに向きます。再帰深度の問題を `deque` で回避し、型ヒント・入力検証・docstringを充実させ、後から読んだ人がすぐ理解できる構造にしています。 + +```python +from typing import Optional +from collections import deque + + +# LeetCode 提供の TreeNode クラス(変更不可) +class TreeNode: + def __init__( + self, + val: int = 0, + left: Optional["TreeNode"] = None, + right: Optional["TreeNode"] = None, + ) -> None: + self.val = val + self.left = left + self.right = right + + +class Solution: + """ + Path Sum 解決クラス(業務開発版) + 再帰を使わず反復DFS(dequeスタック)で実装。 + Pythonの再帰深度制限を回避し、最大5000ノードの木でも安全に動作する。 + """ + + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + """ + 根から葉までのパスの合計が targetSum と等しいパスが存在するか判定する。 + + Args: + root: 二分木の根ノード(None の場合は空の木) + targetSum: 目標とするパスの合計値 + + Returns: + 条件を満たすパスが存在すれば True、存在しなければ False + + Raises: + TypeError: root が TreeNode でも None でもない場合 + TypeError: targetSum が int でない場合 + + Complexity: + Time: O(n) — 全ノードを最大1回ずつ訪問 + Space: O(h) — スタックの最大サイズは木の高さ h に比例 + """ + # --- 入力検証 --- + # targetSum の型チェック。Python は動的型付けなので、 + # 呼び出し元が float や str を渡してもコンパイル時には気づけない。 + # isinstance() で実行時に検証することで pylance + 実行時の両方で安全性を確保する。 + if not isinstance(targetSum, int): + raise TypeError( + f"targetSum must be int, got {type(targetSum).__name__}" + ) + + # root の型チェック。TreeNode または None のみを受け付ける。 + if root is not None and not isinstance(root, TreeNode): + raise TypeError( + f"root must be TreeNode or None, got {type(root).__name__}" + ) + + # --- エッジケース:空の木 --- + # root が None のとき、そもそもパスが存在しないので即 False を返す。 + if root is None: + return False + + # --- 反復DFS の準備 --- + # 再帰を避け、Python の再帰深度制限(約1000)を回避するために明示的なスタックを使用。 + # (ノード, 現時点までの残り目標値) のペアを積んでいく。 + # スタックとしての意図を明確にし、必要に応じて効率的な popleft() も可能な deque を採用。 + stack: deque[tuple[TreeNode, int]] = deque() + + # 最初は根ノードと初期の targetSum をスタックに積む。 + stack.append((root, targetSum)) + + # --- 反復DFS 本体 --- + # スタックが空になるまで繰り返す = 全パスを探索し終えるまで続ける。 + while stack: + # スタックの末尾(最後に積んだもの)を取り出す。 + # これが DFS(深さ優先)になる理由:直前に積んだ子を先に処理するから。 + node, remaining = stack.pop() + + # 残り目標値から現在のノードの値を引く。 + # 例)targetSum=22, node.val=5 → remaining=17(残りあと17必要) + remaining -= node.val + + # --- 葉(leaf)の判定 --- + # 葉 = 左の子も右の子も存在しないノード = パスの終点。 + is_leaf: bool = node.left is None and node.right is None + + if is_leaf: + # 葉に到達したとき、残り目標値がちょうど 0 になっていれば + # 「根からこの葉までの合計 = targetSum」が成立している。 + if remaining == 0: + return True + # 0 でなければこのパスは条件を満たさないので、次のパスへ。 + continue + + # --- 子ノードをスタックに積む --- + # 葉でない場合は、存在する子ノードを次の探索対象としてスタックに積む。 + # None チェックを行い、存在する子だけを積む(None を積んでしまうとエラーになる)。 + if node.right is not None: + # 右の子を先に積む。後で左の子を積むと、 + # スタックからは「左の子が先に取り出される」→ 左優先の DFS になる。 + stack.append((node.right, remaining)) + + if node.left is not None: + stack.append((node.left, remaining)) + + # すべてのパスを探索し終えたが条件を満たすものがなかった。 + return False +``` + +--- + +### 【競技プログラミング版を使う場面】 + +LeetCode などのオンラインジャッジで、制限時間内に正解を出すことが目的のコードに向きます。入力検証・docstring を省き、再帰DFS でコードを最短・最速にしています。ただし、最大ノード数が5000という制約のため、一直線に伸びた最悪ケースの木ではCPythonのデフォルト再帰制限(1000)を超えて `RecursionError` が発生するリスクがある点に注意してください。その場合は、反復処理(DFS/BFS)への書き換えや、`sys.setrecursionlimit()` による制限の引き上げが必要です。 + +```python +# Runtime 3 ms +# Beats 29.65% +# Memory 20.24 MB +# Beats 21.80% +from typing import Optional + + +class TreeNode: + def __init__( + self, + val: int = 0, + left: Optional["TreeNode"] = None, + right: Optional["TreeNode"] = None, + ) -> None: + self.val = val + self.left = left + self.right = right + + +class Solution: + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + # root が None = 空の木 or 葉を超えた = パスなし → False + # Python では "if not root:" でも動くが、 + # "root is None" の方が pylance が型を正確に絞り込める(型ガードとして機能する)。 + if root is None: + return False + + # 現在のノードの値を targetSum から引く。 + # こうすることで「残りいくら必要か」を次の階層に伝えられる。 + targetSum -= root.val + + # 葉(left も right も None)に到達したら、残りが 0 かどうかで答えを確定する。 + # Python の "and" は短絡評価(左が False なら右を評価しない)なので効率的。 + if root.left is None and root.right is None: + return targetSum == 0 + + # 葉でなければ、左右の子のどちらかで条件を満たすパスがあれば True。 + # "or" も短絡評価:左が True なら右は評価しない → 早期リターン。 + return ( + self.hasPathSum(root.left, targetSum) + or self.hasPathSum(root.right, targetSum) + ) +``` + +--- + +### 💡 動作トレース(Example 1) + +``` +入力: root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 + +木の形: + 5 + / \ + 4 8 + / / \ + 11 13 4 + / \ \ + 7 2 1 + +─── 競技版(再帰DFS)のトレース ─────────────────────────── + +Step 1: hasPathSum(node=5, targetSum=22) + → root is not None → 処理続行 + → targetSum = 22 - 5 = 17 + → 葉でない(left=4, right=8) + → 左 hasPathSum(4, 17) を先に評価(or の短絡評価) + +Step 2: hasPathSum(node=4, targetSum=17) + → targetSum = 17 - 4 = 13 + → 葉でない(left=11, right=None) + → 左 hasPathSum(11, 13) を評価 + +Step 3: hasPathSum(node=11, targetSum=13) + → targetSum = 13 - 11 = 2 + → 葉でない(left=7, right=2) + → 左 hasPathSum(7, 2) を評価 + +Step 4: hasPathSum(node=7, targetSum=2) + → targetSum = 2 - 7 = -5 + → 葉(left=None, right=None) + → -5 == 0 ? → False ❌ + +Step 5: hasPathSum(node=2, targetSum=2) ← or の右側を評価 + → targetSum = 2 - 2 = 0 + → 葉(left=None, right=None) + → 0 == 0 ? → True ✅ + +Step 6: True が or を通じて Step 3 → Step 2 → Step 1 へ伝播 +───────────────────────────────────────────────────── +出力: True ✅ +パス: 5 → 4 → 11 → 2 合計 = 22 +``` + +``` +─── 業務版(反復DFS + deque)のトレース ────────────────────── + +初期状態: + stack = [(node=5, remaining=22)] + +Iteration 1: pop → (node=5, remaining=22) + remaining = 22 - 5 = 17 + is_leaf = False(left=4, right=8) + → stack に right(8, 17) を追加 → stack = [(8, 17)] + → stack に left(4, 17) を追加 → stack = [(8, 17), (4, 17)] + +Iteration 2: pop → (node=4, remaining=17) ← 左が先に取り出される + remaining = 17 - 4 = 13 + is_leaf = False(left=11, right=None) + → right=None なのでスキップ + → stack に left(11, 13) を追加 → stack = [(8, 17), (11, 13)] + +Iteration 3: pop → (node=11, remaining=13) + remaining = 13 - 11 = 2 + is_leaf = False(left=7, right=2) + → stack に right(2, 2) を追加 → stack = [(8, 17), (2, 2)] + → stack に left(7, 2) を追加 → stack = [(8, 17), (2, 2), (7, 2)] + +Iteration 4: pop → (node=7, remaining=2) + remaining = 2 - 7 = -5 + is_leaf = True → -5 == 0 ? → False ❌ → continue + +Iteration 5: pop → (node=2, remaining=2) + remaining = 2 - 2 = 0 + is_leaf = True → 0 == 0 ? → True ✅ → return True +───────────────────────────────────────────────────── +出力: True ✅ +``` + +--- + +## 4. 検証 + +> 💡 エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。特に木の問題は「空の木」「葉が1枚だけ」「負の値が含まれる」などのケースでバグが起きやすいです。 + +| テストケース | 入力 | 期待出力 | 確認ポイント | +| ------------- | -------------------------------------------------------------- | -------- | ----------------------------- | +| 空の木 | `root=None, targetSum=0` | `False` | `root is None` の即時リターン | +| 葉1枚・一致 | `root=[1], targetSum=1` | `True` | 根が葉でもある場合 | +| 葉1枚・不一致 | `root=[1], targetSum=2` | `False` | 葉での等値判定 | +| 負の値を含む | `root=[-5,3], targetSum=-2` | `True` | `-5 + 3 = -2` 負数の合計 | +| 全部同じ値 | `root=[0,0,0], targetSum=0` | `True` | `0+0=0` のゼロ加算 | +| Example 1 | `root=[5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum=22` | `True` | 通常ケース | +| Example 2 | `root=[1,2,3], targetSum=5` | `False` | すべてのパスが不一致 | + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力・負の値など、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること +> - **型ガード(Python)**:`if root is None: return False` のように書くと、以降のブロックで pylance が `root` を `TreeNode` 型として認識してくれる仕組み +> - **短絡評価(Short-circuit Evaluation)**:`A or B` で A が `True` なら B を評価しない、`A and B` で A が `False` なら B を評価しない仕組み。Pythonも同様に動作し、無駄な計算を防ぐ + +--- + +## Python版 vs TypeScript版 比較まとめ + +| 観点 | Python(競技版) | TypeScript(LeetCode版) | +| -------------- | --------------------------------------- | ---------------------------------------------- | +| null 安全 | `if root is None:` + pylance の型ガード | `if (root === null):` + TSコンパイラの型ガード | +| 再帰深度制限 | デフォルト約1000(業務版で回避が必要) | JavaScriptエンジン依存(通常より深い) | +| コード量 | 最短(動的型付け・`or` 一行で書ける) | やや長い(型注釈が必須) | +| 型安全性 | 型ヒント + pylance(オプショナル) | コンパイル時に強制(必須) | +| 業務版の優位点 | `deque` で再帰深度問題を回避 | `strict mode` でコンパイル時エラー検出 | diff --git a/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Typescript.md b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Typescript.md new file mode 100644 index 0000000..6c56b20 --- /dev/null +++ b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/Path_Sum_Typescript.md @@ -0,0 +1,277 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: TypeScript +> 適用ルールセット: 共通5ルール + TS固有5ルール +> 参照ファイル: references/common.md + references/typescript.md + +--- + +# Path Sum(パスの合計)— LeetCode 112 + +--- + +## 1. 問題の分析 + +> 💡 **この問題は一言で言うと:** +> 「木の根(てっぺん)から葉(末端)まで下りる道の数値の合計が、目標値と一致するルートが存在するか?」を調べる問題です。 + +### 競技プログラミング視点での分析 + +木(ツリー)構造の問題では、**全ノード(節点)を一度ずつ訪れるだけで答えが出る**ため、最低でも O(n) の時間が必要です。各ノードで「今の合計が targetSum に達したか」をチェックしながら下に進めばよく、余計なメモリを使わずに解けます。 + +- 実行速度最優先 → **再帰DFS(深さ優先探索)**。関数呼び出しスタックのみを利用し、追加の配列不要 +- メモリ最小化 → 木の高さ分のスタックしか使わない。最悪 O(n)(一直線の木)、均衡木なら O(log n) + +### 業務開発視点での分析 + +- `TreeNode | null` という**Union型(複数の型を `|` でつなげた型)** を正確に扱う必要がある +- `null` チェックを忘れると実行時にクラッシュするため、型ガードで防御する +- 再帰関数はテストしやすく、ロジックが短く読みやすい → 保守性◎ + +### TypeScript特有の考慮点 + +- `root: TreeNode | null` を受け取る関数では、最初に `null` チェック(型ガード)を入れることで TypeScript コンパイラが以降の処理で `TreeNode` 型として扱えるようになる +- 今回は LeetCode 提供の `TreeNode` クラスを使うため、ジェネリクスは不要(型が固定されているため) +- `boolean` の戻り値型を明示することで、誤って数値や `undefined` を返すミスをコンパイル時に防げる + +> 📖 **このセクションで登場した用語** +> +> - **DFS(深さ優先探索)**:木を「できるだけ深く」潜ってから引き返す探索方法。迷路を一本道ずつ試すイメージ +> - **Union型**:`A | B` のように「AまたはB」を表す型。`TreeNode | null` は「ノードかnullのどちらか」を意味する +> - **型ガード**:`if (root === null)` のように実行時に型を絞り込む仕組み。以降のコードで型を安全に使えるようにする +> - **コンパイル時**:TypeScriptのコードをJavaScriptに変換する段階。ここでエラーを検出できると実行前にバグを防げる + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ問題でも複数の解き方があります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。「O(n)」は「ノードを全部一度ずつ見る」という意味です。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| --------------------------------- | ---------- | ---------- | ------------ | -------- | ------ | --------------------------------------- | +| ① 再帰DFS(残りの合計を減らす) | O(n) | O(h) | 低 | 高 | 高 | hは木の高さ。均衡木でO(log n)、最悪O(n) | +| ② 反復DFS(スタックを自前で管理) | O(n) | O(h) | 中 | 高 | 中 | スタックオーバーフローの心配なし | +| ③ BFS(幅優先探索、キューを使う) | O(n) | O(w) | 中 | 高 | 低 | wは木の最大幅。パスの復元には不向き | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(n)`:ノード数が2倍になると処理も約2倍(全ノードを一度見る) +> - `O(h)`:木の高さ分のメモリだけ使う(hはheightの略) +> - `O(log n)`:均衡した木では高さ ≈ log₂(n) なので、1000ノードでも高さは約10 + +> 📖 **このセクションで登場した用語** +> +> - **再帰(Recursion)**:関数が自分自身を呼び出す仕組み。「根→左の子→さらに左…」と自動的に深く潜れる +> - **スタック(Stack)**:後から積んだものを先に取り出す構造(皿の積み重ねイメージ)。再帰の内部実装でも使われている +> - **BFS(幅優先探索)**:木を「同じ深さ」の階層ごとに横向きに探索する方法 + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**: ① 再帰DFS(`targetSum` を減らしながら下に進む方法) + +- **理由**: + - BFS(方法③)を選ばない理由 → パスの「合計」を追跡するのに、幅広く横に探索するBFSより、根から葉まで縦に深く追うDFSの方が直感的で合う + - 反復DFS(方法②)を選ばない理由 → 制約がノード数5000以下のため、再帰の深さ上限に引っかかるリスクが極めて低い。実装もシンプルな再帰の方が読みやすい + - **再帰DFS(方法①)を選ぶ理由** → コードが最も短く、「左右それぞれの子を同じルールで調べる」という木の性質(再帰的構造)を自然に表現できる + +- **TypeScript特有の最適化ポイント**: + - `root === null` の null チェックで型ガードを使い、コンパイラに「この行以降は `TreeNode` 型が確定」と教える + - 戻り値 `boolean` を明示することで、誤って `void` や `undefined` を返すコードをコンパイル時にブロック + +> 📖 **このセクションで登場した用語** +> +> - **再帰的な構造**:木が「根+左の小さな木+右の小さな木」という同じ形の組み合わせでできていること。再帰関数と相性が良い +> - **null チェック(null guard)**:`null` の可能性がある値を使う前に `=== null` で確認する処理 + +--- + +## 4. 実装コード + +> 💡 **コード全体の骨格(構造の概要)** +> +> 1. `root` が `null` なら木が空(または葉を超えた)→ `false` を返す +> 2. 今いるノードが **葉(子が両方 null)** なら、残り合計が0かどうかを確認して答えを返す +> 3. 葉でなければ、左の子・右の子のどちらかで条件を満たすパスがあるか再帰的に確認する + +```typescript +// Runtime 0 ms +// Beats 100.00% +// Memory 59.53 MB +// Beats 41.61% +/** + * 二分木の根から葉までのパスの合計が targetSum と等しいパスが存在するか判定する + * @param root - 二分木の根ノード(null の場合は空の木) + * @param targetSum - 目標とする合計値 + * @returns パスが存在すれば true、存在しなければ false + * @complexity Time: O(n), Space: O(h) n=ノード数, h=木の高さ + */ +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { + // --- ベースケース①:root が null の場合 --- + // 木が空、または葉を超えて「存在しないノード」に来た場合。 + // このルートにはパスが存在しないので false を返す。 + if (root === null) { + return false; + // ↑ TypeScriptの型ガード:この行以降 root は TreeNode 型として確定する + } + + // --- ベースケース②:現在のノードが「葉(leaf)」かどうかを確認 --- + // 葉とは「左の子も右の子も存在しないノード」のこと。 + // 根から葉まで来たということは、パスの終点に到達したことを意味する。 + const isLeaf: boolean = root.left === null && root.right === null; + + if (isLeaf) { + // 葉に到達したとき、残りの目標値(targetSum)がちょうど現在のノードの値と等しければ + // 「このパスの合計 = 最初の targetSum」が成立する。 + // *ここで root.val を引いた結果が 0 になるか、直接比較するかは好みだが + // 「今のノードの値で最後の差し引きが済む」と考えると分かりやすい。 + return root.val === targetSum; + } + + // --- 再帰ステップ:左の子・右の子へ潜る --- + // 現在のノードの値 (root.val) を targetSum から引くことで + // 「残りあとどれだけ合計が必要か」を次の階層に渡す。 + // 例:targetSum=22, root.val=5 → 次の階層では targetSum=17 を目指す + const remaining: number = targetSum - root.val; + + // 左の子ツリーか、右の子ツリーのどちらか一方でも条件を満たすパスがあれば true。 + // || (論理和)なので、どちらかが true なら即座に true を返す(短絡評価)。 + return hasPathSum(root.left, remaining) || hasPathSum(root.right, remaining); +} +``` + +--- + +### 💡 動作トレース(Example 1 での変数変化) + +``` +入力: root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22 + +木の形(図解): + 5 + / \ + 4 8 + / / \ + 11 13 4 + / \ \ + 7 2 1 + +───────────────────────────────────────────────────── +Step 1: hasPathSum(5, 22) + → root=5, isLeaf=false + → remaining = 22 - 5 = 17 + → 左(4, 17) と 右(8, 17) を調べる + +Step 2: hasPathSum(4, 17) ← 左の子を先に調べる + → root=4, isLeaf=false + → remaining = 17 - 4 = 13 + → 左(11, 13) と 右(null, 13) を調べる + +Step 3: hasPathSum(11, 13) + → root=11, isLeaf=false + → remaining = 13 - 11 = 2 + → 左(7, 2) と 右(2, 2) を調べる + +Step 4: hasPathSum(7, 2) + → root=7, isLeaf=true(子が両方null) + → 7 === 2 ? → false ❌ + +Step 5: hasPathSum(2, 2) + → root=2, isLeaf=true(子が両方null) + → 2 === 2 ? → true ✅ ← ここで条件達成! + +Step 6: Step 5 の true が || で Step 3 に伝わる → true +Step 7: Step 3 の true が || で Step 2 に伝わる → true +Step 8: Step 2 の true が || で Step 1 に伝わる → true +───────────────────────────────────────────────────── +出力: true +パス: 5 → 4 → 11 → 2 合計 = 22 ✅ +``` + +``` +入力: root = [1,2,3], targetSum = 5 + +木の形: + 1 + / \ + 2 3 + +Step 1: hasPathSum(1, 5) + → remaining = 5 - 1 = 4 + → 左(2, 4) と 右(3, 4) を調べる + +Step 2: hasPathSum(2, 4) + → root=2, isLeaf=true → 2 === 4 ? → false ❌ + +Step 3: hasPathSum(3, 4) + → root=3, isLeaf=true → 3 === 4 ? → false ❌ + +Step 4: false || false → false +───────────────────────────────────────────────────── +出力: false ✅ +``` + +``` +入力: root = [], targetSum = 0 (空の木) + +Step 1: hasPathSum(null, 0) + → root === null → 即 false を返す +───────────────────────────────────────────────────── +出力: false ✅ +``` + +> 📖 **このセクションで登場した用語** +> +> - **ベースケース(Base Case)**:再帰関数が「これ以上潜らなくていい」と判断して結果を返す条件。ここでは「null に到達」と「葉に到達」の2つ +> - **葉(Leaf Node)**:子ノードが1つも存在しない末端のノード。パスの終点となる +> - **短絡評価(Short-circuit Evaluation)**:`A || B` で A が true なら B を評価せず即 true を返す仕組み。無駄な処理を省ける +> - **再帰ステップ(Recursive Step)**:関数が自分自身を呼び出す部分。今回は左右の子ノードに対して同じ処理を繰り返す +> - **remaining**:「残り目標合計」。現在のノードの値を差し引いた後、次の階層に渡す値 + +--- + +## TypeScript固有の最適化観点まとめ + +### 型安全性の活用 + +```typescript +// ❌ JavaScriptだと null チェックを忘れても実行時まで気づけない +function hasPathSumJS(root, targetSum) { + return root.val === targetSum; // null.val → クラッシュ! +} + +// ✅ TypeScriptなら root: TreeNode | null と宣言するだけで +// コンパイラが「null かもしれないのに使っている」と警告してくれる +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { + if (root === null) return false; // ← 型ガードでここ以降は TreeNode 確定 + // この行では root.val に安全にアクセスできる + return root.val === targetSum; +} +``` + +**JavaScriptにはない理由**: JavaScriptは実行するまで型のミスに気づけません。TypeScriptの `TreeNode | null` という型宣言により、コンパイル時(コードをJavaScriptに変換する段階)に「null の可能性があるのに使っている」とエラーを出してくれます。 + +### readonly 修飾子について + +今回は LeetCode が `TreeNode` クラスを提供するため自分で定義しませんが、業務コードとして自分で定義するなら以下のように `readonly` をつけることで意図しない書き換えを防げます: + +```typescript +// 業務開発版:ノードの値が外から書き換えられないように readonly で守る +class SafeTreeNode { + readonly val: number; // val を書き換え不可にする(JavaScriptにはない保護) + readonly left: SafeTreeNode | null; + readonly right: SafeTreeNode | null; + + constructor(val = 0, left: SafeTreeNode | null = null, right: SafeTreeNode | null = null) { + this.val = val; + this.left = left; + this.right = right; + } +} +``` + +> 📖 **このセクションで登場した用語** +> +> - **readonly**:変数の値を変更できないようにする修飾子。JavaScriptにはなく、TypeScript独自の機能。意図せぬ書き換えをコンパイル時(実行前)に防げる +> - **型ガード(Type Guard)**:`if (root === null)` のように実行時に型を絞り込む処理。TypeScriptコンパイラはこれを認識し、以降のブロックで型を自動的に絞り込んでくれる(型推論の一種) +> - **コンパイル時エラー**:TypeScriptをJavaScriptに変換する段階で検出するエラー。実行前に気づけるため、実行時クラッシュより安全 diff --git a/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README.md b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README.md new file mode 100644 index 0000000..07761fc --- /dev/null +++ b/Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README.md @@ -0,0 +1,713 @@ +# Path Sum — 根から葉へのパス合計判定 + +
+ 再帰DFS(深さ優先探索)による O(n) 実装 +
++ 💡 + 一言で言うと:「木の根から葉まで下りるパスの合計が目標値と一致するか判定する問題」です。 +
+ ++ 💡 この問題を一言で言うと:「根から葉まで下りるルートの数値の合計が + targetSum と一致するパスが1本でも存在するか?」を判定する問題です。 +
++ 木(ツリー)は配列と違い、インデックスで直接アクセスできません。根から分岐を辿りながら合計を積み上げ、葉(末端)に到達した瞬間に目標値と比較する必要があります。 +
++ ⚠️ なぜ単純な方法では解けないのか +
+None)を正確に判定しないと、木の途中のノードで誤って答えを確定してしまいます。
+ + 例:root = [5,4,8,11,null,13,4,7,2,...], targetSum = 22 を使って解説します。 +
+ ++ 📋 このコードの構造(先に全体像を把握しよう) +
+# LeetCode 112 – Path Sum(競技版:再帰DFS)
+# Time: O(n) Space: O(h) n=ノード数, h=木の高さ
+
+class Solution(object):
+ def hasPathSum(self, root, targetSum):
+ """
+ :type root: Optional[TreeNode]
+ :type targetSum: int
+ :rtype: bool
+ """
+ # ベースケース①: root が None = 空の木 or 葉を超えた
+ # パスが存在しないので即 False を返す
+ if root is None:
+ return False
+
+ # 現在ノードの値を targetSum から引く
+ # 「残りあとどれだけ合計が必要か」を次の階層に引き継ぐ
+ # 例) targetSum=22, root.val=5 → 次は 17 を目標にする
+ targetSum -= root.val
+
+ # ベースケース②: 葉(左右両方の子が None)に到達した
+ # パスの終点なので、残り目標値がちょうど 0 か確認する
+ if root.left is None and root.right is None:
+ return targetSum == 0
+
+ # 再帰ステップ: 左または右の子ツリーで条件を満たすパスがあれば True
+ # 「or」は短絡評価: 左が True なら右を評価せずに即 True を返す
+ return (self.hasPathSum(root.left, targetSum) or
+ self.hasPathSum(root.right, targetSum))
+
+ + ▶ 入力例 root=[5,4,8,11,null,13,4,7,2,...], targetSum=22 での動作トレース +
++hasPathSum(Node(5), 22) → targetSum = 22-5 = 17 → 葉でない → 左へ + hasPathSum(Node(4), 17) → targetSum = 17-4 = 13 → 葉でない → 左へ + hasPathSum(Node(11), 13) → targetSum = 13-11 = 2 → 葉でない → 左へ + hasPathSum(Node(7), 2) → targetSum = 2-7 = -5 → 葉!→ -5==0? → False ❌ + hasPathSum(Node(2), 2) → targetSum = 2-2 = 0 → 葉!→ 0==0? → True ✅ + ← True が伝播 + ← True が伝播 +← True が伝播 + +最終出力: True(パス 5→4→11→2, 合計=22)+
deque
+ を使って再帰なしで安全に実装します。
+ from collections import deque
+
+class Solution:
+ def hasPathSum(self, root, targetSum):
+ if root is None:
+ return False
+
+ stack = deque([(root, targetSum)])
+
+ while stack:
+ node, remaining = stack.pop()
+ remaining -= node.val
+
+ if node.left is None and node.right is None:
+ if remaining == 0:
+ return True
+ continue
+
+ if node.right is not None:
+ stack.append((node.right, remaining))
+ if node.left is not None:
+ stack.append((node.left, remaining))
+
+ return False
+
+ + 🗺️ フローチャートの読み方 +
++ ✅ return False の出口は1か所に統合しています(② and ⑤ + の両方が合流) +
++ ✅ + 「はい」「いいえ」ラベルは分岐点の直隣に配置し、視線移動を最小化しています +
++ ✅ + 再帰呼び出し⑥⑦ は二重縦線ボックスで通常の処理と区別しています +
+
+%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13.5px', 'lineColor': '#64748b', 'primaryBorderColor': '#2563eb', 'tertiaryColor': '#ede9fe'}}}%%
+flowchart TD
+ A(["① 開始\nhasPathSum( root, targetSum )"]):::start
+ A --> B{"② root is None?\n空の木・存在しない子"}:::cond
+ B -- "はい" --> F(["return False ❌\n【共通の False 出口】\n② と ⑤ の両方がここへ合流"]):::falseNode
+ B -- "いいえ" --> C["③ remaining = targetSum − root.val\n例: 22 − 5 = 17"]:::proc
+ C --> D{"④ 葉ノードか?\nleft is None かつ right is None"}:::cond
+ D -- "はい(葉に到達)" --> E{"⑤ remaining == 0?\n合計がぴったり一致したか"}:::cond
+ E -- "はい" --> T(["return True ✅\nパスが見つかった!"]):::trueNode
+ E -- "いいえ" --> F
+ D -- "いいえ(葉でない)" --> G[["⑥ hasPathSum( root.left, remaining )\n左の子ツリーへ再帰\nTrue なら即 True(短絡評価 or)"]]:::rec
+ G --> H[["⑦ hasPathSum( root.right, remaining )\n右の子ツリーへ再帰\nleft or right の結果を返す"]]:::rec
+ H --> R(["⑧ 結果を上の階層へ返す\nleft_result or right_result"]):::result
+
+ classDef start fill:#d1fae5,stroke:#059669,stroke-width:2.5px,color:#065f46,font-weight:bold
+ classDef cond fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#78350f
+ classDef proc fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
+ classDef falseNode fill:#fee2e2,stroke:#dc2626,stroke-width:2.5px,color:#7f1d1d,font-weight:bold
+ classDef trueNode fill:#dcfce7,stroke:#16a34a,stroke-width:2.5px,color:#14532d,font-weight:bold
+ classDef rec fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#3b0764
+ classDef result fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#1e293b
+
+ linkStyle 1 stroke:#dc2626,stroke-width:2.5px,color:#dc2626
+ linkStyle 2 stroke:#16a34a,stroke-width:2px
+ linkStyle 4 stroke:#16a34a,stroke-width:2px
+ linkStyle 5 stroke:#16a34a,stroke-width:2.5px
+ linkStyle 6 stroke:#dc2626,stroke-width:2.5px
+ linkStyle 7 stroke:#7c3aed,stroke-width:2px
+ linkStyle 8 stroke:#7c3aed,stroke-width:2px
+ linkStyle 9 stroke:#64748b,stroke-width:2px
+
+ + 🔎 入力例 root=[5,4,8,11,null,13,4,7,2,...], targetSum=22 でのフロー追跡 +
++ 🔴 return False の2つの経路について +
+
+ ② → False:root が
+ None(空の木・存在しない子ノードを辿った)場合。パス自体が存在しない。
+ ⑤ → False:葉に到達したが残り目標値が 0
+ でない場合。このパスの合計は targetSum と一致しない。
+ 両経路とも「このパスに答えはない」という同じ意味を持つため、図では1つの出口ノードに合流させています。
+
📖 Big-O 記法の読み方
+| + 項目 + | ++ 競技版(再帰) + | ++ 業務版(反復deque) + | +
|---|---|---|
| + 時間計算量 + | ++ O(n) + | ++ O(n) + | +
| + 空間計算量 + | ++ O(h) + | ++ O(h) + | +
| + 均衡木の空間 + | ++ O(log n) + | ++ O(log n) + | +
| + 最悪(一直線) + | ++ O(n) ⚠️再帰制限 + | ++ O(n) ✅安全 + | +
| + 可読性 + | ++ ★★★ 高い + | ++ ★★☆ 中程度 + | +
+ 🔍 なぜこの計算量になるのか +
+
+ 時間計算量 O(n):すべてのノードをちょうど1回ずつ訪問するためです。最良ケース(根が葉)でも最悪ケース(全ノードを見る)でも、訪問回数は必ずノード数
+ n 以下に収まります。
+ 空間計算量 O(h):再帰呼び出しのたびに関数の情報がコールスタックに積まれます。その最大の深さが木の高さ
+ h です。均衡した木では h ≈ log₂(n)(5000ノードで約12段)、一直線の木では h =
+ n(最悪5000段)になります。
+
+ このページで登場した専門用語を五十音順でまとめました。分からない言葉が出てきたときに参照してください。 +
+list
+ は先頭への追加・削除が O(n) かかりますが、deque
+ は O(1) です。スタックやキューとして使うのに最適です。
+ RecursionError)が発生します。
+ sys.getrecursionlimit()
+ で確認できます。5000
+ ノードの一直線の木では超過する可能性があるため、業務版では
+ deque
+ を使った反復 DFS で回避します。
+ A or B
+ という式で、A が
+ True なら B
+ を評価せずに即
+ True
+ を返す仕組みです。今回の実装では左のサブツリーで答えが見つかった場合に右のサブツリー全体の探索をスキップできるため、最良ケースで処理を大幅に短縮できます。
+ + 再帰DFS(深さ優先探索)による O(n) 実装 +
++ 💡 + 一言で言うと:「木の根から葉まで下りるパスの合計が目標値と一致するか判定する問題」です。 +
+ ++ 💡 この問題を一言で言うと:「根から葉まで下りるルートの数値の合計が + targetSum と一致するパスが1本でも存在するか?」を判定する問題です。 +
++ 木(ツリー)は配列と違い、インデックスで直接アクセスできません。根から分岐を辿りながら合計を積み上げ、葉(末端)に到達した瞬間に目標値と比較する必要があります。 +
++ ⚠️ なぜ単純な方法では解けないのか +
+None)を正確に判定しないと、木の途中のノードで誤って答えを確定してしまいます。
+ + 例:root = [5,4,8,11,null,13,4,7,2,...], targetSum = 22 を使って解説します。 +
+ ++ 📋 このコードの構造(先に全体像を把握しよう) +
+# LeetCode 112 – Path Sum(競技版:再帰DFS)
+# Time: O(n) Space: O(h) n=ノード数, h=木の高さ
+
+class Solution(object):
+ def hasPathSum(self, root, targetSum):
+ """
+ :type root: Optional[TreeNode]
+ :type targetSum: int
+ :rtype: bool
+ """
+ # ベースケース①: root が None = 空の木 or 葉を超えた
+ # パスが存在しないので即 False を返す
+ if root is None:
+ return False
+
+ # 現在ノードの値を targetSum から引く
+ # 「残りあとどれだけ合計が必要か」を次の階層に引き継ぐ
+ # 例) targetSum=22, root.val=5 → 次は 17 を目標にする
+ targetSum -= root.val
+
+ # ベースケース②: 葉(左右両方の子が None)に到達した
+ # パスの終点なので、残り目標値がちょうど 0 か確認する
+ if root.left is None and root.right is None:
+ return targetSum == 0
+
+ # 再帰ステップ: 左または右の子ツリーで条件を満たすパスがあれば True
+ # 「or」は短絡評価: 左が True なら右を評価せずに即 True を返す
+ return (self.hasPathSum(root.left, targetSum) or
+ self.hasPathSum(root.right, targetSum))
+
+ + ▶ 入力例 root=[5,4,8,11,null,13,4,7,2,...], targetSum=22 での動作トレース +
++hasPathSum(Node(5), 22) → targetSum = 22-5 = 17 → 葉でない → 左へ + hasPathSum(Node(4), 17) → targetSum = 17-4 = 13 → 葉でない → 左へ + hasPathSum(Node(11), 13) → targetSum = 13-11 = 2 → 葉でない → 左へ + hasPathSum(Node(7), 2) → targetSum = 2-7 = -5 → 葉!→ -5==0? → False ❌ + hasPathSum(Node(2), 2) → targetSum = 2-2 = 0 → 葉!→ 0==0? → True ✅ + ← True が伝播 + ← True が伝播 +← True が伝播 + +最終出力: True(パス 5→4→11→2, 合計=22)+
deque
+ を使って再帰なしで安全に実装します。
+ from collections import deque
+
+class Solution:
+ def hasPathSum(self, root, targetSum):
+ if root is None:
+ return False
+
+ stack = deque([(root, targetSum)])
+
+ while stack:
+ node, remaining = stack.pop()
+ remaining -= node.val
+
+ if node.left is None and node.right is None:
+ if remaining == 0:
+ return True
+ continue
+
+ if node.right is not None:
+ stack.append((node.right, remaining))
+ if node.left is not None:
+ stack.append((node.left, remaining))
+
+ return False
+
+ + 🗺️ フローチャートの読み方 +
++ ✅ return False の出口は1か所に統合しています(② and ⑤ + の両方が合流) +
++ ✅ + 「はい」「いいえ」ラベルは分岐点の直隣に配置し、視線移動を最小化しています +
++ ✅ + 再帰呼び出し⑥⑦ は二重縦線ボックスで通常の処理と区別しています +
+
+%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13.5px', 'lineColor': '#64748b', 'primaryBorderColor': '#2563eb', 'tertiaryColor': '#ede9fe'}}}%%
+flowchart TD
+ A(["① 開始\nhasPathSum( root, targetSum )"]):::start
+ A --> B{"② root is None?\n空の木・存在しない子"}:::cond
+ B -- "はい" --> F(["return False ❌\n【共通の False 出口】\n② と ⑤ の両方がここへ合流"]):::falseNode
+ B -- "いいえ" --> C["③ remaining = targetSum − root.val\n例: 22 − 5 = 17"]:::proc
+ C --> D{"④ 葉ノードか?\nleft is None かつ right is None"}:::cond
+ D -- "はい(葉に到達)" --> E{"⑤ remaining == 0?\n合計がぴったり一致したか"}:::cond
+ E -- "はい" --> T(["return True ✅\nパスが見つかった!"]):::trueNode
+ E -- "いいえ" --> F
+ D -- "いいえ(葉でない)" --> G[["⑥ hasPathSum( root.left, remaining )\n左の子ツリーへ再帰\nTrue なら即 True(短絡評価 or)"]]:::rec
+ G --> H[["⑦ hasPathSum( root.right, remaining )\n右の子ツリーへ再帰\nleft or right の結果を返す"]]:::rec
+ H --> R(["⑧ 結果を上の階層へ返す\nleft_result or right_result"]):::result
+
+ classDef start fill:#d1fae5,stroke:#059669,stroke-width:2.5px,color:#065f46,font-weight:bold
+ classDef cond fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#78350f
+ classDef proc fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a
+ classDef falseNode fill:#fee2e2,stroke:#dc2626,stroke-width:2.5px,color:#7f1d1d,font-weight:bold
+ classDef trueNode fill:#dcfce7,stroke:#16a34a,stroke-width:2.5px,color:#14532d,font-weight:bold
+ classDef rec fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#3b0764
+ classDef result fill:#f1f5f9,stroke:#64748b,stroke-width:2px,color:#1e293b
+
+ linkStyle 1 stroke:#dc2626,stroke-width:2.5px,color:#dc2626
+ linkStyle 2 stroke:#16a34a,stroke-width:2px
+ linkStyle 4 stroke:#16a34a,stroke-width:2px
+ linkStyle 5 stroke:#16a34a,stroke-width:2.5px
+ linkStyle 6 stroke:#dc2626,stroke-width:2.5px
+ linkStyle 7 stroke:#7c3aed,stroke-width:2px
+ linkStyle 8 stroke:#7c3aed,stroke-width:2px
+ linkStyle 9 stroke:#64748b,stroke-width:2px
+
+ + 🔎 入力例 root=[5,4,8,11,null,13,4,7,2,...], targetSum=22 でのフロー追跡 +
++ 🔴 return False の2つの経路について +
+
+ ② → False:root が
+ None(空の木・存在しない子ノードを辿った)場合。パス自体が存在しない。
+ ⑤ → False:葉に到達したが残り目標値が 0
+ でない場合。このパスの合計は targetSum と一致しない。
+ 両経路とも「このパスに答えはない」という同じ意味を持つため、図では1つの出口ノードに合流させています。
+
📖 Big-O 記法の読み方
+| + 項目 + | ++ 競技版(再帰) + | ++ 業務版(反復deque) + | +
|---|---|---|
| + 時間計算量 + | ++ O(n) + | ++ O(n) + | +
| + 空間計算量 + | ++ O(h) + | ++ O(h) + | +
| + 均衡木の空間 + | ++ O(log n) + | ++ O(log n) + | +
| + 最悪(一直線) + | ++ O(n) ⚠️再帰制限 + | ++ O(n) ✅安全 + | +
| + 可読性 + | ++ ★★★ 高い + | ++ ★★☆ 中程度 + | +
+ 🔍 なぜこの計算量になるのか +
+
+ 時間計算量 O(n):すべてのノードをちょうど1回ずつ訪問するためです。最良ケース(根が葉)でも最悪ケース(全ノードを見る)でも、訪問回数は必ずノード数
+ n 以下に収まります。
+ 空間計算量 O(h):再帰呼び出しのたびに関数の情報がコールスタックに積まれます。その最大の深さが木の高さ
+ h です。均衡した木では h ≈ log₂(n)(5000ノードで約12段)、一直線の木では h =
+ n(最悪5000段)になります。
+
+ このページで登場した専門用語を五十音順でまとめました。分からない言葉が出てきたときに参照してください。 +
+list
+ は先頭への追加・削除が O(n) かかりますが、deque
+ は O(1) です。スタックやキューとして使うのに最適です。
+ RecursionError)が発生します。
+ sys.getrecursionlimit()
+ で確認できます。5000
+ ノードの一直線の木では超過する可能性があるため、業務版では
+ deque
+ を使った反復 DFS で回避します。
+ A or B
+ という式で、A が
+ True なら B
+ を評価せずに即
+ True
+ を返す仕組みです。今回の実装では左のサブツリーで答えが見つかった場合に右のサブツリー全体の探索をスキップできるため、最良ケースで処理を大幅に短縮できます。
+ 176 interactive lessons across 6 domains
+177 interactive lessons across 6 domains