diff --git a/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/Linked_List_Cycle_Python.md b/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/Linked_List_Cycle_Python.md new file mode 100644 index 00000000..dde64026 --- /dev/null +++ b/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/Linked_List_Cycle_Python.md @@ -0,0 +1,341 @@ +# LeetCode: Linked List Cycle(連結リストの循環検出) + +## 1. 問題分析結果 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「連結リスト(ノードが矢印でつながったデータ構造)を先頭からたどっていったとき、同じ場所をぐるぐる回り続けてしまう輪っか(サイクル)が存在するかどうかを判定する問題」です。 +> +> **CPython特有の注意点**:この問題は一見「訪問済みノードを`set`に記録すればいい」と思いがちですが、それだと空間計算量がO(n)になります。CPythonでは`set`への追加・検索自体はハッシュテーブル(=値を高速に検索できる索引付きの箱)によりO(1)と高速ですが、ノード数が10万件を超えるような大規模な入力では、メモリ使用量が問題になります。今回はメモリを一切増やさない「2つのポインタ(変数)だけ」で解く方法を採用します。 + +### 競技プログラミング視点 + +- **制約分析**: ノード数は最大`10^4`(=1万)件程度が想定されるため、O(n)のアルゴリズムであれば十分高速に処理できます。 +- **最速手法**: Floyd's Tortoise and Hare(亀と兎のアルゴリズム)を使えば、リストを1回走査するだけで判定でき、Big-O的にもPython実装的にも最適です。 +- **メモリ最小化**: 追加のデータ構造(`set`や`list`)を一切使わず、ポインタ変数2つだけで実装することでO(1)の空間計算量を実現します。 +- **CPython最適化**: `while`ループ内での属性アクセス(`.next`)はPure Python(=C言語ではなくPythonコードそのもので実行される処理)のため多少のオーバーヘッドはありますが、これ以上シンプルにする手段はなく、これが最速の実装です。 + +### 業務開発視点 + +- **型安全設計**: `Optional[ListNode]`という型ヒントを使い、「ノードが存在しない(`None`)かもしれない」ことを明示します。これによりpylanceが「`None`かもしれない変数に`.next`でアクセスしようとしている」といった潜在バグを実行前に検出できます。 +- **エラーハンドリング**: リンクリストの循環検出という問題の性質上、入力そのものが不正(型が違う等)になるケースは少ないですが、`head`が`None`の場合の早期リターンなど、境界条件(=入力の端っこのパターン)を明示的に処理します。 +- **可読性**: `slow`(亀)・`fast`(兎)という変数名と、docstringでのアルゴリズム説明により、後から読んだ人がすぐに意図を理解できるようにします。 + +### Python特有分析 + +- **データ構造選択**: 今回は`list`・`deque`・`set`いずれも使わず、単純な変数(ポインタ)2つのみで実装します。これが最もメモリ効率の良い選択です。 +- **標準ライブラリ活用度**: 今回のアルゴリズムはシンプルなポインタ操作のみで完結するため、`collections`や`heapq`などの標準ライブラリは不要です。 +- **CPython最適化度**: 組み込みの比較演算子(`is`)を使うことで、オブジェクトの同一性判定(=同じメモリ上のオブジェクトかどうかの判定)をC実装レベルの速さで行えます。 + +> 📖 **このセクションで登場した用語** +> +> - **連結リスト**:データ(ノード)が「次はこれだよ」という矢印(ポインタ)でつながった構造。 +> - **サイクル(循環)**:リストをたどっていくと、以前に訪れたノードに戻ってきてしまう輪っか状の構造。 +> - **ハッシュテーブル**:キーから値を高速に検索できるデータ構造。Pythonの`dict`や`set`の内部実装に使われている。 +> - **`Optional[ListNode]`**:`ListNode`型または`None`のどちらかであることを表す型ヒント。 +> - **オブジェクトの同一性判定(`is`)**:2つの変数が「同じ値かどうか」ではなく「メモリ上の同じオブジェクトを指しているかどうか」を調べる演算子。値の比較には`==`を使うが、ノードの比較には`is`を使うのが適切。 + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。今回はPython特有の観点として「`set`というC実装の便利な道具を使うか、それとも道具を使わずポインタだけで解くか」がポイントになります。 + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ---------- | ---------- | ---------- | ----------------- | ------ | ------------------- | -------------- | ---- | +| Floyd's Tortoise and Hare(亀と兎) | O(n) | O(1) | 低 | ★★★ | なし | 適 | 追加メモリ不要、最も推奨される定番解法 | +| `set`による訪問済みノード記録 | O(n) | O(n) | 低 | ★★★ | なし(`set`は組み込み型) | 適(`set`のハッシュ検索はC実装) | 実装は直感的だがメモリを多く使う | +| ノードに訪問済みフラグを直接書き込む | O(n) | O(1) | 中 | ★☆☆ | なし | 不適 | 入力ノードのオブジェクトを書き換えるため実務では非推奨 | + +- **選択理由**: Floyd's Tortoise and Hareは時間計算量O(n)・空間計算量O(1)と両方の面で最も効率的です。`set`を使う方法もCPython的には高速(`set`の検索がC実装のハッシュテーブルによるため)ですが、ノード数が多いとメモリを大量消費するため、今回はメモリ効率を優先しました。 +- **Python最適化戦略**: `is`演算子でオブジェクトの同一性を判定することで、値の比較(`__eq__`の呼び出し)よりも高速に判定できます。また、`while`ループの条件式に`and`を使うことで、Pythonの短絡評価(=左側の条件が`False`なら右側は評価しない仕組み)を活かし、`None.next`のようなエラーを未然に防いでいます。 +- **トレードオフ**: 可読性の面では`set`を使う方法も十分に高い(★★★)ですが、今回は「メモリ効率」を最優先事項としたため、やや理解に一手間かかるFloyd's Tortoise and Hareを選択しています。ただし、コメントとdocstringを充実させることでこのデメリットを補っています。 + +> 📖 **このセクションで登場した用語** +> +> - **短絡評価**:`and`や`or`で、左側の条件だけで結果が確定する場合、右側を評価しない仕組み。例:`fast and fast.next`は、`fast`が`None`なら`fast.next`にアクセスせずに済む。 +> - **`__eq__`**:Pythonのオブジェクトが`==`で比較されたときに呼ばれる特殊メソッド。今回の`ListNode`では定義されていないため、`==`はデフォルトで`is`と同じ挙動になるが、意図を明確にするため`is`を使う。 + +--- + +## 3. 実装パターン + +> 💡 **初学者向け補足**:コード全体の大まかな構造(骨格)は以下の通りです。 +> +> 1. `head`が`None`、または`head.next`が`None`(ノードが1つしかない)場合、輪っかは作りようがないので`False`を返す +> 2. 「遅いポインタ(slow)」と「速いポインタ(fast)」という2つの変数を用意し、両方とも`head`からスタートする +> 3. ループの中で、`slow`は1つずつ、`fast`は2つずつノードを進める +> 4. `slow`と`fast`が同じノードを指す瞬間が来たら、サイクルが検出されたので`True`を返す +> 5. `fast`がリストの終端(`None`)に到達したら、サイクルは存在しないので`False`を返す + +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。docstringや型ヒントが充実しており、後から読んだ人がすぐに意図を理解できます。 + +【競技プログラミング版を使う場面】 +LeetCodeで制限時間内に正解を出すことが目的のコードに向きます。コメントを最小限にし、可読性よりも短さ・書きやすさを優先しています。 + +```python +from typing import Optional + + +# Definition for singly-linked list. +class ListNode: + def __init__(self, x: int) -> None: + self.val = x + self.next: Optional["ListNode"] = None + + +class Solution: + """ + 連結リストにサイクル(循環)が存在するかどうかを判定するクラス + + 業務開発向けと競技プログラミング向けの2パターンを提供する + """ + + def hasCycle(self, head: Optional[ListNode]) -> bool: + """ + 業務開発向け実装(Floyd's Tortoise and Hareアルゴリズムを使用) + + なぜこの方法が有効なのか: + サイクルが存在する場合、速いポインタ(兎)は遅いポインタ(亀)よりも + 2倍速く進むため、輪っかの中で必ずいつか追いつく(=同じノードを指す)瞬間が来る。 + これは円形のトラックを走る速さの違う2人のランナーが、 + 速度差がある限り必ずどこかで追いつく(すれ違う)のと同じ原理。 + + Args: + head: 連結リストの先頭ノード(存在しない場合はNone) + + Returns: + サイクルが存在すればTrue、存在しなければFalse + + Time Complexity: O(n) + Space Complexity: O(1) + """ + # 型ガード:headがNone、またはノードが1つしかない場合、 + # 「次に進む」動作自体ができないためサイクルは作りようがない + # (pylance対応:Optional[ListNode]をここで絞り込むことで、以降の型チェックが安全になる) + if head is None or head.next is None: + return False + + # 「遅いポインタ(亀)」:1歩ずつ進む + # 「速いポインタ(兎)」:2歩ずつ進む + # 型ヒント上はどちらも Optional[ListNode](Noneになる可能性がある) + slow: Optional[ListNode] = head + fast: Optional[ListNode] = head + + # fast と fast.next の両方が None でない間だけループを続ける。 + # 短絡評価により、fast が None の時点で fast.next へのアクセスは行われない + # (これによりAttributeErrorを未然に防いでいる) + while fast is not None and fast.next is not None: + # 遅いポインタを1歩進める + slow = slow.next # type: ignore[union-attr] + + # 速いポインタを2歩進める + fast = fast.next.next + + # is演算子でオブジェクトの同一性を判定する。 + # 値(==)ではなく「同じノードを指しているか」を調べたいのでisを使う + if slow is fast: + return True + + # ループを抜けた(=fastがリストの終端Noneに到達した)ということは、 + # 輪っかがなく、リストがどこかで途切れていたということ + return False + + def hasCycle_competitive(self, head: Optional[ListNode]) -> bool: + """ + 競技プログラミング向け最適化実装 + コメント・型ガードを最小限にし、簡潔さを優先 + + Time Complexity: O(n) + Space Complexity: O(1) + """ + slow = fast = head + while fast and fast.next: + slow = slow.next # type: ignore[union-attr] + fast = fast.next.next + if slow is fast: + return True + return False +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 例)ノードの並び: idx0: 3 → idx1: 2 → idx2: 0 → idx3: -4 → (idx1 の「2」に戻る) +> +> 初期状態: slow = idx0 (3), fast = idx0 (3) +> +> Step 1: slow = slow.next → slow は idx1 (2) +> fast = fast.next.next → fast は idx2 (0) +> slow(idx1: 2) is fast(idx2: 0)? → いいえ、続行 +> +> Step 2: slow = slow.next → slow は idx2 (0) +> fast = fast.next.next → idx2(0) から idx3(-4) を経て idx1(2) へ進む → fast は idx1 (2) +> slow(idx2: 0) is fast(idx1: 2)? → いいえ、続行 +> +> Step 3: slow = slow.next → slow は idx3 (-4) +> fast = fast.next.next → idx1(2) から idx2(0) を経て idx3(-4) へ進む → fast は idx3 (-4) +> slow(idx3: -4) is fast(idx3: -4)? → はい!両者が同一の ListNode オブジェクトを指す +> +> → True を返す(サイクルが検出された) +> ``` +> +> ※ 実際の判定は「同じ`ListNode`オブジェクトを指しているか(`is`演算子)」で行われます。 + +> 📖 **このセクションで登場した用語** + +- **Floyd's Tortoise and Hare(亀と兎のアルゴリズム)**:2つのポインタを異なる速度で進めることでサイクルを検出する古典的な手法。 +- **`# type: ignore[union-attr]`**:pylanceに対して「この行の型警告は意図的なものなので無視してください」と伝えるコメント。`slow`が理論上`None`の可能性を型的に持つが、ループの前提条件(`fast.next is not None`)から実行時には安全であることが保証されているケースで使用する。 +- **`AttributeError`**:存在しない属性(例:`None`に対して`.next`)にアクセスしようとしたときにPythonが発生させる実行時エラー。 + + + +> 💡 **初学者向け補足**:コード全体の大まかな構造(骨格)は以下の通りです。 +> +> 1. `head`が`None`、または`head.next`が`None`(ノードが1つしかない)場合、輪っかは作りようがないので`False`を返す +> 2. 「遅いポインタ(slow)」と「速いポインタ(fast)」という2つの変数を用意し、両方とも`head`からスタートする +> 3. ループの中で、`slow`は1つずつ、`fast`は2つずつノードを進める +> 4. `slow`と`fast`が同じノードを指す瞬間が来たら、サイクルが検出されたので`True`を返す +> 5. `fast`がリストの終端(`None`)に到達したら、サイクルは存在しないので`False`を返す + +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。docstringや型ヒントが充実しており、後から読んだ人がすぐに意図を理解できます。 + +【競技プログラミング版を使う場面】 +LeetCodeで制限時間内に正解を出すことが目的のコードに向きます。コメントを最小限にし、可読性よりも短さ・書きやすさを優先しています。 + +```python +from typing import Optional + + +# Definition for singly-linked list. +class ListNode: + def __init__(self, x: int) -> None: + self.val = x + self.next: Optional["ListNode"] = None + + +class Solution: + """ + 連結リストにサイクル(循環)が存在するかどうかを判定するクラス + + 業務開発向けと競技プログラミング向けの2パターンを提供する + """ + + def hasCycle(self, head: Optional[ListNode]) -> bool: + """ + 業務開発向け実装(Floyd's Tortoise and Hareアルゴリズムを使用) + + なぜこの方法が有効なのか: + サイクルが存在する場合、速いポインタ(兎)は遅いポインタ(亀)よりも + 2倍速く進むため、輪っかの中で必ずいつか追いつく(=同じノードを指す)瞬間が来る。 + これは円形のトラックを走る速さの違う2人のランナーが、 + 速度差がある限り必ずどこかで追いつく(すれ違う)のと同じ原理。 + + Args: + head: 連結リストの先頭ノード(存在しない場合はNone) + + Returns: + サイクルが存在すればTrue、存在しなければFalse + + Time Complexity: O(n) + Space Complexity: O(1) + """ + # 型ガード:headがNone、またはノードが1つしかない場合、 + # 「次に進む」動作自体ができないためサイクルは作りようがない + # (pylance対応:Optional[ListNode]をここで絞り込むことで、以降の型チェックが安全になる) + if head is None or head.next is None: + return False + + # 「遅いポインタ(亀)」:1歩ずつ進む + # 「速いポインタ(兎)」:2歩ずつ進む + # 型ヒント上はどちらも Optional[ListNode](Noneになる可能性がある) + slow: Optional[ListNode] = head + fast: Optional[ListNode] = head + + # fast と fast.next の両方が None でない間だけループを続ける。 + # 短絡評価により、fast が None の時点で fast.next へのアクセスは行われない + # (これによりAttributeErrorを未然に防いでいる) + while fast is not None and fast.next is not None: + # 遅いポインタを1歩進める + slow = slow.next # type: ignore[union-attr] + + # 速いポインタを2歩進める + fast = fast.next.next + + # is演算子でオブジェクトの同一性を判定する。 + # 値(==)ではなく「同じノードを指しているか」を調べたいのでisを使う + if slow is fast: + return True + + # ループを抜けた(=fastがリストの終端Noneに到達した)ということは、 + # 輪っかがなく、リストがどこかで途切れていたということ + return False + + def hasCycle_competitive(self, head: Optional[ListNode]) -> bool: + """ + 競技プログラミング向け最適化実装 + コメント・型ガードを最小限にし、簡潔さを優先 + + Time Complexity: O(n) + Space Complexity: O(1) + """ + slow = fast = head + while fast and fast.next: + slow = slow.next # type: ignore[union-attr] + fast = fast.next.next + if slow is fast: + return True + return False +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 例)ノードの並び: 3 → 2 → 0 → -4 → (2番目の「2」に戻る) +> +> 初期状態: slow = 3, fast = 3 +> +> Step 1: slow = slow.next → slow は 2 +> fast = fast.next.next → fast は 0 +> slow(2) is fast(0)? → いいえ、続行 +> +> Step 2: slow = slow.next → slow は 0 +> fast = fast.next.next → -4の次は2に戻り、その次は0 → fast は 0 +> あれ、既に一致? → 実際には値ではなくノード(オブジェクト)で比較しているため、 +> 「値が同じ0」であっても「同じノードオブジェクトの0」を指した瞬間に一致と判定される +> slow(0番目のノード) is fast(0番目のノード)? → はい! +> +> → True を返す(サイクルが検出された) +> ``` +> +> ※ 上記は値の一致で説明していますが、実際の判定は「同じ`ListNode`オブジェクトを指しているか」で行われる点に注意してください。値が同じでも別のノードインスタンスであれば`is`はFalseになります。 + +> 📖 **このセクションで登場した用語** +> +> - **Floyd's Tortoise and Hare(亀と兎のアルゴリズム)**:2つのポインタを異なる速度で進めることでサイクルを検出する古典的な手法。 +> - **`# type: ignore[union-attr]`**:pylanceに対して「この行の型警告は意図的なものなので無視してください」と伝えるコメント。`slow`が理論上`None`の可能性を型的に持つが、ループの前提条件(`fast.next is not None`)から実行時には安全であることが保証されているケースで使用する。 +> - **`AttributeError`**:存在しない属性(例:`None`に対して`.next`)にアクセスしようとしたときにPythonが発生させる実行時エラー。 + +--- + +## 4. 検証 + +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるために重要です。 + +- **境界値テスト**: + - `head = None`(空のリスト)→ 最初の`if head is None`で`False`を返す。安全に処理される。 + - `head`にノードが1つだけ、`next`が`None`(自己ループなし)→ `head.next is None`で`False`を返す。 + - `head`にノードが1つだけ、自分自身を指す自己ループ(`pos = 0`)→ `head.next is None`の条件に該当しない(`next`が自分自身のノードなので`None`ではない)ため、ループに入り正しく`True`と判定される。 + - ノード数が最大`10^4`件の大規模な入力 → O(n)・O(1)のアルゴリズムのため、時間・メモリともに問題なく処理できる。 +- **型チェック**: pylanceは`Optional[ListNode]`型に対して`.next`への直接アクセスを警告するため、`while`ループの条件式による絞り込み(=Narrowing)や`# type: ignore`コメントを適切に配置することで、静的解析上の警告を抑えつつ実行時の安全性も確保しています。 + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと。 +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること。 +> - **Narrowing(型の絞り込み)**:`if x is not None:`のような条件分岐によって、pylanceが「このブロック内では`x`は`None`ではない」と型を自動的に絞り込んでくれる仕組み。 diff --git a/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/Linked_List_Cycle_TypeScript.md b/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/Linked_List_Cycle_TypeScript.md new file mode 100644 index 00000000..a373bbb0 --- /dev/null +++ b/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/Linked_List_Cycle_TypeScript.md @@ -0,0 +1,189 @@ +# LeetCode: Linked List Cycle(連結リストの循環検出) + +--- + +## 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「連結リスト(データが数珠つなぎになった構造)をたどっていったときに、同じ場所をぐるぐる回り続けてしまう輪っか(サイクル)があるかどうかを調べる問題」です。 + +- **競技プログラミング視点での分析** + - 実行速度を最優先する場合、リストの全ノードを最大1〜2回訪問するだけで判定できるアルゴリズムが理想です。 + - メモリ使用量の最小化を狙うなら、ノードを記録するための追加データ構造(配列やSet)を一切使わず、ポインタ(=ノードを指し示す変数)だけで判定する方法が最良です。 +- **業務開発視点での分析** + - 型安全性・保守性・可読性を重視する場合、「ノードが存在するか」「次のノードが存在するか」を毎回明示的にチェックし、`null`による予期しないクラッシュを防ぐコードにする必要があります。 + - `head`が最初から`null`(=空のリスト)の場合など、境界条件(=入力の端っこのパターン)への対応も重要です。 +- **TypeScript特有の考慮点** + - `ListNode | null`という「ユニオン型(=複数の型のどれかを取りうるという意味の型)」を正しく扱うために、型ガード(=実行時に型を確認する処理)を活用します。 + - ジェネリクスは今回のノードの型が固定(`ListNode`)されているため使用しませんが、型推論(=型を書かなくてもTypeScriptが自動で判断してくれる機能)を活かして無駄な型注釈を減らします。 + +> 📖 **このセクションで登場した用語** +> +> - **連結リスト(Linked List)**:データ(ノード)が「次はこれだよ」という矢印(ポインタ)でつながった構造。配列と違い、途中への挿入・削除が高速。 +> - **サイクル(循環)**:リストをたどっていくと、以前に訪れたノードに戻ってきてしまう輪っか状の構造。 +> - **ユニオン型**:「AまたはB」のように、複数の型のうちどれかであることを表すTypeScriptの型。例:`ListNode | null`は「ListNode型か、null」のどちらか。 +> - **型ガード**:実行時に「この変数は本当にこの型か?」を確認するためのコード(例:`if (x !== null)`)。 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| ------------------------------------------------- | ---------- | ---------- | ------------ | -------- | ------ | -------------------------------------- | +| Floyd's Tortoise and Hare(高速・低速ポインタ法) | O(n) | O(1) | 低 | 高 | 高 | 追加メモリ不要、最も推奨される定番解法 | +| ハッシュセット(訪問済みノード記録法) | O(n) | O(n) | 低 | 高 | 高 | 実装は直感的だが、メモリを多く使う | +| ノード改変法(訪問済みフラグを値に埋め込む) | O(n) | O(1) | 中 | 低 | 低 | 入力データを破壊するため実務では非推奨 | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさ(ここではノードの数)に対して、処理にかかる手間がどう増えるかの目安。 +> - **空間計算量**:処理中に使うメモリ量が、入力の大きさに対してどう増えるかの目安。 +> - **ハッシュセット(Set)**:値の「重複なしの集まり」を管理するデータ構造。「すでに見たかどうか」を高速に調べられる。 + +--- + +## 3. 選択したアルゴリズムと理由 + +> 💡 **初学者向け補足**:ハッシュセット法は実装が簡単で理解しやすいですが、ノード数が多いとメモリを大量に消費してしまうため選びませんでした。ノード改変法は入力のデータそのものを書き換えてしまい、他の処理に悪影響を与える可能性があるため選びませんでした。 + +- **選択したアプローチ**: Floyd's Tortoise and Hare(高速・低速ポインタ法) +- **理由**: + - **計算量的な優位性**: 時間計算量O(n)・空間計算量O(1)と、両方の面で最も効率的です。ノードを2つのポインタ(「遅い亀」と「速いウサギ」に例えられる)だけで管理するため、追加のメモリが一切不要です。 + - **TypeScript環境での型安全性**: `ListNode | null`という型を正しく扱うための型ガードを丁寧に書くだけで実装でき、複雑な型定義は不要です。 + - **保守性・可読性の観点**: アルゴリズム自体がシンプルなループ構造のみで表現できるため、コードの見通しが良く、他の開発者にも理解しやすいです。 +- **TypeScript特有の最適化ポイント**: + - コンパイル時の型チェックにより、「`next`が`null`かもしれないのにアクセスしてしまう」といったバグを未然に防げます。 + - 今回はジェネリクスは不要ですが、`readonly`な引数として扱うことで「関数内で誤ってリストを書き換えてしまう」事故を防ぎます(※ただし今回はポインタの移動のみで値の書き換えは発生しません)。 + +> 📖 **このセクションで登場した用語** +> +> - **ポインタ**:ある変数が「別のデータ(ここではノード)を指し示している」状態、またはその変数自体のこと。 +> - **型チェック**:TypeScriptがコードをコンパイル(変換)する際に「型が正しく使われているか」を確認する処理。 + +--- + +## 4. 実装コード + +> 💡 **初学者向け補足**:コード全体を示す前に、このコードの大まかな構造(骨格)を示します。 +> +> 1. まず、`head`が`null`(空リスト)または`head.next`が`null`(次のノードが存在しない)の場合は`false`を返す(※単一ノードでも自身を指すサイクルは作れるが、`head.next`が`null`ならサイクルは存在しない) +> 2. 「遅いポインタ(slow)」と「速いポインタ(fast)」という2つの変数を用意し、両方とも`head`からスタートする +> 3. ループの中で、`slow`は1つずつ、`fast`は2つずつノードを進める +> 4. もし`slow`と`fast`が同じノードを指す瞬間が来たら、それは輪っかの中をぐるぐる回っている証拠なので`true`を返す +> 5. `fast`がリストの終端(`null`)に到達したら、輪っかは存在しないので`false`を返す + +```typescript +/** + * Definition for singly-linked list. + * class ListNode { + * val: number + * next: ListNode | null + * constructor(val?: number, next?: ListNode | null) { + * this.val = (val===undefined ? 0 : val) + * this.next = (next===undefined ? null : next) + * } + * } + */ + +/** + * 連結リストにサイクル(循環)が存在するかどうかを判定する + * Floyd's Tortoise and Hare(亀と兎のアルゴリズム)を使用する + * + * なぜこの方法が有効なのか: + * サイクルが存在する場合、速いポインタ(兎)は遅いポインタ(亀)よりも + * 2倍速く進むため、輪っかの中で必ずいつか追いつく(=同じノードを指す)瞬間が来る。 + * これは、円形のトラックを走る2人のランナーが、速度差がある限り + * 必ずどこかですれ違う(追いつく)のと同じ原理。 + * + * @param head - 連結リストの先頭ノード(存在しない場合はnull) + * @returns サイクルが存在すればtrue、存在しなければfalse + * @complexity Time: O(n), Space: O(1) + */ +function hasCycle(head: ListNode | null): boolean { + // 型ガード:リストが空、またはノードが1つしかない場合、 + // 「次に進む」動作自体ができないためサイクルは作りようがない + if (head === null || head.next === null) { + return false; + } + + // 「遅いポインタ(亀)」:1歩ずつ進む + // 「速いポインタ(兎)」:2歩ずつ進む + // どちらもTypeScript上は ListNode | null 型として扱う(nullになる可能性があるため) + let slow: ListNode | null = head; + let fast: ListNode | null = head; + + // fastとfast.nextの両方がnullでない間だけループを続ける + // (fastが2歩ずつ進むため、fast.nextが先にnullになるかもしれないことを考慮) + while (fast !== null && fast.next !== null) { + // 遅いポインタを1歩進める + slow = slow!.next; + + // 速いポインタを2歩進める + fast = fast.next.next; + + // もし遅いポインタと速いポインタが同じノードを指していたら、 + // それはサイクルの中で追いついたということなのでtrueを返す + if (slow === fast) { + return true; + } + } + + // ループを抜けた(=fastがリストの終端に到達した)ということは、 + // 輪っかがなく、リストがどこかで途切れていたということ + return false; +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 例)入力: head = [3, 2, 0, -4]、pos = 1(末尾の -4 が 2番目のノード「2」に戻る) +> +> ノードの並び: idx0: 3 → idx1: 2 → idx2: 0 → idx3: -4 → (idx1 の「2」に戻る) +> +> 初期状態: slow = 3 (idx0), fast = 3 (idx0) +> +> Step 1: slow = slow.next = 2 (idx1) +> fast = fast.next.next = 0 (idx2) +> slow(2) === fast(0)? → いいえ、続行 +> +> Step 2: slow = slow.next = 0 (idx2) +> fast = fast.next.next = 2 (idx1) (idx2 から idx3(-4) を経て idx1(2) へ進む) +> slow(0) === fast(2)? → いいえ、続行 +> +> Step 3: slow = slow.next = -4 (idx3) +> fast = fast.next.next = -4 (idx3) (値 2(idx1) から fast.next.next により 0(idx2) → -4(idx3) へ2歩進む) +> slow(-4) === fast(-4)? → はい!同じ ListNode オブジェクトを指している +> +> → true を返す(サイクルが検出された) +> ``` + +> 📖 **このセクションで登場した用語** +> +> - **Floyd's Tortoise and Hare(亀と兎のアルゴリズム)**:2つのポインタを異なる速度で進めることでサイクルを検出する古典的な手法。 +> - **`!`(非nullアサーション演算子)**:TypeScriptに対して「ここは絶対に`null`ではないから安心して」と明示的に伝える記法。今回は直前の`while`条件で`fast !== null`(つまりループの1歩目では`slow`も`fast`も`head`であり非null)が保証されているため使用しています。ただし乱用すると型安全性が失われるため、根拠がある場合のみ使うべきです。 +> - **`ListNode | null`**:ノードが存在する場合と、リストの終端(存在しない場合)の両方を表現するための型。 + +--- + +## TypeScript固有の最適化観点 + +### 型安全性の活用 + +1. **コンパイル時エラー防止** + - `head: ListNode | null`という型により、呼び出し側が誤って`undefined`など不正な値を渡した場合にコンパイル段階で警告できます。 + - `slow`と`fast`を`ListNode | null`型として宣言することで、「`.next`にアクセスする前に`null`チェックを忘れる」というミスをコンパイラが指摘してくれます。 + +2. **null安全性の確保** + - `while (fast !== null && fast.next !== null)`という条件により、`fast.next.next`にアクセスする前に必ず安全性を確認しています。 + +### 開発効率と保守性 + +- 型定義(`ListNode | null`)自体が「このノードは存在しないかもしれない」というドキュメントの役割を果たしており、コードを読むだけで境界条件への配慮が伝わります。 +- ループ構造がシンプルなため、将来的に「サイクルの開始ノードを求める」(LeetCode 142番の発展問題)といった機能追加も同じ型定義のまま自然に拡張できます。 diff --git a/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/README.md b/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/README.md new file mode 100644 index 00000000..cb511e26 --- /dev/null +++ b/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/README.md @@ -0,0 +1,385 @@ +# LeetCode 141: Linked List Cycle - 連結リストの循環をポインタ2つで見抜く + +

目次

+ +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python実装](#impl) +- [CPython最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +> 💡 **初学者向け補足**:この問題は、一言で言うと「連結リスト(ノードが`next`という矢印でつながったデータ構造)を先頭から`next`をたどっていったとき、以前に通ったノードにまた戻ってきてしまう輪っか(サイクル)があるかどうかを判定する問題」です。 + +この問題が難しく感じられるポイントは、「サイクルがあるかどうか」を確認するために**リスト全体を配列にコピーして目で見て確認する、というわけにはいかない**点です。リストが無限に長く見えてしまう(実際には輪っかの中を何周もするだけ)ため、素朴に「先頭から順番に全部たどって終端`None`を待つ」だけのコードだと、サイクルがある場合に永遠に終わらない(無限ループになる)危険があります。そこで、「輪っかがあるかどうか」を**有限のステップで確実に判定できる工夫**が必要になります。今回はその工夫として、進む速さの異なる2つのポインタ(変数)を使う古典的な手法を採用します。 + +- **入力**: 連結リストの先頭ノード `head`(`Optional[ListNode]`型。リストが空の場合は`None`) +- **出力**: サイクルが存在すれば`True`、存在しなければ`False`(`bool`型) +- **制約**(例): ノード数は`0`から`10^4`個程度、各ノードの値は`-10^5`から`10^5`の範囲 +- **正当性の要件**: どんな形の連結リスト(空・単一ノード・自己ループ・長い輪っか)に対しても、有限のステップで正しく`True`/`False`を返せること + +> 📖 **この章で登場した用語** +> +> - **連結リスト**:データ(ノード)が`next`という矢印でつながった構造。配列と違い、途中への挿入・削除が高速。 +> - **サイクル(循環)**:リストをたどっていくと、以前に訪れたノードにまた戻ってきてしまう輪っか状の構造。 +> - **制約**:入力として与えられる値の範囲や条件のこと。例:「ノード数は`10^4`以下」。 +> - **正当性**:アルゴリズムがどんな入力に対しても常に正しい答えを返すことの保証。 + +--- + +

アルゴリズム要点(TL;DR)

+ +> 💡 **初学者向け補足**:TL;DR(Too Long; Didn't Read=長くて読めない人向けの要約)とは、細かい説明を飛ばして「なんとなくこういう手順で解くんだな」というイメージを掴むための章です。詳しい理屈は後の章で説明します。 + +- **戦略**: Floyd's Tortoise and Hare(亀と兎のアルゴリズム)を使う。「遅いポインタ(亀)」を1歩ずつ、「速いポインタ(兎)」を2歩ずつ進めることで、追加のメモリなしにサイクルを検出できるため選択。 +- **データ構造**: 追加のデータ構造は使わない。`slow`と`fast`という2つの変数(ポインタ)だけで済ませることで、メモリ効率を最大化できるため。 +- **判定条件**: `slow`と`fast`が同じノードを指した瞬間、サイクルが存在すると判定する。輪っかの中では速いポインタが必ずいつか遅いポインタに追いつくという性質を利用しているため。 +- **終了条件**: `fast`(またはその次のノード)が`None`に到達したら、輪っかがなくリストが途切れていると判定して終了するため。 +- **計算量**: 時間計算量 O(n)、空間計算量 O(1)。リストを1回走査するだけで済み、追加メモリも不要なため最も効率的。 + +> 📖 **この章で登場した用語** +> +> - **TL;DR**:「長くて読めない人向けの要約」を意味する略語。 +> - **Floyd's Tortoise and Hare**:進む速さの異なる2つのポインタを使ってサイクルを検出する古典的な手法。 +> - **ポインタ**:ある変数が「別のデータ(ここではノード)を指し示している」状態、またはその変数自体のこと。 + +--- + +

図解

+ +> 💡 **初学者向け補足**:以下の図はアルゴリズムの「処理の流れ」を視覚的に示したものです。Mermaidフローチャートでは、ひし形(`{}`)は「条件分岐(=はい/いいえで処理が分かれるポイント)」、長方形(`[]`)は「処理ステップ(=実際に何かを行う操作)」を表します。上から下へ矢印をたどりながら読み進めてください。 + +### フローチャート + +この図は`hasCycle`関数がどのような順序で処理を行い、いつ`True`または`False`を返すのかという処理の流れを表しています。上から下へ読み進めてください。 + +```mermaid +flowchart TD + Start[Start hasCycle head] + Start --> Base{head is None or head next is None} + Base -- Yes --> RetFalseEmpty[Return False] + Base -- No --> Init[Set slow equals head and fast equals head] + Init --> LoopCond{fast is not None and fast next is not None} + LoopCond -- No --> RetFalseEnd[Return False] + LoopCond -- Yes --> MoveSlow[Move slow one step forward] + MoveSlow --> MoveFast[Move fast two steps forward] + MoveFast --> Check{slow is fast} + Check -- Yes --> RetTrue[Return True] + Check -- No --> LoopCond +``` + +主要なノードの意味: + +- `Start[Start hasCycle head]`:関数の入り口。`head`を受け取る。 +- `Base{head is None or head next is None}`:リストが空、またはノードが1つしかないかを判定するひし形(条件分岐)。 +- `RetFalseEmpty[Return False]`:輪っかを作りようがないため`False`を返すステップ。 +- `Init[Set slow equals head and fast equals head]`:2つのポインタを両方とも先頭ノードで初期化するステップ。 +- `LoopCond{fast is not None and fast next is not None}`:`fast`があと2歩進めるかどうかを判定するひし形。 +- `MoveSlow` / `MoveFast`:それぞれ`slow`を1歩、`fast`を2歩進めるステップ。 +- `Check{slow is fast}`:2つのポインタが同じノードを指したかどうかを判定するひし形。 +- `RetTrue[Return True]`:サイクルが検出されたことを表すステップ。 + +### データフロー図 + +この図は、入力の`head`から出力の`bool`値までデータがどのように変換されていくかを表しています。 + +```mermaid +graph LR + subgraph Precheck + A[Input head] + A --> B[Check head or head next is None] + B --> C[Early return False if empty] + end + subgraph Core + C --> D[Init slow and fast at head] + D --> E[Advance slow and fast in loop] + E --> F[Compare slow and fast by identity] + end + F --> G[Output bool result] +``` + +主要な流れの説明: + +- `Precheck`から`Core`へ:まず空リスト・単一ノードといった「輪っかを作れないケース」を除外してから、本格的なアルゴリズムに入る。 +- `Core`内:`slow`と`fast`を交互に進めながら、毎回「同じノードを指しているか」を確認するループを繰り返す。 +- `Core`から`Output`へ:ループの結果(サイクルを検出したか、リストの終端に到達したか)を`bool`値として返す。 + +> 💡 **代表例でのトレース**:入力 `head = [3,2,0,-4]`(`pos=1`で末尾の`-4`がインデックス1の`2`に戻る)を使って、上記フローチャートの各ノードをどのように通過するかを示します。 +> +> ``` +> Step 1: Start → head = [3,2,0,-4](末尾が2番目のノードに戻る) +> Step 2: Base 判定 → head is None?No、head.next is None?No → No側へ +> Step 3: Init → slow = 3, fast = 3 +> Step 4: LoopCond 判定 → fast=3, fast.next=2 → 両方Noneでない → Yes側へ +> Step 5: MoveSlow → slow = 2 +> Step 6: MoveFast → fast = 0(3 → 2 → 0 と2歩進む) +> Step 7: Check 判定 → slow(2) is fast(0)? → No → LoopCondへ戻る +> Step 8: LoopCond 判定 → fast=0, fast.next=-4 → 継続 +> Step 9: MoveSlow → slow = 0 +> Step 10: MoveFast → fast = 2(0 → -4 → 2 と2歩進む。-4の次は2番目のノード) +> Step 11: Check 判定 → slow(0) is fast(2)? → No → LoopCondへ戻る +> Step 12: LoopCond 判定 → fast=2, fast.next=0 → 継続 +> Step 13: MoveSlow → slow = -4 +> Step 14: MoveFast → fast = -4(2 → 0 → -4 と2歩進む) +> Step 15: Check 判定 → slow(-4) is fast(-4)? → Yes(同じノードオブジェクト) +> 結果: True を返す +> ``` + +> 📖 **この章で登場した用語** +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理。 +> - **データフロー図**:データがどのように変換・移動するかを示す図。 +> - **サブグラフ**:フローチャートの中で関連する処理をグループ化したもの(今回は`Precheck`と`Core`の2グループ)。 + +--- + +

正しさのスケッチ

+ +> 💡 **初学者向け補足**:「正しさのスケッチ」とは、アルゴリズムが**常に正しい答えを返すことの根拠**を整理したものです。厳密な数学的証明ではなく、「なぜ正しいと言えるか」を直感的に説明します。 + +- **不変条件**(=処理中ずっと成り立ち続けるべき条件): ループの各回において、`slow`は`fast`よりも常に「進んだ歩数が半分以下」である。具体的には、`slow`が`k`歩進む間に`fast`は`2k`歩進んでいる。 +- **網羅性**(=すべてのケースをもれなく処理できているという保証): リストの形は「輪っかがない(一本道)」か「輪っかがある」のどちらかしかなく、この2つのケースをそれぞれ`LoopCond`が`No`で抜ける場合と、`Check`が`Yes`になる場合として網羅している。 +- **基底条件**(=再帰やループの終了条件): `head`が`None`、または`head.next`が`None`の場合、それ以上ノードを進められないため輪っかは作りようがなく、即座に`False`を返す。これは「ノードが0個または1個(自己ループなし)のリストにサイクルは存在しない」という直感とも一致する。 +- **終了性**(=アルゴリズムが必ず有限ステップで終わるという保証): 輪っかがない場合、`fast`は有限個のノードを進んだのちに必ず`None`(またはその手前)に到達するためループは終了する。輪っかがある場合、`fast`は`slow`よりも1歩ずつ相対的に速く輪っかの中を進むため、輪っかの長さを超えないうちに必ず`slow`に追いつく(すれ違うのではなく、同じ場所に重なる)。したがって、いずれの場合も有限回のループで終了することが保証される。 + +> 📖 **この章で登場した用語** +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件。 +> - **基底条件**:ループや再帰の終了条件。これがないと無限ループになる。 +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証。 +> - **網羅性**:すべてのケースをもれなく処理できているという保証。 + +--- + +

計算量

+ +> 💡 **初学者向け補足**:計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。 + +| 記法 | 意味 | 直感的なイメージ | +| ---- | ---- | ---------------- | +| `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +| `O(n)` | 入力に比例して増加 | リストを端から順に読む | + +- **時間計算量**: `O(n)`。輪っかがない場合は`fast`が最大`n`個のノードを走査して終わる。輪っかがある場合も、`slow`が輪っかの長さを超える前に必ず`fast`と一致するため、走査するノードの延べ数は`n`に比例する範囲に収まる。 +- **空間計算量**: `O(1)`。`slow`と`fast`という2つの変数(ポインタ)以外に、入力サイズに応じて増えるデータ構造を一切使っていない。 + +比較として、「訪問済みノードを`set`に記録する方法」は時間計算量`O(n)`は同じだが、空間計算量が`O(n)`になる(最大でリストの全ノードを`set`に記録する可能性があるため)。今回のFloyd's Tortoise and Hareは、その空間コストをゼロにできる点が最大の利点。 + +> 📖 **この章で登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安。 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安。 + +--- + +

Python実装

+ +> 💡 **初学者向け補足**:コードを読む前に、実装の全体的な骨格を示します。 +> +> 1. `head`が`None`、またはノードが1つしかない場合、即座に`False`を返す +> 2. `slow`と`fast`という2つのポインタ変数を、両方とも`head`で初期化する +> 3. `fast`があと2歩進める間だけループを繰り返す +> 4. ループの中で`slow`を1歩、`fast`を2歩進める +> 5. `slow`と`fast`が同じノードを指したら`True`を返す +> 6. ループを抜けたら(=`fast`が終端に到達したら)`False`を返す + +```python +from __future__ import annotations + +from typing import Optional, TYPE_CHECKING + +# pylance(静的型チェッカー)向けの型スタブ定義。 +# LeetCodeの実行環境では ListNode クラスが既に用意されているため、 +# 実行時にはこの定義を使わず、型チェック時(TYPE_CHECKING=True)にのみ参照される。 +if TYPE_CHECKING: + class ListNode: + val: int + next: Optional["ListNode"] +else: + # 実行時にListNodeが未定義(=ローカルで動作確認する場合など)であれば、 + # 最小限のフォールバック定義を用意する。 + # __slots__ を使うことで、インスタンスごとの辞書生成を省略しメモリを節約する。 + try: + ListNode # type: ignore[used-before-def] + except NameError: + class ListNode: + __slots__ = ("val", "next") + + def __init__(self, x: int) -> None: + self.val = x + self.next: Optional["ListNode"] = None + + +class Solution: + """ + 連結リストにサイクル(循環)が存在するかどうかを判定するクラス。 + + Floyd's Tortoise and Hare(亀と兎のアルゴリズム)を用いて、 + 追加のメモリを使わずに O(n) 時間で判定する。 + """ + + def hasCycle(self, head: Optional[ListNode]) -> bool: + """ + 連結リストにサイクルが存在するかを判定する(Pure function)。 + + Args: + head: 連結リストの先頭ノード。空リストの場合は None。 + + Returns: + サイクルが存在すれば True、存在しなければ False。 + + Time Complexity: O(n) + Space Complexity: O(1) + """ + # 基底条件:head が None、またはノードが1つしかない場合、 + # 「次に進む」動作自体ができないため輪っかは作りようがない。 + # pylance対応:ここで None を除外することで、以降の型チェックが安全になる。 + if head is None or head.next is None: + return False + + # 「遅いポインタ(亀)」は1歩ずつ、「速いポインタ(兎)」は2歩ずつ進む。 + # 型ヒント上はどちらも Optional[ListNode](None になる可能性がある)。 + slow: Optional[ListNode] = head + fast: Optional[ListNode] = head + + # fast と fast.next の両方が None でない間だけループを続ける。 + # Python の and は短絡評価(=左が False なら右を評価しない)を行うため、 + # fast が None の時点で fast.next へアクセスされることはなく、 + # AttributeError(存在しない属性へのアクセスエラー)を未然に防いでいる。 + while fast is not None and fast.next is not None: + # 遅いポインタを1歩進める。 + slow = slow.next # type: ignore[union-attr] + + # 速いポインタを2歩進める。 + fast = fast.next.next + + # is 演算子でオブジェクトの同一性(=同じメモリ上のノードかどうか)を判定する。 + # 値の比較(==)ではなく「同じノードを指しているか」を確認したいので is を使う。 + if slow is fast: + return True + + # ループを抜けた(=fast が終端 None に到達した)ということは、 + # 輪っかがなく、リストがどこかで途切れていたということ。 + return False +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 例)入力: head = [3,2,0,-4](-4 の next が インデックス1の 2 に戻る) +> +> 呼び出し: hasCycle(head) +> 初期状態: head.next は None ではないので基底条件はスキップ +> slow = 3, fast = 3 +> +> ループ1回目: fast(3), fast.next(2) → 両方Noneでないので継続 +> slow = slow.next → 2 +> fast = fast.next.next → 0 +> slow(2) is fast(0)? → No +> +> ループ2回目: fast(0), fast.next(-4) → 継続 +> slow = slow.next → 0 +> fast = fast.next.next → 2(-4 の次が 2 に戻り、その次が 0) +> slow(0) is fast(2)? → No +> +> ループ3回目: fast(2), fast.next(0) → 継続 +> slow = slow.next → -4 +> fast = fast.next.next → -4(2 の次が 0、その次が -4) +> slow(-4) is fast(-4)? → Yes(同じノードオブジェクト) +> +> 最終結果: True +> ``` + +> 📖 **この章で登場した用語** +> +> - **`from __future__ import annotations`**:型ヒントを文字列として遅延評価する宣言。前方参照(まだ定義していないクラス名を型として使うこと)を解決できる。 +> - **`TYPE_CHECKING`**:実行時には`False`、型チェッカー(pylance等)がコードを解析するときだけ`True`として扱われる特別な定数。実行時には不要なコードを型チェックのためだけに書きたいときに使う。 +> - **`__slots__`**:クラスのインスタンスが持てる属性をあらかじめ固定し、通常発生する属性辞書(`__dict__`)の生成を省略することでメモリを節約する仕組み。 +> - **`# type: ignore[union-attr]`**:pylanceに対して「この行の型警告は意図的なので無視してください」と伝えるコメント。ループの前提条件から実行時には安全であることが保証されている場合に限定して使用する。 +> - **`AttributeError`**:存在しない属性(例:`None`に対する`.next`)にアクセスしようとしたときにPythonが発生させる実行時エラー。 + +--- + +

CPython最適化ポイント

+ +> 💡 **初学者向け補足**:この章では「同じ処理でもPythonの書き方によって速さが変わる理由」を説明します。 + +- **属性アクセスの最小化**: `fast.next.next`のように1行で2段階の属性アクセスを行うことで、中間変数を作らずに済ませています。中間変数を作らないことでオブジェクト生成のオーバーヘッド(=余分な処理コスト)を避けられます。 + + ```python + # 最適化前:中間変数を経由する書き方 + tmp = fast.next + fast = tmp.next + + # 最適化後:1行でまとめて2歩進める + fast = fast.next.next + # 理由:中間変数への代入自体にもわずかなコストがあるため、 + # 不要な代入を減らすことでわずかに高速化できる。 + ``` + +- **`is`演算子によるオブジェクト同一性判定**: `slow == fast`ではなく`slow is fast`を使うことで、値の比較(`__eq__`メソッドの呼び出し)ではなく、ポインタとしての同一性を直接比較しています。`ListNode`クラスは`__eq__`を独自定義していないため挙動上の差はほぼありませんが、「同じノードオブジェクトを指しているか」という意図が明確になり、かつ`is`はC実装レベルでの単純なポインタ比較のため`==`より高速です。 +- **短絡評価の活用**: `while fast is not None and fast.next is not None:`という条件式は、Pythonの`and`が左から右へ評価され、左が`False`なら右を評価しないという性質(短絡評価)を利用しています。これにより`fast`が`None`の場合に`fast.next`へアクセスしてエラーになることを防ぎつつ、余計な条件チェックのコードを書かずに済んでいます。 + +> 📖 **この章で登場した用語** +> +> - **属性アクセス**:`obj.x`のようにオブジェクトのプロパティを参照する操作。 +> - **`__eq__`**:Pythonのオブジェクトが`==`で比較されたときに呼ばれる特殊メソッド。 +> - **短絡評価**:`and`や`or`で、左側の条件だけで結果が確定する場合、右側を評価しない仕組み。 + +--- + +

エッジケースと検証観点

+ +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。 + +- **空リスト(`head = None`)**: なぜ問題になりうるか → `head.next`にいきなりアクセスするコードだと`AttributeError`が発生する。今回は最初の`if head is None`で安全に`False`を返している。 +- **ノードが1つだけ、自己ループなし(`head.next = None`)**: なぜ問題になりうるか → ループ条件を先にチェックせずにいきなり`slow.next`にアクセスすると、`fast.next`が`None`のケースを見逃す可能性がある。今回は`head.next is None`の基底条件で正しく`False`を返している。 +- **ノードが1つだけ、自己ループあり(`pos = 0`)**: なぜ問題になりうるか → 基底条件の判定を誤ると、自己ループを「輪っかなし」と誤判定してしまう可能性がある。今回は`head.next`が自分自身を指すため`None`ではなく、ループに正しく入り`True`と判定される。 +- **ノード数が最大(`10^4`件程度)の大規模な入力**: なぜ問題になりうるか → 空間計算量が`O(n)`のアルゴリズム(`set`を使う方法など)だとメモリ使用量が大きくなる。今回のアルゴリズムは`O(1)`の空間計算量のため、この点は問題にならない。 +- **輪っかの入り口がリストの途中にあるケース(`pos > 0`)**: なぜ問題になりうるか → 輪っかの位置によって`slow`と`fast`が出会うタイミングが変わるため、境界条件の実装ミスがあると誤判定につながる。本実装はアルゴリズムの数学的性質により、輪っかの位置に関わらず正しく検出できる。 + +> 📖 **この章で登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件の入力。 +> - **境界値**:制約の上限・下限にあたる値。 +> - **インデックスエラー**:リストや文字列の範囲外の要素にアクセスしようとしたときのエラー。 + +--- + +

FAQ

+ +**Q1. なぜ`set`で訪問済みノードを記録する方法を使わないのですか?** + +結論:メモリ効率(空間計算量)を優先したためです。 +理由:`set`を使う方法は実装が直感的で分かりやすい反面、最悪の場合リストの全ノードを`set`に記録するため空間計算量が`O(n)`になります。一方、Floyd's Tortoise and Hareは変数2つだけで済むため`O(1)`です。 +補足:可読性を最優先したい場合は`set`を使う方法も十分実用的な選択肢です。ノード数が少ない、あるいはメモリ制約が厳しくない場面ではむしろ`set`版の方が読みやすいこともあります。 + +**Q2. `slow`が1歩、`fast`が2歩進むと、なぜ必ず追いつくのですか?** + +結論:輪っかの中では、2つのポインタの「相対的な速度差」が1歩であるため、いずれ必ず追いつきます。 +理由:輪っかに入った後、`fast`は`slow`より毎回1歩多く進みます。これは円形のトラックを走る2人のランナーが、速度差がある限り必ずどこかですれ違う(あるいは追いつく)のと同じ原理です。輪っかの長さを`L`とすると、`fast`は最大`L`ステップ以内に`slow`に追いつきます。 +補足:輪っかがない場合は、この「追いつく」現象自体が起こらず、代わりに`fast`が先にリストの終端`None`に到達するため、そこでループが終了します。 + +**Q3. `slow.next`の行にある`# type: ignore[union-attr]`はなぜ必要なのですか?** + +結論:pylanceの型推論の限界を補うためのコメントです。 +理由:`slow`は型ヒント上`Optional[ListNode]`(`None`かもしれない)と宣言されています。pylanceは`while`ループの条件式(`fast is not None`)だけでは`slow`が`None`でないことまでは保証できないため、`slow.next`に警告を出します。しかし実際には、ループの1歩目では`slow`は`head`(`None`でないことが基底条件で確認済み)であり、以降も`fast`が`None`にならない限り`slow`も進み続けるため、実行時には安全です。 +補足:`# type: ignore`は「警告を黙らせる魔法」ではなく、「なぜ安全と言えるかを人間が検証した上で使うもの」です。乱用すると本当のバグを見逃す原因になるため、根拠が明確な箇所にのみ使うべきです。 + +**Q4. なぜノードの値(`val`)ではなく`is`でノードを比較するのですか?** + +結論:値ではなく「同じノードオブジェクトを指しているか」を確認したいからです。 +理由:連結リストには同じ`val`(値)を持つノードが複数存在する可能性があります(例:`[1,1,1]`)。値だけで比較すると、実際には別々のノードなのに「同じ場所に到達した」と誤判定してしまう危険があります。`is`はメモリ上の同一オブジェクトかどうかを判定するため、この誤判定を防げます。 +補足:今回の`ListNode`クラスは`__eq__`を独自定義していないため、`==`を使っても結果的に`is`と同じ挙動になりますが、意図を明確にするため`is`を使うのがより安全な書き方です。 + +> 📖 **この章で登場した用語** +> +> - **FAQ**:Frequently Asked Questions(よくある質問)の略。 +> - **トレードオフ**:何かを得ると何かを失う関係。例:メモリを節約すると実装がやや複雑になる、など。 diff --git a/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/README_react.html b/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/README_react.html new file mode 100644 index 00000000..4ea91bb7 --- /dev/null +++ b/Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/README_react.html @@ -0,0 +1,1112 @@ + + + + + + LeetCode 141: Linked List Cycle - 亀と兎のアルゴリズムで解説 + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/Pascal's_Triangle_II_Python.md b/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/Pascal's_Triangle_II_Python.md new file mode 100644 index 00000000..925caba1 --- /dev/null +++ b/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/Pascal's_Triangle_II_Python.md @@ -0,0 +1,207 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: Python +> 適用ルールセット: 共通5ルール + Python固有4ルール +> 参照ファイル: references/common.md + references/python.md + +--- + +## 1. 問題分析結果 + +> 💡 この問題は一言で言うと「**パスカルの三角形の、指定された行だけをメモリ効率よく作る問題**」です。TypeScript版と本質的なアルゴリズムは同じですが、Pythonで解く際には特有の注意点があります。 + +**CPython特有の注意点(最初に必ず押さえること)** +Pythonの `/`(スラッシュ)演算子は「真の除算(true division)」と呼ばれ、割り切れる場合でも常に `float` 型を返します(例:`6 / 2` は `3` ではなく `3.0`)。今回の戻り値の型は `List[int]` なので、`float` が紛れ込むとpylance(型チェッカー)がエラーを出します。そのため、割り切れることが数学的に保証されている今回のような計算では、`//`(フロア除算=小数点以下を切り捨てて整数を返す演算子)を使う必要があります。これはTypeScriptの `/` が常に数値型を返すのとは異なる、Python特有の落とし穴です。 + +**競技プログラミング視点** + +- 制約 `0 <= rowIndex <= 33` から、最大値 `C(33, 16) ≈ 1.17 × 10⁹` 程度で収まるため、`int` の任意精度(Pythonの `int` はCPythonでは桁数に応じて自動的に多倍長になる仕組みで、オーバーフローの心配が原理的にない)をそのまま使えば十分です。 +- 標準ライブラリの `itertools.accumulate`(=累積合計の `sum()` を、足し算以外の任意の二項演算に一般化した関数)を使うと、`for`文を書かずに1行で漸化式を回せます。 + +**業務開発視点** + +- 型ヒント(`rowIndex: int`, `-> List[int]`)を書いても、Pythonは動的型付け言語のため実行時には無視されます。誤った型が渡された場合に備え、`isinstance()` による実行時チェックを別途用意しておくと安全です。 +- `bool` は Python では `int` のサブクラスである(=`True == 1`, `False == 0` が成立し、`isinstance(True, int)` が `True` になる)という言語特有の仕様があるため、`True`/`False` が誤って渡されるケースも弾いておくとより堅牢になります。 + +**Python特有分析** + +- **データ構造選択**:今回は単純な数値の並びなので `list` で十分です。両端からの追加・削除が頻発するわけではないため `deque`(デック=前からも後ろからも高速に出し入れできる両端開きの構造)は不要です。 +- **標準ライブラリ活用度**:`itertools.accumulate` を活用する主なメリットは、ループ処理を宣言的かつ簡潔に表現できる可読性の向上にあります。カスタム演算子(lambda関数など)を伴う場合はPython関数のコールバックが発生するため、必ずしもPure Pythonの`for`文より高速になるわけではなく、実際の性能比較はPythonバージョンや入力サイズに応じたベンチマークに基づいて判断する必要があります。 +- **CPython最適化度**:`math.comb`(二項係数を直接計算する標準ライブラリ関数)を各 `k` について個別に呼び出す方法も存在しますが、後述の理由で今回は採用しません。 + +> 📖 このセクションで登場した用語 +> +> - **真の除算(true division)**:Pythonの `/` 演算子。割り切れる場合でも常に `float` を返す +> - **フロア除算(floor division)**:Pythonの `//` 演算子。小数点以下を切り捨てて `int` を返す +> - **多倍長整数**:桁数に応じて自動的にメモリを拡張できる整数表現。CPythonの `int` はこの仕組みを持つため、桁あふれ(オーバーフロー)が原理的に起きない +> - **itertools.accumulate**:`sum()` の考え方を一般化し、足し算以外の任意の二項演算で「1つ前の結果」から「次の結果」を作れるようにした標準ライブラリ関数 + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 同じ「パスカルの三角形の1行を求める」という目的でも、実装方法によって速さや標準ライブラリの活かし方が変わります。ここでは3つのアプローチを比較します。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| -------------------------------------------------------- | ----------------------- | ---------- | ---------------- | ------ | ------------------ | ------------------------------- | ------------------------------------------- | +| A. 前の行から毎回新しいリストで行を生成 | O(n²) | O(n²) | 低 | ★★★ | なし | 不適(Pure Pythonの二重ループ) | 直感的だが遅い | +| B. `math.comb(n, k)` を各kごとに個別に呼ぶ | O(n²)〜O(n・min(k,n-k)) | O(n) | 低 | ★★★ | `math` | 適(C実装) | 各回の計算がO(k)かかるため、合計はO(n²)寄り | +| C. 二項係数の漸化式を `itertools.accumulate` で1行で回す | **O(n)** | O(n) | 低 | ★★☆ | `itertools` | 適(C実装のループ) | 採用 | + +`n = rowIndex` です。`O(n²)`(=入力が2倍になると処理は約4倍になる)はAとBの両方に当てはまります。Aは二重ループが必要なため、Bは `math.comb` の内部計算が毎回 `O(min(k, n-k))` かかり、それを `n` 回繰り返すために合計で `O(n²)` 相当になるためです。一方Cは「1つ前の値」から「次の値」を定数時間で計算する漸化式を使うため、`n` 回のループ(=`itertools.accumulate`の内部ループ)だけで完結し `O(n)` になります。 + +**選択理由**:計算量の観点でCが唯一 `O(n)` を達成でき、かつ `itertools.accumulate` を使うことでPure Pythonの `for` 文を書かずに済みます。`accumulate` はC言語で実装されているため、Pythonインタープリタを介した加算・比較のオーバーヘッドが少なく、Pure Pythonの `for` ループより高速に動作します。 + +**Python最適化戦略**:`accumulate` に `initial=1` という引数(Python 3.8以降で使える機能)を渡すことで、「最初の値(`row[0] = 1`)」を明示的に指定できます。これがないと、最初の要素をどう扱うかを別途コードで書く必要があり、コードが長くなってしまいます。 + +**トレードオフ**:AやBは1行1行が素直で読みやすい一方、遅くなります。Cは計算量では最適ですが、`accumulate` + `lambda` という書き方に慣れていない人には可読性がやや下がります(後述の実装パターンでコメントを厚めに付けて補います)。 + +> 📖 このセクションで登場した用語 +> +> - **`math.comb(n, k)`**:`n`個から`k`個を選ぶ組み合わせの数を直接計算する標準ライブラリ関数 +> - **トレードオフ**:何かを得ると何かを失う関係。ここでは「計算速度」と「読みやすさ」の間のバランスを指す +> - **`initial`引数**:`itertools.accumulate`にPython 3.8以降で追加された、累積計算の初期値を指定できる引数 + +--- + +## 3. 実装パターン + +**コードの大まかな構造(骨格)** + +1. `rowIndex` の型と範囲を検証する(業務開発版のみ) +2. `k = 1` から `rowIndex` まで、二項係数の漸化式を1つ前の値から計算していく +3. 完成したリストを返す + +**【業務開発版を使う場面】** +チームで長期間メンテナンスするプロダクションコードに向きます。型ヒントと `isinstance()` による実行時チェックの両方を備えているため、pylanceによる静的解析と、実行時の想定外入力の両方から守られます。 + +```python +from typing import List + + +class Solution: + """ + 119. Pascal's Triangle II を解決するクラス + + パスカルの三角形の rowIndex 行目を、二項係数の漸化式を使って + O(rowIndex) 時間・O(rowIndex) 追加空間で求める。 + """ + + def getRow(self, rowIndex: int) -> List[int]: + """ + パスカルの三角形の rowIndex 行目(0-indexed)を返す + + Args: + rowIndex: 求めたい行番号(0 <= rowIndex <= 33) + + Returns: + rowIndex 行目の値を格納したリスト + + Raises: + TypeError: rowIndex が int 型でない場合 + ValueError: rowIndex が 0〜33 の範囲外の場合 + """ + # 型ヒントは実行時には無視されるため、isinstance()で実行時にも検証する。 + # これによりpylance(静的解析)と実行時チェックの両方で安全性を確保する。 + self._validate_input(rowIndex) + + # 結果を格納するリストを先に確保する。 + # フォローアップの「O(rowIndex)の追加空間」を満たすため、 + # 過去の行を丸ごと保持する二次元リストは作らず、この1本のリストだけを使い回す + row: List[int] = [0] * (rowIndex + 1) + + # パスカルの三角形の左端は、どの行でも必ず1になる + # (n個からk=0個を選ぶ組み合わせは「何も選ばない」の1通りしかないため) + row[0] = 1 + + # 二項係数の漸化式 C(n, k) = C(n, k-1) * (n - k + 1) // k を使い、 + # 1つ左の値から次の値を順番に計算していく(同じ行の中だけで完結する) + for k in range(1, rowIndex + 1): + # 掛け算を先に行ってから割り算をする。 + # 「//」(フロア除算)を使うのは、Pythonの「/」が常にfloatを返してしまい + # List[int]という戻り値の型と食い違ってしまうため。 + # 二項係数は必ず整数になる値なので、掛け算を先に行えば割り切れずに誤差が出ることはない + row[k] = row[k - 1] * (rowIndex - k + 1) // k + + return row + + def _validate_input(self, row_index: int) -> None: + """入力値が問題の制約を満たすかを検証する(業務開発版のみで使用)""" + # isinstance()で型チェックする。 + # bool は int のサブクラスなので、isinstance(True, int) は True になってしまう。 + # そのため True/False が誤って渡されるケースも別途弾いておく。 + if not isinstance(row_index, int) or isinstance(row_index, bool): + raise TypeError("rowIndex must be an int") + + if not (0 <= row_index <= 33): + raise ValueError("rowIndex must be between 0 and 33") +``` + +**【競技プログラミング版を使う場面】** +LeetCodeなど、制限時間内に正解を出すことが目的のコードに向きます。可読性よりも短さを優先し、`itertools.accumulate` を使って計算部分を1行にまとめます。 + +```python +from itertools import accumulate +from typing import List + + +class Solution: + def getRow(self, rowIndex: int) -> List[int]: + # accumulate(反復対象, 関数, initial=初期値) は、 + # 「1つ前の結果」と「次の要素」を受け取って「次の結果」を返す関数を + # 繰り返し適用しながらリストを組み立てる(sum()の一般化版)。 + # ここでは prev(1つ前の値)と k(現在のステップ番号)から、 + # 二項係数の漸化式で次の値を計算している。 + return list( + accumulate( + range(1, rowIndex + 1), + lambda prev, k: prev * (rowIndex - k + 1) // k, + initial=1, + ) + ) +``` + +**動作トレース**(`rowIndex = 4` の場合、業務開発版のロジックで追跡) + +``` +初期状態: rowIndex = 4 +Step 1: _validate_input(4) → isinstance(4, int)かつbool型ではない → OK、0<=4<=33 → OK +Step 2: row = [0, 0, 0, 0, 0] を確保 → row[0] = 1 → row = [1, 0, 0, 0, 0] +Step 3: k=1 → row[1] = row[0] * (4-1+1) // 1 = 1 * 4 // 1 = 4 → row = [1, 4, 0, 0, 0] +Step 4: k=2 → row[2] = row[1] * (4-2+1) // 2 = 4 * 3 // 2 = 6 → row = [1, 4, 6, 0, 0] +Step 5: k=3 → row[3] = row[2] * (4-3+1) // 3 = 6 * 2 // 3 = 4 → row = [1, 4, 6, 4, 0] +Step 6: k=4 → row[4] = row[3] * (4-4+1) // 4 = 4 * 1 // 4 = 1 → row = [1, 4, 6, 4, 1] +最終結果: [1, 4, 6, 4, 1] +``` + +競技版の `accumulate` 呼び出しも同じ値をたどります。`initial=1` が `row[0] = 1` に相当し、以降 `k=1,2,3,4` が順番に `lambda` に渡されて `prev` を更新していきます。問題文の例(`rowIndex = 3`)でも同様に `[1, 3, 3, 1]` が得られ、Example 1の出力と一致することを確認できます。 + +**型ヒントとpylanceの関係** +`rowIndex: int` や `-> List[int]` という型ヒントは、実行時には何も強制しません。しかし書いておくことで、pylanceが「`getRow("3")` のように文字列を渡している」といった呼び出しミスを、実行前にエディタ上で赤い波線として教えてくれます。今回の `_validate_input` は、pylanceが検出できない「実行時にしか分からない誤り」(例:呼び出し元がJSONから受け取った値をそのまま渡してしまうケース)を補うためのものです。 + +> 📖 このセクションで登場した用語 +> +> - **`lambda`**:名前を付けずにその場で定義できる小さな関数。`lambda prev, k: ...` は「`prev`と`k`を受け取って式の結果を返す関数」という意味 +> - **docstring**:関数やクラスの先頭に書く説明文。`"""三重クォート"""`で囲む +> - **`_validate_input`のようなアンダースコア始まりの名前**:Pythonの慣習で「クラス内部だけで使う想定のメソッド」を表す印 + +--- + +## 4. 検証 + +> 💡 エッジケース(=空・最小値・最大値など境界的な入力)のテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。 + +- **境界値**: `rowIndex = 0` → ループが0回実行され `row = [1]` がそのまま返る(特別な分岐を書かなくても自然に正しい結果になる)。`rowIndex = 33` → 最大値でも `int` の多倍長整数によりオーバーフローの心配がない。 +- **型チェック**: `_validate_input` により、`rowIndex` に `float` や `str`、`bool` が渡された場合も実行前に検出できる。pylance側でも `getRow` の呼び出し箇所に誤った型を渡すとエディタ上で警告が出る。 + +> 📖 このセクションで登場した用語 +> +> - **エッジケース**:空・要素1つ・最大サイズなど、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること + +--- + +## Python特有の追加考慮事項 + +- **CPython 3.11+ 新機能**: 今回の実装では例外グループ(`ExceptionGroup`)のような複数例外の同時処理は不要です。単一の入力に対する単純な検証で済むため、通常の `TypeError` / `ValueError` で十分です。 +- **並行処理考慮(GIL)**: このアルゴリズムはCPU時間を使う処理(CPU-bound)ですが、`rowIndex <= 33` という非常に小さな制約のため、GIL(グローバルインタープリタロック=Pythonのスレッドが同時に実行されないようにする仕組み。1人しか入れない部屋のようなもの)が並列化のボトルネックになることはありません。`multiprocessing` や `asyncio` を持ち出す必要はない規模の処理です。 +- **プロダクション考慮**: 実際のサービスに組み込む場合は、`_validate_input` が送出する `TypeError` / `ValueError` を呼び出し元でキャッチし、`logging` モジュールでエラー内容を記録しておくと、後から原因を追跡しやすくなります。 diff --git a/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/Pascal's_Triangle_II_Typescript.md b/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/Pascal's_Triangle_II_Typescript.md new file mode 100644 index 00000000..fd47b692 --- /dev/null +++ b/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/Pascal's_Triangle_II_Typescript.md @@ -0,0 +1,187 @@ +Pascal’s Triangle IIについて、TypeScript specialist の視点から段階的に解説します。まず「なぜこの解き方を選ぶのか」を明確にしてから、コードと具体的なトレースを示します。 + +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: TypeScript +> 適用ルールセット: 共通5ルール + TS固有5ルール +> 参照ファイル: references/common.md + references/typescript.md + +--- + +## 1. 問題の分析 + +> 💡 この問題は一言で言うと「**パスカルの三角形の、指定された行だけをメモリ効率よく作る問題**」です。三角形全体を作ってから該当行を取り出すのではなく、必要な行だけを直接組み立てる方法が求められています。 + +**競技プログラミング視点** + +- `rowIndex <= 33` という制約から、計算結果の最大値は `C(33, 16) = 1,166,803,110` 程度で収まります。JavaScriptの `number` 型(=内部的には64bit浮動小数点数で、2^53までは整数を誤差なく表現できる型)で十分扱えるため、BigIntのような特殊な型は不要です。 +- フォローアップ条件「`O(rowIndex)` の追加空間」を満たすには、三角形を丸ごと二次元配列に保存する方法は避け、**1本の配列だけを使い回す**必要があります。 + +**業務開発視点** + +- 入力値が `rowIndex` という単純な `number` 型なので、複雑な型設計は不要です。むしろ「不要な複雑さを持ち込まないこと」自体が保守性の観点で重要です。 +- 実行時エラー処理としては、`rowIndex` が整数でない・範囲外である場合の検証(型ガード=実行時に値の型や範囲を確認する処理)を入れておくと、他のコードから関数を再利用する際にも安全です。 + +**TypeScript特有の考慮点** + +- この関数は「`number` 型の `rowIndex` を受け取り、`number[]` を返す」という単一の型シグネチャで完結しています。そのため、ジェネリクス(=型を後から自由に差し込める仕組み。`Array` の `T` 部分がその例)を導入する必要はありません。ジェネリクスは「複数の異なる型に対して同じロジックを再利用したいとき」に真価を発揮する仕組みで、ここでは扱う型が最初から `number` に固定されているため、無理に使うとコードが複雑になるだけで恩恵がありません。 +- `strict mode`(`strictNullChecks`)単体では `noUncheckedIndexedAccess` オプションは有効化されず、配列のインデックス読み取りが `number | undefined` と推論されるのは後者を明示的に有効にした場合のみです。本実装における配列アクセスの安全性は、`strict mode` だけによる保証ではなく、`Array(rowIndex + 1).fill(0)` による適切な初期化とループ境界条件の厳密な管理によって確保されています。 + +> 📖 このセクションで登場した用語 +> +> - **型ガード**:実行時に値の型や範囲をチェックし、想定外の値によるクラッシュを防ぐ処理 +> - **ジェネリクス**:型を後から自由に差し込める仕組み。同じロジックを複数の型で使い回したいときに使う +> - **strict mode**:TypeScriptコンパイラの厳格な型チェックを有効にする設定 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ「パスカルの三角形の1行を求める」という目的でも、計算の組み立て方によって速さ(時間計算量)とメモリの使い方(空間計算量)が変わります。ここでは3つの方法を比べます。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | +| ----------------------------------------------- | ---------- | --------------------------- | ------------ | -------- | ------ | +| A. 前の行から毎回新しい行を配列で生成 | O(n²) | O(n²)(各行を保持する場合) | 低 | 高 | 高 | +| B. 1本の配列を右から左へ更新(in-place) | O(n²) | O(n) | 中 | 高 | 中 | +| C. 二項係数の漸化式で同じ行の中を左から右へ計算 | **O(n)** | O(n) | 低 | 高 | 中〜高 | + +- `n = rowIndex` としています。 +- `O(n²)`(=入力が2倍になると処理は約4倍になること)は、行を1つずつ積み上げるために二重ループが必要になるためです。 +- `O(n)`(=入力が2倍になると処理も約2倍になること)は、方法Cが「同じ行の中だけ」で完結する漸化式を使い、行を積み上げる必要がないためです。 + +> 📖 このセクションで登場した用語 +> +> - **in-place(インプレース)**:新しい配列を作らず、既存の配列を直接書き換える処理方式 +> - **漸化式**:ある項の値を、1つ前の項の値から計算するための式 + +--- + +## 3. 選択したアルゴリズムと理由 + +**選択したアプローチ:方法C(二項係数の漸化式)** + +方法A・Bは「三角形の定義通り、上の行から下の行を作る」という直感的な発想ですが、どちらも二重ループが必要でO(n²)になります。一方、パスカルの三角形の各値は二項係数 `C(n, k)`(=n個からk個を選ぶ組み合わせの数)そのものであり、次の関係が成り立ちます。 + +``` +C(n, k) = C(n, k-1) × (n - k + 1) ÷ k +``` + +この式を使えば、**同じ行の中だけ**を左から右に1回なぞるだけで全ての値が求まるため、行を積み上げる必要がなくなり計算量がO(n)まで下がります。「二重ループが必要だから選ばなかった」方法A・Bに対して、方法Cは一重ループで済むという点が決定的な違いです。 + +**TypeScript特有の最適化ポイント** + +- `Number.isInteger` による型ガードで、コンパイル時の `number` 型チェックだけではすり抜けてしまう「小数」や `NaN` の混入を実行時にも防ぎます。 +- 出力先の配列を `new Array(rowIndex + 1)` と型引数付きで初期化することで、後続の代入時にコンパイラが型の不一致を検出できます。 + +ここで整数演算の精度について1点補足します。二項係数は理論上必ず整数になりますが、計算順序を誤ると浮動小数点の誤差が出る可能性があります。**「先に掛け算、後で割り算」**という順序を守ることで、割り算の時点で必ず割り切れることが保証されるため、誤差は発生しません(`rowIndex <= 33` の範囲では、掛け算の中間結果も `Number.MAX_SAFE_INTEGER`(2^53−1)を大きく下回るため、桁あふれの心配もありません)。 + +> 📖 このセクションで登場した用語 +> +> - **二項係数**:n個の中からk個を選ぶ組み合わせの数。`C(n, k)` と表記する +> - **Number.MAX_SAFE_INTEGER**:JavaScriptのnumber型が誤差なく表現できる整数の上限(2^53−1) + +![alt text](image.png) + +この漸化式の動きを図で見てみましょう。`rowIndex = 4` の場合、`row[0] = 1` から出発し、1つ左の値に「掛け算してから割り算する」という操作を繰り返すことで `[1, 4, 6, 4, 1]` が同じ行の中だけで完成していきます。図の通り、行の中を左から右へなぞるだけで三角形の各行が組み立てられます。行を積み上げる必要がないため、ループは1重で済みます。 + +--- + +## 4. 実装コード + +**コードの大まかな構造(骨格)** + +1. `rowIndex` が整数かどうかを検証する(型ガード) +2. `rowIndex` が問題の制約範囲内(0〜33)かどうかを検証する +3. 結果を格納する配列を `rowIndex + 1` 個分だけ確保し、先頭を `1` にする +4. 二項係数の漸化式を使い、`k = 1` から `rowIndex` まで1つずつ値を計算していく +5. 完成した配列を返す + +```typescript +/** + * パスカルの三角形の rowIndex 行目(0-indexed)を返す + * @param rowIndex - 求めたい行番号(0 <= rowIndex <= 33) + * @returns rowIndex 行目の数値配列 + * @throws {TypeError} rowIndex が整数でない場合 + * @throws {RangeError} rowIndex が 0〜33 の範囲外の場合 + * @complexity Time: O(rowIndex), Space: O(rowIndex)(出力用配列のみ、追加の一時配列は使わない) + */ +function getRow(rowIndex: number): number[] { + // 型ガード:整数であることを実行時に確認する(number型だけでは + // 「3.5」のような小数やNaNが渡ってきた場合を防げないため) + if (!Number.isInteger(rowIndex)) { + throw new TypeError('rowIndex must be an integer'); + } + + // 問題の制約(0 <= rowIndex <= 33)を実行時にも検証しておく。 + // LeetCodeの採点環境は制約を守った値しか送らないが、 + // 関数を他の場所から再利用する可能性を考えて防御的に書いておく + if (rowIndex < 0 || rowIndex > 33) { + throw new RangeError('rowIndex must be between 0 and 33'); + } + + // 結果を格納する配列を先に確保する。 + // フォローアップの「O(rowIndex)の追加空間」を満たすため、 + // 過去の行を丸ごと保持する二次元配列は作らず、この1本の配列だけを使い回す + const row: number[] = new Array(rowIndex + 1).fill(0); + + // パスカルの三角形の左端は、どの行でも必ず1になる + // (n個からk=0個を選ぶ組み合わせは、常に「何も選ばない」の1通りしかないため) + row[0] = 1; + + // 二項係数の漸化式 C(n, k) = C(n, k-1) × (n - k + 1) ÷ k を使い、 + // 1つ左の値から次の値を順番に計算していく(行を積み上げず、同じ行の中だけで完結する) + for (let k = 1; k <= rowIndex; k++) { + // 掛け算を先に行ってから割り算をする。 + // 二項係数は必ず整数になる値なので、この順序で計算すれば + // 途中で割り切れずに誤差が発生することはない + row[k] = (row[k - 1] * (rowIndex - k + 1)) / k; + } + + // 完成した行を返す + return row; +} +``` + +**動作トレース**(`rowIndex = 4` の場合) + +``` +初期状態: rowIndex = 4 +Step 0: Number.isInteger(4) → true、0 <= 4 <= 33 → true(検証通過) +Step 1: row = [0, 0, 0, 0, 0] を確保 → row[0] = 1 → row = [1, 0, 0, 0, 0] +Step 2: k=1 → row[1] = row[0] × (4-1+1) ÷ 1 = 1 × 4 ÷ 1 = 4 → row = [1, 4, 0, 0, 0] +Step 3: k=2 → row[2] = row[1] × (4-2+1) ÷ 2 = 4 × 3 ÷ 2 = 6 → row = [1, 4, 6, 0, 0] +Step 4: k=3 → row[3] = row[2] × (4-3+1) ÷ 3 = 6 × 2 ÷ 3 = 4 → row = [1, 4, 6, 4, 0] +Step 5: k=4 → row[4] = row[3] × (4-4+1) ÷ 4 = 4 × 1 ÷ 4 = 1 → row = [1, 4, 6, 4, 1] +最終結果: [1, 4, 6, 4, 1] +``` + +問題文の例(`rowIndex = 3`)でも同じロジックで確認できます。`row = [1, 0, 0, 0]` から出発し、`k=1: 1×3÷1=3` → `k=2: 3×2÷2=3` → `k=3: 3×1÷3=1` となり、`[1, 3, 3, 1]` が得られます。これは問題文のExample 1の出力と一致します。 + +**業務版と競技版の使い分け** + +``` +【業務開発版】上記コードそのまま。型ガードとRangeErrorによる入力検証を残し、 + 関数が他の場所から再利用されても壊れないようにする。 +【競技版(LeetCode提出用)】制約が保証されているため、Number.isIntegerと + RangeErrorのチェックを省略し、row配列の確保とforループだけに + 絞ってもよい。行数(33行)程度なら性能差はほぼ出ないため、 + このままの型安全なバージョンで提出して問題ない。 +``` + +> 📖 このセクションで登場した用語 +> +> - **readonly**:変更できないようにする修飾子。今回は戻り値・引数を単純な `number[]` としているため使っていないが、外部に渡した配列を呼び出し側が誤って書き換えるのを防ぎたい場合は `readonly number[]` として返す設計も選択肢になる +> - **TypeError / RangeError**:エラーの種類。`TypeError` は型が不正な場合、`RangeError` は値の範囲が不正な場合に使う組み込みのエラークラス + +--- + +## 5. TypeScript固有の最適化観点 + +- **型推論の活用**:`const row: number[] = ...` のように型注釈を最小限にとどめ、ループ内の代入では型推論(=明示しなくてもTypeScriptが自動で型を判断してくれる機能)に任せることで、コードの見通しを保っています。 +- **ジェネリクスを使わない判断もTS特有の最適化**:この問題は入力・出力ともに `number` 型に固定されているため、ジェネリクスを導入すると型パラメータの解決コストがコンパイル時に増えるだけで実行時の恩恵がありません。「型が1種類しかない場面ではジェネリクスを使わない」という判断自体が、TypeScriptにおけるコンパイル時最適化の一つです。 +- **コンパイル時エラー防止**:`new Array(rowIndex + 1)` と型引数を明示することで、後続の `row[k] = ...` が `number` 以外の値を代入しようとした場合にコンパイル時にエラーとして検出できます。 +- **strict modeとの相性**:`strictNullChecks` が有効な環境でも、`row` は `number[]` として一貫して扱われるため、`undefined` が混入する余地がなく、null安全性(=`null`や`undefined`による予期しないクラッシュを防ぐ仕組み)が保たれます。 + +> 📖 このセクションで登場した用語 +> +> - **型推論**:型を明示しなくてもTypeScriptが自動で型を判断してくれる機能 +> - **strictNullChecks**:`null`や`undefined`を明示的に扱わないとコンパイルエラーになる設定。null安全性を高めるための機能の一つ diff --git a/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README.md b/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README.md new file mode 100644 index 00000000..65431a48 --- /dev/null +++ b/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README.md @@ -0,0 +1,375 @@ +# Pascal's Triangle II - 二項係数の漸化式で1行だけを効率よく求める + +## 目次 + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +> 💡 **初学者向け補足**:この問題は、一言で言うと「**パスカルの三角形の、指定された行だけを無駄なく作る問題**」です。 + +パスカルの三角形は、各行の両端が常に `1` で、それ以外の値は「1つ上の行の隣り合う2つの数の和」になっている数の並びです。今回の問題では、三角形全体を作る必要はなく、`rowIndex` 番目(0-indexed=0から数え始める番号付け)の行だけを返せばよいという点がポイントです。 + +この問題が難しく感じられるとしたら、それは「三角形全体を作ってから該当行を取り出す」という発想に引っ張られてしまうためです。実際には、二項係数(=n個からk個を選ぶ組み合わせの数)という数学的な性質を使うことで、**行を積み上げずに、同じ行の中だけで完結する計算**に置き換えられます。これによりフォローアップ条件である「O(rowIndex) の追加空間」も自然に満たせます。 + +制約は `0 <= rowIndex <= 33` と小さく、正当性(=アルゴリズムが常に正しい答えを返すことの保証)を保ったまま最速のアプローチを選べる余地があります。 + +> 📖 **この章で登場した用語** +> +> - **0-indexed**:数え始めを0からとする番号付け方式。`rowIndex = 0` は「1行目」ではなく「0番目の行」を指す +> - **二項係数**:n個の中からk個を選ぶ組み合わせの数。`C(n, k)` と表記する +> - **制約**:入力として与えられる値の範囲や条件のこと。今回は `0 <= rowIndex <= 33` +> - **正当性**:アルゴリズムが常に正しい答えを返すことの保証 + +--- + +

アルゴリズム要点(TL;DR)

+ +> 💡 **初学者向け補足**:TL;DR(Too Long; Didn't Read=長くて読めない人向けの要約)とは、詳細を読む前に「なんとなくこういう手順で解くんだな」というイメージを掴むための章です。 + +- **戦略**:三角形を上から積み上げず、二項係数の漸化式 `C(n, k) = C(n, k-1) * (n - k + 1) / k` を使い、**同じ行の中を左から右へ**計算していく。行を積み上げる必要がないため、ループが1重で済む。 +- **データ構造**:単純な `list[int]` のみを使う。両端からの追加・削除は発生しないため、`deque`(両端が開いた箱のような構造)のような特殊な構造は不要。 +- **計算量**:時間 `O(rowIndex)`、空間 `O(rowIndex)`(出力用のリスト以外に追加のメモリを使わない)。 +- **Python特有の工夫**:Pythonの `/` は常に `float` を返してしまうため、割り切れることが保証されているこの計算では `//`(フロア除算=小数点以下を切り捨てて整数を返す演算子)を使う。 + +> 📖 **この章で登場した用語** +> +> - **TL;DR**:「長くて読めない人向けの要約」を意味する略語 +> - **漸化式**:ある項の値を、1つ前の項の値から計算するための式 +> - **フロア除算**:Pythonの `//` 演算子。小数点以下を切り捨てて `int` を返す + +--- + +

図解

+ +> 💡 **初学者向け補足**:Mermaidのフローチャートでは、ひし形(`{}`)は条件分岐を、長方形(`[]`)は処理ステップを表します。矢印を上から下へ、あるいは左から右へたどることで、処理の流れを追うことができます。 + +### フローチャート + +この図は `getRow` 関数の処理の流れを表しています。上から下へ読み進めてください。 + +```mermaid +flowchart TD + Start[Start getRow] + Validate{Is rowIndex valid} + Err[Raise TypeError or ValueError] + Init[Create row list of zeros sized rowIndex plus one] + Base[Set row zero to one] + Loop{Any k left from one to rowIndex} + Compute[Compute rowk from rowk minus one times n minus k plus one divided by k] + Ret[Return row] + Start --> Validate + Validate -- No --> Err + Validate -- Yes --> Init + Init --> Base + Base --> Loop + Loop -- Yes --> Compute + Compute --> Loop + Loop -- No --> Ret +``` + +主要なノードの意味: + +- `Start[Start getRow]`:関数の入り口。引数 `rowIndex` を受け取る +- `Validate{Is rowIndex valid}`:入力値が型・範囲の制約を満たすかを判定する条件分岐 +- `Init[Create row list of zeros sized rowIndex plus one]`:結果を格納するリストを、必要なサイズ分だけ最初に確保するステップ +- `Loop{Any k left from one to rowIndex}`:`k = 1` から `rowIndex` まで処理が残っているかを判定する条件分岐 +- `Compute[Compute rowk ...]`:二項係数の漸化式で、1つ前の値から次の値を計算するステップ + +### データフロー図 + +この図は、入力の `rowIndex` がどのように変換されて最終的な出力になるかを表しています。 + +```mermaid +graph LR + subgraph Precheck + A[Input rowIndex] + A --> B[Validate type and range] + end + subgraph Core + B --> C[Allocate row list] + C --> D[Apply binomial recurrence loop] + D --> E[Row fully computed] + end + E --> F[Output row] +``` + +主要な流れの説明: + +- `Input rowIndex` から `Validate type and range` へ:型が `int` か、値が `0` 〜 `33` の範囲内かを確認する +- `Allocate row list` から `Apply binomial recurrence loop` へ:確保したリストに対して、1つ前の値から次の値を順に埋めていく +- `Row fully computed` から `Output row` へ:全ての値が埋まったリストをそのまま返す + +> 💡 **代表例でのトレース**:`rowIndex = 4` を入力として、上記フローチャートの各ノードをどのように通過するかを示します。 + +``` +Step 1: Start → rowIndex = 4 +Step 2: Validate → isinstance(4, int)かつboolではない、0<=4<=33 → Yes +Step 3: Init → row = [0, 0, 0, 0, 0] +Step 4: Base → row[0] = 1 → row = [1, 0, 0, 0, 0] +Step 5: Loop(k=1) → Yes → Compute → row[1] = 1*4//1 = 4 → row = [1, 4, 0, 0, 0] +Step 6: Loop(k=2) → Yes → Compute → row[2] = 4*3//2 = 6 → row = [1, 4, 6, 0, 0] +Step 7: Loop(k=3) → Yes → Compute → row[3] = 6*2//3 = 4 → row = [1, 4, 6, 4, 0] +Step 8: Loop(k=4) → Yes → Compute → row[4] = 4*1//4 = 1 → row = [1, 4, 6, 4, 1] +Step 9: Loop(k=5) → No(5 > rowIndex) → Ret +結果: [1, 4, 6, 4, 1] +``` + +> 📖 **この章で登場した用語** +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理 +> - **データフロー図**:データがどのように変換・移動するかを示す図 +> - **サブグラフ**:フローチャートの中で関連する処理をグループ化したもの + +--- + +

正しさのスケッチ

+ +> 💡 **初学者向け補足**:「正しさのスケッチ」とは、アルゴリズムが常に正しい答えを返すことの根拠を整理したものです。数学的な証明ではなく「なぜ正しいと言えるか」の説明です。 + +- **不変条件(=処理中ずっと成り立ち続けるべき条件)**:ループの `k` 回目が終わった時点で、`row[k]` は常に `C(rowIndex, k)`(=rowIndex個からk個を選ぶ組み合わせの数)と等しくなっている。これは漸化式 `C(n, k) = C(n, k-1) * (n - k + 1) / k` が数学的に成り立つ関係式であり、`row[k-1]` が既に正しい値であればその値から計算される `row[k]` も必ず正しくなるためです。 +- **基底条件(=再帰やループの出発点となる条件)**:`row[0] = 1` は「rowIndex個から0個を選ぶ組み合わせは、何も選ばないという1通りしかない」という事実に対応しており、これは直感的にも正しいことが確認できます。 +- **網羅性(=すべてのケースをもれなく処理できているという保証)**:ループは `k = 1` から `rowIndex` まで1つも飛ばさずに実行されるため、行のすべての要素(`row[0]` から `row[rowIndex]` まで)が過不足なく計算されます。 +- **終了性(=アルゴリズムが必ず有限ステップで終わるという保証)**:ループの範囲は `range(1, rowIndex + 1)` という有限の範囲であり、`rowIndex` は制約により最大でも `33` なので、必ず有限回で終了します。 + +> 📖 **この章で登場した用語** +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +> - **基底条件**:再帰やループの出発点となる条件。これがないと処理が始められない +> - **網羅性**:すべてのケースをもれなく処理できているという保証 +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 + +--- + +

計算量

+ +> 💡 **初学者向け補足**:計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。 +> +> | 記法 | 意味 | 直感的なイメージ | +> | --- | --- | --- | +> | `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +> | `O(n)` | 入力に比例して増加 | リストを端から順に読む | +> | `O(n log n)` | nよりやや速く増加 | 辞書を二分探索で引く×n回 | +> | `O(n²)` | 入力の2乗で増加 | 全ペアを総当たりで確認する | + +- **時間計算量**:`O(rowIndex)`。ループが `rowIndex` 回だけ実行され、各回の計算は定数時間(掛け算1回・割り算1回)で終わるため。 +- **空間計算量**:`O(rowIndex)`。出力用のリスト `row` 以外に、追加の一時配列や二次元リストを作らないため、フォローアップの条件をそのまま満たす。 + +| アプローチ | 時間計算量 | 空間計算量 | 備考 | +| --- | --- | --- | --- | +| 三角形全体を二次元リストで構築 | O(rowIndex²) | O(rowIndex²) | 直感的だが不要な行まで保持してしまう | +| 1本のリストを右から左へ更新 | O(rowIndex²) | O(rowIndex) | in-place(=新しいメモリを確保せず元のデータを直接書き換える操作)だが二重ループが必要 | +| 二項係数の漸化式(本実装) | O(rowIndex) | O(rowIndex) | 同じ行の中だけで完結するため最速 | + +> 📖 **この章で登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **in-place**:新しいメモリを確保せず元のデータを直接書き換える操作。空間計算量を抑えられる + +--- + +

Python 実装

+ +> 💡 **初学者向け補足**:コードの全体的な骨格は以下の通りです。 +> +> 1. `from __future__ import annotations` で型ヒントの前方参照を有効にする +> 2. `rowIndex` の型と範囲を実行時にも検証する(pylanceの静的チェックだけでは実行時の誤りは防げないため) +> 3. 結果を格納するリストを先に確保し、先頭を `1` にする +> 4. 二項係数の漸化式を使い、`k = 1` から `rowIndex` まで1つずつ値を計算する +> 5. 完成したリストを返す +> +> 型ヒント `List[int]` は「intのリスト」を意味します。Python 3.9以降は組み込みの `list[int]` と書くこともできますが、ここでは `typing.List` を明示的にimportして使います。 + +```python +from __future__ import annotations + +from typing import List + + +class Solution: + """ + 119. Pascal's Triangle II を解決するクラス + + パスカルの三角形の rowIndex 行目を、二項係数の漸化式を使って + O(rowIndex) 時間・O(rowIndex) 追加空間で求める。 + """ + + def getRow(self, rowIndex: int) -> List[int]: + """ + パスカルの三角形の rowIndex 行目(0-indexed)を返す + + Args: + rowIndex: 求めたい行番号(0 <= rowIndex <= 33) + + Returns: + rowIndex 行目の値を格納したリスト + + Raises: + TypeError: rowIndex が int 型でない場合 + ValueError: rowIndex が 0〜33 の範囲外の場合 + """ + # 型ヒントは実行時には無視される。呼び出し元が誤った型を渡した場合に + # 実行時までエラーに気づけないため、isinstance() で別途検証する。 + self._validate_input(rowIndex) + + # 結果を格納するリストを先に確保する。 + # フォローアップの「O(rowIndex)の追加空間」を満たすため、 + # 過去の行を丸ごと保持する二次元リストは作らず、この1本のリストだけを使い回す。 + row: List[int] = [0] * (rowIndex + 1) + + # パスカルの三角形の左端は、どの行でも必ず1になる + # (rowIndex個からk=0個を選ぶ組み合わせは「何も選ばない」の1通りしかないため) + row[0] = 1 + + # 二項係数の漸化式 C(n, k) = C(n, k-1) * (n - k + 1) // k を使い、 + # 1つ左の値から次の値を順番に計算していく(同じ行の中だけで完結する) + for k in range(1, rowIndex + 1): + # 掛け算を先に行ってから割り算をする。 + # 「//」(フロア除算)を使うのは、Pythonの「/」が常にfloatを返してしまい + # List[int]という戻り値の型と食い違ってしまうため。 + # 二項係数は必ず整数になる値なので、掛け算を先に行えば割り切れずに誤差が出ることはない。 + row[k] = row[k - 1] * (rowIndex - k + 1) // k + + return row + + def _validate_input(self, row_index: int) -> None: + """入力値が問題の制約を満たすかを検証する""" + # isinstance() で型チェックする。 + # bool は int のサブクラスなので、isinstance(True, int) は True になってしまう。 + # そのため True/False が誤って渡されるケースも別途弾いておく。 + if not isinstance(row_index, int) or isinstance(row_index, bool): + raise TypeError("rowIndex must be an int") + + if not (0 <= row_index <= 33): + raise ValueError("rowIndex must be between 0 and 33") +``` + +> 💡 **コードの動作トレース**(`rowIndex = 3` の場合。問題文のExample 1に対応) +> +> ``` +> 呼び出し: Solution().getRow(3) +> Step 1: _validate_input(3) → isinstance(3, int)かつboolではない → OK、0<=3<=33 → OK +> Step 2: row = [0, 0, 0, 0] を確保 → row[0] = 1 → row = [1, 0, 0, 0] +> Step 3: k=1 → row[1] = row[0] * (3-1+1) // 1 = 1 * 3 // 1 = 3 → row = [1, 3, 0, 0] +> Step 4: k=2 → row[2] = row[1] * (3-2+1) // 2 = 3 * 2 // 2 = 3 → row = [1, 3, 3, 0] +> Step 5: k=3 → row[3] = row[2] * (3-3+1) // 3 = 3 * 1 // 3 = 1 → row = [1, 3, 3, 1] +> 最終結果: [1, 3, 3, 1] +> ``` +> +> この結果は問題文のExample 1の出力 `[1,3,3,1]` と一致します。 + +> 📖 **この章で登場した用語** +> +> - **`from __future__ import annotations`**:型ヒントを文字列として遅延評価する宣言。前方参照(まだ定義されていないクラス名を型ヒントで使うこと)を解決できる +> - **`List[int]`**:「intのリスト」を表す型ヒント +> - **`_validate_input`のようなアンダースコア始まりの名前**:Pythonの慣習で「クラス内部だけで使う想定のメソッド」を表す印 + +--- + +

CPython最適化ポイント

+ +> 💡 **初学者向け補足**:この章では「同じ処理でもPythonの書き方によって速さが変わる理由」を、最適化前のコード→最適化後のコード→なぜ速くなるか、の3点セットで説明します。 + +**最適化1:リストの初期化方法** + +```python +# 最適化前:forループでappendしながらリストを作る +row = [] +for _ in range(rowIndex + 1): + row.append(0) + +# 最適化後:乗算で一気に確保する +row = [0] * (rowIndex + 1) +``` + +なぜ速いか:最適化前は `append` の呼び出しのたびにPythonレベルの関数呼び出しと、リストの内部バッファが足りなくなるたびに発生する再確保処理が繰り返し走ります。最適化後の `[0] * n` はCPython内部でC言語レベルの単純なメモリ確保1回に置き換わるため、`n` 回のPython関数呼び出しが発生しません。 + +**最適化2:各値の計算方法** + +```python +# 最適化前:math.combを各kごとに個別に呼び出す +import math +row = [math.comb(rowIndex, k) for k in range(rowIndex + 1)] + +# 最適化後:1つ前の値から漸化式で計算する(本実装の方法) +row: List[int] = [0] * (rowIndex + 1) +row[0] = 1 +for k in range(1, rowIndex + 1): + row[k] = row[k - 1] * (rowIndex - k + 1) // k +``` + +なぜ速いか:`math.comb(n, k)` はC実装(=Python言語ではなくC言語で書かれた関数。Pure Pythonより高速)ではあるものの、呼び出しのたびに `min(k, n-k)` 回程度の掛け算を内部で行うため、`k = 0` から `rowIndex` まで全部呼び出すと合計で `O(rowIndex²)` 相当の計算になります。一方、漸化式を使う方法は「1つ前の結果」を再利用するため、各ステップが定数時間で済み、合計でも `O(rowIndex)` にとどまります。`rowIndex <= 33` という小さな制約では体感できる差ではありませんが、計算量の観点では漸化式方式が本質的に優れています。 + +> 📖 **この章で登場した用語** +> +> - **C実装**:Pythonコードではなく、内部でC言語で実装された関数。Pure Pythonより大幅に高速 +> - **再確保処理**:リストの内部バッファが不足したときに、より大きなメモリ領域を確保し直す処理 + +--- + +

エッジケースと検証観点

+ +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。 + +- **`rowIndex = 0`**:ループが0回実行され、`row = [1]` がそのまま返る。特別な分岐を書かなくても自然に正しい結果になる。もし特別扱いのコードを書いてしまうと、その分岐だけ別のバグを持ち込むリスクがあるため、分岐なしで済むこの実装は望ましい形です。 +- **`rowIndex = 33`(最大値)**:`C(33, 16) ≈ 1.17 × 10&sup9;` 程度の値になるが、CPythonの `int` は多倍長整数(=桁数に応じて自動的にメモリを拡張できる整数表現)のため、オーバーフロー(桁あふれ)の心配がない。 +- **型が不正な入力(例:`"3"` や `3.5`)**:`_validate_input` の `isinstance()` チェックにより `TypeError` が送出される。文字列や小数がそのまま `range()` に渡ると、実行時に分かりにくいエラーメッセージが出てしまうため、事前に明確なエラーを出す方が保守性が高い。 +- **`True` / `False` が渡された場合**:Pythonでは `bool` が `int` のサブクラスであるため、`isinstance(True, int)` は `True` になってしまう。この問題の性質上 `rowIndex` が真偽値であることは意味を持たないため、明示的に弾いている。 + +> 📖 **この章で登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件の入力 +> - **境界値**:制約の上限・下限にあたる値。今回は `rowIndex = 0` と `rowIndex = 33` +> - **多倍長整数**:桁数に応じて自動的にメモリを拡張できる整数表現。CPythonの `int` はこの仕組みを持つ + +--- + +

FAQ

+ +**Q1. なぜ三角形全体を作らずに1行だけを計算するのですか?別の方法ではダメなのですか?** + +結論:三角形全体を作ると不要なメモリと時間を使ってしまうため、1行だけを計算する方法を選びました。 + +理由:三角形全体を二次元リストで保持する方法は `O(rowIndex²)` の空間を必要としますが、この問題が求めているのは最後の1行だけです。フォローアップ条件の「`O(rowIndex)` の追加空間」を満たすには、不要な行を保持しない設計が必須です。 + +補足:二項係数の漸化式を使えば、同じ行の中だけで値を計算できるため、そもそも「前の行」という概念自体が不要になります。 + +**Q2. なぜ `/` ではなく `//` を使っているのですか?`/` を使うとどうなりますか?** + +結論:`/` を使うと戻り値の型が `float` になってしまい、`List[int]` という型ヒントと矛盾するため `//` を使っています。 + +理由:Pythonの `/` は「真の除算」と呼ばれ、割り切れる場合でも常に `float` を返します(例:`6 / 2` は `3.0`)。今回の計算は数学的に必ず割り切れることが保証されていますが、型としては `float` になってしまいます。 + +補足:`//` は「フロア除算」と呼ばれ、小数点以下を切り捨てて `int` を返します。掛け算を先に行ってから割り算をするという計算順序を守っている限り、切り捨てによる誤差は発生しません。 + +**Q3. `math.comb` を使えばもっと簡単に書けるのに、なぜ漸化式を使うのですか?** + +結論:コードの短さでは `math.comb` を使った1行のリスト内包表記も魅力的ですが、計算量の観点では漸化式の方が優れているため、こちらを採用しました。 + +理由:`math.comb(n, k)` を `k = 0` から `rowIndex` まで毎回個別に呼び出すと、合計で `O(rowIndex²)` 相当の計算になります。一方、漸化式は1つ前の結果を再利用するため `O(rowIndex)` で済みます。 + +補足:`rowIndex <= 33` という小さな制約下では体感できる差はほとんどありませんが、より大きな `n` を扱う場面を想定した場合、漸化式の方が本質的にスケールする実装です。 + +**Q4. `isinstance(row_index, bool)` のチェックのイメージがつかめません。もう少し具体的に教えてください。** + +結論:Pythonでは `bool` 型が `int` 型のサブクラス(=派生した型)として定義されているため、`True` や `False` が誤って数値として扱われてしまうことを防ぐためのチェックです。 + +理由:`True == 1` および `False == 0` が成立し、`isinstance(True, int)` も `True` を返します。つまり型チェックだけでは `True` を「有効な `int`」として通してしまいます。 + +補足:例えば呼び出し元が誤って `getRow(True)` のように呼び出してしまった場合、このチェックがなければ `rowIndex = 1` として扱われてしまい、意図しない挙動につながります。明示的に弾くことで、こうした呼び出しミスを早期に検出できます。 + +> 📖 **この章で登場した用語** +> +> - **FAQ**:Frequently Asked Questions の略。よくある質問と回答のこと +> - **サブクラス**:あるクラスを継承して作られた、より特化したクラス。`bool` は `int` のサブクラス diff --git a/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README_React.html b/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README_React.html new file mode 100644 index 00000000..f1f1a306 --- /dev/null +++ b/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README_React.html @@ -0,0 +1,1412 @@ + + + + + + LeetCode 119: Pascal's Triangle II - 二項係数の漸化式で1行を求める + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「パスカルの三角形の、指定された行だけを無駄なく作る問題」です。 +

+

+ パスカルの三角形は、各行の両端が常に + 1 で、 + それ以外の値は「1つ上の行の隣り合う2つの数の和」になっている数の並びです。 + 今回の問題では三角形全体を作る必要はなく、rowIndex + 番目(0-indexed=0から数え始める番号付け)の行だけを返せばよい、という点がポイントです。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + 三角形を上から1行ずつ積み上げる素直な方法は、二重ループが必要になり計算量が + O(rowIndex²) になってしまいます。 +
  • +
  • + フォローアップ条件「O(rowIndex) + の追加空間のみ」を満たすには、過去の行を丸ごと保持する二次元配列を作らない工夫が必要です。 +
  • +
+
+ +
+
+
+ O(rowIndex) +
+
時間計算量
+
+
+
+ O(rowIndex) +
+
追加空間計算量
+
+
+
+ binomial_recurrence +
+
採用アルゴリズム
+
+
+
+ list[int] +
+
使用データ構造
+
+
+ +
+

📥 入出力例

+
+
+
Example 1
+
rowIndex = 3
+
Output: [1,3,3,1]
+

+ 3行目(0-indexed)の両端は1、内側は1つ上の行の隣接する値の和になるため + [1,3,3,1] が正解です。 +

+
+
+
Example 2
+
rowIndex = 0
+
Output: [1]
+

+ 0行目は三角形の頂点そのものなので、要素数1の [1] が正解です。 +

+
+
+
Example 3
+
rowIndex = 1
+
Output: [1,1]
+

+ 1行目は両端の1だけで構成されるため [1,1] が正解です。 +

+
+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 代表例 + rowIndex = 4 + を使って、 配列 + row + がどのように埋まっていくかを1ステップずつ追跡します。 +

+
+
+ + +
+

+ Python実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. rowIndex の型と範囲を検証する(型ガード)
  2. +
  3. 結果を格納するリストを rowIndex + 1 個分だけ確保する
  4. +
  5. + 先頭を1に設定し、二項係数の漸化式で k=1 から rowIndex まで値を埋めていく +
  6. +
  7. 完成したリストを返す
  8. +
+
+ +
from __future__ import annotations
+
+from typing import List
+
+
+class Solution:
+    """
+    119. Pascal's Triangle II を解決するクラス
+
+    パスカルの三角形の rowIndex 行目を、二項係数の漸化式を使って
+    O(rowIndex) 時間・O(rowIndex) 追加空間で求める。
+    """
+
+    def getRow(self, rowIndex: int) -> List[int]:
+        """
+        パスカルの三角形の rowIndex 行目(0-indexed)を返す
+
+        Args:
+            rowIndex: 求めたい行番号(0 <= rowIndex <= 33)
+
+        Returns:
+            rowIndex 行目の値を格納したリスト
+
+        Raises:
+            TypeError: rowIndex が int 型でない場合
+            ValueError: rowIndex が 0〜33 の範囲外の場合
+        """
+        # 型ヒントは実行時には無視される。呼び出し元が誤った型を渡した場合に
+        # 実行時までエラーに気づけないため、isinstance() で別途検証する。
+        self._validate_input(rowIndex)
+
+        # 結果を格納するリストを先に確保する。
+        # フォローアップの「O(rowIndex)の追加空間」を満たすため、
+        # 過去の行を丸ごと保持する二次元リストは作らず、この1本のリストだけを使い回す。
+        row: List[int] = [0] * (rowIndex + 1)
+
+        # パスカルの三角形の左端は、どの行でも必ず1になる
+        # (rowIndex個からk=0個を選ぶ組み合わせは「何も選ばない」の1通りしかないため)
+        row[0] = 1
+
+        # 二項係数の漸化式 C(n, k) = C(n, k-1) * (n - k + 1) // k を使い、
+        # 1つ左の値から次の値を順番に計算していく(同じ行の中だけで完結する)
+        for k in range(1, rowIndex + 1):
+            # 掛け算を先に行ってから割り算をする。
+            # 「//」(フロア除算)を使うのは、Pythonの「/」が常にfloatを返してしまい
+            # List[int]という戻り値の型と食い違ってしまうため。
+            # 二項係数は必ず整数になる値なので、掛け算を先に行えば割り切れずに誤差が出ることはない。
+            row[k] = row[k - 1] * (rowIndex - k + 1) // k
+
+        return row
+
+    def _validate_input(self, row_index: int) -> None:
+        """入力値が問題の制約を満たすかを検証する"""
+        # isinstance() で型チェックする。
+        # bool は int のサブクラスなので、isinstance(True, int) は True になってしまう。
+        # そのため True/False が誤って渡されるケースも別途弾いておく。
+        if not isinstance(row_index, int) or isinstance(row_index, bool):
+            raise TypeError("rowIndex must be an int")
+
+        if not (0 <= row_index <= 33):
+            raise ValueError("rowIndex must be between 0 and 33")
+
+ +
+

+ ▶ 入力例 rowIndex = 3 での動作トレース +

+
+呼び出し: Solution().getRow(3)
+Step 1: _validate_input(3) → isinstance(3, int)かつboolではない → OK、0<=3<=33 → OK
+Step 2: row = [0, 0, 0, 0] を確保 → row[0] = 1 → row = [1, 0, 0, 0]
+Step 3: k=1 → row[1] = row[0] * (3-1+1) // 1 = 1 * 3 // 1 = 3 → row = [1, 3, 0, 0]
+Step 4: k=2 → row[2] = row[1] * (3-2+1) // 2 = 3 * 2 // 2 = 3 → row = [1, 3, 3, 0]
+Step 5: k=3 → row[3] = row[2] * (3-3+1) // 3 = 3 * 1 // 3 = 1 → row = [1, 3, 3, 1]
+最終結果: [1, 3, 3, 1](Example 1の出力と一致)
+
+
+ + +
+

+ 処理フローチャート +

+ +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=はい・正常フロー + 赤=エラー +
+
+

+ 紫の矢印は「同じ行の中で次の k へ戻る」ループを表します。 +

+
+ +
+ + + + + + + + + + + + + + + + + + + + 開始 + + + getRow(rowIndex) を呼び出す + + + + + + + + rowIndexは正しいか + + + int型かつ 0以上33以下か + + + + + + いいえ + + + + エラーを送出 + + + TypeError / + + + ValueError + + + + + + はい + + + + + + 配列を確保する + + + row = [0] × (rowIndex + 1) + + + + + + + + 基底値を設定する + + + row[0] = 1 + + + + + + + + kはrowIndex以下か + + + k = 1, 2, ..., rowIndex + + + + + + はい + + + + + + row[k] を計算する + + + row[k] = row[k-1] × (n-k+1) ÷ k + + + (n = rowIndex、// で整数のまま計算) + + + + + + 次のkへ + + + + + + いいえ(完了) + + + + + + 終了 + + + row を返す + + +
+ +
+

+ 🔎 入力例 rowIndex = 4 でのフロー追跡 +

+
    +
  1. 「開始」ノード → rowIndex = 4 を受け取る
  2. +
  3. + 「rowIndexは正しいか」ノード → + int型かつ0〜33の範囲内なので「はい」の経路へ +
  4. +
  5. + 「配列を確保する」→「基底値を設定する」→ row = [1, 0, 0, 0, 0] になる +
  6. +
  7. + 「kはrowIndex以下か」を4回「はい」で通過し、そのたびに「row[k]を計算する」でrowを更新する(紫の矢印で同じ行の中をループ) +
  8. +
  9. + k=5になった時点で「いいえ(完了)」の経路に進み、「終了」ノードで [1, 4, + 6, 4, 1] を返す +
  10. +
+
+ +

+ フローの説明:
+ このアルゴリズムには「行を積み上げるループ」が存在しません。緑の矢印で示した通り、 + 入力検証を通過した後は「配列の確保」「基底値の設定」を1回ずつ行い、 + あとは紫のループバック矢印が示す通り + k を + 1つずつ増やしながら同じ行の中だけを計算し続けます。ループが二重になっていない点が、 + 三角形全体を作る方法との決定的な違いです。 +

+
+ + +
+

+ 計算量分析 +

+ +
+

+ 📖 Big-O 記法の読み方(入力サイズ n + が大きくなるにつれて処理時間がどう増えるかの目安) +

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ nより少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間計算量空間計算量備考
三角形全体を二次元リストで構築O(n²)O(n²)直感的だが不要な行まで保持してしまう
1本のリストを右から左へ更新(in-place)O(n²)O(n)追加空間は最小だが二重ループが必要
+ 二項係数の漸化式(採用) + O(n)O(n) + 同じ行の中だけで完結するため最速 +
+
+ +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 採用した実装では、ループが + k = 1 から + rowIndex + までの1重だけで、 + 各回の計算が「1つ前の値」を使った掛け算1回・割り算1回という定数時間で終わります。 + そのため合計の時間計算量は入力(rowIndex)に比例する O(n) になります。 + 空間計算量も、出力用のリスト以外に一時配列や二次元リストを作らないため O(n) + に抑えられており、 フォローアップ条件をそのまま満たしています。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください(五十音順)。 +

+
+
+ + 型ヒント + +
+ 関数の引数や戻り値に型を注釈として書く仕組み。def getRow(self, rowIndex: int) -> List[int]: + のように書きます。Pythonは動的型付け言語なので実行時には無視されますが、pylance(VSCodeの型チェッカー)が + 実行前に誤った呼び出しを検出してくれます。 +
+
+ +
+ + 基底値 + +
+ 漸化式の出発点となる、最初から分かっている値のこと。今回は + row[0] = 1 + が基底値にあたります。 + 「rowIndex個から0個を選ぶ組み合わせは、何も選ばないという1通りしかない」という事実に対応しています。 +
+
+ +
+ + 計算量 + +
+ 入力の大きさに対して、処理にかかる時間(時間計算量)やメモリ(空間計算量)がどう増えるかの目安。 + O(1), O(n), O(n log n), O(n²) のようなBig-O記法で表します。 +
+
+ +
+ + 漸化式 + +
+ ある項の値を、1つ前の項の値から計算するための式。今回は + C(n, k) = C(n, k-1) × (n-k+1) ÷ k + という二項係数の漸化式を使い、同じ行の中だけで次の値を求めています。 +
+
+ +
+ + 二項係数 + +
+ n個の中からk個を選ぶ組み合わせの数。C(n, k) + と表記します。パスカルの三角形のrowIndex行目k番目の値は、実は + C(rowIndex, k) + そのものです。 +
+
+ +
+ + フロア除算 + +
+ Pythonの + // + 演算子。小数点以下を切り捨てて + int + を返します。Pythonの + /(真の除算)は割り切れる場合でも 常に + float + を返してしまうため、 + List[int] + という戻り値の型と合わせるために 今回は + // + を使っています。 +
+
+
+
+ +
+ LeetCode 119: Pascal's Triangle II - 初学者向け解説ページ +
+
+ + + + + + + + + + + + + + + diff --git a/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README.md b/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README.md new file mode 100644 index 00000000..405e021e --- /dev/null +++ b/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README.md @@ -0,0 +1,475 @@ +# 125. Valid Palindrome - 英数字だけを比較する回文判定 + +## 目次 + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython 最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +> 💡 この問題は、一言で言うと**「文字列から記号やスペースを取り除き、大文字を小文字に揃えたうえで、前から読んでも後ろから読んでも同じかどうかを判定する問題」**です。 + +**問題(LeetCode 125 Valid Palindrome)** + +- 与えられた文字列 `s` に対して、すべての大文字を小文字に変換し、英数字(アルファベットと数字)以外の文字をすべて取り除いたとき、その結果が回文(=前から読んでも後ろから読んでも同じ並びになる文字列)であれば `True` を、そうでなければ `False` を返します。 + +**この問題が難しく感じられるポイント** + +一見すると「文字列を綺麗にしてから前後を比べるだけ」の単純な問題に見えますが、実は2つの落とし穴があります。1つ目は「綺麗にする(クリーニングする)」という処理を素直に実装すると、新しい文字列を作ってしまい、余計なメモリを消費してしまう点です。2つ目は、記号やスペースが文字列のどこに出現するか分からないため、比較の途中で「読み飛ばし」の判定を挟む必要があり、単純な `s == s[::-1]` のような一行の比較では対応できない点です。この2つの落とし穴を回避するために、後述する二方向ポインタ(=文字列の両端から中央に向かって2つの指し位置を同時に動かしていく手法)という考え方が重要になります。 + +**入出力仕様** + +- 入力: `s: str`(印字可能なASCII文字列、長さは1以上2×10^5以下) +- 出力: `bool`(回文であれば `True`、そうでなければ `False`) + +**要件(正当性・安定性・制約)** + +- **正当性**: すべての入力パターン(記号のみ、英数字混在、大文字小文字混在)に対して常に正しい `True`/`False` を返す必要があります。 +- **制約**: 文字列の長さは最大2×10^5文字であるため、`O(n)` または `O(n log n)` 程度の計算量に収める必要があります。 + +

📖 この章で登場した用語

+ +- **回文**:前から読んでも後ろから読んでも同じ並びになる文字列や文の並び +- **制約**:入力として与えられる値の範囲や条件のこと。例:「文字列の長さは1以上2×10^5以下」 +- **正当性**:アルゴリズムが常に正しい答えを返すことの保証 +- **二方向ポインタ**:配列や文字列の両端から中央に向かって2つの指し位置(インデックス)を同時に動かしていく手法 + +--- + +

アルゴリズム要点(TL;DR)

+ +> 💡 TL;DR(Too Long; Didn't Read=長くて読めない人向けの要約)とは、アルゴリズム全体の戦略を短くまとめたものです。ここでは「なんとなくこういう手順で解くんだな」というイメージを掴んでください。詳細は後の章で説明します。 + +- **戦略**: 文字列の先頭と末尾に2つのポインタ(指し位置)を置き、中央に向かって進めながら1文字ずつ比較します。新しい文字列を作らないため、なぜこの手法を選ぶかというと、メモリ効率が最も良いからです。 +- **データ構造**: 追加のデータ構造は使わず、入力の `str` に対してインデックス(整数の位置番号)でアクセスするだけです。なぜ `list` や `deque` を使わないかというと、両端からのアクセスだけで済み、途中への挿入・削除が発生しないため、単純な整数変数2つで十分だからです。 +- **判定ロジック**: `str.isalnum()` で英数字かどうかを判定し、`str.lower()` で大文字小文字を揃えます。なぜ正規表現(`re` モジュール)を使わないかというと、正規表現エンジンの初期化コストがかかり、単純な走査より遅くなるためです。 +- **計算量**: 時間計算量 `O(n)`、空間計算量 `O(1)`(入力以外に使う追加メモリが、文字列の長さに関係なく一定であることを意味します)。 + +

📖 この章で登場した用語

+ +- **TL;DR**:「長くて読めない人向けの要約」を意味する略語 +- **インデックス**:文字列や配列の中で、何番目の要素かを示す位置番号(0から始まる) +- **正規表現**:文字列のパターンを記述するためのミニ言語。`re` モジュールで使う + +--- + +

図解

+ +> 💡 図を読む前に、Mermaidフローチャートの基本的な読み方を確認しておきましょう。ひし形のノードは「条件分岐(=はい・いいえで処理が枝分かれするポイント)」を表し、長方形のノードは「処理ステップ(=実際に何かを実行する場所)」を表します。矢印は処理の流れる方向を示します。 + +### フローチャート + +この図は `isPalindrome` メソッドの内部で行われる、二方向ポインタによる回文判定の処理の流れを表しています。上から下へ、矢印に沿って読み進めてください。 + +```mermaid +flowchart TD +Start[Start isPalindrome] +Init[Set left to 0 and right to length minus 1] +LoopCheck{Is left less than right} +CheckLeftAlnum{Is char at left alnum} +LeftInc[Increment left] +CheckRightAlnum{Is char at right alnum} +RightDec[Decrement right] +CompareChars{Lowercase chars equal} +MovePointers[Increment left and decrement right] +ReturnTrue[Return True] +ReturnFalse[Return False] +Start --> Init +Init --> LoopCheck +LoopCheck -- Yes --> CheckLeftAlnum +LoopCheck -- No --> ReturnTrue +CheckLeftAlnum -- No --> LeftInc +LeftInc --> LoopCheck +CheckLeftAlnum -- Yes --> CheckRightAlnum +CheckRightAlnum -- No --> RightDec +RightDec --> LoopCheck +CheckRightAlnum -- Yes --> CompareChars +CompareChars -- No --> ReturnFalse +CompareChars -- Yes --> MovePointers +MovePointers --> LoopCheck +``` + +主要なノードの意味: + +- `Start[Start isPalindrome]`:アルゴリズムの入り口。引数 `s` を受け取る +- `Init[Set left to 0 and right to length minus 1]`:左ポインタを先頭、右ポインタを末尾に初期化する処理ステップ +- `LoopCheck{Is left less than right}`:2つのポインタがまだすれ違っていないかを判定するひし形(条件分岐) +- `CheckLeftAlnum{Is char at left alnum}` / `CheckRightAlnum{Is char at right alnum}`:それぞれの位置の文字が英数字かどうかを判定するひし形 +- `LeftInc[Increment left]` / `RightDec[Decrement right]`:非英数字を読み飛ばすためにポインタを1つ動かす処理 +- `CompareChars{Lowercase chars equal}`:小文字化した文字同士が一致するかを判定するひし形 +- `MovePointers[Increment left and decrement right]`:一致した場合に両ポインタを内側へ進める処理 +- `ReturnTrue` / `ReturnFalse`:ループの結果に応じて最終的な真偽値を返す終端ノード + +### データフロー図 + +この図は、この問題を「概念として」理解する場合のデータの変換の流れと、実際に採用する二方向ポインタ実装でのデータの流れを比べたものです。 + +``` +【概念としての理解(実際の実装ではこの通りには動かない)】 + + 元の文字列 + | + v + 英数字だけを残す + | + v + すべて小文字に変換する + | + v + 反転した文字列と比較する + | + v + 真偽値(True または False) + +【実際の二方向ポインタ実装(新しい文字列を作らない)】 + + 元の文字列 s + left位置 -> [ その場で1文字ずつ判定・比較 ] <- right位置 + | + v + 真偽値(True または False) +``` + +主要な流れの説明: + +- 元の文字列 → 英数字だけを残す:この段階で新しい文字列オブジェクトが1つ生成されてしまう、というのが「概念としての理解」における弱点です +- 英数字だけを残す → すべて小文字に変換する:ここでもさらにもう1つ新しい文字列が生成されます +- 実際の実装では、この「新しい文字列を作る」ステップを一切行わず、元の文字列 `s` に対してインデックスでアクセスするだけで同じ判定結果を得ています + +> 💡 **代表例でのトレース**:`s = "A man, a plan, a canal: Panama"` を入力として、上記フローチャートの各ノードをどのように通過するかをステップごとに示します。 + +``` +Step 1: Start → 入力 s = "A man, a plan, a canal: Panama"(長さ30) +Step 2: Init → left=0, right=29 +Step 3: LoopCheck → 0 < 29 → Yes +Step 4: CheckLeftAlnum → s[0]='A' は alnum → Yes +Step 5: CheckRightAlnum → s[29]='a' は alnum → Yes +Step 6: CompareChars → 'a' == 'a' → Yes(一致) +Step 7: MovePointers → left=1, right=28 +Step 8: LoopCheck → 1 < 28 → Yes +Step 9: CheckLeftAlnum → s[1]=' ' は alnum ではない → No +Step 10: LeftInc → left=2 +Step 11: LoopCheck → 2 < 28 → Yes +Step 12: CheckLeftAlnum → s[2]='m' は alnum → Yes +Step 13: CheckRightAlnum → s[28]='m' は alnum → Yes +Step 14: CompareChars → 'm' == 'm' → Yes(一致) +Step 15: MovePointers → left=3, right=27 +...(中略:以降も同じパターンで "an" と "am" 、"a" と "a" の比較が続く)... +Step N: LoopCheck → left と right がすれ違う → No +Step N+1: ReturnTrue → 一度も不一致がなかったため True を返す +結果: True +``` + +

📖 この章で登場した用語

+ +- **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理ステップ +- **データフロー図**:データがどのように変換・移動するかを示す図 +- **ノード**:フローチャートやデータフロー図を構成する箱(図形)1つ1つのこと + +--- + +

正しさのスケッチ

+ +> 💡 「正しさのスケッチ」とは、アルゴリズムが常に正しい答えを返すことの根拠を整理したものです。数学的な証明ではなく、「なぜ正しいと言えるか」を噛み砕いて説明します。 + +- **不変条件(=アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件)**: ループの各反復が終わった時点で、「`s[0..left)` の範囲にある英数字の並び」と「`s(right..n)` の範囲にある英数字の並びを逆順にしたもの」が常に一致している、という条件が成り立ち続けます。この条件が崩れた瞬間(=比較で不一致が見つかった瞬間)に `False` を返すため、途中で条件が崩れているのに処理を続けてしまう、ということが起こりません。 +- **網羅性(=すべてのケースをもれなく処理できているという保証)**: `while left < right` というループ条件と、内部の `if/elif/else` 分岐により、「非英数字である」「英数字だが不一致」「英数字で一致」という3つの状態をすべて網羅しています。記号のみの文字列(例:`",,,,"`)であっても、`CheckLeftAlnum` と `CheckRightAlnum` の分岐が正しく機能し、最終的にポインタがすれ違ってループを抜けます。 +- **基底条件(=再帰の終了条件。この問題ではループの終了条件に相当)**: `left >= right` になった時点でループを終了し `True` を返します。これは「これ以上比較すべきペアが残っていない」ことを意味し、直感的にも正しい終了地点です。1文字だけの文字列(`len(s) == 1`)の場合、最初から `left == right` となるため、ループに一度も入らず即座に `True` が返ります。これも妥当な結果です(1文字は必ず回文であるため)。 +- **終了性(=アルゴリズムが必ず有限ステップで終わるという保証)**: `left` は単調に増加し、`right` は単調に減少します。両方とも `0` から `len(s) - 1` という有限の範囲に収まっているため、ループは必ず有限回で終了します(無限ループにはなりません)。 + +

📖 この章で登場した用語

+ +- **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +- **基底条件**:再帰やループの終了条件。これがないと無限ループになる +- **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 +- **網羅性**:すべてのケースをもれなく処理できているという保証 + +--- + +

計算量

+ +> 💡 計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。まずBig-O記法の読み方を確認しましょう。 + +| 記法 | 意味 | 直感的なイメージ | +| ---------- | ---------------------- | -------------------------- | +| O(1) | 入力サイズによらず一定 | 辞書で直接ページを開く | +| O(n) | 入力に比例して増加 | リストを端から順に読む | +| O(n log n) | nよりやや速く増加 | 辞書を二分探索で引く×n回 | +| O(n²) | 入力の2乗で増加 | 全ペアを総当たりで確認する | + +**この問題の計算量** + +- **時間計算量**: O(n)。文字列の各文字を最大1回ずつ読むだけで判定が完了するためです。 +- **空間計算量**: O(1)。ポインタ `left` と `right` という2つの整数変数以外に、入力サイズに比例して増えるメモリを使わないためです。 + +**in-place vs Pure の比較** + +| 実装方式 | 空間計算量 | 説明 | +| -------------------------- | ---------- | -------------------------------------------------------------------------------------------- | +| in-place(今回採用) | O(1) | 新しいメモリを確保せず元の文字列を直接インデックス参照する。空間計算量を抑えられる | +| Pure(フィルタ+反転比較) | O(n) | 入力を変更せず、クリーニング済みの新しい文字列を生成して比較する。安全だがメモリを余分に使う | + +

📖 この章で登場した用語

+ +- **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +- **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +- **in-place**:新しいメモリを確保せず、元のデータ構造を直接書き換える(または直接参照する)操作 +- **Pure**:入力を変更せず新しいデータを返す操作。安全だがメモリを余分に使う + +--- + +

Python 実装

+ +> 💡 コードを読む前に、実装全体の骨格を確認しておきましょう。 + +- `isinstance()` で入力が本当に `str` 型かどうかを検証する +- 左ポインタ `left` を先頭、右ポインタ `right` を末尾に初期化する +- `left < right` の間、非英数字の読み飛ばし・小文字化しての比較・ポインタの移動を繰り返す +- 不一致が見つかった時点で即座に `False` を返す(早期リターン=条件を満たした時点ですぐに関数を終了させること) +- ループを抜けたら `True` を返す + +LeetCode class形式(`class Solution: def isPalindrome(self, s: str) -> bool:`)で実装します。データ構造は `str` のみを使うため、`ListNode` や `TreeNode` のような補助クラスの定義は不要です。 + +```python +from __future__ import annotations +# from __future__ import annotations は、型ヒントを実行時ではなく +# 文字列として遅延評価するようにする宣言です。 +# クラス自身の型を戻り値に使う場合や、前方参照(まだ定義されていないクラス名を +# 型ヒントに書くこと)が必要な場合に役立ちます。今回のコードでは必須ではありませんが、 +# 業務コードのテンプレートとして先頭に付けておくと安全です。 + +from typing import Final +# Final は「この変数は再代入されない定数である」ことを型チェッカー(pylance)に +# 伝えるための型ヒントです。マジックナンバー(意味の分からない数値)を +# 避けるために、制約の上限値に名前を付けて定義します。 + + +class Solution: + """125. Valid Palindrome を解くクラス""" + + # 問題の制約:文字列の長さは最大 2 * 10**5 + _MAX_LENGTH: Final[int] = 2 * 10**5 + + def isPalindrome(self, s: str) -> bool: + """ + 文字列 s が「英数字だけ・小文字統一」で回文かどうかを判定する + + Args: + s: 判定対象の文字列(大文字・記号・スペースを含んでよい) + + Returns: + 回文であれば True、そうでなければ False + + Raises: + TypeError: s が str 型でない場合 + ValueError: s の長さが制約の上限を超える場合 + """ + # isinstance() で実行時にも型を確認する。 + # Python は動的型付け(=変数の型を実行時まで確定させない)言語のため、 + # pylance の静的チェックをすり抜けてきた値が紛れ込んでも、ここで安全に弾ける。 + if not isinstance(s, str): + raise TypeError("Input must be a string") + + # 制約の上限を超えていないか確認する。 + # 上限チェックを入れておくことで、想定外の巨大入力による + # 予期しない性能劣化を早期に検知できる。 + if len(s) > self._MAX_LENGTH: + raise ValueError("Input length exceeds the problem constraint") + + # 左ポインタは先頭、右ポインタは末尾に置く。 + # ここで s を書き換えたり新しい文字列を作ったりしないため、 + # 追加メモリはこの2つの整数変数だけで済む(空間計算量 O(1) の理由)。 + left: int = 0 + right: int = len(s) - 1 + + # 2つのポインタがすれ違う(left >= right)まで比較を続ける。 + while left < right: + # 左側が非英数字(記号・スペースなど)の間は読み飛ばす。 + # str.isalnum() は CPython 内部で C 実装されているため、 + # Python レベルで文字コード比較を自前実装するより高速かつ読みやすい。 + if not s[left].isalnum(): + left += 1 + continue + + # 右側も同様に非英数字を読み飛ばす。 + if not s[right].isalnum(): + right -= 1 + continue + + # 両方とも英数字にたどり着いたら、小文字化して比較する。 + # str.lower() を文字列全体ではなく1文字だけに対して呼ぶことで、 + # 新しい長い文字列を生成するコストを避けている。 + if s[left].lower() != s[right].lower(): + return False + + # 一致していれば両ポインタを内側に1つずつ進める。 + left += 1 + right -= 1 + + # ループを最後まで抜けられた(不一致が一度もなかった)ということは、 + # 文字列全体が回文であることが確認できたことを意味する。 + return True +``` + +> 💡 **コードの動作トレース**(初学者向け) + +``` +入力: s = "A man, a plan, a canal: Panama" +呼び出し: isPalindrome(s) + → isinstance チェック → str 型なので通過 + → 長さチェック → 31文字 <= 2*10**5 なので通過 + → left=0, right=30 + → ループ開始 + s[0]='A' は isalnum → True + s[30]='a' は isalnum → True + 'A'.lower()='a' と 'a'.lower()='a' を比較 → 一致 + → left=1, right=29 + s[1]=' ' は isalnum → False → left=2 + s[2]='m', s[29]='m' → 一致 → left=3, right=28 + ...(以下同様のパターンが続く)... + 最終的に left と right がすれ違いループ終了 + → return True +最終結果: True +``` + +

📖 この章で登場した用語

+ +- **`from __future__ import annotations`**:型ヒントを文字列として扱うようにする宣言。循環参照や前方参照を解決できる +- **`Final`**:`typing` モジュールが提供する型。「この変数は再代入されない定数である」ことを型チェッカーに伝える +- **動的型付け**:変数の型を実行時まで確定させない言語の性質。Pythonはこれに該当する +- **早期リターン**:条件を満たした時点ですぐに関数を終了させること。無駄な処理を続けずに済む + +--- + +

CPython 最適化ポイント

+ +> 💡 この章では「同じ処理でもPythonの書き方によって速さが変わる理由」を説明します。 + +**属性アクセスの削減** + +`s[left].isalnum()` のように書くと、CPythonは毎回「`s[left]` という `str` インスタンスから `isalnum` という属性(メソッド)を検索し、バインドメソッド(=そのインスタンスに紐づいたメソッドオブジェクト)を新しく作る」という処理を行います。ループが数十万回繰り返される場合、この検索・生成コストが積み重なります。 + +```python +# 最適化前:ループのたびに s[left].isalnum() のように毎回メソッドを検索する +while left < right: + if not s[left].isalnum(): + left += 1 + continue + if not s[right].isalnum(): + right -= 1 + continue + if s[left].lower() != s[right].lower(): + return False + left += 1 + right -= 1 + +# 最適化後:str クラスからメソッドそのものをローカル変数にキャッシュしておく +is_alnum = str.isalnum # 属性検索を1回だけ行い、以降は関数として直接呼び出す +to_lower = str.lower +while left < right: + if not is_alnum(s[left]): + left += 1 + continue + if not is_alnum(s[right]): + right -= 1 + continue + if to_lower(s[left]) != to_lower(s[right]): + return False + left += 1 + right -= 1 + +# 理由:is_alnum(s[left]) という書き方は「str クラスの isalnum 関数に +# s[left] を引数として渡す」という意味になり、毎回のバインドメソッド生成を +# 省略できる。ループ回数が多いほど、この差が積み重なって効いてくる。 +``` + +**スライス回避** + +`s[::-1]` のようなスライス操作(=配列や文字列の一部を切り出す操作)は、新しい文字列を丸ごとコピーして生成するため `O(n)` の時間とメモリを消費します。今回の二方向ポインタ実装では、一度もスライスを使わず、常に1文字だけを `s[i]` で取り出しているため、このコストを完全に回避しています。 + +**`re` モジュールを使わない理由** + +`re.sub(r"[^a-zA-Z0-9]", "", s).lower()` のように正規表現でクリーニングする方法もありますが、正規表現パターンのコンパイル(=パターン文字列を内部的な実行形式に変換する処理)にコストがかかり、さらに `re.sub` は新しい文字列を生成するため、今回のような単純なパターンには `str.isalnum()` / `str.lower()` の組み合わせの方が適しています。 + +

📖 この章で登場した用語

+ +- **属性アクセス**:`s.isalnum` のようにオブジェクトのプロパティやメソッドを参照する操作。CPythonでは内部的に検索処理が発生する +- **バインドメソッド**:特定のインスタンスに紐づけられたメソッドオブジェクト。呼び出すたびに新しく生成されるコストがある +- **スライス**:`s[1:3]` のように文字列やリストの一部を取り出す操作。新しいオブジェクトを生成するのでメモリを消費する +- **ローカル変数キャッシュ**:クラスの属性やグローバル変数への参照を一度ローカル変数に保存することで、アクセスコストを削減するテクニック + +--- + +

エッジケースと検証観点

+ +> 💡 エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。 + +- **1文字だけの文字列(例:`"a"`)**: `left == right` (`0 == 0`) からスタートするため、`while left < right` の条件を満たさず即座に `True` を返します。単一の `while left < right` と `if/continue` 分岐を使う本実装では範囲外アクセスは発生しません。 +- **記号やスペースだけの文字列(例:`" "` や `",,,,"`)**: 内側の `while` ループで非英数字を連続スキップする別実装の場合、`left < right` ガードを付け忘れると範囲外アクセス(インデックスエラー)を起こす危険がありますが、本実装のように単一 `while` 内で 1 ステップずつポインタを移動・再チェックする構造では安全に交差して `True` を返します。 +- **大文字小文字が混在する数字と文字(例:`"0P"`)**: `'0'.lower()` は `'0'` のまま変化しませんが、`'P'.lower()` は `'p'` になります。なぜ問題になりうるかというと、数字に対して `.lower()` を呼んでもエラーにならず元の文字がそのまま返る、という仕様を知らないと「数字には `.lower()` を呼んではいけないと誤解してしまう」可能性があるためです。 +- **制約の上限に近い長さの文字列(長さ2×10^5)**: 時間計算量が `O(n)` であることを確認するためのケースです。なぜ重要かというと、もし実装の途中でうっかりスライスや文字列連結を使ってしまうと、この規模の入力で急激に遅くなる可能性があるためです。 + +

📖 この章で登場した用語

+ +- **エッジケース**:空の入力・要素1つ・最大サイズ入力など、境界的な条件の入力 +- **境界値**:制約の上限・下限にあたる値。例:長さ1や長さ2×10^5の文字列 +- **インデックスエラー**:リストや文字列の範囲外の要素にアクセスしようとしたときのエラー + +--- + +

FAQ

+ +**Q1. なぜ二方向ポインタを使うのですか?フィルタして新しい文字列を作ってから比較する方法ではダメなのですか?** + +結論:フィルタして比較する方法でも正解にはなりますが、二方向ポインタの方がメモリ効率で優れているため、こちらを採用しています。 + +理由:フィルタする方法(`"".join(c for c in s if c.isalnum()).lower()` のような書き方)は、クリーニング済みの新しい文字列を1つ生成します。文字列は2×10^5文字になる可能性があるため、この新しい文字列の生成に `O(n)` の追加メモリがかかります。二方向ポインタなら、`left` と `right` という2つの整数変数だけで済むため `O(1)` に抑えられます。 + +補足:可読性を最優先するチームのコードベースでは、フィルタする方法の方が「直感的で読みやすい」という理由であえて選ばれることもあります。速度・メモリと可読性はトレードオフ(=何かを得ると何かを失う関係)の関係にあるため、状況に応じて選ぶことが大切です。 + +**Q2. `isalnum()` や `lower()` はコードのどの部分で使われていますか?** + +結論:ループ内の「非英数字の読み飛ばし判定」と「文字の比較」の2箇所で使われています。 + +理由:`isalnum()` は `if not s[left].isalnum():` の部分で、その位置の文字が英数字かどうかを判定するために使われています。`lower()` は `if s[left].lower() != s[right].lower():` の部分で、大文字小文字の違いを無視して比較するために使われています。 + +補足:CPython最適化ポイントの章で紹介したように、`str.isalnum` や `str.lower` をローカル変数にキャッシュして使う書き方に変えることもできます。 + +**Q3. 「非英数字をスキップする」というイメージがつかめません。具体例でもう一度説明してください。** + +結論:文字列の中に紛れ込んだ記号やスペースを、比較の対象から除外しながら読み進める、という動きです。 + +理由:例えば `"a, a"` という文字列を考えると、インデックス1にはカンマ `,` があります。もし記号もそのまま比較してしまうと、`left=1` の `,` と `right=2` の(スペースを挟んだ)文字を比較することになり、正しい判定ができません。そこで `left` を1つ進めて `,` を読み飛ばし、本来比較すべき英数字同士(この場合は `'a'` と `'a'`)を見つけてから比較を行います。 + +補足:この「読み飛ばし」の処理があるからこそ、記号やスペースがどこに何個あっても、正しく英数字だけを比較できます。 + +**Q4. なぜ新しい文字列を作らないほうが良いのですか?メモリの話がよく分かりません。** + +結論:Pythonの文字列はイミュータブル(=作成後に内容を変更できない性質)であるため、文字列に対して何らかの変換処理を行うたびに、新しいメモリ領域が確保されるからです。 + +理由:例えば `s.lower()` を文字列全体に対して呼び出すと、元の文字列とは別に、小文字化された新しい文字列がまるごと1つ作られます。文字列が2×10^5文字であれば、その分のメモリがもう1つ必要になります。二方向ポインタでは `s[left].lower()` のように1文字だけを小文字化するため、生成される一時的な文字列も1文字分だけで済みます。 + +補足:この考え方は、図解の章で示した「概念としての理解」と「実際の二方向ポインタ実装」の違いにそのまま対応しています。 + +**Q5. 再帰でも解けますか?なぜ推奨しないのですか?** + +結論:再帰でも実装は可能ですが、この問題の制約下では推奨しません。 + +理由:再帰は「関数の呼び出し履歴」をコールスタック(=関数呼び出しの情報を積み上げるメモリ領域)に積んでいきます。文字列の長さが最大2×10^5であるため、単純な再帰では10万回以上の呼び出しが発生する可能性があり、Pythonのデフォルトの再帰制限(`sys.getrecursionlimit()`、通常1000回程度)を超えて `RecursionError` になる危険があります。 + +補足:どうしても再帰で書きたい場合は `sys.setrecursionlimit()` で上限を引き上げる方法もありますが、コールスタックを使う分メモリ効率が悪化するため、今回のようにループで書ける問題ではループを使う方が安全です。 + +

📖 この章で登場した用語

+ +- **FAQ**:Frequently Asked Questions(よくある質問)の略 +- **トレードオフ**:何かを得ると何かを失う関係。例:速さを得るとメモリが増える +- **コールスタック**:関数呼び出しの情報を積み上げるメモリ領域。再帰呼び出しが深くなるとここが溢れる可能性がある diff --git a/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README_React.html b/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README_React.html new file mode 100644 index 00000000..5c9aa872 --- /dev/null +++ b/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README_React.html @@ -0,0 +1,822 @@ + + + + + +LeetCode 125: Valid Palindrome | 二方向ポインタで学ぶ回文判定 + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+

アルゴリズム概要

+ +
+

+ 💡 この問題を一言で言うと:「文字列から記号やスペースを除き、大文字を小文字に揃えたとき、前から読んでも後ろから読んでも同じかどうかを判定する問題」です。 +

+

+ 英語の文章では、大文字・小文字の違いやカンマ・コロン・スペースといった記号が自由に混ざります。この問題では、それらの「見た目のノイズ」を無視して、純粋に英数字(アルファベットと数字)の並びだけに注目したときに回文(前から読んでも後ろから読んでも同じ並び)になっているかを判定します。 +

+
+ +
+

⚠️ なぜ単純な方法では解けないのか

+
    +
  • 記号やスペースが文字列のどこに何個出現するか分からないため、s == s[::-1] のような一括比較では正しく判定できません。
  • +
  • 「記号を除いて小文字化した新しい文字列を作ってから比較する」という素直な方法は正解にはなりますが、文字列の長さが最大 2×10^5 になりうるため、新しい文字列を作るたびに余分なメモリを消費してしまいます。
  • +
+
+ +
+
+
O(n)
+
時間計算量
+
+
+
O(1)
+
空間計算量
+
+
+
isalnum
+
使用メソッド
+
+
+
Easy
+
難易度
+
+
+ +
+
+

s = "A man, a plan, a canal: Panama"

+

→ True

+

記号とスペースを除いて小文字化すると "amanaplanacanalpanama" となり、前から読んでも後ろから読んでも同じになるため True です。

+
+
+

s = "race a car"

+

→ False

+

クリーニングすると "raceacar" となり、逆から読むと "racaecar" で一致しないため False です。

+
+
+

s = " "(半角スペース1文字)

+

→ True

+

英数字が1つも無いため、クリーニング後は空文字列になります。空文字列は前からも後ろからも「何もない」ので、回文として扱われ True になります。

+
+
+
+ + +
+

ステップバイステップ解説

+

+ 入力例 s = "A man, a plan, a canal: Panama" を使って、二方向ポインタが実際にどう動くかを1ステップずつ確認しましょう。文字の下の数字はインデックス(何番目の文字かを示す位置番号)です。 +

+
+
+ + +
+

Python実装

+ +
+

📋 このコードの構造(先に全体像を把握しよう)

+
    +
  1. 入力が str 型かどうかを検証する(エッジケース処理を含む)
  2. +
  3. 左ポインタ left を先頭、右ポインタ right を末尾に初期化する
  4. +
  5. left < right の間、非英数字の読み飛ばし・小文字化しての比較・ポインタの移動を繰り返す
  6. +
  7. 不一致が見つかれば即座に False を、ループを抜けたら True を返す
  8. +
+
+ +
from __future__ import annotations
+from typing import Final
+
+
+class Solution:
+    """125. Valid Palindrome を解くクラス"""
+
+    _MAX_LENGTH: Final[int] = 2 * 10 ** 5
+
+    def isPalindrome(self, s: str) -> bool:
+        """
+        文字列 s が英数字だけ・小文字統一で回文かどうかを判定する
+
+        Args:
+            s: 判定対象の文字列
+
+        Returns:
+            回文であれば True、そうでなければ False
+        """
+        if not isinstance(s, str):
+            raise TypeError("Input must be a string")
+        if len(s) > self._MAX_LENGTH:
+            raise ValueError("Input length exceeds the constraint")
+
+        left: int = 0
+        right: int = len(s) - 1
+
+        while left < right:
+            if not s[left].isalnum():
+                left += 1
+                continue
+            if not s[right].isalnum():
+                right -= 1
+                continue
+            if s[left].lower() != s[right].lower():
+                return False
+            left += 1
+            right -= 1
+
+        return True
+
+ +
+

▶ 入力例 s = "A man, a plan, a canal: Panama" での動作トレース

+
入力: s = "A man, a plan, a canal: Panama"(長さ30)
+left=0, right=29
+ 0,29: 'A' と 'a' → 小文字化して比較 → 'a' == 'a' → 一致 → left=1, right=28
+ 1,28: s[1]=' ' は非英数字 → left=2
+ 2,28: 'm' と 'm' → 一致 → left=3, right=27
+ 3,27: 'a' と 'a' → 一致 → left=4, right=26
+ 4,26: 'n' と 'n' → 一致 → left=5, right=25
+ 5,25: s[5]=',' は非英数字 → left=6
+ 6,25: s[6]=' ' は非英数字 → left=7
+ 7,25: 'a' と 'a' → 一致 → left=8, right=24
+ 8,24: s[8]=' ' は非英数字 → left=9
+ 9,24: 'p' と 'P' → 小文字化 'p' == 'p' → 一致 → left=10, right=23
+10,23: s[23]=' ' は非英数字 → right=22
+10,22: s[22]=':' は非英数字 → right=21
+10,21: 'l' と 'l' → 一致 → left=11, right=20
+11,20: 'a' と 'a' → 一致 → left=12, right=19
+12,19: 'n' と 'n' → 一致 → left=13, right=18
+13,18: s[13]=',' は非英数字 → left=14
+14,18: s[14]=' ' は非英数字 → left=15
+15,18: 'a' と 'a' → 一致 → left=16, right=17
+16,17: s[16]=' ' は非英数字 → left=17
+left(17) < right(17) は成立しない → ループ終了
+出力: True ✅
+
+
+ + +
+

処理フローチャート

+ +
+

🗺️ フローチャートの読み方

+
+
+ + 楕円(緑)= 開始・終了 +
+
+ + 四角(青)= 処理ステップ +
+
+ + ひし形(黄)= 条件分岐 +
+
+ はい + いいえ + ループ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + 開始 + + + + + + 初期化 + left = 0, right = length - 1 + + + + + + left < right ? + + はい + + + + + left が非英数字 + left += 1 (left < right へ戻る) + + + right が非英数字 + right -= 1 (left < right へ戻る) + + + + + + 小文字化して比較 + s[left] == s[right] ? + + はい + + + + + left += 1, right -= 1 + + + 戻る + + + + いいえ + + + + いいえ + + + + + False を返す + + + + True を返す + +
+ +
+

🔎 入力例 s = "A man, a plan, a canal: Panama" でのフロー追跡

+
    +
  1. 「開始」ノード → 入力 s(長さ30)を受け取る
  2. +
  3. 「初期化」ノード → left=0, right=29 に設定
  4. +
  5. 「left < right ?」ノード → 0 < 29 なので「はい」の経路へ
  6. +
  7. 「非英数字ならポインタを移動」ノード → s[0]='A' も s[29]='a' も英数字なのでそのまま次へ
  8. +
  9. 「小文字化して比較」ノード → 'a' と 'a' が一致するので「はい」の経路へ
  10. +
  11. 「left += 1, right -= 1」ノード → left=1, right=28 に更新し、再び「left < right ?」へ戻る(紫の矢印)
  12. +
  13. (中略)この後、left 側では ',' や ' ' が、right 側では ' ' や ':' がそれぞれスキップされながら、ポインタが中央へ収束していきます
  14. +
  15. 最終的に left=17, right=17 となり「left < right ?」が「いいえ」になるため、「True を返す」ノードに到達し、結果 True が返されます
  16. +
+
+ +

+ フローの説明:
+ このフローチャートは、二方向ポインタが「ループ条件のチェック」→「非英数字の読み飛ばし」→「小文字化しての比較」→「ポインタの移動」という4つの処理を繰り返しながら中央へ収束していく様子を表しています。途中で不一致が見つかれば紫のループを抜けて赤い経路で「False を返す」に到達し、一度も不一致がなければ緑の経路で「True を返す」に到達します。 +

+
+ + +
+

計算量分析

+ +
+

📖 Big-O 記法の読み方(入力サイズ n が大きくなるにつれて処理時間がどう増えるかの目安)

+
+
+
O(1)
+
常に一定
例:辞書の直接引き
+
+
+
O(n)
+
入力に比例
例:リストを1回走査
+
+
+
O(n log n)
+
n より少し多い
例:ソートアルゴリズム
+
+
+
O(n²)
+
入力の2乗
例:二重ループ総当たり
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間計算量空間計算量備考
二方向ポインタ(採用)O(n)O(1)新しい文字列を作らないため最省メモリ
フィルタ+反転比較O(n)O(n)実装は直感的だが新しい文字列を2つ生成する
+
+ +
+

🔍 なぜこの計算量になるのか

+

+ 時間計算量が O(n) になるのは、left と right という2つのポインタがそれぞれ最大でも文字列の長さ分しか動かず、各文字を高々1回ずつしか読まないためです。空間計算量が O(1) になるのは、追加で使うメモリが left と right という2つの整数変数だけであり、入力文字列の長さが変わってもこの追加メモリの量は変化しないためです。 +

+
+
+ + +
+

📖 用語集

+

このページで登場した専門用語を五十音順にまとめました。分からない言葉が出てきたときに参照してください。

+
+ +
+ + インデックス + +
+ 文字列や配列の中で「何番目の要素か」を示す位置番号のことです。多くのプログラミング言語(Pythonを含む)では 0 から数え始めるため、先頭の文字はインデックス 0、2番目の文字はインデックス 1 になります。 +
+
+ +
+ + エッジケース + +
+ 空の入力・要素が1つだけ・制約の上限に近い巨大な入力など、通常とは異なる境界的な条件のことです。普通の入力ではうまく動くのに、エッジケースだけでバグが発生することがあるため、必ず確認すべき対象です。 +
+
+ +
+ + 回文 + +
+ 前から読んでも後ろから読んでも同じ並びになる文字列や文のことです。例えば "level" や "racecar" が回文です。この問題では、記号やスペースを除き大文字小文字を無視した上で回文かどうかを判定します。 +
+
+ +
+ + 空間計算量 + +
+ アルゴリズムの処理中に使うメモリ量が、入力サイズに対してどのくらい増えるかの目安です。O(1) であれば、入力がどれだけ大きくなっても追加で使うメモリは一定のままであることを意味します。 +
+
+ +
+ + 時間計算量 + +
+ 入力サイズが大きくなるにつれて、処理にかかる時間がどのくらい増えるかの目安です。O(n) であれば、入力が2倍になると処理時間もおよそ2倍になることを意味します。 +
+
+ +
+ + 早期リターン + +
+ 条件を満たした時点ですぐに関数を終了させることです。この問題では、不一致が見つかった瞬間に残りの処理を続けず即座に False を返しており、これが早期リターンにあたります。 +
+
+ +
+ + 二方向ポインタ + +
+ 配列や文字列の両端から中央に向かって、2つの指し位置(インデックス)を同時に動かしていく手法です。新しいデータ構造を作らずに1回の走査だけで処理を終えられるため、メモリ効率と実行速度の両方に優れています。 +
+
+ +
+ + 不変条件 + +
+ アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件のことです。この問題では「left より前の英数字の並びと、right より後ろの英数字の並びを逆にしたものが常に一致している」という条件が不変条件にあたります。 +
+
+ +
+
+ +
+ LeetCode 125: Valid Palindrome | 二方向ポインタ走査による学習用解説ページ +
+
+ + + + + + + + + + + + + + + + diff --git a/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/Valid_Palindrome_Python.md b/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/Valid_Palindrome_Python.md new file mode 100644 index 00000000..fae59f54 --- /dev/null +++ b/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/Valid_Palindrome_Python.md @@ -0,0 +1,233 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: Python +> 適用ルールセット: 共通5ルール + Python固有5ルール +> 参照ファイル: references/common.md + references/python.md + +![alt text](image.png) + +--- + +## 1. 問題分析結果 + +> 💡 この問題は、一言で言うと**「文字列から記号やスペースを取り除き、大文字を小文字に揃えたうえで、前から読んでも後ろから読んでも同じかどうかを判定する問題」**です(TypeScript版と同じ問題です)。 + +> ⚠️ **CPython特有の注意点(最初にまとめて確認)** +> Pythonの文字列(`str`)は**イミュータブル(=作成後に内容を変更できない)**です。そのため `s = s.lower()` のような処理を書くたびに、Python内部では**新しい文字列オブジェクトが生成**されます。また、`s.isalnum()` や `s.lower()` のようなメソッドは**CPython内部でC言語として実装されている**ため、1回の呼び出しコストは非常に軽いのですが、「新しい文字列を作るかどうか」は空間計算量(メモリ効率)に直結します。この問題では「元の文字列に対してインデックス(位置番号)だけを動かして判定するか」「フィルタして新しい文字列を作ってから比較するか」で、メモリ効率が大きく変わります。 + +### 競技プログラミング視点 + +- **制約分析**: 文字列長は最大 `2×10^5`。`O(n log n)` 以上のアルゴリズムを使う必要はなく、`O(n)` の一回走査で十分間に合います。 +- **最速手法**: 新しい文字列や配列を作らず、元の文字列 `s` に対して2つのインデックスを動かす**二方向ポインタ**が最速かつ最省メモリです。 +- **メモリ最小化**: `s.lower()` を文字列全体に対して呼ぶと `O(n)` の新しい文字列ができてしまうため、**1文字ずつ `.lower()` を呼ぶ**ことで無駄な文字列生成を避けます。 +- **CPython最適化**: `str.isalnum()` と `str.lower()` は両方ともC実装のメソッドです。`for`文の中でPythonレベルのif分岐を使うより、これらの組み込みメソッドを使う方が高速です。 + +### 業務開発視点 + +- **型安全設計**: 引数 `s: str`、戻り値 `bool` という型ヒントに加え、`isinstance()` による実行時チェックを入れることで、pylanceの静的解析と実行時の安全性を両立させます。 +- **エラーハンドリング**: LeetCodeの制約上 `s` は必ず1文字以上の文字列ですが、業務コードとして再利用する場合は `None` や別の型が渡ってくる可能性があるため、`TypeError` を明示的に送出します。 +- **可読性**: `_check_palindrome` のような内部メソッドに処理を分割し、二重ループではなく単一の `while` ループと条件分岐(`if`/`continue`)を使うシンプルな制御フロー構造にすることで可読性と保守性を高めます。 + +### Python特有分析 + +- **データ構造選択**: この問題では `list` や `dict` などの複雑なデータ構造は不要で、`str` に対するインデックスアクセスだけで完結します。 +- **標準ライブラリ活用度**: `re`(正規表現)モジュールを使って `re.sub(r'[^a-zA-Z0-9]', '', s).lower()` と書く方法も可能ですが、正規表現のコンパイルコストと新しい文字列生成のコストがかかるため、今回は使いません。 +- **CPython最適化度**: `str.isalnum()` / `str.lower()` はC実装のため、Pythonレベルで文字コード比較を自前実装するより高速かつ可読性が高くなります(この点はTypeScript版とは逆の結論になります。TypeScriptでは文字コード比較の方が高速でした)。 + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。組み込み関数の多くがC言語実装のため高速 +> - **イミュータブル**:一度作成すると内容を変更できない性質。Pythonの `str` や `tuple` がこれに該当する +> - **静的解析**:プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法(pylanceがこれを行う) + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 同じ「回文判定」でも実現方法は複数あります。それぞれの「速さ」「メモリ効率」「Pythonでの書きやすさ」を比較して最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 使用する標準ライブラリ・組み込み | CPython最適化 | 備考 | +| ----------------------------- | ---------- | ---------- | ---------------- | ------ | ----------------------------------------------- | ------------- | ---------------------------------------------------- | +| A. 二方向ポインタ | O(n) | O(1) | 低 | ★★★ | `str.isalnum()`, `str.lower()` | 適 | 追加の文字列生成なし。最速・最省メモリ | +| B. フィルタ+反転比較 | O(n) | O(n) | 低 | ★★★ | リスト内包表記, `str.join()`, スライス `[::-1]` | 適 | 実装は最も直感的だが新しい文字列を2つ作る | +| C. 正規表現によるクリーニング | O(n) | O(n) | 中 | ★★☆ | `re.sub()` | 不適 | 正規表現のコンパイルコストがあり、単純な走査より不利 | + +> 💡 **Big-O記法の読み方**(再掲) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む +> - `O(n)`:入力が2倍になると、処理も約2倍になる + +- **選択理由**: 方法Aは時間計算量・空間計算量の両方で最も優れています。方法B(フィルタ+反転)は可読性が非常に高く「Pythonic(=Python言語らしい簡潔な書き方)」ですが、`2×10^5` 文字という制約下では新しい文字列を2つ生成するオーバーヘッドが無視できません。方法C(正規表現)はさらに正規表現エンジンの初期化コストが加わるため、今回は不採用としました。 +- **Python最適化戦略**: `.isalnum()` と `.lower()` はどちらもCPythonの内部でC言語として実装されたメソッドです。そのため、自前で `ord(c)` を使った文字コード比較をするよりも、これらの組み込みメソッドをそのまま使う方が速く、かつ読みやすいコードになります。 +- **トレードオフ**: 方法Aは方法Bよりわずかにコードの行数が増えますが(`while`ループが2つネストする)、その分メモリ効率は大きく改善します。可読性とパフォーマンスのバランスを考えても、この問題の制約(最大`2×10^5`文字)では方法Aを選ぶ価値が十分にあります。 + +> 📖 **このセクションで登場した用語** +> +> - **Pythonic**:Python言語らしい、簡潔で慣用的な書き方をしていること +> - **トレードオフ**:何かを得ると何かを失う関係。速くするとメモリが増える、など +> - **C実装**:Pythonコードではなく、内部でC言語で実装された関数・メソッド。Pure Pythonより大幅に高速 + +--- + +## 3. 実装パターン + +> 💡 コード全体を示す前に、大まかな構造(骨格)を確認します。 + +1. まず入力が本当に `str` 型かどうかを検証する +2. 左ポインタ `left` を先頭、右ポインタ `right` を末尾に置く +3. `left < right` の間、以下を繰り返す + - 左側が英数字でなければ `left` を1つ進める + - 右側が英数字でなければ `right` を1つ戻す + - 両方とも英数字なら、小文字化して比較。違えば `False` を返す + - 一致していれば、両ポインタを内側に1つずつ動かす +4. ループを抜けたら(=すべて一致した)`True` を返す + +【**業務開発版**を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。型ヒント・docstring・入力検証が揃っているため、後から読んだ人が「なぜこう書いたか」を追いやすくなっています。 + +```python +from typing import Final + + +class Solution: + """125. Valid Palindrome を解くクラス(業務開発版)""" + + # 定数はクラス変数として明示し、マジックナンバー(意味の分からない数値)を避ける + _MAX_LENGTH: Final[int] = 2 * 10**5 + + def isPalindrome(self, s: str) -> bool: + """ + 文字列 s が「英数字だけ・小文字統一」で回文かどうかを判定する + + Args: + s: 判定対象の文字列(大文字・記号・スペースを含んでよい) + + Returns: + 回文であれば True、そうでなければ False + + Raises: + TypeError: s が str 型でない場合 + ValueError: s の長さが制約の上限を超える場合 + """ + # isinstance() で実行時にも型を確認する。 + # Python は動的型付け(=変数の型を実行時まで確定させない)言語のため、 + # pylance の静的チェックをすり抜けてきた値(例えばJSONやAPIレスポンス)が + # 紛れ込んでも、ここで安全に弾ける。 + if not isinstance(s, str): + raise TypeError("Input must be a string") + + # 制約の上限を超えていないか確認する。 + # 上限チェックを入れておくことで、想定外の巨大入力による + # 予期しない性能劣化を早期に検知できる。 + if len(s) > self._MAX_LENGTH: + raise ValueError("Input length exceeds the problem constraint") + + return self._check_palindrome(s) + + def _check_palindrome(self, s: str) -> bool: + """ + 二方向ポインタで回文判定を行う内部メソッド + + Time Complexity: O(n) + Space Complexity: O(1)(ポインタ2つ分のみ) + """ + # 左ポインタは先頭、右ポインタは末尾に置く。 + # ここで s を書き換えたり新しい文字列を作ったりしないため、 + # 追加メモリはこの2つの整数変数だけで済む。 + left: int = 0 + right: int = len(s) - 1 + + while left < right: + # 左側が非英数字(記号・スペースなど)の間は読み飛ばす。 + # str.isalnum() は CPython 内部で C 実装されているため、 + # Python レベルで ord() 比較を自前実装するより高速かつ読みやすい。 + if not s[left].isalnum(): + left += 1 + continue + + # 右側も同様に非英数字を読み飛ばす。 + if not s[right].isalnum(): + right -= 1 + continue + + # 両方とも英数字にたどり着いたら、小文字化して比較する。 + # str.lower() を文字列全体ではなく1文字だけに対して呼ぶことで、 + # 新しい長い文字列を生成するコストを避けている。 + if s[left].lower() != s[right].lower(): + return False + + # 一致していれば両ポインタを内側に1つずつ進める。 + left += 1 + right -= 1 + + # ループを最後まで抜けられた(不一致が一度もなかった)ということは、 + # 文字列全体が回文であることが確認できたことを意味する。 + return True +``` + +【**競技プログラミング版**を使う場面】 +LeetCodeなど、制限時間内に正解を出すことが目的のコードに向きます。エラーハンドリングを省略し、行数を最小限に抑えています。 + +```python +class Solution: + def isPalindrome(self, s: str) -> bool: + left, right = 0, len(s) - 1 + while left < right: + if not s[left].isalnum(): + left += 1 + elif not s[right].isalnum(): + right -= 1 + elif s[left].lower() != s[right].lower(): + return False + else: + left += 1 + right -= 1 + return True +``` + +> 💡 **コードの動作トレース**(初学者向け) +> 入力例 `s = "A man, a plan, a canal: Panama"` を使って、主要なステップを追ってみます。 + +``` +初期状態: left=0 ('A'), right=30 ('a') +Step 1: s[0]='A' は isalnum() → True +Step 2: s[30]='a' は isalnum() → True +Step 3: 'A'.lower()='a' と 'a'.lower()='a' を比較 → 一致 +Step 4: left=1, right=29 に移動 +Step 5: s[1]=' ' は isalnum() → False → left += 1 → left=2 ('m') +Step 6: s[2]='m' は isalnum() → True、s[29]='m' も isalnum() → True +Step 7: 'm' vs 'm' → 一致 → left=3, right=28 +... +(中略:同様の手順で "an" と "am" 、"a" と "a" の比較が続く) +... +Step N: left と right がすれ違う(left >= right)→ ループ終了 +Step N+1: 一度も不一致がなかったため True を返す +``` + +> 💡 **型ヒントとpylanceの関係(初学者向け)** +> `left: int = 0` のように変数にも型ヒントを書いておくと、pylanceが「このあと `left` に文字列を代入しようとしていないか」などを実行前に検出できます。今回のコードでは `s: str`、`left: int`、`right: int`、戻り値 `bool` のすべてに型が明示されているため、pylanceによる型エラーは発生しません。 + +> 📖 **このセクションで登場した用語** +> +> - **型ヒント**:関数の引数や戻り値、変数に型を注釈として書く仕組み。`left: int = 0` のように書く +> - **pylance**:VSCodeで使えるPythonの静的型チェックツール。実行前にバグを検出できる +> - **`Final`**:`typing` モジュールが提供する型。「この変数は再代入されない定数である」ことを型チェッカーに伝える +> - **docstring**:関数やクラスの先頭に書く説明文。`"""三重クォート"""`で囲む +> - **動的型付け**:変数の型を実行時まで確定させない言語の性質。Pythonはこれに該当する + +--- + +## 4. 検証 + +> 💡 エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。 + +- **境界値テスト**(考え方の確認。テストコードは省略): + - `s = " "`(半角スペース1文字)→ 英数字が1つも無いため、`left` と `right` が同じ位置のまま交差せず `True` を返す + - `s = "a"`(1文字)→ `left == right` からループに入らず即座に `True` を返す + - `s = "0P"`(大文字小文字混在の数字と文字)→ `'0'.lower()` は `'0'` のまま、`'P'.lower()` は `'p'` になるため `'0' != 'p'` で `False` + - `s = ",,,,,"`(記号のみ)→ すべて非英数字のため両ポインタが読み飛ばし続け、交差してループを抜け `True` を返す(空文字列扱いと同じ結果) +- **型チェック**: pylanceは `isPalindrome` の引数 `s: str`、戻り値 `bool`、内部変数 `left: int`/`right: int` のすべてに矛盾がないことを確認できるため、静的解析エラーは発生しません。 + +> 📖 このセクションで登場した用語 +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること diff --git a/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/Valid_Palindrome_Typescript.md b/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/Valid_Palindrome_Typescript.md new file mode 100644 index 00000000..2345758e --- /dev/null +++ b/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/Valid_Palindrome_Typescript.md @@ -0,0 +1,216 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: TypeScript +> 適用ルールセット: 共通5ルール + TS固有5ルール +> 参照ファイル: references/common.md + references/typescript.md + +--- + +## 1. 問題の分析 + +> 💡 この問題は、一言で言うと**「文字列から記号やスペースを取り除き、大文字を小文字に揃えたうえで、前から読んでも後ろから読んでも同じかどうかを判定する問題」**です。 + +### 競技プログラミング視点での分析 + +- 実行速度を最優先するなら、文字列をいったん新しい配列やStringに変換(=フィルタリングして作り直す)せず、**元の文字列 `s` に対してインデックス(=文字の位置番号)だけを動かして直接判定する**のが最速です。新しい配列を作らない分、メモリコピーのコストがかかりません。 +- メモリ使用量を最小化するなら、`filter()` や `split('').reverse().join('')` のように**新しい文字列・配列を作る操作を避ける**ことが重要です。これらは内部的に元の文字列と同じ長さのデータをもう一つ作るため、余分なメモリ(=空間計算量)を消費します。 + +### 業務開発視点での分析 + +- 型安全性の面では、この問題の入力は `string` 型に固定されているため、複雑なジェネリクス(=型を後から自由に差し込める仕組み)は不要です。むしろ「入力が本当に文字列か」「空文字列や1文字だけの特殊ケースをどう扱うか」といった**実行時の防御的チェック**の方が価値があります。 +- 保守性の面では、「英数字かどうかを判定するロジック」と「小文字化するロジック」を**小さな関数に切り出す**ことで、後から読む人が処理の意図を追いやすくなります。 +- エラーハンドリングの面では、LeetCodeの制約上 `s` は必ず1文字以上の印字可能なASCII文字列と保証されていますが、業務コードとして再利用する場合は `null` や `undefined` が渡ってくる可能性を考慮し、型ガード(= `typeof` などで実行時に型を絞り込む仕組み)を入れておくと安全です。 + +### TypeScript特有の考慮点 + +- **型推論**(=型を明示しなくてもTypeScriptが自動で判断してくれる機能)を活かし、`left`・`right` のようなローカル変数には型注釈を省略しても `number` 型だとコンパイラが自動判定してくれます。 +- 今回は入力が `string` 固定のためジェネリクスは使いませんが、代わりに `charCodeAt` の戻り値である `number` 型に対して**関数の引数・戻り値の型を明示**することで、意図しない型の混入をコンパイル時に防げます。 +- `unknown` 型(= `any` より安全な「型不明」を表す型。使う前に必ず型チェックが必要)は今回の問題では直接使いませんが、入力検証部分で `typeof s !== 'string'` という**型ガード**を使うことで、実行時にも安全性を確保します。 + +> 📖 このセクションで登場した用語 +> +> - **型推論**:型を明示しなくてもTypeScriptが自動で型を判断してくれる機能 +> - **型ガード**:`typeof` や `Array.isArray` などを使って、実行時に値の型を絞り込む仕組み +> - **空間計算量**:処理中に使うメモリ量が、入力サイズに対してどう増えるかの目安 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ「回文判定」という目的でも、実現方法はいくつかあります。ここでは3つのアプローチを比べ、それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を確認したうえで最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| ------------------------------------------------ | ---------- | ------------------------ | ------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| A. 二方向ポインタ(左右から中央に向かって走査) | O(n) | O(1) | 低 | 高 | 中〜高 | 追加の配列を作らないため最速・最省メモリ | +| B. フィルタ+反転比較(`filter`→`reverse`→比較) | O(n) | O(n) | 低 | 高 | 高(直感的) | 実装は簡単だが新しい文字列を2つ分作る | +| C. 再帰による比較 | O(n) | O(n)(コールスタック分) | 中 | 中 | 中 | `s.length` が最大 `2×10^5` のため、再帰の呼び出し回数が多すぎてスタックオーバーフロー(=呼び出し履歴を積むメモリ領域が溢れること)の危険がある | + +> 💡 **Big-O記法の読み方**(再掲) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の手間で済む(今回の空間計算量がこれに該当) +> - `O(n)`:入力が2倍になると、処理の手間も約2倍になる(今回の時間計算量はどのアプローチでもこれに該当) + +📊 3つのアプローチの違いを図で見てみましょう。それでは、二方向ポインタ(two-pointer)のアルゴリズム全体の流れを図で確認してから、実装に入りましょう。図の通り、ループの中で「非英数字のスキップ → 小文字化して比較 → 一致判定 → ポインタ移動」を繰り返し、途中で不一致が見つかれば即座に `false`、ループを抜けられれば `true` を返す、という流れになります。 + +![alt text](image-1.png) + +> 📖 このセクションで登場した用語 +> +> - **二方向ポインタ(two-pointer)**:配列や文字列の両端から中央に向かって2つの指し位置(インデックス)を同時に動かしていく手法。1回の走査(=データを端から端まで見ていくこと)だけで済むため効率的 +> - **早期リターン(early return)**:条件を満たした時点ですぐに関数を終了させること。無駄な処理を続けずに済む + +--- + +## 3. 選択したアルゴリズムと理由 + +> 💡 選んだ理由を、選ばなかった方法との対比で説明します。 + +- **選択したアプローチ**: A. 二方向ポインタ(in-place走査) + +- **理由**: + - **方法B(フィルタ+反転比較)を選ばなかった理由**:`filter()` で新しい配列を作り、さらに `reverse()` や `join('')` でもう一つの文字列を作るため、入力が最大 `2×10^5` 文字の場合、余分なメモリを2回分確保することになります。方法Aなら追加のメモリはポインタ変数2つ(`left`, `right`)だけで済むため、空間計算量が `O(n)` から `O(1)` に改善します。 + - **方法C(再帰)を選ばなかった理由**:再帰は「関数の呼び出し履歴」をスタック(=後入れ先出しで積み上がるメモリ領域)に積んでいくため、文字数が多いとスタックオーバーフロー(=スタック領域が溢れてクラッシュすること)を起こす危険があります。今回の制約では最大10万回以上の再帰呼び出しが発生しうるため、リスクが高いと判断しました。 + - 方法Aは時間計算量 `O(n)`・空間計算量 `O(1)` という**両方の指標で最も優れている**うえに、実装もシンプルで型安全性も損なわれません。 + +- **TypeScript特有の最適化ポイント**: + - コンパイル時の型チェックにより、`s: string` という引数の型が保証されるため、実行時に型が違う値が渡ってきた場合のみ `TypeError` を投げる最小限のチェックで済みます。 + - `charCodeAt` の戻り値は `number` 型として推論されるため、文字コードによる比較処理を型安全に書けます。 + - 今回の問題では入力型が `string` に固定されているため、ジェネリクス `` は使わず、あえてシンプルな関数シグネチャ(=関数の入出力の型の宣言)にすることで、可読性を優先しています。 + +> 📖 このセクションで登場した用語 +> +> - **スタックオーバーフロー**:関数呼び出しの履歴を積むメモリ領域(コールスタック)が溢れてプログラムがクラッシュする現象 +> - **関数シグネチャ**:関数の名前・引数の型・戻り値の型をまとめた宣言部分のこと + +--- + +## 4. 実装コード + +> 💡 コード全体を示す前に、まず大まかな構造(骨格)を確認します。 + +1. まず入力が本当に `string` 型かどうかを検証する(型ガード) +2. 左ポインタ `left` を文字列の先頭に、右ポインタ `right` を末尾に置く +3. `left < right` の間、以下を繰り返す + - 左ポインタが英数字を指すまで右方向に進める + - 右ポインタが英数字を指すまで左方向に進める + - 両ポインタの文字を小文字化して比較し、違えば即座に `false` を返す + - 両ポインタを1つずつ内側に動かす +4. ループを抜けたら(=すべての比較が一致した)`true` を返す + +```typescript +/** + * 与えられた文字列が「英数字だけ・小文字統一」で回文かどうかを判定する + * @param s - 判定対象の文字列(大文字・記号・スペースを含んでよい) + * @returns 回文であれば true、そうでなければ false + * @throws {TypeError} s が string 型でない場合 + * @complexity Time: O(n), Space: O(1) + */ +function isPalindrome(s: string): boolean { + // 型ガード:s が本当に string かどうかを実行時にも確認する。 + // TypeScript のコンパイル時チェックだけに頼ると、 + // JSONやAPIレスポンスなど「型情報を失った値」が紛れ込んだ場合に + // 予期しないクラッシュにつながるため、防御的に確認しておく。 + if (typeof s !== 'string') { + throw new TypeError('Input must be a string'); + } + + // 文字コード(charCode)から「英数字かどうか」を判定するヘルパー。 + // 正規表現(regex)を使う方法もあるが、1文字ずつ呼び出す場合は + // 文字コードの数値比較の方が呼び出しコストが軽いため、こちらを採用する。 + const isAlphanumeric = (code: number): boolean => { + // '0'-'9' の範囲(文字コード48〜57) + const isDigit = code >= 48 && code <= 57; + // 'A'-'Z' の範囲(文字コード65〜90) + const isUpper = code >= 65 && code <= 90; + // 'a'-'z' の範囲(文字コード97〜122) + const isLower = code >= 97 && code <= 122; + return isDigit || isUpper || isLower; + }; + + // 大文字の文字コードを小文字の文字コードに変換するヘルパー。 + // 'A'(65) と 'a'(97) の差は常に32なので、大文字の範囲内であれば + // +32するだけで小文字化できる(toLowerCase() を呼ぶより軽量)。 + const toLowerCode = (code: number): number => { + const isUpper = code >= 65 && code <= 90; + return isUpper ? code + 32 : code; + }; + + // 左ポインタは文字列の先頭、右ポインタは文字列の末尾に置く。 + // ここで新しい配列や文字列を作らないため、追加メモリは + // この2つの数値変数だけで済む(空間計算量 O(1) の理由)。 + let left = 0; + let right = s.length - 1; + + // 2つのポインタがすれ違う(left >= right)まで比較を続ける。 + // すれ違った時点で「中央まで比較し終えた」ことを意味する。 + while (left < right) { + // 左側が非英数字(記号・スペースなど)の間は読み飛ばす。 + // left < right の条件も一緒に確認するのは、 + // 記号だけの文字列(例:" ,,,")でポインタが末尾を超えないようにするため。 + while (left < right && !isAlphanumeric(s.charCodeAt(left))) { + left++; + } + + // 右側も同様に、非英数字の間は読み飛ばす。 + while (left < right && !isAlphanumeric(s.charCodeAt(right))) { + right--; + } + + // 両方とも英数字にたどり着いたら、小文字化して比較する。 + // 一致しなければ、その時点で回文ではないと確定するため、 + // 残りの処理を続ける意味がなく、即座に false を返す(早期リターン)。 + if (toLowerCode(s.charCodeAt(left)) !== toLowerCode(s.charCodeAt(right))) { + return false; + } + + // 一致していれば、両ポインタをそれぞれ内側に1つ進めて次の比較に備える。 + left++; + right--; + } + + // ループを最後まで抜けられた(不一致が一度もなかった)ということは、 + // 文字列全体が回文であることが確認できたことを意味する。 + return true; +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> 入力例 `s = "A man, a plan, a canal: Panama"` を使って、主要なステップを追ってみます。 + +``` +初期状態: left=0 ('A'), right=30 ('a') +Step 1: left=0 は英数字 'A' → スキップ不要 +Step 2: right=30 は英数字 'a' → スキップ不要 +Step 3: 'A' を小文字化 → 'a'、right側 'a' と比較 → 一致 +Step 4: left=1, right=29 に移動 +Step 5: left=1 は ' '(スペース)→ 非英数字なので left++ → left=2 ('m') +Step 6: right=29 は 'm' → 英数字なのでそのまま +Step 7: 'm' vs 'm' → 一致 → left=3, right=28 +... +(中略:同様の手順で "man" と "nam" 、"a" と "a" の比較が続く) +... +Step N: left と right がすれ違う(left >= right)→ ループ終了 +Step N+1: 一度も不一致がなかったため true を返す +``` + +> 📖 このセクションで登場した用語 +> +> - **readonly**:変数の値を変更できないようにする修飾子。今回は `s` 自体を書き換える必要がないため使っていないが、配列やオブジェクトを扱う際は「意図せぬ書き換え」をコンパイル時に防げる +> - **文字コード(charCode)**:文字をコンピュータが扱える数値として表したもの。例えば `'A'` は65、`'a'` は97 +> - **Pure function(純粋関数)**:同じ入力 `s` に対して必ず同じ結果を返し、外部の変数を変更しない関数。テストしやすく、バグの原因を追いやすいという利点がある + +--- + +## 5. TypeScript固有の最適化観点 + +【業務開発版】として今回のコードを見た場合、`typeof s !== 'string'` という型ガードにより、コンパイル時の型チェックをすり抜けてきた不正な値(例えば実行時に `null` が渡ってきた場合など)にも対応できる作りになっています。また、`isAlphanumeric` と `toLowerCode` という小さな関数に処理を分割したことで、それぞれが「何をする関数か」を名前から読み取りやすく、保守性が高まっています。 + +【競技版】として見た場合、正規表現(`/[a-z0-9]/i` など)を使わずに文字コードの数値比較で判定しているため、1文字あたりの処理コストが軽く、`2×10^5` 文字という制約下でも高速に動作します。また `filter` や `split` のような中間配列を一切作らないため、メモリ確保(=アロケーション)の回数も最小限に抑えられています。 + +- **コンパイル時の型チェック**: `s: string` という型注釈により、呼び出し側が誤って `number` や `object` を渡した場合、実行前(コンパイル時)にエラーとして検出できます。 +- **型推論の活用**: `left`、`right`、`isAlphanumeric` の戻り値などは明示的な型注釈を書かなくても、TypeScriptが文脈から自動的に `number` や `boolean` と判断してくれるため、コードがすっきりします。 +- **readonlyの適用余地**: もし本問題を「複数の文字列をまとめて判定する」ように拡張するなら、引数を `readonly string[]` とすることで、配列の中身を誤って書き換えてしまうミスをコンパイル時に防げます。 + +> 📖 このセクションで登場した用語 +> +> - **アロケーション(allocation)**:プログラムがメモリ領域を新しく確保すること。回数が多いほど処理が遅くなりやすい +> - **型注釈**:変数や関数の引数・戻り値に対して、どんな型のデータかを明示的に書くこと(例:`s: string`) diff --git a/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README.md b/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README.md new file mode 100644 index 00000000..e32a42c6 --- /dev/null +++ b/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README.md @@ -0,0 +1,389 @@ +# Single Number - XORで"はぐれ者"を一撃で見つける + +## 目次 + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +💡 この章では、問題が何を求めているのかを一言で言い換え、なぜこの問題が興味深いのかを説明します。ここを理解しておくと、後続の章でXORという少し変わった発想を使う理由が腑に落ちやすくなります。 + +この問題は、一言で言うと「配列の中からペアを組んでいない“はぐれ者”の数字を1つだけ見つける問題」です。配列 `nums` の中では、1つの数字を除いてすべての数字がちょうど2回ずつ登場します。その「1回しか登場しない数字」を答えとして返す必要があります。 + +この問題が難しく感じられるポイントは、制約にあります。 + +- `1 <= nums.length <= 3 * 10^4` +- `-3 * 10^4 <= nums[i] <= 3 * 10^4` +- **必ず線形時間(O(n))で解くこと** +- **必ず定数空間(O(1))だけを使うこと** + +直感的には「辞書(=キーと値をセットで記録できる構造)で出現回数を数えればいい」と考えがちですが、それだと辞書の分だけ追加のメモリ(O(n))を使ってしまい、「定数空間」という制約(=入力として与えられる値の範囲や条件のこと。ここでは"追加メモリはO(1)まで"という条件)に違反してしまいます。この制約こそが、XOR(排他的論理和)という一見遠回りに見える手法を使う最大の理由です。 + +> 📖 この章で登場した用語 +> +> - **制約**:入力として与えられる値の範囲や条件のこと。例:「配列の長さは1以上3万以下」 +> - **正当性**:アルゴリズムが常に正しい答えを返すことの保証 +> - **定数空間**:入力サイズに関係なく一定量のメモリしか使わないこと + +--- + +

アルゴリズム要点(TL;DR)

+ +💡 TL;DR(Too Long; Didn't Read=長くて読めない人向けの要約という意味の略語)とは、細かい理由は後回しにして「なんとなくこういう手順で解くんだな」というイメージを掴むための章です。詳しい理由は後の章で1つずつ説明します。 + +- **戦略**:配列を1回だけ走査(=先頭から末尾まで1周すること)しながら、XOR(排他的論理和=2つのビットを比較し、違っていれば1、同じなら0を返す演算)を使って全要素を1つの値にまとめていきます。なぜXORを選ぶかというと、「同じ数字同士をXORすると0になる」という性質が、まさに「2回登場する数字を打ち消し合わせる」という今回のゴールにぴったり合うからです。 +- **データ構造**:追加のデータ構造は一切使いません。`int`型の変数1つ(`result`)だけで完結します。辞書や配列を新たに作らないことで、空間計算量をO(1)に抑えられます。 +- **計算量**:時間計算量 O(n)、空間計算量 O(1)。制約が要求する条件を過不足なく満たします。 +- **メモリ**:`result`という変数1つ分のメモリしか消費しません。入力サイズが3万でも300万でも、使うメモリは変わりません。 + +> 📖 この章で登場した用語 +> +> - **TL;DR**:「長くて読めない人向けの要約」を意味する略語 +> - **XOR(排他的論理和)**:2つのビットを比較し、違っていれば1、同じなら0を返す演算 +> - **走査**:データ構造の要素を先頭から順番に見ていく操作 + +--- + +

図解

+ +💡 この章では、アルゴリズムの処理の流れを図で確認します。Mermaidフローチャートでは、ひし形(`{}`で囲まれた形)は「条件分岐」を、長方形(`[]`で囲まれた形)は「処理ステップ」を表します。上から下へ矢印をたどりながら読み進めてください。 + +### フローチャート + +この図は、`singleNumber`関数が呼び出されてから結果を返すまでの処理の流れを表しています。上から下へ読み進めてください。 + +```mermaid +flowchart TD + Start[Start singleNumber] --> Validate{Is nums a valid list} + Validate -- No --> RaiseType[Raise TypeError] + Validate -- Yes --> CheckEmpty{Is nums empty} + CheckEmpty -- Yes --> RaiseValue[Raise ValueError] + CheckEmpty -- No --> Init[Set result to 0] + Init --> LoopCheck{More elements remain} + LoopCheck -- Yes --> XorStep[XOR result with current element] + XorStep --> LoopCheck + LoopCheck -- No --> Return[Return result] +``` + +主要なノードの意味: + +- `Start[Start singleNumber]`:関数の入り口。引数`nums`を受け取る +- `Validate{Is nums a valid list & elements are ints}`:`isinstance(nums, list)`でリストであることを確認し、各要素が `int` (`bool` 除外)であるかを検証する条件分岐 +- `RaiseType[Raise TypeError]`:`nums`がリストでない場合、またはリスト内に非整数要素が含まれる場合に `TypeError` をスローする処理 +- `CheckEmpty{Is nums empty}`:配列が空かどうかを確認する条件分岐 +- `RaiseValue[Raise ValueError]`:空配列の場合にエラーを投げる処理 +- `Init[Set result to 0]`:累積変数`result`を0で初期化するステップ +- `LoopCheck{More elements remain}`:まだ走査していない要素が残っているかを判定する条件分岐 +- `XorStep[XOR result with current element]`:現在の要素を`result`にXORで重ねる処理 +- `Return[Return result]`:走査が終わったら`result`を返す最終ステップ + +### データフロー図 + +この図は、入力データがどのように変換されて最終的な出力になるかを表しています。 + +```mermaid +graph LR + subgraph Precheck + A[Input nums] --> B[Validate type] + B --> C[Validate not empty] + end + subgraph Core + C --> D[Initialize result to 0] + D --> E[Iterate and XOR each element] + end + E --> F[Output single number] +``` + +主要な流れの説明: + +- `Precheck`(入力→型検証→空チェック):入力が「本当に処理してよいデータか」を確認する前処理段階 +- `Core`(初期化→XOR走査):実際にアルゴリズムの本体が動く段階。ここで答えが計算される +- `Core`から`F`へ:計算済みの`result`をそのまま出力として返す + +> 💡 **代表例でのトレース**:`nums = [4, 1, 2, 1, 2]` を入力として、上記フローチャートの各ノードをどのように通過するかを示します。 + +``` +Step 1: Start → nums = [4, 1, 2, 1, 2] を受け取る +Step 2: Validate → isinstance(nums, list) は True → Yes +Step 3: CheckEmpty → len(nums) == 0 は False → No +Step 4: Init → result = 0 +Step 5: LoopCheck → 要素が残っている → Yes +Step 6: XorStep → result = 0 ^ 4 = 4 +Step 7: LoopCheck → 残っている → Yes → XorStep → result = 4 ^ 1 = 5 +Step 8: LoopCheck → 残っている → Yes → XorStep → result = 5 ^ 2 = 7 +Step 9: LoopCheck → 残っている → Yes → XorStep → result = 7 ^ 1 = 6 +Step 10: LoopCheck → 残っている → Yes → XorStep → result = 6 ^ 2 = 4 +Step 11: LoopCheck → 要素がなくなった → No +Step 12: Return → result = 4 を返す +結果: 4 +``` + +> 📖 この章で登場した用語 +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理 +> - **データフロー図**:データがどのように変換・移動するかを示す図 +> - **サブグラフ**:フローチャートの中で関連する処理をグループ化したもの + +--- + +

正しさのスケッチ

+ +💡 この章では、「なぜこのアルゴリズムが常に正しい答えを返すと言えるのか」を、数学的な証明ではなく直感的な根拠として整理します。 + +- **不変条件(=アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件)**:ループの各ステップが終わった時点で、`result`は「それまでに走査した要素をすべてXORで重ねた値」になっています。XORには交換法則・結合法則(=計算の順番を入れ替えたり、まとめる位置を変えても結果が変わらない性質)が成り立つため、要素をどの順番でXORしても最終結果は変わりません。 +- **網羅性(=すべてのケースをもれなく処理できているという保証)**:`for num in nums`というforループは、配列の全要素を1つも飛ばさずに処理します。したがって、2回登場する数字は必ずどこかで2回XORされ、`a ^ a = 0`によって打ち消されます。 +- **基底条件(=再帰の終了条件。これがないと無限ループになる)**:この問題は再帰ではなくループですが、対応する概念として「ループの開始時点」があります。`result = 0`という初期値は、`0 ^ a = a`という性質により「まだ何も処理していない状態」を正しく表現しています。例えば`nums = [1]`のとき、ループは1回しか回らず`result = 0 ^ 1 = 1`となり、直感通り「唯一の要素がそのまま答えになる」ことが確認できます。 +- **終了性(=アルゴリズムが必ず有限ステップで終わるという保証)**:`nums`の長さは制約により最大でも`3 * 10^4`個であり、forループは配列の長さ分だけ回れば必ず終了します。無限ループになる要素は存在しません。 + +> 📖 この章で登場した用語 +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +> - **基底条件**:再帰やループの出発点となる初期状態の条件 +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 +> - **網羅性**:すべてのケースをもれなく処理できているという保証 + +--- + +

計算量

+ +💡 計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。まずBig-O記法の読み方を確認してから、この問題の計算量を見ていきます。 + +| 記法 | 意味 | 直感的なイメージ | +| ------------ | ---------------------- | ------------------------------ | +| `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +| `O(n)` | 入力に比例して増加 | リストを端から順に読む | +| `O(n log n)` | nよりやや速く増加 | 辞書を二分探索で引く×n回 | +| `O(n²)` | 入力の2乗で増加 | 全ペアを総当たりで確認する | + +この問題の計算量は以下の通りです。 + +- **時間計算量**: O(n) — 配列を1回だけ走査するため、要素数に比例した時間がかかります +- **空間計算量**: O(1) — `result`という変数1つ分のメモリしか使わないため、入力サイズによらず一定です + +**in-place vs Pure の比較** + +| 観点 | in-place(=新しいメモリを確保せず、元のデータ構造を直接書き換える操作) | Pure(=入力を変更せず新しいデータを返す操作) | +| ---------------- | ------------------------------------------------------------------------ | ---------------------------------------------- | +| 入力配列への影響 | 今回の実装では`nums`自体は書き換えない | 今回の実装は入力を一切変更しないPureな関数 | +| 追加メモリ | O(1)(`result`のみ) | O(1)(同上) | +| 安全性 | 呼び出し元の配列を壊す心配がない | 同上。今回はどちらの性質も同時に満たしている | + +> 📖 この章で登場した用語 +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **in-place**:新しいメモリを確保せず元のデータを直接書き換える操作。空間計算量を抑えられる +> - **Pure**:入力を変更せず新しいデータを返す操作。安全だがメモリを余分に使う + +--- + +

Python 実装

+ +💡 コードを読む前に、実装の全体的な骨格を確認します。 + +1. `isinstance()`で`nums`が本当に`list`かどうかを検証する(実行時の型安全性を確保するため) +2. `all()` + ジェネレータ式で、全要素が`int`型かどうかを検証する +3. 配列が空でないことを確認する(制約上は保証されているが、防御的に確認する) +4. 累積変数`result`を`0`で初期化する(`0 ^ a = a`という性質を使うため) +5. forループで配列を1回だけ走査し、各要素を`result`にXORで重ねていく +6. 走査が終わったら`result`を返す + +コード内で使う型ヒント(=関数の引数や戻り値に型を注釈として書く仕組み)は`List[int]`のみです。「`int`のリスト」を表し、pylance(VSCodeで使える静的型チェックツール)が呼び出し側の間違いを実行前に検出できるようにします。 + +```python +from __future__ import annotations + +from typing import List + + +class Solution: + """ + 136. Single Number 解決クラス + + 配列内で1回だけ登場する数値を、XOR(排他的論理和)を使って + O(n)時間・O(1)空間で見つける。 + """ + + def singleNumber(self, nums: List[int]) -> int: + """ + 配列内で1回だけ登場する数値を返す。 + + Args: + nums: 1回だけ登場する要素が1つ含まれる整数配列。 + それ以外の要素はすべてちょうど2回登場する。 + + Returns: + 1回だけ登場する整数。 + + Raises: + TypeError: nums がリストでない、または要素に int 以外が含まれる場合。 + ValueError: nums が空の場合。 + """ + # isinstance() で型チェックする。 + # Python は動的型付け(=実行するまで型が確定しない仕組み)のため、 + # 呼び出し元が誤った型を渡しても実行時まで気づけない。 + # pylance と組み合わせることで、実行前にも型の誤りを検出できるようにする。 + if not isinstance(nums, list): + raise TypeError("Input must be a list") + + # all() + ジェネレータ式(=値を必要なときだけ1つずつ生成する仕組み。 + # リストのように全要素を一度にメモリへ展開しないためメモリ効率が良い)で + # 全要素が int かどうかを1行でチェックする。 + # for ループを自分で回すより可読性が高く、all() 自体も C 実装のため高速。 + if not all(isinstance(x, int) for x in nums): + raise TypeError("All elements must be integers") + + # 制約上は長さ1以上が保証されているが、防御的プログラミング + # (=想定外の入力にも備えて壊れないようにする書き方)として + # 空リストを明示的に弾いておく。 + if len(nums) == 0: + raise ValueError("Input list must not be empty") + + # result を「これまでXORを重ねた累積値」として使う。 + # 初期値を 0 にするのは、0 ^ a = a という性質があり、 + # 最初の要素をそのまま取り込めるようにするため。 + result: int = 0 + + # 配列を1回だけ走査する(for ループは Python で最も基本的な反復方法)。 + # 各要素を result に XOR で重ねていく。 + # 同じ数字が2回現れると a ^ a = 0 で打ち消し合い、 + # 最終的にペアのない「はぐれ者」の数字だけが result に残る。 + for num in nums: + result ^= num + + # すべての走査が終わった時点で、result には + # ペアを持たない唯一の数値が残っている。 + return result +``` + +> 💡 **コードの動作トレース**:入力 `nums = [4, 1, 2, 1, 2]` を例に、各ステップで変数がどう変化するかを示します。 + +``` +呼び出し: singleNumber([4, 1, 2, 1, 2]) + +Step 1: isinstance(nums, list) → True → 検証通過 +Step 2: all(isinstance(x, int) for x in nums) → True → 検証通過 +Step 3: len(nums) == 0 → False → 空リストではない +Step 4: result = 0 で初期化 + +Step 5: num = 4 → result = 0 ^ 4 = 4 +Step 6: num = 1 → result = 4 ^ 1 = 5 +Step 7: num = 2 → result = 5 ^ 2 = 7 +Step 8: num = 1 → result = 7 ^ 1 = 6 (1が2回目 → 打ち消し合いの途中経過) +Step 9: num = 2 → result = 6 ^ 2 = 4 (2が2回目 → 打ち消し合い完了) + +走査終了 → result = 4 を返す +最終結果: 4 +``` + +> 📖 この章で登場した用語 +> +> - **`from __future__ import annotations`**:型ヒントを文字列として扱うようにする宣言。前方参照(まだ定義されていないクラス名などを型ヒントで使うこと)を解決できる +> - **動的型付け**:実行するまで変数の型が確定しない仕組み。Python はこの方式を採用している +> - **防御的プログラミング**:想定外の入力にも備えて、プログラムが壊れないようにあらかじめ対処を書いておく考え方 +> - **`List[int]`**:「int のリスト」を表す型ヒント + +--- + +

CPython最適化ポイント

+ +💡 この章では「同じ処理でも Python の書き方によって速さが変わる理由」を説明します。最適化前のコード → 最適化後のコード → なぜ速くなるか、の3点セットで見ていきます。 + +```python +# 最適化前:明示的な for ループで result を更新する +result = 0 +for num in nums: + result ^= num + +# 最適化後:functools.reduce と operator.xor を使い、ループ制御をC実装に任せる +from functools import reduce +from operator import xor + +result = reduce(xor, nums, 0) + +# なぜ速いか: +# reduce のループ制御自体が C 言語で実装されているため、 +# Python インタープリタが1行ずつバイトコードを解釈する回数を減らせる。 +# ただし可読性は for ループの方が高いため、 +# 業務開発版では for ループ、競技プログラミング版では reduce を使い分けるとよい。 +``` + +もう1つの観点として、XOR演算子`^`自体はCPythonのバイトコードレベルで直接処理される軽量な演算です。ハッシュマップを使う方法(`Counter`など)と比べると、辞書のハッシュ計算や衝突解決といった内部処理が一切発生しないため、単純な整数演算だけで完結するXOR方式は非常に軽量です。 + +> 📖 この章で登場した用語 +> +> - **属性アクセス**:`self.x`のようにオブジェクトのプロパティを参照する操作。Pythonでは内部的に辞書検索が発生する +> - **`functools.reduce`**:配列の全要素を順番に処理しながら1つの値にまとめていく関数。C実装 +> - **バイトコード**:Pythonのソースコードをコンパイルした中間表現。CPythonのインタープリタはこれを1つずつ解釈して実行する + +--- + +

エッジケースと検証観点

+ +💡 エッジケース(=空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと)を見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。それぞれのケースがなぜ問題になりうるかを確認します。 + +- **要素数が1個の場合**(`nums = [1]`):ループは1回しか回らず、`result = 0 ^ 1 = 1`となります。ペアがなくてもXORの初期値`0`が正しく機能するため、問題なく動作します。 +- **負の数が含まれる場合**(`nums = [-1, -1, -2]`):Pythonの整数は符号付きビット表現を内部で適切に扱うため、負の数同士でも`a ^ a = 0`という性質は変わらず成り立ちます。特別な分岐は不要です。 +- **入力サイズが上限に近い場合**(要素数が`3 * 10^4`個に近い):O(n)アルゴリズムのため、処理時間は要素数に比例するだけで、極端に遅くなることはありません。 +- **重複の並び順がバラバラな場合**(例:`[1, 2, 1, 2, 4]`のように隣接していないペア):XORの交換法則・結合法則(=計算の順番を入れ替えても結果が変わらない性質)により、ペアが配列内でどこに配置されていても正しく打ち消し合います。 +- **型が不正な場合**(`nums`がリストでない、または要素に`int`以外が含まれる):`isinstance`チェックにより`TypeError`が送出され、原因不明のクラッシュを防ぎます。 + +> 📖 この章で登場した用語 +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件の入力 +> - **境界値**:制約の上限・下限にあたる値 +> - **符号付きビット表現**:負の数をコンピュータ内部でビット列として表現する方式 + +--- + +

FAQ

+ +💡 この章では、初学者がつまずきやすいポイントを想定した質問と回答をまとめます。各回答は「結論 → 理由 → 補足」の順で書かれています。 + +**Q1. なぜハッシュマップ(辞書)で出現回数を数える方法ではダメなのですか?** + +結論:ハッシュマップ方式は動作こそ正しいものの、この問題が要求する「定数空間(O(1))」という制約に違反してしまうため採用しませんでした。 + +理由:ハッシュマップに要素を記録すると、配列の要素数に比例したメモリ(O(n))を使ってしまいます。問題文には「must implement a solution with a linear runtime complexity and use only constant extra space」と明記されており、追加メモリはO(1)でなければなりません。 + +補足:例えば`nums`の要素数が3万個ある場合、ハッシュマップ方式では最大で3万個分のキーと値を記録する可能性がありますが、XOR方式では常に`result`という変数1つ分のメモリしか使いません。 + +--- + +**Q2. XORの交換法則・結合法則という性質のイメージがつかめません。もう一度具体例で説明してください。** + +結論:XORは「足し算のように、どの順番で計算しても結果が変わらない」演算です。 + +理由:交換法則とは`a ^ b = b ^ a`が成り立つこと、結合法則とは`(a ^ b) ^ c = a ^ (b ^ c)`が成り立つことを指します。これにより、配列の要素をどんな順番でXORしても最終結果は同じになります。 + +補足:`nums = [1, 2, 1, 2, 4]`という配列を例にすると、`1 ^ 2 ^ 1 ^ 2 ^ 4`という計算は、順番を入れ替えて`(1 ^ 1) ^ (2 ^ 2) ^ 4 = 0 ^ 0 ^ 4 = 4`と考えることもできます。実際のコードは配列の先頭から順番に処理するだけですが、数学的にはどの順番で計算しても答えは`4`になります。 + +--- + +**Q3. なぜ`functools.reduce`版の方が速いと言われるのですか?実務でも使うべきですか?** + +結論:`reduce`版はループ制御がC実装であるため、理論上はわずかに高速になる場合がありますが、実務では明示的な`for`ループを使うことをおすすめします。 + +理由:`reduce`はPythonインタープリタがバイトコードを1行ずつ解釈する回数を減らせるため、CPython内部の処理としては効率的です。しかし、`reduce(xor, nums, 0)`という1行は、初めて読む人にとって「何をしているか」が直感的にわかりにくく、チーム開発では可読性の方が優先されることが多いためです。 + +補足:本README内の実装では、業務開発向けには`for`ループを使ったコードのみを掲載していますが、競技プログラミングの場面では`reduce(xor, nums, 0)`という1行で書くことも可能です。 + +--- + +**Q4. 負の数が含まれる場合でも本当に正しく動きますか?** + +結論:はい、正しく動きます。負の数に対して特別な処理を追加する必要はありません。 + +理由:XOR演算はビット単位(=0か1で表される情報の最小単位ごと)で行われる演算であり、Pythonの整数は内部的に符号を適切に扱う仕組みを持っています。そのため`a`が負の数であっても`a ^ a = 0`という性質は変わらず成り立ちます。 + +補足:例えば`nums = [-1, -1, -2]`の場合、`-1 ^ -1 = 0`となり、最終的に`0 ^ -2 = -2`が返されます。これは「`-2`が1回しか登場しない」という入力の意図と一致します。 diff --git a/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README_React.html b/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README_React.html new file mode 100644 index 00000000..fdf063b8 --- /dev/null +++ b/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README_React.html @@ -0,0 +1,1408 @@ + + + + + + LeetCode 136. Single Number - XORで学ぶ1パス走査 + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「配列の中でペアを組んでいない、1回だけ登場する数字を見つける問題」 +

+

+ 配列 + nums + の要素はほとんどが2回ずつ登場し、1つだけ1回しか登場しない要素が混ざっています。それを効率よく見つけ出すのがゴールです。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + ハッシュテーブル(=キーと値をセットで記録できる辞書のような構造)で出現回数を数えると、配列の要素数に比例した追加メモリ(O(n))を使ってしまい、「定数空間(O(1))」という制約に違反します。 +
  • +
  • + 配列をソートしてから隣同士を比較する方法は、ソート自体にO(n log + n)の時間がかかり、「線形時間(O(n))」という制約に違反します。 +
  • +
+
+ +
+
+
O(n)
+
時間計算量
+
+
+
O(1)
+
空間計算量
+
+
+
Easy
+
難易度
+
+
+
XOR
+
キーとなる演算
+
+
+ +
+
+

nums = [2, 2, 1]

+

出力: 1

+

+ 2は2回登場して打ち消し合うため、1回しか登場しない1が答えになります。 +

+
+
+

nums = [4, 1, 2, 1, 2]

+

出力: 4

+

+ 1と2はそれぞれ2回登場して打ち消し合うため、1回しか登場しない4が答えになります。このページのステップ解説では、この例を使って動作を追跡します。 +

+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 入力例 + nums = [4, 1, 2, 1, 2] + を使って、result 変数がどう変化していくかを1ステップずつ確認しましょう。 +

+
+
+ + +
+

+ Python実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. nums が list かどうか、要素がすべて int かどうかを検証する
  2. +
  3. + 配列が空でないことを確認する(制約上は保証されているが、防御的に確認する) +
  4. +
  5. result を 0 で初期化し、1回のループで各要素を result にXORで重ねる
  6. +
  7. 走査が終わったら result を返す
  8. +
+
+ +
from __future__ import annotations
+
+from typing import List
+
+
+class Solution:
+    """
+    136. Single Number 解決クラス
+
+    配列内で1回だけ登場する数値を、XOR(排他的論理和)を使って
+    O(n)時間・O(1)空間で見つける。
+    """
+
+    def singleNumber(self, nums: List[int]) -> int:
+        """
+        配列内で1回だけ登場する数値を返す。
+
+        Args:
+            nums: 1回だけ登場する要素が1つ含まれる整数配列。
+                  それ以外の要素はすべてちょうど2回登場する。
+
+        Returns:
+            1回だけ登場する整数。
+
+        Raises:
+            TypeError: nums がリストでない、または要素に int 以外が含まれる場合。
+            ValueError: nums が空の場合。
+        """
+        # isinstance() で型チェックする。
+        # Python は動的型付け(=実行するまで型が確定しない仕組み)のため、
+        # 呼び出し元が誤った型を渡しても実行時まで気づけない。
+        if not isinstance(nums, list):
+            raise TypeError("Input must be a list")
+
+        # all() + ジェネレータ式で全要素が int かどうかを1行でチェックする。
+        if not all(isinstance(x, int) for x in nums):
+            raise TypeError("All elements must be integers")
+
+        # 制約上は長さ1以上が保証されているが、防御的プログラミングとして
+        # 空リストを明示的に弾いておく。
+        if len(nums) == 0:
+            raise ValueError("Input list must not be empty")
+
+        # result を「これまでXORを重ねた累積値」として使う。
+        # 初期値を 0 にするのは、0 ^ a = a という性質があるため。
+        result: int = 0
+
+        # 配列を1回だけ走査し、各要素を result に XOR で重ねていく。
+        # 同じ数字が2回現れると a ^ a = 0 で打ち消し合い、
+        # 最終的にペアのない「はぐれ者」の数字だけが result に残る。
+        for num in nums:
+            result ^= num
+
+        return result
+
+ +
+

+ ▶ 入力例 nums = [4, 1, 2, 1, 2] での動作トレース +

+
+初期状態: result = 0
+
+num = 4 → result = 0 ^ 4 = 4
+num = 1 → result = 4 ^ 1 = 5
+num = 2 → result = 5 ^ 2 = 7
+num = 1 → result = 7 ^ 1 = 6   (1が2回目 → 打ち消し合いの途中経過)
+num = 2 → result = 6 ^ 2 = 4   (2が2回目 → 打ち消し合い完了)
+
+走査終了 → result = 4 を返す
+
+
+ + +
+

+ 処理フローチャート +

+ +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=正常な進行/成功 + 赤=検証エラー +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + 開始 + + + nums を受け取る + + + + + + + + + 入力を検証 + + + 型・空をチェック + + + + + + いいえ + + + + + + 検証エラー + + + 例外を送出 + + + + + + はい + + + + + + 初期化 + + + result = 0 + + + + + + + + + 走査ループ + + + 要素は残っているか + + + + + + はい + + + + + + XOR適用 + + + result^=num + + + + + + 次の要素へ(ループ) + + + + + + いいえ(走査完了) + + + + + + 終了 + + + result を返す + + +
+ +
+

+ 🔎 入力例 nums = [4, 1, 2, 1, 2] でのフロー追跡 +

+
    +
  1. 「開始」ノード → nums = [4, 1, 2, 1, 2] を受け取る
  2. +
  3. + 「入力を検証」ひし形 → list型・全要素int・空でないため「はい」の経路へ +
  4. +
  5. 「初期化」ノード → result = 0 にセット
  6. +
  7. + 「走査ループ」ひし形 → + 要素が残っているので「はい」の経路へ、XOR適用を5回繰り返す(result は 4 + → 5 → 7 → 6 → 4 と変化) +
  8. +
  9. + 「走査ループ」ひし形 → + すべての要素を処理し終えたので「いいえ(走査完了)」の経路へ +
  10. +
  11. 「終了」ノード → result = 4 を返す
  12. +
+
+ +

+ フローの説明:
+ このアルゴリズムは「検証 → 初期化 → + ループ」というシンプルな3段構成です。ループ部分だけが配列の要素数に応じて繰り返され、それ以外は1回きりの処理です。ループが要素数分だけ回ることが、時間計算量がO(n)になる直接の理由です。 +

+
+ + +
+

+ 計算量分析 +

+ +
+

+ 📖 Big-O 記法の読み方(入力サイズ n + が大きくなるにつれて処理時間がどう増えるかの目安) +

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ nより少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間空間備考
Counterで出現回数を数えるO(n)O(n)追加メモリが制約に違反
sortedで隣同士を比較O(n log n)O(n)ソート自体が線形時間の制約に違反
+ XOR + forループ(採用) + O(n)O(1)制約に完全一致・可読性も高い
XOR + functools.reduceO(n)O(1)競技プログラミング向けの簡潔版
+
+ +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 配列を1回だけ走査するため、時間計算量はO(n)になります。XORの結果を保持する + result + という変数1つしか使わないため、空間計算量は入力サイズに関係なく常にO(1)です。ハッシュテーブルを使う方法はO(n)の追加メモリを必要とし、ソートを使う方法はO(n + log + n)の時間がかかるため、この問題の制約(線形時間・定数空間)を同時に満たせるのはXOR方式だけです。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。 +

+
+
+ + インデックス + +
+ 配列の中で各要素の位置を表す番号のこと。多くのプログラミング言語では0から数え始める。 +
+
+
+ + XOR(排他的論理和) + +
+ 2つのビットを比較し、違っていれば1、同じなら0を返す演算。同じ値同士をXORすると必ず0になる性質があり、この問題ではこの性質を使ってペアの数字を打ち消し合わせている。 +
+
+
+ + 型ヒント + +
+ 関数の引数や戻り値に型を注釈として書く仕組み。def f(x: int) -> str: + のように書く。pylanceのような静的解析ツールがバグを実行前に検出できるようになる。 +
+
+
+ + 交換法則・結合法則 + +
+ 計算の順番を入れ替えたり、まとめる位置を変えても結果が変わらない性質。XORはこの2つの法則が成り立つため、配列の要素をどの順番でXORしても最終結果は同じになる。 +
+
+
+ + 定数空間 + +
+ 入力サイズに関係なく一定量のメモリしか使わないこと。O(1)の空間計算量とも呼ばれる。 +
+
+
+ + 動的型付け + +
+ 実行するまで変数の型が確定しない仕組み。Pythonが採用している方式で、誤った型を渡しても実行時まで気づけないという特徴がある。 +
+
+
+ + ハッシュテーブル + +
+ キーと値をセットで記録できる辞書のようなデータ構造。図書館の索引カードのように、キーからすぐに値を探せる。Pythonでは + dict や + Counter + がこれに相当する。 +
+
+
+ + ビット + +
+ 0か1で表される情報の最小単位。コンピュータ内部のすべての数値はビットの並びとして表現される。 +
+
+
+ + pylance + +
+ VSCodeで使えるPythonの静的型チェックツール。型ヒントをもとに、実行前にコードの誤りを検出してくれる。 +
+
+
+ + 防御的プログラミング + +
+ 想定外の入力にも備えて、プログラムが壊れないようにあらかじめ対処を書いておく考え方。 +
+
+
+
+ +
+ LeetCode 136. Single Number — algo-beginner学習用インタラクティブ解説 +
+
+ + + + + + + + + + diff --git a/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/Single_Number_Python.md b/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/Single_Number_Python.md new file mode 100644 index 00000000..dafd14ec --- /dev/null +++ b/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/Single_Number_Python.md @@ -0,0 +1,248 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: Python +> 適用ルールセット: 共通5ルール + Python固有4ルール +> 参照ファイル: references/common.md + references/python.md + +# 136. Single Number(Python版) + +--- + +## 1. 問題分析結果 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「ペアを組んでいない“はぐれ者”の数字を1つだけ見つける問題」です。配列内のほとんどの数字は2回ずつ登場していて、1つだけ1回しか出てこない数字が混ざっています。 + +CPython(=最も広く使われるPythonの実装。C言語で作られており、組み込み関数の多くがC言語実装のため高速)で解く際の注意点は、「Pythonらしい書き方(`functools.reduce`や組み込みの`^`演算子活用)を使うと、forループを自分で回すよりも高速になりやすい」という点です。一方で、`sum()`のような集計系の組み込み関数はXOR(排他的論理和)には使えないため、`functools.reduce`または明示的なループのどちらかを選ぶ必要があります。 + +### 競技プログラミング視点 + +- **制約分析**: `1 <= nums.length <= 3 * 10^4` なので、O(n)であれば最大でも3万回程度の処理で済み、余裕を持って制限時間内に収まります。 +- **最速手法**: XORを使えばBig-O的にもPython実装的にも最適な O(n)時間・O(1)空間が達成できます。 +- **メモリ最小化**: 追加の配列やハッシュマップ(=キーと値をセットで記録できる辞書のような構造)を一切使わず、変数1つだけで完結させます。 +- **CPython最適化**: `functools.reduce`と`operator.xor`を組み合わせると、Pythonのforループを自分で書くよりも内部的にC実装のループが使われるため、わずかに高速になる場合があります。 + +### 業務開発視点 + +- **型安全設計**: `nums: list[int]` という型ヒント(=関数の引数や戻り値に型を注釈として書く仕組み)を明示し、pylance(VSCodeで使える静的型チェックツール)が呼び出し側の間違いを実行前に検出できるようにします。 +- **エラーハンドリング**: 空リストや、要素の型が`int`でない場合に備えて`ValueError`や`TypeError`を投げるようにします。 +- **可読性**: docstring(=関数やクラスの先頭に書く説明文)とコメントを充実させ、「なぜXORで解けるのか」を後から読む人が理解できるようにします。 + +### Python特有分析 + +- **データ構造選択**: 追加のデータ構造は不要です。変数1つ(`int`型)だけで完結します。 +- **標準ライブラリ活用度**: `functools.reduce`と`operator.xor`を使うことで、Pure Python(=C実装を使わない素のPythonコード)のforループより内部処理をC言語レベルに近づけられます。 +- **CPython最適化度**: XOR演算子`^`はCPythonのバイトコードレベルで直接処理される軽量な演算のため、追加のオーバーヘッドがほとんどありません。 + +> 📖 このセクションで登場した用語 +> +> - **CPython**:最も広く使われるPythonの実装。組み込み関数の多くがC言語実装のため高速 +> - **XOR(排他的論理和)**:2つのビットを比較し、違っていれば1、同じなら0を返す演算 +> - **型ヒント**:関数の引数や戻り値に型を注釈として書く仕組み。`def f(x: int) -> str:`のように書く +> - **pylance**:VSCodeで使えるPythonの静的型チェックツール。実行前にバグを検出できる +> - **制約分析**:問題の入力サイズ上限から「どのくらいの計算量まで許容されるか」を逆算すること + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ」と「メモリの使いやすさ」に加えて、Python固有の観点(組み込み関数で代替できるか、C実装かPure Pythonか)も比較して最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ------------------------------ | ---------- | ---------- | ---------------- | ------ | ---------------------------------- | ------------- | ----------------------------------------------------- | +| A. `Counter`で出現回数を数える | O(n) | O(n) | 低 | ★★★ | `collections.Counter` | 適 | 追加メモリを使うため空間計算量が条件に合わない | +| B. `sorted()`で隣同士を比較 | O(n log n) | O(n) | 中 | ★★☆ | 組み込み`sorted()` | 適 | ソート自体がO(n log n)で線形時間の制約を満たさない | +| C. XOR + `for`ループ | O(n) | O(1) | 低 | ★★★ | なし(素の演算子) | 適 | 本問題の制約に完全一致、最も直感的 | +| D. XOR + `functools.reduce` | O(n) | O(1) | 低 | ★★☆ | `functools.reduce`, `operator.xor` | 適 | 関数型スタイル。forループよりわずかに高速な場合がある | + +> 💡 **Big-O記法の読み方(初学者向け)** +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む +> - `O(n)`:入力が2倍になると、処理も約2倍になる +> - `O(n log n)`:入力が2倍になると、処理は約2倍強になる(ソートアルゴリズムに多い) + +- **選択理由**: 方法A(`Counter`)と方法B(`sorted()`)はどちらも「定数空間(O(1))」または「線形時間(O(n))」のどちらかの制約を満たせないため除外しました。方法Cと方法Dはどちらも制約を完全に満たしますが、業務開発版では読みやすさを優先して**方法C(明示的なforループ)**を、競技プログラミング版では簡潔さを優先して**方法D(`reduce`)**を採用します。 +- **Python最適化戦略**: XOR演算子`^`はCPythonのバイトコードレベルで直接処理されるため、追加のオーバーヘッドがほとんどありません。`functools.reduce`を使う場合、ループの制御自体をC実装の`reduce`関数に任せることで、Pythonインタープリタが1行ずつバイトコードを解釈する回数をわずかに減らせます。 +- **トレードオフ**: `reduce`版は簡潔ですが、初めて読む人にとっては「何をしているか」が直感的にわかりにくいというデメリットがあります。業務開発では可読性を優先し、明示的なforループを使う方が保守しやすいです。 + +> 📖 このセクションで登場した用語 +> +> - **`Counter`**:`collections`モジュールの辞書拡張版。要素の出現回数を自動でカウントしてくれる +> - **`functools.reduce`**:配列の全要素を順番に処理しながら1つの値にまとめていく関数。C実装 +> - **トレードオフ**:何かを得ると何かを失う関係。今回は「簡潔さ」と「直感的な読みやすさ」の間のトレードオフ + +--- + +## 3. 実装パターン + +> 💡 **初学者向け補足**:コード全体を示す前に、大まかな構造(骨格)を示します。 +> +> 1. まず入力が`list`かどうか、要素がすべて`int`かどうかを検証する +> 2. 空リストであればエラーを投げる(制約上は保証されているが、防御的に確認する) +> 3. 累積変数`result`を0で初期化する +> 4. リストを1回だけ走査し、各要素を`result`にXORで重ねていく +> 5. 走査が終わったら`result`を返す + +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。エラーの原因が分かりやすく、後から読んだ人が「なぜXORで解けるのか」を理解しやすい構造になっています。 + +【競技プログラミング版を使う場面】 +LeetCodeなど、制限時間内に正解を出すことが目的のコードに向きます。可読性よりも実行速度・コードの短さを優先した書き方になっています。 + +```python +from typing import List +from functools import reduce +from operator import xor + + +class Solution: + """ + 136. Single Number 解決クラス + + 配列内で1回だけ登場する数値を、XOR(排他的論理和)を使って + O(n)時間・O(1)空間で見つける。 + 業務開発向け(solve_production)と競技プログラミング向け + (solve_competitive)の2パターンを提供する。 + """ + + def singleNumber(self, nums: List[int]) -> int: + """ + LeetCode提出用エントリーポイント。 + 内部では業務開発版のロジックを呼び出す。 + + Args: + nums: 1回だけ登場する要素が1つ含まれる整数配列 + + Returns: + 1回だけ登場する整数 + + Raises: + TypeError: nums がリストでない、または要素にint以外が含まれる場合 + ValueError: nums が空の場合 + """ + return self.solve_production(nums) + + def solve_production(self, nums: List[int]) -> int: + """ + 業務開発向け実装(型安全・可読性・エラーハンドリング重視) + + Time Complexity: O(n) + Space Complexity: O(1) + """ + # isinstance() で型チェックする。 + # Pythonは動的型付け(=実行するまで型が確定しない仕組み)のため、 + # 呼び出し元が誤った型を渡しても実行時まで気づけない。 + # pylanceと組み合わせることで、実行前にも型の誤りを検出できるようにする。 + if not isinstance(nums, list): + raise TypeError("Input must be a list") + + # all() + ジェネレータ式(=値を必要なときだけ1つずつ生成する仕組み。 + # リストのように全要素を一度にメモリへ展開しないためメモリ効率が良い)で + # 全要素がintかどうかを1行でチェックする。 + # forループを自分で回すより可読性が高く、all()自体もC実装のため高速。 + if not all(isinstance(x, int) for x in nums): + raise TypeError("All elements must be integers") + + # 制約上は長さ1以上が保証されているが、防御的プログラミング + # (=想定外の入力にも備えて壊れないようにする書き方)として + # 空リストを明示的に弾いておく。 + if len(nums) == 0: + raise ValueError("Input list must not be empty") + + # result を「これまでXORを重ねた累積値」として使う。 + # 初期値を0にするのは、0 ^ a = a という性質があり、 + # 最初の要素をそのまま取り込めるようにするため。 + result: int = 0 + + # リストを1回だけ走査する(forループはPythonで最も基本的な反復方法)。 + # 各要素を result にXORで重ねていく。 + # 同じ数字が2回現れると a ^ a = 0 で打ち消し合い、 + # 最終的にペアのない「はぐれ者」の数字だけが result に残る。 + for num in nums: + result ^= num + + # すべての走査が終わった時点で、result には + # ペアを持たない唯一の数値が残っている。 + return result + + def solve_competitive(self, nums: List[int]) -> int: + """ + 競技プログラミング向け最適化実装 + 型チェック・エラーハンドリングを省略し、簡潔さと実行速度を優先する。 + + Time Complexity: O(n) + Space Complexity: O(1) + """ + # functools.reduce は「配列の全要素を順番に処理しながら + # 1つの値にまとめていく」処理をC実装のループに任せる関数。 + # operator.xor は「^」演算子を関数として扱えるようにしたもの。 + # 第3引数の 0 は初期値(0 ^ a = a という性質を利用するため)。 + # 最適化前: result = 0; for num in nums: result ^= num + # 最適化後: reduce(xor, nums, 0) + # なぜ速いか: reduceのループ制御自体がC実装であり、 + # Pythonインタープリタがバイトコードを1行ずつ解釈する回数を減らせるため。 + return reduce(xor, nums, 0) +``` + +> 💡 **型ヒントとpylanceの関係(初学者向け)** +> Pythonは動的型付け言語なので、型を書かなくても動きます。しかし型ヒントを書いておくと、pylanceが実行前に「型が合っていない」エラーを検出してくれます。これはコンパイル言語(JavaやC++)が持つ安全性をPythonでも実現する仕組みです。 +> +> ```python +> # 型ヒントなし → pylanceは問題を検出できない +> def singleNumber(self, nums): +> ... +> +> # 型ヒントあり → pylanceが誤った呼び出しを実行前に検出できる +> def singleNumber(self, nums: List[int]) -> int: +> ... +> ``` + +### 動作トレース(初学者向け) + +``` +入力例: nums = [4, 1, 2, 1, 2] + +Step 1: isinstance(nums, list) → True → 検証通過 +Step 2: all(isinstance(x, int) for x in nums) → True → 検証通過 +Step 3: len(nums) == 0 → False → 空リストではない +Step 4: result = 0 で初期化 + +Step 5: num = 4 → result = 0 ^ 4 = 4 (2進数: 000 ^ 100 = 100) +Step 6: num = 1 → result = 4 ^ 1 = 5 (2進数: 100 ^ 001 = 101) +Step 7: num = 2 → result = 5 ^ 2 = 7 (2進数: 101 ^ 010 = 111) +Step 8: num = 1 → result = 7 ^ 1 = 6 (2進数: 111 ^ 001 = 110)※1が2回目→打ち消し合いの途中経過 +Step 9: num = 2 → result = 6 ^ 2 = 4 (2進数: 110 ^ 010 = 100)※2が2回目→打ち消し合い完了 + +走査終了 → result = 4 を返す + +検算: 1は2回登場(打ち消し合って消える)、2は2回登場(打ち消し合って消える)、 + 4だけが1回しか登場しないため、最終的に4が残る +``` + +> 📖 このセクションで登場した用語 +> +> - **動的型付け**:実行するまで変数の型が確定しない仕組み。Pythonはこの方式を採用している +> - **防御的プログラミング**:想定外の入力にも備えて、プログラムが壊れないようにあらかじめ対処を書いておく考え方 +> - **docstring**:関数やクラスの先頭に書く説明文。`"""三重クォート"""`で囲む +> - **`operator.xor`**:`^`演算子を関数として扱えるようにした標準ライブラリの関数 + +--- + +## 4. 検証 + +_テストコードは不要です!具体的な計測値、メモリの出力も不要です!_ + +エッジケース(=空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと)のテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるために重要です。 + +- **境界値テスト**: 要素数1(`nums = [1]`)の場合、ループは1回しか回らず`result = 0 ^ 1 = 1`となり正しく動作します。要素数が制約上限の`3 * 10^4`に近い場合でも、O(n)アルゴリズムなので処理時間は問題になりません。 +- **型チェック**: pylanceによる静的解析(=プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法)により、`nums: List[int]`という型ヒントに反する呼び出し(例:`nums`に文字列のリストを渡す)は実行前に警告されます。 + +> 📖 このセクションで登場した用語 +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと +> - **静的解析**:プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法 + +--- + +## Python特有の追加考慮事項 + +- **並行処理考慮**: 本問題は単純な配列走査であり、GIL(グローバルインタープリタロック=1人しか入れない部屋のようなもの。Pythonのスレッドは交代で部屋に入る)の影響を受けるほど重い処理ではないため、`multiprocessing`や`asyncio`を使う必要はありません。 +- **プロダクション考慮**: 実務であれば、`_validate_input`のような入力検証を独立したメソッドに切り出し、他の問題でも再利用できるようにする設計も考えられます。今回はLeetCodeの制約に合わせてシンプルな構成にしています。 diff --git a/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/Single_Number_Typescript.md b/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/Single_Number_Typescript.md new file mode 100644 index 00000000..b550bd3f --- /dev/null +++ b/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/Single_Number_Typescript.md @@ -0,0 +1,199 @@ +> 🎯 **[algo-beginner スキル発火]** +> 言語/カテゴリ: TypeScript +> 適用ルールセット: 共通5ルール + TS固有4ルール +> 参照ファイル: references/common.md + references/typescript.md + +# 136. Single Number + +--- + +## 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「ペアを組んでいない“はぐれ者”の数字を1つだけ見つける問題」です。配列の中身をよく見ると、ほとんどの数字は2回ずつ登場していて、1つだけ1回しか出てこない数字が混ざっています。それを探し出すのがゴールです。 + +### 競技プログラミング視点での分析 + +- 実行速度を最優先するなら、配列を**1回だけ走査(=先頭から末尾まで1周すること)**して答えを出したい。2回以上ループを回すと、入力サイズが大きいときに時間がかかりすぎます。 +- メモリ使用量の最小化方針としては、追加の配列やハッシュマップ(=キーと値をセットで記録できる辞書のような構造)を作らず、**変数1つだけ**で答えを求める方法が理想です。 + +### 業務開発視点での分析 + +- 型安全性(=コンパイル時に型の間違いを検出できる性質)の観点では、`nums: number[]` という入力の型がそのまま守られているかを確認し、空配列や不正な値が来た場合にも壊れないようにエラーハンドリング(=想定外の入力への対処)を入れておくと安心です。 +- 可読性・保守性の面では、後からコードを読む人が「なぜこの1行で答えが求まるのか」を理解できるように、コメントと補助説明が重要になります。 + +### TypeScript特有の考慮点 + +- 今回は`number[]`という単純な配列なので、ジェネリクス(=型を後から自由に差し込める仕組み。`Array`の`number`部分がその例)を使うほどの複雑さはありませんが、**型ガード**(=`if (Array.isArray(x))`のように実行時に型を絞り込む仕組み)を使って「本当に配列が渡されたか」を確認することで、コンパイル時だけでなく実行時の安全性も高められます。 +- 型推論(=型を明示しなくてもTypeScriptが自動で型を判断してくれる機能)により、`let result: number = 0;` のように書かなくても `let result = 0;` だけでTypeScriptが `number` 型だと推論してくれます。 + +> 📖 このセクションで登場した用語 +> +> - **ハッシュマップ**:キーと値をセットで記録できる辞書のような構造。図書館の索引カードのように、キーからすぐに値を探せる +> - **型安全性**:コンパイル時に型の間違いを検出できる性質のこと +> - **エラーハンドリング**:想定外の入力やエラーが起きたときに、プログラムが安全に対処する仕組み +> - **型ガード**:`if (Array.isArray(x))` のように、実行時に「この値は本当にこの型か」を確認して型を絞り込む仕組み +> - **型推論**:型を明示しなくてもTypeScriptが自動で型を判断してくれる機能 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて、この問題の制約(線形時間・定数空間)に最も合うものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| ---------------------------------------- | ---------- | ---------- | ------------ | -------- | ------------------------- | ---------------------------------------------- | +| A. ハッシュマップで出現回数を数える | O(n) | O(n) | 低 | 高 | 高 | 追加メモリを使うため制約違反 | +| B. 配列をソートして隣同士を比較する | O(n log n) | O(1)〜O(n) | 中 | 高 | 中 | ソート関数の仕様により追加メモリを使う場合あり | +| C. XOR(排他的論理和)で全要素をまとめる | O(n) | O(1) | 低 | 高 | 中(XORの性質理解が前提) | 本問題の制約に完全一致 | +| D. 二重ループで総当たり比較 | O(n²) | O(1) | 低 | 高 | 高 | 遅すぎて大きい入力で破綻 | + +> 💡 **Big-O記法の読み方(初学者向け)** +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(辞書で直接ページを開くイメージ) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(リストを端から順に読むイメージ) +> - `O(n log n)`:入力が2倍になると、処理は約2倍強になる(ソートアルゴリズムに多い) +> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループの総当たりに多い) + +> 📖 このセクションで登場した用語 +> +> - **XOR(排他的論理和)**:2つのビット(0か1)を比較し、「違っていれば1、同じなら0」を返す演算。詳しくは次の章で説明します +> - **時間計算量**:入力の大きさに対して、処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 + +--- + +## 3. 選択したアルゴリズムと理由 + +> 💡 **初学者向け補足**:他の方法と比べながら、なぜXORを選んだのかを説明します。 + +- **選択したアプローチ**: C. XOR(排他的論理和)で全要素をまとめる方法 + +- **理由**(他の方法と対比しながら): + - **方法A(ハッシュマップ)は選ばなかった**:出現回数を数える辞書を作る必要があり、配列の要素数に比例したメモリ(O(n))を使ってしまいます。本問題は「定数空間(=入力サイズに関係なく一定量のメモリしか使わないこと)」を要求しているため、条件に合いません。 + - **方法B(ソート)は選ばなかった**:ソート自体にO(n log n)の時間がかかり、「線形時間(=O(n))」という制約より遅くなってしまいます。 + - **方法D(二重ループ)は選ばなかった**:O(n²)は入力が大きくなると極端に遅くなり、実用的ではありません。 + - **方法C(XOR)を選んだ**:XORには「同じ数字同士をXORすると0になる」「0とXORしても値は変わらない」「順番を入れ替えても結果が変わらない(=交換法則・結合法則が成り立つ)」という3つの性質があります。この性質を使うと、配列全体を1回だけ走査しながら変数1つにXORを重ねていくだけで、ペアになった数字は打ち消し合って消え、最後に残るのが「はぐれ者」の数字になります。これはまさにO(n)時間・O(1)空間という本問題の要求にぴったり一致します。 + +### XORの基礎知識を「基礎 → 応用 → この問題での使い方」で理解する + +1. **基礎**:XORとは、2つのビット(=0か1で表される情報の最小単位)を比較し、「違っていれば1、同じなら0」を返す演算です。日常のたとえで言うと、「2人が同じ意見なら黙る(0)、違う意見なら手を挙げる(1)」というルールに似ています。 + + ``` + 0 XOR 0 = 0(同じ→打ち消し合う) + 1 XOR 1 = 0(同じ→打ち消し合う) + 0 XOR 1 = 1(違う→残る) + 1 XOR 0 = 1(違う→残る) + ``` + +2. **応用**:数値同士のXORでは、2進数(=0と1だけで数を表す方法)の各桁ごとにこのルールが適用されます。例えば `5 XOR 3` は、5が`101`、3が`011`なので、桁ごとに比較して `110`(=6)になります。重要なのは、**同じ数字を2回XORすると必ず元に戻る**という性質です(`a XOR a = 0`、`0 XOR a = a`)。 + +3. **この問題での使い方**:配列の全要素を順番にXORで重ねていくと、2回登場する数字同士は`a XOR a = 0`で必ず打ち消し合います。最後に`0 XOR (はぐれ者の数字)`が残るので、答えがそのまま得られます。順番を入れ替えても結果が変わらない(交換法則・結合法則)ため、配列の並び順を気にする必要もありません。 + +> 📖 このセクションで登場した用語 +> +> - **XOR(排他的論理和)**:2つのビットを比較し、違っていれば1、同じなら0を返す演算 +> - **ビット**:0か1で表される情報の最小単位 +> - **2進数**:0と1だけで数を表す方法。コンピュータの内部表現の基本 +> - **交換法則・結合法則**:計算の順番を入れ替えたり、まとめる位置を変えても結果が変わらない性質 +> - **定数空間**:入力サイズに関係なく一定量のメモリしか使わないこと + +--- + +## 4. 実装コード + +> 💡 **初学者向け補足**:コード全体を示す前に、大まかな構造(骨格)を示します。 +> +> 1. まず入力が配列かどうかを検証する +> 2. 空配列であればエラーを投げる(本問題の制約では長さ1以上が保証されているが、防御的に確認する) +> 3. 変数`result`を0で初期化する +> 4. 配列を1回だけ走査し、各要素を`result`にXORで重ねていく +> 5. 走査が終わったら`result`を返す + +【業務開発版】型安全・可読性・エラーハンドリングを優先し、実務のコードレビューに耐える書き方にしています。 +【競技版】LeetCodeの制限時間内に通すことを目的に、簡潔さと実行速度を優先した書き方にしています(後述)。 + +### 業務開発版(型安全性・保守性重視) + +```typescript +/** + * 配列内で1回だけ登場する数値を見つける。 + * 他の要素はすべて2回ずつ登場することが保証されている。 + * + * @param nums - 検証対象の整数配列(1回だけ登場する要素が1つ含まれる) + * @returns 1回だけ登場する数値 + * @throws {TypeError} numsが配列でない場合、または要素が32ビット符号付き整数でない場合 + * @throws {RangeError} numsが空配列の場合 + * @complexity Time: O(n), Space: O(1) + */ +function singleNumber(nums: number[]): number { + // 入力が配列かどうかを確認する + if (!Array.isArray(nums)) { + throw new TypeError('Input must be an array'); + } + + // 空配列を明示的に弾く + if (nums.length === 0) { + throw new RangeError('Input array must not be empty'); + } + + let result = 0; + + // 各要素が 32 ビット符号付き整数であるかを検証しながら走査する + for (const num of nums) { + if (!Number.isInteger(num) || num < -2147483648 || num > 2147483647) { + throw new TypeError('Each element must be a signed 32-bit integer'); + } + result ^= num; + } + + return result; +} +``` + +> 💡 **TypeScript固有ポイント**:ここでは`readonly`やジェネリクスを使っていませんが、その理由も説明します。今回の入力は`number[]`という単一の型しか扱わないため、ジェネリクス``(=型を後から自由に差し込める仕組み)を使うメリットがありません。ジェネリクスは「同じロジックを複数の型で使い回したいとき」に真価を発揮する仕組みなので、この問題には不要と判断しました。また`nums`自体を書き換えていない(読み取り専用の使い方しかしていない)ため、`readonly number[]`とすることでコンパイル時に「うっかり書き換え」を防ぐこともできます。 + +### 競技版(簡潔さ・速度優先) + +```typescript +// LeetCodeの入力制約(要素が有効な32ビット符号付き整数であること)に依存し、 +// 追加の要素検証を行わずに1行のreduceで累積計算する +function singleNumber(nums: number[]): number { + return nums.reduce((acc, num) => acc ^ num, 0); +} +``` + +> 💡 **業務版と競技版の使い分け**:業務版は型ガードとエラーハンドリングを備え、チーム開発でのレビューや将来の仕様変更に耐えられるようにしています。競技版はLeetCodeの実行環境(入力が保証されている前提)に最適化し、1行で本質のロジックだけを表現しています。同じアルゴリズムでも「誰が・どんな場面で読むコードか」によって最適な書き方は変わります。 + +### 動作トレース(初学者向け) + +``` +入力例: nums = [4, 1, 2, 1, 2] + +初期状態: result = 0 + +Step 1: num = 4 → result = 0 XOR 4 = 4 (2進数: 000 XOR 100 = 100) +Step 2: num = 1 → result = 4 XOR 1 = 5 (2進数: 100 XOR 001 = 101) +Step 3: num = 2 → result = 5 XOR 2 = 7 (2進数: 101 XOR 010 = 111) +Step 4: num = 1 → result = 7 XOR 1 = 6 (2進数: 111 XOR 001 = 110)※1が2回目→打ち消し合いの途中経過 +Step 5: num = 2 → result = 6 XOR 2 = 4 (2進数: 110 XOR 010 = 100)※2が2回目→打ち消し合い完了 + +走査終了 → result = 4 を返す + +検算: 1は2回登場(打ち消し合って消える)、2は2回登場(打ち消し合って消える)、 + 4だけが1回しか登場しないため、最終的に4が残る +``` + +> 📖 このセクションで登場した用語 +> +> - **readonly**:変更できないようにする修飾子。意図せぬ書き換えをコンパイル時に防ぐ +> - **防御的プログラミング**:想定外の入力にも備えて、プログラムが壊れないようにあらかじめ対処を書いておく考え方 +> - **reduce**:配列の全要素を順番に処理しながら1つの値にまとめていく配列メソッド +> - **累積値**:ループを回しながら少しずつ更新していき、最終的な結果を保持する変数 + +--- + +## TypeScript固有の最適化観点 + +1. **コンパイル時エラー防止**:`nums: number[]`という型注釈により、呼び出し側が誤って`string[]`などを渡した場合、実行前(コンパイル時)にエラーとして検出できます。これはJavaScriptにはない仕組みで、バグを実行前に発見できる点が大きな利点です。 +2. **型推論の活用**:`let result = 0;`のように書くだけで、TypeScriptが自動的に`result`を`number`型だと推論します。明示的に`let result: number = 0;`と書く必要がなく、コードがすっきりします。 +3. **型ガードによる実行時安全性**:`Array.isArray(nums)`は、TypeScriptの型注釈だけではカバーできない「実行時に本当に配列が来たか」を確認する仕組みです。コンパイル時のチェックと実行時のチェックを両方備えることで、より頑丈なコードになります。 diff --git a/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 4.5/README_react.html b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 4.5/README_react.html deleted file mode 100644 index eafdc7b2..00000000 --- a/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 4.5/README_react.html +++ /dev/null @@ -1,1326 +0,0 @@ - - - - - - LeetCode 9: Palindrome Number - 数値反転による回文判定 - - - - - - - - - - - - - - - - - -
- - - - -
-

- アルゴリズム概要 -

- -

- 整数 - x - が 10 進表記で回文(左右対称)かどうかを判定します。 -

- -
-

入出力例

-
例1: x = 121  → true  (121は左右対称)
-例2: x = -121 → false (負数は先頭に-が付く)
-例3: x = 10   → false (01とは読めない)
-
- -
-

制約条件

-
    -
  • - -2³¹ ≤ x ≤ 2³¹ - 1 -
  • -
  • Follow up: 文字列変換なしで解けるか?
  • -
-
- -
-

戦略

-
    -
  • 早期リターン: 負数と末尾0(0自身を除く)を先に弾く
  • -
  • - 半分反転: 右半分だけを数値のまま反転し、左半分と比較 -
  • -
  • 偶数/奇数桁対応: 中央の1桁を考慮した判定
  • -
  • 時間 O(d): d = 桁数 ≒ log₁₀(|x|)
  • -
  • 空間 O(1): 定数個の整数変数のみ使用
  • -
-
-
- - -
-

- ステップバイステップ解説 -

-
-
- - -
-

- Python実装 -

-
class Solution:
-    def isPalindrome(self, x: int) -> bool:
-        """
-        整数 x が 10 進表記で回文かどうかを判定する。
-
-        Args:
-            x: 判定対象の整数(32bit 符号付き整数)
-
-        Returns:
-            x が 10 進表記で回文であれば True、そうでなければ False。
-
-        Time Complexity: O(d)  (d は x の桁数 ≒ log10(|x|))
-        Space Complexity: O(1)  追加メモリは定数個の整数のみ。
-        """
-        # 負数、0 以外で末尾が 0 の数は回文にならない
-        if x < 0 or (x % 10 == 0 and x != 0):
-            return False
-
-        # 0〜9 は 1 桁なので必ず回文
-        if x < 10:
-            return True
-
-        rev: int = 0
-
-        # 右半分を反転しつつ、左半分と比較できる状態まで進める
-        # ループを抜ける条件:
-        #   - 偶数桁: x と rev が同じ桁数になった時点で x <= rev
-        #   - 奇数桁: 中央 1 桁を含んだ rev の方が 1 桁多くなった時点で x < rev
-        while x > rev:
-            digit: int = x % 10  # 末尾 1 桁を取得
-            rev = rev * 10 + digit  # rev に桁を追加
-            x //= 10  # 整数除算で末尾 1 桁を削除
-
-        # 偶数桁: x == rev
-        # 奇数桁: 中央 1 桁を無視するため、rev // 10 と比較
-        return x == rev or x == rev // 10
-
- - -
-

- フローチャート -

-
- - - - - - - - - - - - - - - - - - - - 開始 - - - - - - - - - x < 0? - 負数判定 - - - - - - はい - - - - - Return - False - - - - - - いいえ - - - - - - x % 10 == 0 - and x != 0? - 末尾0判定 - - - - - - はい - - - - - - いいえ - - - - - - x < 10? - 1桁数判定 - - - - - - はい - - - - - Return - True - - - - - - いいえ - - - - - - 初期化 - rev = 0 - - - - - - - - - x > rev? - ループ継続 - - - - - - はい - - - - - - digit取得 - x % 10 - - - - - - - - - rev更新 - rev*10 + digit - - - - - - - - - x縮小 - x //= 10 - - - - - - 次の反復へ - - - - - - いいえ - - - - - - x == rev - or rev//10? - - - - - - はい - - - - - - いいえ - - -
- -

- フローの説明:
- 1. 基底条件で負数・末尾0・1桁数を早期判定
- 2. rev = 0 で初期化し、ループに入る
- 3. x > rev の間、右側の桁を反転して rev に積み上げる
- 4. 同時に x を縮小し、x <= rev になったらループ終了
- 5. 最後に x == rev または x == rev // 10 で回文判定
- 6. 左側の紫色の矢印でループバックし、次の反復へ進む -

-
- - -
-

- 計算量分析 -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- 項目 - - 本実装(数値反転) - - 代替案(文字列変換) -
- 時間計算量 - - O(d) - - O(d) -
- 空間計算量 - - O(1) - - O(d) - (文字列生成) -
- Follow up対応 - - ✓ 満たす - - ✗ 満たさない -
- 実装コスト - - 中(ループロジック) - - 低(str()+スライス) -
-
- -
-

補足

-
    -
  • d は桁数(≒ log₁₀(|x|))
  • -
  • 数値反転版はメモリ効率が優れており、追加データ構造を使わない
  • -
  • 文字列版は実装が簡単だが、Follow upの要件を満たさない
  • -
  • LeetCode環境では実行時間にノイズがあり、両者の実測差は小さい
  • -
-
-
-
- - - - - - - - - - - - - - - - - diff --git a/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/Palindrome_Number_Python.md b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/Palindrome_Number_Python.md new file mode 100644 index 00000000..a2b3047a --- /dev/null +++ b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/Palindrome_Number_Python.md @@ -0,0 +1,243 @@ +# 1. 問題分析結果 + +> 💡 この問題は、一言で言うと「**整数を数字の並びとして見たときに、前から読んでも後ろから読んでも同じかどうかを判定する問題**」です。 + +Pythonでこの問題を解く際に特に気をつけるべき点は、**「文字列に変換すれば一瞬で解けてしまう」という誘惑に負けないこと**です。`str(x)[::-1] == str(x)` と書けば1行で終わりますが、follow-upで「文字列変換せずに解けるか?」と問われている通り、整数のまま計算する方法(=メモリに新しい文字列オブジェクトを作らない方法)を知っておくことは、CPython(=最も広く使われるPythonの実装。C言語で作られている)が内部でどのように整数を扱っているかを理解する良い練習になります。 + +### 競技プログラミング視点 + +- **制約分析**: `-2^31 <= x <= 2^31 - 1` なので、桁数は最大でも10桁程度。どんな方法でも一瞬で終わる規模だが、follow-up対応(非文字列変換)が本質的な課題。 +- **採用手法**: 数値の後半だけを反転させて前半と比較する方法。ループ回数が桁数の半分で済む。 +- **メモリ最小化**: 追加のリストや文字列を一切作らず、整数変数を2〜3個使うだけ(O(1)空間)。 + +### 業務開発視点 + +- **型安全設計**: `int` 以外の型が来たら `TypeError` を投げる。範囲外なら `ValueError` を投げる。 +- **エラーハンドリング**: LeetCode環境では入力は保証されるが、業務コードとして再利用する場合を想定し検証を入れる。 +- **可読性**: 「なぜ末尾が0の数は回文になり得ないか」など、一見トリッキーな分岐に日本語コメントで理由を明記する。 + +### Python特有分析 + +- **データ構造選択**: 追加のデータ構造は不要(整数演算のみで完結)。 +- **標準ライブラリ活用度**: 今回は `collections` や `heapq` は使わない(数値演算のみで十分効率的なため)。 +- **CPython最適化度**: 整数の四則演算(`%`, `//`, `*`, `+`)はCPythonの組み込み演算子として直接C実装で処理されるため、Pure Pythonループでも十分高速。 + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、組み込み演算・組み込み関数の多くが高速なC実装で動く。 +> - **O(1)空間**:入力サイズに関わらず、使うメモリ量が一定であること(=追加のデータ構造を作らない)。 +> - **型安全設計**:想定外の型が渡されたときに、実行時エラーではなく分かりやすい例外を出すように設計すること。 + +--- + +# 2. 採用アルゴリズムと根拠 + +なぜ複数のアプローチを比較するのかというと、「文字列変換版」は実装が簡単な反面follow-up要件(非文字列変換)を満たせず、**Pythonでは"簡単に書ける方法"と"効率的な方法"がしばしば異なる**ため、両方を並べて選択理由を明確にする必要があるからです。 + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む +> - `O(n)`:入力が2倍になると、処理も約2倍になる +> - `O(log n)`:入力が2倍になっても、処理はわずかしか増えない(桁数ベースの処理に多い) + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ------------------------------------------------- | ---------- | ---------- | ---------------- | ------ | ----------------------- | --------------------------------------------- | ------------------------- | +| 方法A: 文字列変換 + スライス反転 (`str(x)[::-1]`) | O(桁数) | O(桁数) | 低 | ★★★ | なし(スライスはC実装) | 適(スライスはC実装だが新規オブジェクト生成) | follow-up要件を満たさない | +| 方法B: 整数のまま後半だけ反転 | O(log10 x) | O(1) | 中 | ★★☆ | なし | 適(`%`, `//`はC実装の演算子) | 採用 | + +- **選択理由**: 方法Bは追加のメモリ(文字列オブジェクト)を一切生成せず、O(1)空間で処理を完結させます。follow-up要件を満たし、メモリ効率とアルゴリズムの学習効果が高いため方法Bを採用します。 +- **Python最適化戦略**: `%`(剰余)と `//`(整数除算)はCPythonの組み込み演算子としてC実装で処理されるため、Pure Pythonの`while`ループで書いても十分高速です。関数呼び出しのオーバーヘッド(=関数を呼ぶたびに発生するわずかな処理コスト)を避けるため、競技版では1メソッドに処理をまとめます。 +- **トレードオフ**: 方法Bは方法Aに比べて「なぜこの計算で回文判定できるのか」が直感的に分かりにくく、可読性がやや下がります。そのため業務版ではコメントで丁寧に補足します。 + +> 📖 **このセクションで登場した用語** +> +> - **スライス**:`[start:stop:step]` の形でリストや文字列の一部を取り出す構文。`[::-1]`は「全体を逆順に取り出す」という意味。 +> - **オーバーヘッド**:本質的な処理以外にかかる余分な処理コスト。関数呼び出しや型チェックなど。 +> - **整数除算 (`//`)**:小数点以下を切り捨てた割り算。例: `123 // 10 = 12`。 + +--- + +# 3. 実装パターン + +このコードの大まかな構造は以下の通りです。 + +- `_validate_input`:型・範囲チェック(業務版のみ) +- `_is_edge_case` / `_handle_edge_case`:負数・末尾0の数を即座に`False`判定 +- `_main_algorithm`:数値の後半だけを反転して前半と比較する本体ロジック +- `isPalindrome`:LeetCode提出用のエントリーポイント(業務版を呼び出す) + +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。入力検証やdocstringが充実しているため、後から読んだ人が「なぜこの分岐があるのか」を理解しやすい構造です。 + +【競技プログラミング版を使う場面】 +LeetCodeでの提出そのものが目的の場合に向きます。可読性よりも行数の少なさ・関数呼び出しの少なさを優先します。 + +```python +from typing import Any + + +class Solution: + """ + Palindrome Number(回文数判定)問題の解決クラス。 + + 与えられた整数 x が「回文数」(前から読んでも後ろから読んでも + 同じ並びになる数)かどうかを判定する。 + 業務開発向け・競技プログラミング向けの2パターンを提供する。 + """ + + # ------------------------------------------------------------ + # 業務開発向け実装 + # ------------------------------------------------------------ + def solve_production(self, x: int) -> bool: + """ + 業務開発向け実装(型安全・エラーハンドリング・可読性重視)。 + + 文字列に変換せず、整数のまま「数値の後半だけを反転」させて + 前半部分と比較することで回文判定を行う。 + + Args: + x: 判定対象の整数。制約: -2**31 <= x <= 2**31 - 1 + + Returns: + x が回文数であれば True、そうでなければ False。 + + Raises: + TypeError: x が int 型でない場合。 + ValueError: x が制約範囲外の場合。 + """ + # 1. 入力検証(型安全性を担保する) + self._validate_input(x) + + # 2. エッジケース処理 + # 負の数、および「0以外で末尾が0の数」は必ず False になる + if self._is_edge_case(x): + return self._handle_edge_case(x) + + # 3. メインアルゴリズム + return self._main_algorithm(x) + + def _validate_input(self, x: Any) -> None: + """ + 型安全な入力検証を行う。 + + isinstance() で型チェックする。Python は bool が int のサブクラスであるため、 + isinstance(x, bool) を明示的に排除する。 + """ + if isinstance(x, bool) or not isinstance(x, int): + raise TypeError("x must be an integer") + if not (-(2**31) <= x <= 2**31 - 1): + raise ValueError("x is out of the allowed range") + + def _is_edge_case(self, x: int) -> bool: + """ + エッジケース(境界的な入力)かどうかを判定する。 + + ・x < 0 → 先頭に "-" が付くため絶対に回文にならない + ・x % 10 == 0 かつ x != 0 → 反転すると先頭が 0 になり整数として矛盾するため + 絶対に回文にならない(0 自身だけは例外) + """ + return x < 0 or (x % 10 == 0 and x != 0) + + def _handle_edge_case(self, x: int) -> bool: + """エッジケースは常に False を返す(x == 0 はこの分岐に入らない)。""" + return False + + def _main_algorithm(self, x: int) -> bool: + """ + 整数のまま「後半部分だけ」を反転して前半と比較する。 + + 文字列化しない理由: + 文字列に変換すると桁数分の新しい文字列オブジェクトを + 追加でメモリに確保することになる。整数演算だけで完結させれば + 余分なメモリ確保を避けられる(follow-up要件への対応)。 + + Time Complexity: O(log10 x) … 桁数の半分だけループするため + Space Complexity: O(1) … 追加のデータ構造を使わないため + """ + reverted_half: int = 0 + + # x(元の数の前半に相当する残り)と reverted_half(後半を反転させた数)を + # 桁数が半分ずつ入れ替わるまで競わせる。x > reverted_half の間だけ回すことで、 + # ちょうど半分の桁数まで処理したところでループが自然に止まる。 + while x > reverted_half: + # % 10 で最下位の桁を取り出し、reverted_half の末尾に追加する。 + # 例: reverted_half=12, x%10=3 → 12*10+3=123 + reverted_half = reverted_half * 10 + x % 10 + # // 10(整数除算)で最下位の桁を切り捨てる。 + # 例: x=123 → 123//10=12 + x //= 10 + + # 桁数が偶数: x と reverted_half が完全一致(例: 1221 → 12, 12) + # 桁数が奇数: reverted_half の真ん中の桁が1つ多いので //10 で捨てて比較 + # (例: 12321 → x=12, reverted_half=123 → 123//10=12 で一致) + return x == reverted_half or x == reverted_half // 10 + + # ------------------------------------------------------------ + # 競技プログラミング向け実装 + # ------------------------------------------------------------ + def solve_competitive(self, x: int) -> bool: + """ + 競技プログラミング向け最適化実装。 + 入力検証用の別メソッド呼び出しを省略し、 + 1メソッドに処理をまとめて関数呼び出しのオーバーヘッドを削減する。 + + Time Complexity: O(log10 x) + Space Complexity: O(1) + """ + if x < 0 or (x % 10 == 0 and x != 0): + return False + + rev: int = 0 + while x > rev: + rev = rev * 10 + x % 10 + x //= 10 + return x == rev or x == rev // 10 + + # LeetCode 提出用エントリーポイント + def isPalindrome(self, x: int) -> bool: + """LeetCode提出用エントリーポイント。業務開発版を採用する。""" + return self.solve_production(x) +``` + +> 💡 **コードの動作トレース**(初学者向け、入力 `x = 12321`) +> +> ``` +> 初期状態: x=12321, reverted_half=0 +> Step 1: x(12321) > reverted_half(0) → reverted_half=0*10+1=1, x=12321//10=1232 +> Step 2: x(1232) > reverted_half(1) → reverted_half=1*10+2=12, x=1232//10=123 +> Step 3: x(123) > reverted_half(12) → reverted_half=12*10+3=123, x=123//10=12 +> Step 4: x(12) > reverted_half(123)? → False → ループ終了 +> 判定: x(12) == reverted_half(123)? False +> x(12) == reverted_half//10(12)? True → 回文である +> 結果: True +> ``` +> +> 桁数が奇数(5桁)のため、真ん中の桁「3」が`reverted_half`側に1つ多く残っている点に注目してください。それを`// 10`で切り捨てることで正しく比較できます。 + +> 💡 **型ヒントとpylanceの関係**(初学者向け) +> Pythonは動的型付け言語なので型を書かなくても動きますが、型ヒント(`x: int`, `-> bool` など)を書いておくと、pylance(VSCodeの静的型チェッカー)が実行前に「型が合っていない」ミスを検出してくれます。これはJavaやC++のようなコンパイル言語が持つ安全性を、実行前の静的解析という形でPythonにも取り入れる仕組みです。 + +> 📖 **このセクションで登場した用語** +> +> - **エントリーポイント**:プログラムの実行が最初に始まる場所。ここではLeetCodeが呼び出す`isPalindrome`メソッドを指す。 +> - **静的型チェッカー**:コードを実行せずに型の整合性をチェックするツール(pylanceなど)。 +> - **docstring**:関数やクラスの先頭に書く説明文。`"""三重クォート"""`で囲む。 + +--- + +# 4. 検証 + +エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。 + +- `x = 0` → `_is_edge_case`の条件(`x % 10 == 0 and x != 0`)に該当しない → メインアルゴリズムへ進み `True`(0は回文) +- `x = -121` → `x < 0` → 即座に `False`(例2と一致) +- `x = 10` → `x % 10 == 0 and x != 0` → 即座に `False`(例3と一致) +- `x = 121` → メインアルゴリズムで `True`(例1と一致) +- `x = 2**31 - 1`(制約上限)→ 桁数が多くても`while`ループの回数は桁数の半分なので問題なく処理できる +- 型チェック: `solve_production`に`str`や`None`を渡すと`TypeError`、範囲外の`int`を渡すと`ValueError`が発生することをpylanceの型注釈と実行時検証の両面で保証している + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと。 +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること。 +> - **静的解析**:プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法。 diff --git a/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/Palindrome_Number_Typescript.md b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/Palindrome_Number_Typescript.md new file mode 100644 index 00000000..beb55982 --- /dev/null +++ b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/Palindrome_Number_Typescript.md @@ -0,0 +1,216 @@ +# 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「**整数を数字の並びとして見たときに、前から読んでも後ろから読んでも同じかどうかを判定する問題**」です。 + +## 競技プログラミング視点での分析 + +- **実行速度最優先の方針**: 文字列変換(`String(x)`)を使えば1行で解けますが、follow-up要件(文字列に変換せず解く)に応えるため、数値のまま計算する方が「新しいオブジェクトを作らない」分だけ有利です。 +- **メモリ使用量の最小化方針**: ループの中で使う変数は数値2〜3個のみ(O(1)空間)。文字列や配列など、桁数に比例したメモリを使う構造は一切作りません。 + +## 業務開発視点での分析 + +- **型安全性・保守性・可読性**: `x: number` という型注釈だけでは「整数か小数か」をTypeScriptのコンパイラは区別できません(TypeScriptには`int`型が存在しないため)。そのため、実行時に`Number.isInteger()`で追加チェックすることが重要です。 +- **エラーハンドリング**: LeetCode環境では入力は保証されますが、業務コードとして再利用する場合を想定し、範囲外の値・非整数値に対する例外処理を入れます。 + +## TypeScript特有の考慮点 + +- **型推論とコンパイル時最適化**: `let original = x;` のように書けば、TypeScriptは自動的に`original`の型を`number`と推論してくれます(=型を明示しなくてもTypeScriptが自動で型を判断してくれる機能)。これにより余計な型注釈を省きつつ安全性を保てます。 +- **JavaScriptの数値表現に関する注意点**: TypeScript(=JavaScript)の`number`型は**倍精度浮動小数点数**(=小数も整数も同じ形式で表現する数値型)であり、Java/C++の`int`のような32bit固定長ではありません。そのため「桁を反転させた結果が32bit整数の範囲を超えてオーバーフローする」という、他言語でこの問題を解く際によくある心配は基本的に不要です(`2^53 - 1`まで整数として正確に扱えるため)。ただし、**業務コードとしての契約を守るため**、この解答では制約通りに32bit範囲チェックを明示的に行います。 +- **ジェネリクスの要否**: 今回は入力が`number`固定のため、ジェネリクス(=型を後から自由に差し込める仕組み)は不要です。無理に汎用化するとかえって可読性が落ちるため使いません。 + +> 📖 **このセクションで登場した用語** +> +> - **型推論**:型を明示しなくてもTypeScriptが自動で型を判断してくれる機能。 +> - **倍精度浮動小数点数**:JavaScript/TypeScriptの`number`型の実体。小数と整数を区別せず同じ形式で保持する。 +> - **O(1)空間**:入力サイズに関わらず、使うメモリ量が一定であること。 +> - **オーバーフロー**:計算結果が型が表現できる範囲を超えてしまい、値が壊れる現象。JS/TSの`number`では起きにくいが、他言語(Java/C++の`int`)では起こりうる。 + +--- + +# 2. アルゴリズムアプローチ比較 + +同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」、そして follow-up 要件(文字列に変換しないこと)を満たせるかどうかを比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | follow-up対応 | 備考 | +| --------------------------------------------------------------- | ------------------- | ---------- | ------------ | -------- | ------ | ------------- | ------------------------------------------- | +| 方法A: 文字列変換 + 反転比較(`String(x)`, `split`, `reverse`) | O(d) | O(d) | 低 | 高 | 高 | ✗ | dは桁数。実装は最も簡単だが要件を満たさない | +| 方法B: 数値全体を反転して比較 | O(d) | O(1) | 中 | 高 | 中 | ○ | ループが桁数分まわる | +| 方法C: 数値の後半だけ反転して比較(早期終了) | O(d/2) ≈ O(log₁₀ x) | O(1) | 中 | 高 | 中 | ○ | 採用。ループ回数が最小 | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(d)`:桁数 `d` に比例して処理が増える(このケースでは最大でも10桁程度なので、実質ほぼ一定時間) +> - `O(log₁₀ x)`:値 `x` の常用対数(=10進数での桁数)に比例して処理が増える。`d`と同じ意味で使っています。 + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して、処理にかかる手間がどう増えるかの目安。 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安。 +> - **早期終了**:ループの半分の時点で処理を打ち切ること。全体を反転させるより演算回数を減らせる。 + +--- + +# 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**: 方法C(数値の後半だけ反転して比較) +- **理由**: + - 方法Aは実装コストが最も低いものの、follow-up要件(文字列変換禁止)を満たさないため選びませんでした。 + - 方法Bも要件は満たしますが、数値全体を反転させるため、方法Cに比べてループ回数が約2倍になります(例:5桁の数なら方法Bは5回、方法Cは3回でループが止まる)。同じO(1)空間・同じ要件対応であれば、演算回数が少ない方法Cの方が競技プログラミング視点でも有利です。 + - **TypeScript環境での型安全性**: `x: number`という単一の型のみを扱うため複雑なジェネリクスは不要ですが、`Number.isInteger()`による実行時チェックを組み合わせることで「コンパイル時に防げない誤り」(小数が渡された場合など)も防げます。 +- **TypeScript特有の最適化ポイント**: + - **コンパイル時の型チェックによるエラー防止**: 引数の型を`number`に固定することで、呼び出し側が文字列や`null`を渡すコードはコンパイルの時点でエラーになります。 + - **`Number.isInteger`による実行時の補完**: TypeScriptの型システムは「整数」と「小数」を区別できないため、実行時チェックでその弱点を補います。 + - **`Math.trunc`の活用**: JavaScript/TypeScriptには整数専用の除算演算子(Pythonの`//`のような記法)が存在しないため、`Math.trunc(a / b)`で「小数点以下切り捨て」を明示的に行います。`a | 0`のようなビット演算トリックも存在しますが、32bit精度に丸められてしまい可読性も落ちるため、この解答では使いません。 + +> 📖 **このセクションで登場した用語** +> +> - **実行時チェック**:プログラムが実際に動いているときに行う検証。コンパイル時には検出できない誤りを防ぐ。 +> - **`Math.trunc()`**:数値の小数部分を切り捨てて整数部分だけを返す関数。負の数でも「0に近づく方向」に切り捨てる点が`Math.floor()`と異なる。 + +--- + +# 4. 実装コード + +このコードの大まかな構造は以下の通りです。 + +1. 入力が整数かどうか、制約範囲内かどうかを検証する(型ガード) +2. 負の数・末尾が0の数(0自身を除く)を即座に`false`と判定する(エッジケース処理) +3. 数値の後半部分だけを反転させながら前半部分と比較するメインループを実行する +4. 桁数の偶奇に応じて最終比較を行い、結果を返す + +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。入力検証とJSDocコメントが充実しているため、後から読んだ人が「なぜこの分岐があるのか」を理解しやすい構造です。 + +【競技プログラミング版を使う場面】 +LeetCodeでの提出そのものが目的の場合に向きます。検証処理を省略し、行数と関数呼び出し回数を最小限に抑えます。 + +```typescript +/** + * 整数 x が回文数(Palindrome Number)かどうかを判定する。 + * + * 「回文数」とは、前から読んでも後ろから読んでも同じ並びになる整数のこと。 + * 文字列に変換せず、数値の後半部分だけを反転させて前半部分と比較することで + * follow-up要件(文字列変換なしでの実装)に対応する。 + * + * @param x - 判定対象の整数。制約: -2**31 <= x <= 2**31 - 1 + * @returns x が回文数であれば true、そうでなければ false + * @throws {TypeError} x が整数でない場合 + * @throws {RangeError} x が制約範囲外の場合 + * @complexity Time: O(log10 x), Space: O(1) + */ +function isPalindrome(x: number): boolean { + // Number.isInteger() で「整数かどうか」を実行時にも確認する。 + // TypeScriptの number 型は整数・小数を区別しないため、 + // コンパイル時の型チェックだけでは「3.14 が渡されないこと」を保証できない。 + if (!Number.isInteger(x)) { + throw new TypeError('x must be an integer'); + } + + // 32bit符号付き整数の範囲チェック(問題の制約 -2^31 <= x <= 2^31 - 1 に対応) + const INT32_MIN = -(2 ** 31); + const INT32_MAX = 2 ** 31 - 1; + if (x < INT32_MIN || x > INT32_MAX) { + throw new RangeError('x is out of the allowed range'); + } + + // エッジケース処理: + // ・x < 0 → 先頭に "-" が付くため絶対に回文にならない + // ・x % 10 === 0 かつ x !== 0 → 反転すると先頭が 0 になってしまい矛盾するため + // 絶対に回文にならない(0 自身だけは例外) + if (x < 0 || (x % 10 === 0 && x !== 0)) { + return false; + } + + let original: number = x; + let revertedHalf: number = 0; + + // original(元の数の前半に相当する残り)と revertedHalf(後半を反転させた数)を、 + // 桁数がちょうど半分ずつ入れ替わるまで競わせる。 + // original > revertedHalf の間だけループすることで、半分の桁数まで処理したところで自然に止まる。 + while (original > revertedHalf) { + // % 10 で最下位の桁を取り出し、revertedHalf の末尾に追加する。 + // 例: revertedHalf=12, original%10=3 → 12*10+3=123 + revertedHalf = revertedHalf * 10 + (original % 10); + + // Math.trunc(original / 10) で最下位の桁を切り捨てる。 + // JavaScript/TypeScriptには専用の整数除算演算子がないため Math.trunc を使う + // (Pythonの `//` や他言語の整数除算に相当する処理)。 + original = Math.trunc(original / 10); + } + + // 桁数が偶数: original と revertedHalf が完全一致(例: 1221 → 12, 12) + // 桁数が奇数: revertedHalf の真ん中の桁が1つ多いので /10 で切り捨てて比較 + // (例: 12321 → original=12, revertedHalf=123 → 123/10を切り捨てて12 で一致) + return original === revertedHalf || original === Math.trunc(revertedHalf / 10); +} + +export { isPalindrome }; +``` + +> 💡 **競技プログラミング版**(検証を省略し、関数呼び出しのオーバーヘッドを削減した最小構成) +> +> ```typescript +> function isPalindromeCompetitive(x: number): boolean { +> if (x < 0 || (x % 10 === 0 && x !== 0)) return false; +> let rev = 0; +> while (x > rev) { +> rev = rev * 10 + (x % 10); +> x = Math.trunc(x / 10); +> } +> return x === rev || x === Math.trunc(rev / 10); +> } +> ``` +> +> LeetCodeにそのまま提出する場合は、上の業務開発版(`isPalindrome`)の`export`行を削除してそのまま貼り付けるだけで動作します。ESM形式(`export`構文を使うモジュール形式)でNode.js上で再利用する場合は`export`を残してください。 + +## 動作トレース(初学者向け、入力 `x = 12321`) + +``` +初期状態: original=12321, revertedHalf=0 +Step 1: original(12321) > revertedHalf(0) → + revertedHalf = 0*10+1 = 1, original = trunc(12321/10) = 1232 +Step 2: original(1232) > revertedHalf(1) → + revertedHalf = 1*10+2 = 12, original = trunc(1232/10) = 123 +Step 3: original(123) > revertedHalf(12) → + revertedHalf = 12*10+3 = 123, original = trunc(123/10) = 12 +Step 4: original(12) > revertedHalf(123)? → false → ループ終了 +判定: original(12) === revertedHalf(123)? false + original(12) === trunc(revertedHalf/10)(12)? true → 回文である +結果: true +``` + +桁数が奇数(5桁)のため、真ん中の桁「3」が`revertedHalf`側に1つ多く残っている点に注目してください。それを`Math.trunc(revertedHalf / 10)`で切り捨てることで正しく比較できます。 + +## 主要なエッジケースの動作確認 + +- `x = 0` → `x % 10 === 0 && x !== 0`が`false`(`x`自体が0のため)→ メインループへ進み`original === revertedHalf`(共に0)で`true` +- `x = -121` → `x < 0`が`true`→ 即座に`false` +- `x = 10` → `x % 10 === 0 && x !== 0`が`true`→ 即座に`false` +- `x = 121` → メインループで`true` +- `x = 2 ** 31 - 1`(制約上限)→ ループ回数は桁数の半分(約5回)で済み、`number`型の精度上限(`2^53 - 1`)にも余裕があるため問題なく処理できる + +> 📖 **このセクションで登場した用語** +> +> - **JSDoc**:関数の説明・引数・戻り値・計算量などをコメントとして構造化して書く記法。IDEの補完やドキュメント生成にも使われる。 +> - **型ガード**:実行時に値の型を確認し、後続の処理で安全にその型として扱えるようにするチェック。 +> - **早期終了ループ**:全データを処理せず、条件を満たした時点でループを打ち切る書き方。今回は桁数の半分でループが自然に止まる。 + +--- + +## TypeScript固有の最適化観点 + +### 型安全性の活用 + +1. **コンパイル時エラー防止**: 引数を`number`型に固定しているため、文字列や`null`を渡すコードはコンパイル時点で弾かれます。 +2. **実行時の補完**: `Number.isInteger()`により、コンパイル時には検出できない「整数のつもりで渡された小数」も検出します。 + +### コンパイル時最適化 + +1. **型推論の活用**: `let original = x;`のように書けば、TypeScriptが`original: number`と自動推論するため、冗長な型注釈を省けます。 +2. **strict mode**: `tsconfig.json`の`strict: true`を前提とすることで、`original`や`revertedHalf`に`undefined`が紛れ込むミスをコンパイル時に検出できます。 + +### 開発効率と保守性 + +- **IntelliSenseによる開発支援**: JSDocの`@param`・`@returns`・`@throws`がエディタ上に表示されるため、呼び出し側が使い方を誤りにくくなります。 +- **ドキュメント代わりの型定義**: `x: number`という型注釈自体が「この関数は数値しか受け取らない」という契約を表現しており、追加のドキュメントなしでも意図が伝わります。 diff --git a/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 4.5/README.md b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/README.md similarity index 98% rename from Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 4.5/README.md rename to Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/README.md index 9f688cbc..78d6f1e8 100644 --- a/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 4.5/README.md +++ b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/README.md @@ -201,8 +201,8 @@ class Solution:

CPython最適化ポイント

1. **整数演算のみ使用**: - - `//` と `%` は C レベルで実装されており、Python の文字列操作より高速 - - `str()` やスライスを使わないことで、オブジェクト生成コストを回避 + - `//` と `%` は C レベルで組み込み演算として実装 + - `str()` やスライスを使わないことで、新しい文字列オブジェクトの生成を回避し O(1) 空間効率を実現 2. **ローカル変数のみ**: - ループ内は `x`, `rev`, `digit` のみで完結 diff --git a/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/README_react.html b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/README_react.html new file mode 100644 index 00000000..0e66bea8 --- /dev/null +++ b/Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/README_react.html @@ -0,0 +1,2083 @@ + + + + + + LeetCode 9: Palindrome Number | 数値反転比較法による解説 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README.md b/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README.md new file mode 100644 index 00000000..def0fb0a --- /dev/null +++ b/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README.md @@ -0,0 +1,434 @@ +# Roman to Integer - ローマ数字の文字列を整数に変換する問題 + +## 目次 + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl) +- [CPython最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +> 💡 **初学者向け補足**:この問題は、一言で言うと「ローマ数字(`I`, `V`, `X`, `L`, `C`, `D`, `M`)で書かれた文字列を読み取り、対応する整数に変換する問題」です。 + +ローマ数字は基本的に「大きい記号から小さい記号へ左から右に並べる」というルールで書かれますが、`IV`(4)や`IX`(9)のように「小さい記号を大きい記号の前に置くことで引き算を表す」という例外ルールが6パターン存在します。この問題の難しさは、単純に文字を数値に置き換えて足し算するだけでは正しい答えが出せず、**「今見ている文字」と「次に来る文字」の大小関係を見て、足すか引くかを判断する必要がある**という点にあります。 + +制約は `1 <= s.length <= 15` と非常に小さく、かつ「`s` は `[1, 3999]` の範囲で有効なローマ数字であることが保証されている」ため、複雑な検証ロジックや高度なアルゴリズムは不要です。むしろ、いかにシンプルで読みやすく、かつ型安全なコードを書けるかがポイントになります。 + +- 入力:ローマ数字を表す文字列 `s`(例:`"MCMXCIV"`) +- 出力:対応する整数(例:`1994`) +- 制約:`s` は有効なローマ数字であることが保証されている(不正な文字列は入力として来ない) + +> 📖 **この章で登場した用語** +> +> - **制約**:入力として与えられる値の範囲や条件のこと。今回は「文字列の長さは1〜15」「有効なローマ数字であること」が制約 +> - **正当性**:アルゴリズムが常に正しい答えを返すことの保証 + +--- + +

アルゴリズム要点(TL;DR)

+ +> 💡 **初学者向け補足**:TL;DR(Too Long; Didn't Read、=「長くて読めない人向けの要約」)とは、詳しい説明を読む前に「なんとなくこういう手順で解くんだな」というイメージを掴むための要約です。詳しい理由は後の章で説明します。 + +- **文字→数値の対応表(`dict`)を用意する**:7種類の記号を毎回`if-elif`で判定するより、辞書(=キーから値を高速に取り出せるデータ構造)を1回引く方が速く、コードも短くなるため。 +- **文字列を左から右へ1回だけ走査する**:追加のメモリやソートを必要とせず、`O(n)` 時間・`O(1)` 空間で解けるため。 +- **「現在の文字の値」と「次の文字の値」を比較する**:現在値が次の値より小さければ引き算(例:`IV`のとき`I`は引く)、そうでなければ足し算する、というルールで判定できるため。 +- **走査が終わったら合計値をそのまま返す**:追加の後処理は不要なため、シンプルな構造で完結する。 + +> 📖 **この章で登場した用語** +> +> - **TL;DR**:「長くて読めない人向けの要約」を意味する略語 +> - **辞書(`dict`)**:キーを指定して値を取り出せるデータ構造。今回は文字(キー)から数値(値)を取り出すのに使う +> - **走査**:データを順番に1つずつ調べていくこと + +--- + +

図解

+ +> 💡 **初学者向け補足**:図はアルゴリズムの「処理の流れ」を視覚的に示したものです。Mermaidフローチャートでは、**ひし形(`{}`)は条件分岐**、**長方形(`[]`)は処理ステップ**を表します。矢印を上から下へたどっていくことで、プログラムがどう動くかを追うことができます。 + +### フローチャート + +この図は、`romanToInt` 関数が入力文字列を受け取ってから合計値を返すまでの処理の流れを表しています。上から下へ読み進めてください。 + +```mermaid +flowchart TD + Start[Start romanToInt] + Start --> Validate[Validate input type and length] + Validate --> CheckEmpty{Is string empty} + CheckEmpty -- Yes --> ErrEmpty[ValueError: Input cannot be empty] + CheckEmpty -- No --> CheckInvalid{Contains invalid character} + CheckInvalid -- Yes --> ErrInvalid[ValueError: Invalid Roman numeral character] + CheckInvalid -- No --> InitTotal[Initialize total to 0] + InitTotal --> LoopCheck{More characters remain} + LoopCheck -- No --> ReturnTotal[Return total] + LoopCheck -- Yes --> GetCurrent[Look up current char value] + GetCurrent --> GetNext[Look up next char value or none] + GetNext --> Compare{Current less than next} + Compare -- Yes --> Subtract[Subtract current from total] + Compare -- No --> Add[Add current to total] + Subtract --> Advance[Advance index by one] + Add --> Advance + Advance --> LoopCheck +``` + +主要なノードの意味: + +- `Start[Start romanToInt]`:関数の入り口。ローマ数字の文字列を受け取る +- `CheckEmpty{Is string empty}`:入力が空文字列かどうかを判定するひし形(条件分岐) +- `ErrEmpty[ValueError: Input cannot be empty]`:空文字列が渡された場合に発生する例外ノード +- `CheckInvalid{Contains invalid character}`:無効なローマ数字が含まれていないかをチェックする分岐 +- `ErrInvalid[ValueError: Invalid Roman numeral character]`:無効な文字が含まれる場合に発生する例外ノード +- `LoopCheck{More characters remain}`:まだ処理していない文字が残っているかを判定する、ループの継続条件 +- `Compare{Current less than next}`:「今見ている文字の値」が「次の文字の値」より小さいかを判定する、このアルゴリズムの核心部分 +- `Subtract[Subtract current from total]` / `Add[Add current to total]`:判定結果に応じて合計値を引くか足すかを行うステップ + +### データフロー図 + +この図は、入力文字列がどのようなデータの変換を経て最終的な整数値になるかを表しています。 + +```mermaid +graph LR + subgraph Precheck + A[Input string s] + A --> B[Type and length check] + B --> C[Character validity check] + end + subgraph Core + C --> D[Roman value lookup map] + D --> E[Left to right scan] + E --> F[Compare current and next] + end + F --> G[Accumulated total] + G --> H[Output integer] +``` + +主要な流れの説明: + +- 入力文字列から検証済みの文字列へ:型・長さ・文字の妥当性をチェックする +- 検証済みの文字列から対応表の参照へ:`dict`を使って文字を数値に変換する準備を行う +- スキャンから比較・蓄積へ:1文字ずつ処理しながら合計値(`total`)を更新していく + +> 💡 **代表例でのトレース**:入力 `s = "MCMXCIV"` を使って、上記フローチャートの各ノードをどのように通過するかを示します。 + +``` +Step 1: Start → 入力 = "MCMXCIV" +Step 2: Validate → 文字列であり、長さも1〜15の範囲内 → 問題なし +Step 3: CheckEmpty → 空ではない → No +Step 4: InitTotal → total = 0 +Step 5: i=0, current='M'(1000), next='C'(100) → Compare: 1000 < 100 は False → Add → total=1000 +Step 6: i=1, current='C'(100), next='M'(1000) → Compare: 100 < 1000 は True → Subtract → total=900 +Step 7: i=2, current='M'(1000), next='X'(10) → Compare: False → Add → total=1900 +Step 8: i=3, current='X'(10), next='C'(100) → Compare: True → Subtract → total=1890 +Step 9: i=4, current='C'(100), next='I'(1) → Compare: False → Add → total=1990 +Step 10: i=5, current='I'(1), next='V'(5) → Compare: True → Subtract → total=1989 +Step 11: i=6, current='V'(5), next=なし → 無条件でAdd → total=1994 +Step 12: LoopCheck → 残り文字なし → ReturnTotal +結果: 1994 +``` + +> 📖 **この章で登場した用語** +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理 +> - **データフロー図**:データがどのように変換・移動するかを示す図 +> - **サブグラフ**:フローチャートの中で関連する処理をグループ化したもの。今回は「事前チェック(Precheck)」と「本処理(Core)」に分けている + +--- + +

正しさのスケッチ

+ +> 💡 **初学者向け補足**:「正しさのスケッチ」とは、アルゴリズムが常に正しい答えを返すことの根拠を整理したものです。数学的な厳密証明ではなく、「なぜ正しいと言えるか」を直感的に説明するものです。 + +- **不変条件(=処理中ずっと成り立ち続けるべき条件)**:ループの各ステップが終わった時点で、`total`には「それまでに処理した文字列の先頭部分(プレフィックス)を正しく変換した値」が入っている。例えば `"MCM"` まで処理し終えた時点で `total = 1900` となっており、これは`"MCM"`単体を正しく変換した値と一致する。 +- **網羅性(=すべてのケースをもれなく処理できているという保証)**:ローマ数字の減算ルールは「`I`は`V`,`X`の前」「`X`は`L`,`C`の前」「`C`は`D`,`M`の前」の6パターンしかなく、いずれも「現在の文字の値 < 次の文字の値」という条件に一致する。それ以外のすべての並び(大きい記号が先、または同じ記号の連続)は「現在の文字の値 >= 次の文字の値」となり、単純な加算で正しく処理される。つまり、比較条件だけで全パターンを漏れなくカバーできる。 +- **基底条件(=再帰やループの終了条件)**:このアルゴリズムはループ(再帰ではない)だが、対応する終了条件は「走査するインデックス`i`が文字列の長さに達したとき」。このとき`LoopCheck`が`No`となり、`total`をそのまま返す。 +- **終了性(=アルゴリズムが必ず有限ステップで終わるという保証)**:ループは毎回インデックス`i`を1ずつ増やしており、文字列の長さは最大15という有限の値なので、必ず有限回のステップでループを抜ける。 + +> 📖 **この章で登場した用語** +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +> - **基底条件**:ループや再帰の終了条件。これがないと無限ループになる +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 +> - **網羅性**:すべてのケースをもれなく処理できているという保証 + +--- + +

計算量

+ +> 💡 **初学者向け補足**:計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。 + +| 記法 | 意味 | 直感的なイメージ | +| --- | --- | --- | +| `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +| `O(n)` | 入力に比例して増加 | リストを端から順に読む | +| `O(n log n)` | nよりやや速く増加 | 辞書を二分探索で引く×n回 | +| `O(n²)` | 入力の2乗で増加 | 全ペアを総当たりで確認する | + +- **時間計算量:`O(n)`**(`n`は文字列の長さ)。文字列を左から右へ1回だけ走査するため、文字数に比例した時間しかかからない。 +- **空間計算量:`O(1)`**。文字→数値の対応表は文字の種類数(7種類)に固定されており入力サイズに依存しない。合計値を保持する変数も1つだけなので、追加のメモリ使用量は入力サイズによらず一定。 + +> 📖 **この章で登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **in-place**:新しいメモリを確保せず元のデータを直接書き換える操作。空間計算量を抑えられる +> - **Pure**:入力を変更せず新しいデータを返す操作。今回の実装は入力文字列を変更しないPureな実装 + +--- + +

Python 実装

+ +> 💡 **初学者向け補足**:コードを読む前に、実装の全体的な骨格を示します。 +> +> 1. 文字→数値の対応表をクラス変数として用意する +> 2. `_validate`メソッドで入力が文字列であること・空でないこと・不正な文字を含まないことを確認する +> 3. 合計値を表す変数`total`を`0`で初期化する +> 4. 文字列を先頭から1文字ずつ見ていき、「今の文字の値」が「次の文字の値」より小さければ引く、そうでなければ足す +> 5. 走査が終わったら`total`を返す +> +> コード内の型ヒント`Optional[int]`は「`int`または`None`のどちらか」を表します。次の文字が存在しない(今見ている文字が最後の文字である)場合に`None`を使うことで、pylance(VSCodeの型チェッカー)にも意図が正しく伝わります。 + +```python +from __future__ import annotations + +from typing import Dict, Final, Optional + + +class Solution: + """ + Roman to Integer (LeetCode 13) を解決するクラス。 + + ローマ数字を表す文字列を受け取り、対応する整数値を返す。 + """ + + # クラス変数として文字→数値の対応表を保持する。 + # インスタンス(クラスから作られた個々のオブジェクト)ごとに + # 作り直す必要がないデータなので、__init__の中ではなく + # クラス変数として持たせることでメモリを節約できる。 + # Final を付けることで「この変数は再代入されない」ことを + # pylanceに伝え、誤って書き換えるバグを防ぐ。 + _ROMAN_VALUES: Final[Dict[str, int]] = { + "I": 1, + "V": 5, + "X": 10, + "L": 50, + "C": 100, + "D": 500, + "M": 1000, + } + + def romanToInt(self, s: str) -> int: + """ + ローマ数字の文字列を整数に変換する。 + + Args: + s: 変換対象のローマ数字文字列(例: "MCMXCIV")。 + 制約上、長さは1以上15以下で、有効なローマ数字である + ことが保証されている。 + + Returns: + 変換後の整数値(例: 1994)。 + + Raises: + TypeError: 入力が文字列でない場合。 + ValueError: 入力が空、または未知の文字を含む場合。 + """ + # 入力検証(防御的プログラミング:制約上は保証されているが、 + # 誤った呼び出しをされても安全に動作するようにする) + self._validate(s) + + # ループの外で対応表への参照をローカル変数に束縛しておく。 + # self._ROMAN_VALUES というアクセスはPythonの内部で + # 属性検索(辞書検索)を伴うため、ループの外で1回だけ + # 済ませておくとループ内での検索コストを省ける。 + values = self._ROMAN_VALUES + total = 0 + length = len(s) + + # 左から右へ1文字ずつ走査する + for i in range(length): + current = values[s[i]] + + # 次の文字が存在する場合のみその値を取得し、 + # 存在しない(今の文字が最後の文字である)場合はNoneとして扱う。 + # Optional[int] は「int または None のどちらか」を表す型ヒント。 + next_value: Optional[int] = values[s[i + 1]] if i + 1 < length else None + + # 現在値が次の値より小さい場合は「引き算ルール」 + # (IV, IX, XL, XC, CD, CM の6パターン)に該当する。 + if next_value is not None and current < next_value: + total -= current + else: + total += current + + return total + + def _validate(self, data: object) -> None: + """ + 型安全な入力検証を行う。 + + isinstance()で型チェックする。Pythonは動的型付け(=変数の型を + 実行するまで確定しない性質)のため、呼び出し元が誤った型を + 渡しても実行時まで気づかない。pylance(型チェッカー)と + 組み合わせることでコンパイル言語相当の安全性を実現できる。 + """ + if not isinstance(data, str): + raise TypeError("Input must be a string") + + if len(data) == 0: + raise ValueError("Input cannot be empty") + + # all() + ジェネレータ式(=リストを作らずに値を1つずつ + # 順番に生成する書き方)で全文字の妥当性チェックを1行で行う。 + # forループを毎回書くより可読性が高く、内部的にはC実装の + # all()が高速に処理してくれる。 + if not all(char in self._ROMAN_VALUES for char in data): + raise ValueError("Input contains invalid roman numeral characters") +``` + +> 💡 **コードの動作トレース**(初学者向け) + +``` +入力: s = "LVIII" +呼び出し: romanToInt("LVIII") + +Step 1: _validate("LVIII") → 文字列・空でない・全文字が対応表に存在 → 検証通過 +Step 2: values = _ROMAN_VALUES, total = 0, length = 5 + +Step 3: i=0, current='L'(50), next='V'(5) + 50 < 5 は False → 加算 → total = 0 + 50 = 50 + +Step 4: i=1, current='V'(5), next='I'(1) + 5 < 1 は False → 加算 → total = 50 + 5 = 55 + +Step 5: i=2, current='I'(1), next='I'(1) + 1 < 1 は False(同じ値は減算対象にならない)→ 加算 → total = 55 + 1 = 56 + +Step 6: i=3, current='I'(1), next='I'(1) + 1 < 1 は False → 加算 → total = 56 + 1 = 57 + +Step 7: i=4, current='I'(1), next=なし + next_value が None → 無条件で加算 → total = 57 + 1 = 58 + +最終結果: 58 +``` + +> 📖 **この章で登場した用語** +> +> - **`from __future__ import annotations`**:型ヒントを文字列として遅延評価するようにする宣言。今回は必須ではないが、前方参照が必要になった場合に備えて先頭に記述する慣習がある +> - **`Final`**:一度代入した値を再代入できないことをpylanceに伝える型ヒント。定数であることを明示できる +> - **`@lru_cache`**:関数の結果をキャッシュするデコレータ。今回は再帰を使わないため未使用 +> - **`Optional[int]`**:`int` または `None` のどちらかであることを表す型ヒント +> - **ジェネレータ式**:`(x for x in data)`のように、リストを作らずに値を1つずつ順番に生成する書き方。全要素を一度にメモリへ載せないためメモリ効率が良い + +--- + +

CPython最適化ポイント

+ +> 💡 **初学者向け補足**:この章では「同じ処理でもPythonの書き方によって速さが変わる理由」を説明します。今回の問題は入力サイズが最大15文字と非常に小さいため体感できるほどの差は出ませんが、大規模な文字列を扱う場合を想定して最適化テクニックを紹介します。 + +**属性アクセスの削減** + +```python +# 最適化前:ループの中で毎回 self._ROMAN_VALUES を検索する(遅い) +for i in range(length): + current = self._ROMAN_VALUES[s[i]] + +# 最適化後:ループの外で参照をローカル変数にキャッシュしておく(速い) +values = self._ROMAN_VALUES # 属性アクセスを1回だけ行う +for i in range(length): + current = values[s[i]] +# 理由:Pythonは self._ROMAN_VALUES にアクセスするたびに +# インスタンス→クラスの順で辞書検索を行う。 +# 参照をローカル変数に保存しておくとこの検索コストを +# ループの外で1回だけに抑えられる。 +``` + +**辞書ルックアップ vs if-elif連鎖** + +```python +# if-elif連鎖:記号が増えるたびに分岐が増え、可読性も低下する +if char == "I": + value = 1 +elif char == "V": + value = 5 +# ...(以下同様に7分岐) + +# 辞書(dict)ルックアップ:CPython内部でハッシュテーブルとして +# 実装されているため、記号の種類が増えても検索速度はO(1)のまま変わらない +value = values[char] +``` + +- **`bisect`や`heapq`について**:この問題は固定の対応表を1回引くだけで済むため、二分探索(`bisect`)や優先度付きキュー(`heapq`)を使う必要はありません。それらは「ソート済みデータへの高速な挿入・検索」や「最小値・最大値の高速な取り出し」が必要な問題で効果を発揮します。 +- **スライスの回避**:今回の実装では`s[i]`という1文字アクセスのみを行い、`s[0:3]`のようなスライス(=リストや文字列の一部を新しく作り出す操作)は使っていません。スライスは新しいオブジェクトを生成するためメモリと処理時間を消費しますが、今回は不要です。 + +> 📖 **この章で登場した用語** +> +> - **属性アクセス**:`self.x`のようにオブジェクトのプロパティを参照する操作。Pythonでは内部的に辞書検索が発生する +> - **スライス**:`arr[1:3]`のようにリストや文字列の一部を取り出す操作。新しいオブジェクトを生成するのでメモリを消費する +> - **ローカル変数キャッシュ**:属性やグローバル変数への参照を一度ローカル変数に保存することで、アクセスコストを削減するテクニック +> - **ハッシュテーブル**:キーを特殊な計算(ハッシュ関数)で変換し、値の格納場所を高速に特定できるデータ構造。`dict`の内部実装に使われている + +--- + +

エッジケースと検証観点

+ +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。 + +- **最小長の文字列(`s = "I"`)**:1文字しかないため「次の文字」が存在しない。`next_value`が`None`になるケースを正しく処理できるかがポイント。この実装では`i + 1 < length`の条件チェックにより、`IndexError`(範囲外アクセスによるエラー)を起こさずに安全に処理できる。 +- **減算ルールが登場しないケース(`s = "III"`)**:同じ文字が連続する場合、`current < next_value`は常に`False`になるため、単純な加算のみが行われる。 +- **6種類すべての減算パターン(`IV`, `IX`, `XL`, `XC`, `CD`, `CM`)**:それぞれが正しく「大きい値から小さい値を引く」形で計算されるかを確認する必要がある。特に`CM`(900)のように、百の位の記号が千の位の記号の前に来るケースは見落としやすい。 +- **最大値に近いケース(`s = "MMMCMXCIX"` = 3999)**:制約の上限値付近でも、走査回数が増えるだけでロジックが破綻しないことを確認する。 +- **最大長の文字列(長さ15文字)**:制約上の最大長でもループが正しく最後まで完走し、`IndexError`が発生しないことを確認する。 + +> 📖 **この章で登場した用語** +> +> - **エッジケース**:空文字列・最小長・最大長など、境界的な条件の入力 +> - **境界値**:制約の上限・下限にあたる値。今回は文字列長の`1`と`15`が該当する +> - **インデックスエラー(`IndexError`)**:リストや文字列の範囲外の要素にアクセスしようとしたときに発生するエラー + +--- + +

FAQ

+ +**Q1. なぜ`if-elif`の連鎖ではなく辞書(`dict`)を使うのですか?** + +結論:辞書を使う方が高速かつ簡潔だからです。 +理由:`dict`はCPythonの内部でハッシュテーブルとして実装されており、キーの数(今回は7種類)が増えても検索速度は変わりません。一方`if-elif`は記号の種類が増えるほど分岐が増え、可読性も低下します。 +補足:今回のように記号の種類が少なくても、辞書を使うことで「対応表とロジックの分離」ができ、後から記号を追加・変更する際にも辞書の中身を書き換えるだけで済みます。 + +**Q2. なぜ「次の文字」を先読みする必要があるのですか?** + +結論:ローマ数字の減算ルール(`IV`や`IX`など)は「今の文字」だけでは判断できず、「次に来る文字」との比較が必要だからです。 +理由:例えば`I`という文字だけを見ても、それが単独の`1`なのか、`IV`の一部として`-1`扱いになるのかは、次に`V`や`X`が来るかどうかを見ないと分かりません。 +補足:この実装では`s[i + 1]`にアクセスする前に`i + 1 < length`を確認することで、文字列の末尾で範囲外アクセスが発生しないようにしています。 + +**Q3. 右から左へ走査する方法(直前の最大値と比較する方法)ではダメなのですか?** + +結論:どちらの方法でも正しく動作しますが、今回は左から右へ走査する方法を採用しました。 +理由:問題文の説明(「`I`が`V`の前にあるので4になる」)が「今の文字と次の文字の関係」で語られているため、左から右への走査の方が問題文の説明と直感的に一致し、コードを読む人にも意図が伝わりやすいからです。 +補足:右から左へ走査する方法は「直前に見た最大値より小さい文字が来たら引く」というロジックになり、計算量は同じ`O(n)`ですが、比較対象が「過去の最大値」という抽象的な概念になるため、初学者にはやや理解しにくい場合があります。 + +**Q4. `Optional[int]`という型ヒントはなぜ必要なのですか?** + +結論:「次の文字の値」が存在する場合と存在しない場合の両方を型として正しく表現するためです。 +理由:文字列の最後の文字を処理するとき、「次の文字」は存在しません。この「存在しない」状態を`None`で表現し、`Optional[int]`という型ヒントを付けることで、pylance(VSCodeの型チェッカー)が「`None`かもしれない値に対して不正な演算をしていないか」を実行前にチェックできるようになります。 +補足:もし`Optional[int]`を使わず単に`int`とだけ書いてしまうと、pylanceは`next_value`が常に整数であると誤解し、`None`が渡された場合のバグを検出できなくなります。 + +**Q5. このアルゴリズムが常に正しい答えを返すと言えるのはなぜですか?** + +結論:ローマ数字の減算ルールが「現在の文字の値 < 次の文字の値」という1つの条件で完全にカバーできるからです。 +理由:[正しさのスケッチ](#correctness)で説明した通り、ローマ数字における例外パターン(`IV`, `IX`, `XL`, `XC`, `CD`, `CM`)はすべて「小さい記号が大きい記号の直前に置かれる」という共通の形をしており、それ以外の並び方(大きい記号が先、または同じ記号の繰り返し)はすべて単純な加算で正しく処理されます。 +補足:この性質のおかげで、特別な例外処理(例えば「もし`IV`という並びが来たら」のような文字列パターンマッチング)を書く必要がなく、たった1つの比較条件だけで問題を解決できています。 + +> 📖 **この章で登場した用語** +> +> - **FAQ**:Frequently Asked Questions(よくある質問)の略 +> - **トレードオフ**:何かを得ると何かを失う関係。例:可読性を優先すると、多少のパフォーマンスを犠牲にすることがある diff --git a/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README_react.html b/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README_react.html new file mode 100644 index 00000000..bedff337 --- /dev/null +++ b/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README_react.html @@ -0,0 +1,1380 @@ + + + + + + Roman to Integer(LeetCode 13)解説 - ハッシュテーブル1パス走査 + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「ローマ数字の文字列を読み取り、対応する整数に変換する問題」です。 +

+

+ ローマ数字は基本的に大きい記号から小さい記号へ左から右に並べますが、IV(4)や + IX(9)のように、小さい記号を大きい記号の前に置いて引き算を表す例外パターンが6つあります。 + この問題では、その例外パターンを見落とさずに正しく数値へ変換することがポイントになります。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + すべての文字の値をそのまま足し算するだけでは、IV + が「1+5=6」になってしまい、正しい答え「4」にならない。 +
  • +
  • + 「今見ている文字」だけでは、足すべきか引くべきかを判断できず、「次に来る文字」との大小比較が必要になる。 +
  • +
+
+ +
+
+
+ O(n) +
+
時間計算量
+
+
+
+ O(1) +
+
空間計算量
+
+
+
+ 1 <= n <= 15 +
+
入力文字列長
+
+
+
+ dict +
+
主に使うデータ構造
+
+
+ +
+
+

+ 入力: s = "III"  + 出力: 3 +

+

+ 同じ記号Iが3つ並んでいるだけなので、単純にI+I+I=3となる。 +

+
+
+

+ 入力: s = "LVIII"  + 出力: 58 +

+

+ L=50, V=5, + III=3で、いずれも減算パターンを含まないため単純な足し算で58になる。 +

+
+
+

+ 入力: s = "MCMXCIV"  + 出力: 1994 +

+

+ M=1000, CM=900, XC=90, + IV=4で、CM・XC・IVの3つの減算パターンをすべて含む代表的な例。 +

+
+
+
+ + +
+ + +
+

+ Python実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. + 入力が文字列であり、空でなく、有効な文字のみで構成されているかを検証する +
  2. +
  3. + ループの外で対応表への参照をローカル変数に束縛し、合計値totalを0で初期化する +
  4. +
  5. + 文字列を先頭から1文字ずつ走査し、次の文字と比較しながらtotalを更新する +
  6. +
  7. 走査が終わったらtotalをそのまま返す
  8. +
+
+ +
from __future__ import annotations
+
+from typing import Dict, Final, Optional
+
+
+class Solution:
+    """
+    Roman to Integer (LeetCode 13) を解決するクラス。
+
+    ローマ数字を表す文字列を受け取り、対応する整数値を返す。
+    """
+
+    # クラス変数として文字→数値の対応表を保持する。
+    # インスタンスごとに作り直す必要がないデータなので、
+    # __init__の中ではなくクラス変数として持たせることでメモリを節約できる。
+    _ROMAN_VALUES: Final[Dict[str, int]] = {
+        "I": 1,
+        "V": 5,
+        "X": 10,
+        "L": 50,
+        "C": 100,
+        "D": 500,
+        "M": 1000,
+    }
+
+    def romanToInt(self, s: str) -> int:
+        """
+        ローマ数字の文字列を整数に変換する。
+
+        Args:
+            s: 変換対象のローマ数字文字列(例: "MCMXCIV")。
+
+        Returns:
+            変換後の整数値(例: 1994)。
+
+        Raises:
+            TypeError: 入力が文字列でない場合。
+            ValueError: 入力が空、または未知の文字を含む場合。
+        """
+        # 入力検証(防御的プログラミング:制約上は保証されているが、
+        # 誤った呼び出しをされても安全に動作するようにする)
+        self._validate(s)
+
+        # ループの外で対応表への参照をローカル変数に束縛しておく。
+        # self._ROMAN_VALUES というアクセスはPythonの内部で属性検索を
+        # 伴うため、ループの外で1回だけ済ませておくと検索コストを省ける。
+        values = self._ROMAN_VALUES
+        total = 0
+        length = len(s)
+
+        # 左から右へ1文字ずつ走査する
+        for i in range(length):
+            current = values[s[i]]
+
+            # 次の文字が存在する場合のみその値を取得し、
+            # 存在しない場合はNoneとして扱う。
+            next_value: Optional[int] = values[s[i + 1]] if i + 1 < length else None
+
+            # 現在値が次の値より小さい場合は「引き算ルール」
+            # (IV, IX, XL, XC, CD, CM の6パターン)に該当する。
+            if next_value is not None and current < next_value:
+                total -= current
+            else:
+                total += current
+
+        return total
+
+    def _validate(self, data: object) -> None:
+        """型安全な入力検証を行う"""
+        if not isinstance(data, str):
+            raise TypeError("Input must be a string")
+
+        if len(data) == 0:
+            raise ValueError("Input cannot be empty")
+
+        # all() + ジェネレータ式で全文字の妥当性チェックを1行で行う。
+        if not all(char in self._ROMAN_VALUES for char in data):
+            raise ValueError("Input contains invalid roman numeral characters")
+ +
+

+ ▶ 入力例 "MCMXCIV" での動作トレース +

+
+i=0: current='M'(1000), next='C'(100)  → 1000 < 100 は偽 → 加算 → total=1000
+i=1: current='C'(100),  next='M'(1000) → 100 < 1000 は真  → 減算 → total=900
+i=2: current='M'(1000), next='X'(10)   → 1000 < 10 は偽   → 加算 → total=1900
+i=3: current='X'(10),   next='C'(100)  → 10 < 100 は真    → 減算 → total=1890
+i=4: current='C'(100),  next='I'(1)    → 100 < 1 は偽     → 加算 → total=1990
+i=5: current='I'(1),    next='V'(5)    → 1 < 5 は真       → 減算 → total=1989
+i=6: current='V'(5),    next=なし      → 無条件で加算       → total=1994
+出力: 1994 ✅
+
+
+ + +
+ + +
+

+ 計算量分析 +

+ +
+

📖 Big-O 記法の読み方

+
+
+
O(1)
+
入力サイズに関わらず
常に一定の速さ
+
+
+
O(n)
+
入力が2倍になると
処理も約2倍
+
+
+
O(n log n)
+
入力が2倍になると
処理は約2倍強
+
+
+
O(n²)
+
入力が2倍になると
処理は約4倍
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間計算量空間計算量備考
+ ハッシュテーブル1パス走査(採用) + O(n)O(1) + 文字列を1回だけ走査。対応表は固定サイズ(7種類)でメモリは入力サイズに依存しない +
2文字パターンの事前置換O(n)O(n) + CM・XC・IV等を先に置換してから加算。新しい文字列を都度生成するため余分なメモリを消費する +
+
+ +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 時間計算量が O(n) + になるのは、文字列の各文字をちょうど1回ずつしか見ないためです。次の文字を先読みする処理も、 + ループの中でインデックスを1つずらして参照するだけなので、追加のループを必要としません。 + 空間計算量が O(1) + になるのは、文字→数値の対応表が常に7種類固定であり、入力文字列がどれだけ長くなっても + (制約上は最大15文字ですが)対応表のサイズや追加変数の数は変わらないためです。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください(五十音順)。 +

+
+
+ + インデックス + +
+ 配列や文字列の中で、要素が何番目にあるかを表す番号のこと。多くのプログラミング言語では0から数え始めるため、先頭の文字はインデックス0、2番目の文字はインデックス1となる。 +
+
+
+ + 型ヒント + +
+ 関数の引数や戻り値に、想定している型を注釈として書く仕組み。def f(x: int) -> str:のように書く。pylanceのような型チェッカーが、実行前に型の誤りを検出できるようになる。 +
+
+
+ + 計算量 + +
+ 入力の大きさに対して、処理にかかる手間(時間計算量)やメモリ量(空間計算量)がどう増えるかを表す目安。Big-O記法(O(n)など)で表現されることが多い。 +
+
+
+ + 走査 + +
+ 配列や文字列などのデータを、順番に1つずつ調べていくこと。今回の実装では、文字列を左から右へ1回だけ走査することで答えを求めている。 +
+
+
+ + ハッシュテーブル + +
+ キーから値をO(1)(一定時間)で取り出せるデータ構造。図書館の索引カードに例えると、タイトル(キー)から棚番号(値)を即座に引けるイメージ。Pythonではdictがこれに相当する。 +
+
+
+ + 不変条件 + +
+ アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件のこと。今回の実装では「ループの各時点でtotalには、それまでに処理した文字列の先頭部分を正しく変換した値が入っている」という不変条件が成り立っている。 +
+
+
+
+ +
+ LeetCode 13 · Roman to Integer · ハッシュテーブル1パス走査による解説ページ +
+
+ + + + + + + + + + + + + + + + + + + diff --git a/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/Roman_to_Integer_Python.md b/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/Roman_to_Integer_Python.md new file mode 100644 index 00000000..9ae495e8 --- /dev/null +++ b/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/Roman_to_Integer_Python.md @@ -0,0 +1,286 @@ +# Roman to Integer(ローマ数字を整数に変換する) + +--- + +## 1. 問題分析結果 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「ローマ数字の文字列を読み取って、対応する整数に変換する問題」です。 +> +> Pythonでこの問題を解く際に特に気をつけるべき点は、**文字列のインデックスアクセスが範囲外になったときの挙動**です。C言語などと違い、Pythonの文字列は「範囲外にアクセスするとエラーになる」ため、「次の文字が存在するかどうか」を先に確認する必要があります。また、辞書(`dict`)を使った文字→数値の変換はC実装のハッシュテーブルによって行われるため、`if-elif`の連鎖よりも高速かつ読みやすくなります。 + +### 競技プログラミング視点 + +- **制約分析**: 文字列長は最大15文字(`1 <= s.length <= 15`)という非常に小さい制約のため、どんなアプローチを選んでも実行時間は問題になりません。むしろコードの短さ・書きやすさが重要になります。 +- **最速手法**: 文字列を1回だけ左から右へ走査(=順番に1つずつ調べていくこと)する `O(n)` 時間のアプローチが理論上も実装上も最適です。 +- **メモリ最小化**: 追加のリストや文字列を新たに作らず、合計値を表す変数1つだけを更新していく「インプレース操作」(=新しいオブジェクトを作らず、既存の変数を直接書き換える操作)に近い形にすることでメモリ使用量を`O(1)`に抑えます。 +- **CPython最適化**: 辞書(`dict`)のルックアップ(=キーから値を取り出す操作)はC言語レベルで実装されているため、`if-elif`の羅列より高速かつ簡潔です。 + +### 業務開発視点 + +- **型安全設計**: `Dict[str, int]` という型ヒントを使い、pylanceが「辞書のキーは文字列、値は整数である」ことを検証できるようにします。 +- **エラーハンドリング**: 入力が文字列でない場合や、ローマ数字として無効な文字が含まれる場合に、明確なエラーメッセージ付きの例外を送出します。 +- **可読性**: docstring(=関数の先頭に書く説明文)で、引数・戻り値・例外条件を明記します。 + +### Python特有分析 + +- **データ構造選択**: 文字→数値の対応表には `dict` を使います。今回は7種類の固定キーしかないため、`Counter`や`defaultdict`のような特殊な辞書は不要です。 +- **標準ライブラリ活用度**: この問題では外部ライブラリはもちろん、`collections`や`heapq`などの特殊な標準ライブラリも不要です。単純な`dict`だけで十分に効率的です。 +- **CPython最適化度**: 辞書アクセス(`dict[key]`)はCPythonの内部でハッシュテーブルとして実装されており、`O(1)`(=入力サイズに関係なく一定時間)でアクセスできます。 + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、組み込み関数の多くがC実装のため高速 +> - **インプレース操作**:新しいオブジェクトを作らず、既存のオブジェクトを直接書き換える操作。メモリ効率が良い +> - **ハッシュテーブル**:キーを特殊な計算(ハッシュ関数)で変換し、値の格納場所を高速に特定できるデータ構造。辞書(`dict`)の内部実装に使われている +> - **ルックアップ**:辞書やリストなどから、キーやインデックスを指定して値を取り出す操作 + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。今回は入力サイズが最大15文字と非常に小さいため、どちらのアプローチでも実行速度に大きな差は出ませんが、Pythonらしい書き方(=Pythonic)かどうかも比較のポイントにします。 + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) +> - `O(n log n)`:入力が2倍になると、処理は約2倍強になる(ソートアルゴリズムに多い) +> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループに多い) + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ---------- | ---------- | ---------- | ---------------- | ------ | ------------------ | ------------- | ---- | +| A: 左から右へ走査し、次の文字と比較 | O(n) | O(1) | 低 | ★★★ | dict | 適 | 1回のループで完結し、最もシンプル | +| B: 右から左へ走査し、直前の最大値と比較 | O(n) | O(1) | 低 | ★★☆ | dict | 適 | Aと同等の効率だが直感的にやや理解しにくい | +| C: `re`モジュールで2文字パターン(`CM`, `CD`, `XC`など)を先に置換してから1文字ずつ加算 | O(n) | O(n) | 中 | ★★☆ | re, dict | 不適(余分な文字列生成) | 標準ライブラリの`re`は使えるが正規表現エンジンの初期化コストがかかる | + +- **選択理由**: アプローチAは計算量的に最も効率が良く(`O(n)`時間・`O(1)`空間)、かつコードが最も短くシンプルです。アプローチCのように新しい文字列を都度生成する方法は、C実装の`re`モジュールを使うとはいえ、正規表現のコンパイルや文字列の再生成という余分なコストがかかるため採用しませんでした。 +- **Python最適化戦略**: 辞書(`dict`)のルックアップをC実装のハッシュテーブルに任せることで、`if-elif`の連鎖より高速かつ簡潔なコードになります。また、`s[i + 1] if i + 1 < len(s) else None` という書き方で「次の文字が存在するかどうか」を安全にチェックします。 +- **トレードオフ**: 可読性を最優先しつつ、辞書ルックアップというCPython最適化の恩恵も受けられるため、今回は可読性とパフォーマンスの間でトレードオフがほとんど発生しません。 + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **トレードオフ**:何かを得ると何かを失う関係。速くするとメモリが増える、など +> - **C実装**:Pythonコードではなく、内部でC言語で実装された関数。Pure Pythonより大幅に高速 +> - **Pythonic**:Python言語の思想や慣習に沿った、自然で読みやすい書き方のこと + +--- + +## 3. 実装パターン + +> 💡 **初学者向け補足**:コード全体を示す前に、大まかな構造(骨格)を示します。 +> +> 1. まず、ローマ数字の文字とその値を対応させる辞書を用意する +> 2. 入力値が正しい形式(文字列であること)かを確認する +> 3. 文字列を先頭から1文字ずつ見ていき、「今の文字の値」が「次の文字の値」より小さい場合は引き算、そうでない場合は足し算する +> 4. すべて処理し終えたら、合計値を返す + +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。入力値の検証やエラーメッセージが充実しており、後から読んだ人が「何が起きているか」を理解しやすい構造になっています。 + +```python +from typing import Dict, Optional + + +class Solution: + """ + Roman to Integer 解決クラス + + 競技プログラミング向けと業務開発向けの2パターンを提供 + """ + + # クラス変数としてローマ数字の対応表を定義する。 + # インスタンスごとに作り直す必要がないデータなので、 + # __init__ の中ではなくクラス変数として持たせることでメモリを節約できる。 + _ROMAN_VALUES: Dict[str, int] = { + "I": 1, + "V": 5, + "X": 10, + "L": 50, + "C": 100, + "D": 500, + "M": 1000, + } + + def solve_production(self, s: str) -> int: + """ + 業務開発向け実装(型安全・エラーハンドリング重視) + + Args: + s: 変換対象のローマ数字文字列(例: "MCMXCIV") + + Returns: + 変換後の整数値(例: 1994) + + Raises: + TypeError: 入力が文字列でない場合 + ValueError: 入力に未知の文字が含まれる、または空文字列の場合 + """ + # 1. 入力検証 + self._validate_input(s) + + # 2. エッジケース処理 + # 「空文字列」は制約上あり得ないが、防御的プログラミング + # (=想定外の入力にも備えてコードを書く考え方)として明示的に扱う + if self._is_edge_case(s): + return self._handle_edge_case(s) + + # 3. メインアルゴリズム + result = self._main_algorithm(s) + + # 4. 後処理・検証(今回は追加の後処理は不要なのでそのまま返す) + return result + + def _validate_input(self, data: object) -> None: + """型安全な入力検証""" + # isinstance() で型チェックする。 + # Python は動的型付け(=変数の型を実行するまで確定しない性質)のため、 + # 呼び出し元が誤った型を渡しても実行時まで気づかない。 + # pylance(型チェッカー)と組み合わせることでコンパイル言語相当の安全性を実現できる。 + if not isinstance(data, str): + raise TypeError("Input must be a string") + + if len(data) == 0: + raise ValueError("Input cannot be empty") + + if len(data) > 15: + raise ValueError("Input size exceeds limit") + + # all() + ジェネレータ式で全文字の妥当性チェックを1行で行う。 + # 「ジェネレータ式」とは、リストを作らずに値を1つずつ順番に生成する書き方で、 + # for ループを毎回書くより可読性が高く、内部的にはC実装のall()が高速に処理してくれる。 + if not all(char in self._ROMAN_VALUES for char in data): + raise ValueError("Input contains invalid roman numeral characters") + + def _is_edge_case(self, data: str) -> bool: + """エッジケース判定(1文字のみの場合は特別扱いしなくても動くが、明示のため用意)""" + return len(data) == 1 + + def _handle_edge_case(self, data: str) -> int: + """エッジケース処理:1文字だけの場合はそのまま対応表を引けばよい""" + return self._ROMAN_VALUES[data] + + def _main_algorithm(self, data: str) -> int: + """ + メインアルゴリズム実装 + 左から右へ走査し、次の文字と比較しながら加算・減算する + """ + total: int = 0 + length: int = len(data) + + # range(length) で文字列の全インデックスを走査する + for i in range(length): + current_value: int = self._ROMAN_VALUES[data[i]] + + # 「次の文字が存在するか」を Optional[int] 型で安全に表現する。 + # Optional[int] は「int か None のどちらか」を表す型ヒントで、 + # 存在しない場合に None を使うことで pylance にも意図が伝わる。 + next_value: Optional[int] = ( + self._ROMAN_VALUES[data[i + 1]] if i + 1 < length else None + ) + + # 現在値が次の値より小さい場合は「引き算ルール」(例: IV, IX, XL など) + if next_value is not None and current_value < next_value: + total -= current_value + else: + total += current_value + + return total +``` + +【競技プログラミング版を使う場面】 +LeetCodeやAtCoderなど、制限時間内に正解を出すことが目的のコードに向きます。可読性よりも実行速度・コードの短さを優先した書き方になっています。 + +```python + def solve_competitive(self, s: str) -> int: + """ + 競技プログラミング向け最適化実装 + エラーハンドリング省略、コードの短さを優先 + + Time Complexity: O(n) + Space Complexity: O(1) + """ + # 辞書アクセスをローカル変数に束縛(bind)しておくことで、 + # ループ内での属性検索(self._ROMAN_VALUES というアクセス)の + # コストを毎回発生させないようにする、CPythonでの定番の高速化テクニック。 + values = self._ROMAN_VALUES + total = 0 + n = len(s) + + for i in range(n): + # 三項演算子(条件式)で1行にまとめ、コードを短くする + v = values[s[i]] + total += -v if i + 1 < n and v < values[s[i + 1]] else v + + return total +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 入力: s = "MCMXCIV" +> +> Step 1: i=0, current='M'(1000), next='C'(100) +> 1000 は 100 より小さくない → 加算 → total = 0 + 1000 = 1000 +> +> Step 2: i=1, current='C'(100), next='M'(1000) +> 100 < 1000 → 減算 → total = 1000 - 100 = 900 +> +> Step 3: i=2, current='M'(1000), next='X'(10) +> 1000 は 10 より小さくない → 加算 → total = 900 + 1000 = 1900 +> +> Step 4: i=3, current='X'(10), next='C'(100) +> 10 < 100 → 減算 → total = 1900 - 10 = 1890 +> +> Step 5: i=4, current='C'(100), next='I'(1) +> 100 は 1 より小さくない → 加算 → total = 1890 + 100 = 1990 +> +> Step 6: i=5, current='I'(1), next='V'(5) +> 1 < 5 → 減算 → total = 1990 - 1 = 1989 +> +> Step 7: i=6, current='V'(5), next=None(最後の文字) +> next が None なので無条件に加算 → total = 1989 + 5 = 1994 +> +> 最終結果: total = 1994 ✅ +> ``` + +> 💡 **型ヒントとpylanceの関係(初学者向け)** +> Pythonは動的型付け言語なので、型を書かなくても動きます。しかし型ヒントを書いておくと、pylance(VSCodeの型チェッカー)が実行前に「型が合っていない」エラーを検出してくれます。 +> +> ```python +> # 型ヒントなし → pylanceは "next_value" が int か None かを推測できない +> def f(data, i): +> return data.get(i) +> +> # 型ヒントあり → pylanceが None チェック漏れを実行前に検出できる +> def f(data: Dict[str, int], i: str) -> Optional[int]: +> return data.get(i) +> ``` + +> 📖 **このセクションで登場した用語** +> +> - **クラス変数**:インスタンス(=クラスから作られた個々のオブジェクト)ごとではなく、クラス全体で1つだけ共有される変数。今回の対応表のように「全インスタンスで共通のデータ」を持たせるのに適している +> - **防御的プログラミング**:想定外の入力やエラーが起きても、プログラムが安全に動作し続けるようにあらかじめ備えておく考え方 +> - **ジェネレータ式**:`(x for x in data)`のように、リストを作らずに値を1つずつ順番に生成する書き方。`list`と違い全要素を一度にメモリへ載せないためメモリ効率が良い +> - **Optional[int]**:「int または None のどちらか」であることを表す型ヒント +> - **束縛(バインド)**:ある名前(変数名)に値やオブジェクトを結びつけること。ループの外で `self._ROMAN_VALUES` をローカル変数に束縛しておくと、ループ内で毎回属性検索する手間を省ける + +--- + +## 4. 検証 + +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。 + +- **境界値テスト**: + - `s = "I"`(最小長・1文字)→ 対応表を1回引くだけで正しく `1` を返せるか + - `s = "III"`(減算ルールが一切登場しない、単純な加算のみのケース) + - `s = "MCMXCIV"`(減算ルールが複数回登場する、最も複雑なケース) + - `s = "MMMCMXCIX"`(3999、範囲の最大値に近いケース) +- **型チェック**: `_ROMAN_VALUES: Dict[str, int]`、`Optional[int]` などの型ヒントにより、pylanceが「存在しないキーへのアクセス」や「Noneに対する演算」を実行前に警告してくれます。今回の実装では `data[i] in self._ROMAN_VALUES` によるチェックと `Optional[int]` の明示的な `None` チェックにより、pylanceの警告が発生しない設計になっています。 + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること +> - **静的解析**:プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法 diff --git a/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/Roman_to_Integer_Typescript.md b/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/Roman_to_Integer_Typescript.md new file mode 100644 index 00000000..ef31dc09 --- /dev/null +++ b/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/Roman_to_Integer_Typescript.md @@ -0,0 +1,222 @@ +# Roman to Integer(ローマ数字を整数に変換する) + +--- + +## 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「ローマ数字の文字列を読み取って、対応する整数に変換する問題」です。 + +- **競技プログラミング視点での分析** + - 文字列を1回だけ左から右へなめる(走査する)だけで答えが出せるため、実行速度は最速クラスの `O(n)`(文字数に比例する時間)に抑えられます。 + - 文字と数値の対応表(マップ)は最大でも7種類しかないため、`Map` や `Record` を使ってもメモリ使用量はごくわずかです。 + - 文字列の長さは最大15文字という制約(`1 <= s.length <= 15`)があるため、そもそも巨大な入力を心配する必要はありません。 +- **業務開発視点での分析** + - 「ローマ数字として正しい形式かどうか」を厳密にチェックすると実装が複雑になりすぎるため、今回は「入力は有効なローマ数字である」という制約を信頼しつつ、最低限の型安全性(=間違った型のデータが来てもプログラムが暴走しない仕組み)だけを持たせます。 + - 保守性を高めるため、「文字→数値」の対応表を関数の外に定数として切り出し、ロジックと分離します。 +- **TypeScript特有の考慮点** + - ローマ数字の文字(`I`, `V`, `X`, `L`, `C`, `D`, `M`)を **リテラル型**(=`string`ではなく`'I'`のような特定の値だけを許す型)として定義することで、「存在しない文字キーにアクセスしてしまう」バグをコンパイル時(=コードを実行する前の変換段階)に検出できます。 + - `Record` という型を使うことで、「対応表に7種類全部の文字が漏れなく入っているか」をコンパイラが自動チェックしてくれます。 + +> 📖 **このセクションで登場した用語** +> +> - **走査(そうさ)**:データを順番に1つずつ調べていくこと +> - **リテラル型**:`string`のような広い型ではなく、`'I'`や`'V'`など特定の値だけを許可する型 +> - **Record型**:「キーの型」と「値の型」を指定して、オブジェクトの形を表現するTypeScriptの組み込み型 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| ---------- | ---------- | ---------- | ------------ | -------- | ------ | ---- | +| A: 左から右へ走査し、次の文字と比較 | O(n) | O(1) | 低 | 高 | 高 | 1回のループで完結。最もシンプル | +| B: 右から左へ走査し、直前の最大値と比較 | O(n) | O(1) | 低 | 高 | 中 | Aと同等の効率だが直感的にはやや理解しにくい | +| C: 2文字の組み合わせ(`IV`, `IX`など)を先に文字列置換してから1文字ずつ加算 | O(n) | O(n) | 中 | 中 | 中 | 文字列の置換処理が余分に発生し非効率 | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) +> - `O(n log n)`:入力が2倍になると、処理は約2倍強になる(ソートアルゴリズムに多い) +> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループに多い) + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して、処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 + +--- + +## 3. 選択したアルゴリズムと理由 + +> 💡 **初学者向け補足**:「なぜBやCを選ばなかったのか」という対比で考えると、選択の根拠がより明確になります。 + +- **選択したアプローチ**: A(左から右へ走査し、次の文字と比較する方法) +- **理由**: + - **計算量的な優位性**: Aは文字列を1回だけなめるだけで答えが出るため `O(n)` 時間・`O(1)` 空間と、これ以上削れない最適な効率です。Cのように文字列置換を行う方法(アプローチC)は、置換のたびに新しい文字列がメモリ上に作られてしまい、`O(n)` の追加メモリを消費するため選びませんでした。 + - **TypeScript環境での型安全性**: 対応表を `Record` として定義することで、タイプミスによる存在しないキーへのアクセスをコンパイル時に防げます。 + - **保守性・可読性の観点**: 「今の文字の値」と「次の文字の値」を比較するだけ、というロジックはBの「右から左へ、直前の最大値と比較する」方式よりも問題文の説明(IをVの前に置くと4になる、など)と直感的に一致しており、後から読むメンバーにも意図が伝わりやすいです。 +- **TypeScript特有の最適化ポイント**: + - コンパイル時の型チェックによって、対応表に存在しない文字を渡すバグを未然に防止 + - `readonly` 修飾子を使い、入力配列や対応表を誤って書き換えてしまう副作用を防止 + - 型推論を活用し、ループ変数などに過剰な型注釈を書かずに済ませる + +> 📖 **このセクションで登場した用語** +> +> - **副作用**:関数の外にある変数やデータを書き換えてしまうこと。副作用があると、同じ入力でも結果が変わってしまう可能性があり、バグの原因になりやすい + +--- + +## 4. 実装コード + +> 💡 **初学者向け補足**:コード全体を示す前に、大まかな構造(骨格)を示します。 +> +> 1. まず、ローマ数字の文字とその値を対応させる表(マップ)を用意する +> 2. 入力値が正しい形式(文字列であること)かを確認する +> 3. 文字列を先頭から1文字ずつ見ていき、「今の文字の値」が「次の文字の値」より小さい場合は引き算、そうでない場合は足し算する +> 4. すべて処理し終えたら、合計値を返す + +```typescript +// 1. ローマ数字1文字として許される値だけをリテラル型で定義する +// ('A'や'Z'のような無関係な文字が紛れ込むのをコンパイル時に防ぐため) +type RomanChar = 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'; + +// 2. 文字→数値の対応表を定数として定義する +// Record にすることで「7種類の文字すべてに対応する数値が +// 漏れなく定義されているか」をコンパイラがチェックしてくれる +const ROMAN_VALUES: Readonly> = { + I: 1, + V: 5, + X: 10, + L: 50, + C: 100, + D: 500, + M: 1000, +}; + +/** + * ローマ数字の文字列を整数に変換する + * @param s - 変換対象のローマ数字文字列(例: "MCMXCIV") + * @returns 変換後の整数値(例: 1994) + * @throws {TypeError} 入力が文字列でない場合 + * @throws {RangeError} 入力に未知の文字が含まれる場合 + * @complexity Time: O(n), Space: O(1) + */ +function romanToInt(s: string): number { + // 型ガード(コンパイル時 + 実行時の型安全性) + // 「型ガード」とは、実行時に値の型を確認してから処理を進める仕組みのこと。 + // TypeScriptの型チェックはコンパイル時にしか働かないため、 + // 実行時に想定外の値が来てもクラッシュしないよう、ここでもう一度確認する + if (typeof s !== 'string') { + throw new TypeError('Input must be a string'); + } + + // 文字列が空の場合は合計も0なので、早期に返してしまう + // (これによりループ内で余計な条件分岐をしなくて済む) + if (s.length === 0) { + return 0; + } + + // 合計値を保持する変数。これから1文字ずつ足し引きしていく + let total = 0; + + // 文字列の先頭から末尾の1つ手前まで走査する + // 「s.length - 1」までにしているのは、ループの中で「次の文字」を + // 先読みするため、最後の文字は特別扱いする必要があるから + for (let i = 0; i < s.length; i += 1) { + // 現在の文字を対応表から数値に変換する + const currentChar = s[i] as RomanChar; + const currentValue = ROMAN_VALUES[currentChar]; + + // 未知の文字(対応表に存在しない文字)が来た場合はエラーにする + if (currentValue === undefined) { + throw new RangeError(`Invalid roman numeral character: "${s[i]}"`); + } + + // 「次の文字」が存在する場合のみ、その値を取得する + // 存在しない(今見ている文字が最後の文字である)場合は0として扱う + const nextChar = s[i + 1] as RomanChar | undefined; + const nextValue = nextChar !== undefined ? ROMAN_VALUES[nextChar] : 0; + + // ここが「引き算ルール」の核心部分: + // 例)"IV" の場合、I(1) の次に V(5) が来ている + // 現在値(1) < 次の値(5) なので、1を「引く」ことで + // 結果的に (5 - 1 = 4) という正しい値になる + // + // 例)"VI" の場合、V(5) の次に I(1) が来ている + // 現在値(5) は 次の値(1) より大きいので、そのまま「足す」 + if (currentValue < nextValue) { + total -= currentValue; + } else { + total += currentValue; + } + } + + // すべての文字を処理し終えた合計値を返す + return total; +} + +export { romanToInt }; +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 入力: s = "MCMXCIV" +> +> Step 1: i=0, current='M'(1000), next='C'(100) +> 1000 >= 100 なので加算 → total = 0 + 1000 = 1000 +> +> Step 2: i=1, current='C'(100), next='M'(1000) +> 100 < 1000 なので減算 → total = 1000 - 100 = 900 +> +> Step 3: i=2, current='M'(1000), next='X'(10) +> 1000 >= 10 なので加算 → total = 900 + 1000 = 1900 +> +> Step 4: i=3, current='X'(10), next='C'(100) +> 10 < 100 なので減算 → total = 1900 - 10 = 1890 +> +> Step 5: i=4, current='C'(100), next='I'(1) +> 100 >= 1 なので加算 → total = 1890 + 100 = 1990 +> +> Step 6: i=5, current='I'(1), next='V'(5) +> 1 < 5 なので減算 → total = 1990 - 1 = 1989 +> +> Step 7: i=6, current='V'(5), next=undefined(0扱い) +> 5 >= 0 なので加算 → total = 1989 + 5 = 1994 +> +> 最終結果: total = 1994 ✅ +> ``` + +> 📖 **このセクションで登場した用語** +> +> - **先読み(さきよみ)**:今の要素だけでなく、1つ先の要素も同時に確認しながら処理を進めるテクニック +> - **as(型アサーション)**:「この値はこの型として扱ってよい」とTypeScriptに伝える構文。ただし実行時のチェックは自動では行われないため、今回のコードのように`undefined`チェックを別途行う必要がある +> - **undefined扱い**:配列の範囲外にアクセスすると`undefined`が返る性質を利用し、「最後の文字の次には何もない」ことを表現している + +--- + +## TypeScript固有の最適化観点 + +### 型安全性の活用 + +1. **コンパイル時エラー防止** + - `RomanChar` というリテラル型を使うことで、対応表 `ROMAN_VALUES` に7種類の文字すべての値が定義されているかをコンパイラが自動でチェックします。1つでも書き漏らすとコンパイルエラーになります。 +2. **ジェネリクスによる再利用性** + - 今回の問題は文字の種類が固定されているため、あえてジェネリクス(型を後から差し込める仕組み)は使わず、シンプルな `RomanChar` 型に統一しています。「無理にジェネリクスを使わない」という判断も、可読性を優先したTypeScript設計の一つです。 +3. **型ガード・型アサーション** + - `typeof s !== 'string'` で実行時の型チェックを行い、`as RomanChar` で「この文字はローマ数字の文字である」という前提をコードに明示しつつ、`undefined` チェックで安全性を担保しています。 + +### コンパイル時最適化 + +1. **型推論の活用** + - `let total = 0;` のように、変数の型(`number`)は代入値から自動的に推論されるため、明示的な型注釈を省略しています。 +2. **readonly修飾子** + - `Readonly>` とすることで、対応表 `ROMAN_VALUES` が誤って書き換えられることをコンパイル時に防いでいます。`readonly`はJavaScriptには存在しない概念で、これを使うことで「このデータは変更されないはずだ」という意図をコード上で保証できます。 + +### 開発効率と保守性 + +- 対応表がオブジェクトとして独立しているため、将来的に他のローマ数字関連の関数(整数→ローマ数字への変換など)を追加する際にも再利用できます。 +- 型定義(`RomanChar`)がそのままドキュメントの役割を果たし、「この関数がどんな文字を想定しているか」が型を見ただけで分かります。 diff --git a/generate_index.py b/generate_index.py index dd914c5b..7d655bbc 100644 --- a/generate_index.py +++ b/generate_index.py @@ -40,6 +40,8 @@ def copy_vendor_files(self, output_dir: str) -> None: "node_modules/prismjs/themes/prism.css": "prismjs/themes/prism.css", # FontAwesome (CSS) "node_modules/@fortawesome/fontawesome-free/css/all.min.css": "fontawesome/css/all.min.css", + # Mermaid + "node_modules/mermaid/dist/mermaid.min.js": "mermaid/mermaid.min.js", } # Directory Mapping for Prism Plugins (since individual files are tedious) @@ -121,6 +123,8 @@ def rewrite_html_content(self, content: str) -> str: (r'https://cdn\.jsdelivr\.net/npm/prismjs(?:@[^/]+)?/plugins/([a-zA-Z0-9_-]+)/prism-\1\.min\.js', r'/vendor/prismjs/plugins/\1/prism-\1.js'), (r'https://cdn\.jsdelivr\.net/npm/prismjs(?:@[^/]+)?/plugins/([a-zA-Z0-9_-]+)/prism-\1\.min\.css', r'/vendor/prismjs/plugins/\1/prism-\1.css'), (r'https://cdn\.jsdelivr\.net/npm/prismjs(?:@[^/]+)?/themes/prism(?:-[a-zA-Z0-9_-]+)?\.min\.css', '/vendor/prismjs/themes/prism.css'), + # Mermaid + (r'https://cdn\.jsdelivr\.net/npm/mermaid(?:@[^/]+)?/dist/mermaid(?:\.min)?\.js', '/vendor/mermaid/mermaid.min.js'), ] for pattern_str, new in replacements: diff --git a/public/Algorithm/BinarySearch/leetcode/69. Sqrt(x)/Claude4.6 extended/README_react.html b/public/Algorithm/BinarySearch/leetcode/69. Sqrt(x)/Claude4.6 extended/README_react.html index 897b92de..6377ca09 100644 --- a/public/Algorithm/BinarySearch/leetcode/69. Sqrt(x)/Claude4.6 extended/README_react.html +++ b/public/Algorithm/BinarySearch/leetcode/69. Sqrt(x)/Claude4.6 extended/README_react.html @@ -8,7 +8,7 @@ - + - + - + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/public/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 4.6 adaptive/README_React.html b/public/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 4.6 adaptive/README_React.html new file mode 100644 index 00000000..95354bb0 --- /dev/null +++ b/public/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 4.6 adaptive/README_React.html @@ -0,0 +1,1412 @@ + + + + + + LeetCode 119: Pascal's Triangle II - 二項係数の漸化式で1行を求める + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「パスカルの三角形の、指定された行だけを無駄なく作る問題」です。 +

+

+ パスカルの三角形は、各行の両端が常に + 1 で、 + それ以外の値は「1つ上の行の隣り合う2つの数の和」になっている数の並びです。 + 今回の問題では三角形全体を作る必要はなく、rowIndex + 番目(0-indexed=0から数え始める番号付け)の行だけを返せばよい、という点がポイントです。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + 三角形を上から1行ずつ積み上げる素直な方法は、二重ループが必要になり計算量が + O(rowIndex²) になってしまいます。 +
  • +
  • + フォローアップ条件「O(rowIndex) + の追加空間のみ」を満たすには、過去の行を丸ごと保持する二次元配列を作らない工夫が必要です。 +
  • +
+
+ +
+
+
+ O(rowIndex) +
+
時間計算量
+
+
+
+ O(rowIndex) +
+
追加空間計算量
+
+
+
+ binomial_recurrence +
+
採用アルゴリズム
+
+
+
+ list[int] +
+
使用データ構造
+
+
+ +
+

📥 入出力例

+
+
+
Example 1
+
rowIndex = 3
+
Output: [1,3,3,1]
+

+ 3行目(0-indexed)の両端は1、内側は1つ上の行の隣接する値の和になるため + [1,3,3,1] が正解です。 +

+
+
+
Example 2
+
rowIndex = 0
+
Output: [1]
+

+ 0行目は三角形の頂点そのものなので、要素数1の [1] が正解です。 +

+
+
+
Example 3
+
rowIndex = 1
+
Output: [1,1]
+

+ 1行目は両端の1だけで構成されるため [1,1] が正解です。 +

+
+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 代表例 + rowIndex = 4 + を使って、 配列 + row + がどのように埋まっていくかを1ステップずつ追跡します。 +

+
+
+ + +
+

+ Python実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. rowIndex の型と範囲を検証する(型ガード)
  2. +
  3. 結果を格納するリストを rowIndex + 1 個分だけ確保する
  4. +
  5. + 先頭を1に設定し、二項係数の漸化式で k=1 から rowIndex まで値を埋めていく +
  6. +
  7. 完成したリストを返す
  8. +
+
+ +
from __future__ import annotations
+
+from typing import List
+
+
+class Solution:
+    """
+    119. Pascal's Triangle II を解決するクラス
+
+    パスカルの三角形の rowIndex 行目を、二項係数の漸化式を使って
+    O(rowIndex) 時間・O(rowIndex) 追加空間で求める。
+    """
+
+    def getRow(self, rowIndex: int) -> List[int]:
+        """
+        パスカルの三角形の rowIndex 行目(0-indexed)を返す
+
+        Args:
+            rowIndex: 求めたい行番号(0 <= rowIndex <= 33)
+
+        Returns:
+            rowIndex 行目の値を格納したリスト
+
+        Raises:
+            TypeError: rowIndex が int 型でない場合
+            ValueError: rowIndex が 0〜33 の範囲外の場合
+        """
+        # 型ヒントは実行時には無視される。呼び出し元が誤った型を渡した場合に
+        # 実行時までエラーに気づけないため、isinstance() で別途検証する。
+        self._validate_input(rowIndex)
+
+        # 結果を格納するリストを先に確保する。
+        # フォローアップの「O(rowIndex)の追加空間」を満たすため、
+        # 過去の行を丸ごと保持する二次元リストは作らず、この1本のリストだけを使い回す。
+        row: List[int] = [0] * (rowIndex + 1)
+
+        # パスカルの三角形の左端は、どの行でも必ず1になる
+        # (rowIndex個からk=0個を選ぶ組み合わせは「何も選ばない」の1通りしかないため)
+        row[0] = 1
+
+        # 二項係数の漸化式 C(n, k) = C(n, k-1) * (n - k + 1) // k を使い、
+        # 1つ左の値から次の値を順番に計算していく(同じ行の中だけで完結する)
+        for k in range(1, rowIndex + 1):
+            # 掛け算を先に行ってから割り算をする。
+            # 「//」(フロア除算)を使うのは、Pythonの「/」が常にfloatを返してしまい
+            # List[int]という戻り値の型と食い違ってしまうため。
+            # 二項係数は必ず整数になる値なので、掛け算を先に行えば割り切れずに誤差が出ることはない。
+            row[k] = row[k - 1] * (rowIndex - k + 1) // k
+
+        return row
+
+    def _validate_input(self, row_index: int) -> None:
+        """入力値が問題の制約を満たすかを検証する"""
+        # isinstance() で型チェックする。
+        # bool は int のサブクラスなので、isinstance(True, int) は True になってしまう。
+        # そのため True/False が誤って渡されるケースも別途弾いておく。
+        if not isinstance(row_index, int) or isinstance(row_index, bool):
+            raise TypeError("rowIndex must be an int")
+
+        if not (0 <= row_index <= 33):
+            raise ValueError("rowIndex must be between 0 and 33")
+
+ +
+

+ ▶ 入力例 rowIndex = 3 での動作トレース +

+
+呼び出し: Solution().getRow(3)
+Step 1: _validate_input(3) → isinstance(3, int)かつboolではない → OK、0<=3<=33 → OK
+Step 2: row = [0, 0, 0, 0] を確保 → row[0] = 1 → row = [1, 0, 0, 0]
+Step 3: k=1 → row[1] = row[0] * (3-1+1) // 1 = 1 * 3 // 1 = 3 → row = [1, 3, 0, 0]
+Step 4: k=2 → row[2] = row[1] * (3-2+1) // 2 = 3 * 2 // 2 = 3 → row = [1, 3, 3, 0]
+Step 5: k=3 → row[3] = row[2] * (3-3+1) // 3 = 3 * 1 // 3 = 1 → row = [1, 3, 3, 1]
+最終結果: [1, 3, 3, 1](Example 1の出力と一致)
+
+
+ + +
+

+ 処理フローチャート +

+ +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=はい・正常フロー + 赤=エラー +
+
+

+ 紫の矢印は「同じ行の中で次の k へ戻る」ループを表します。 +

+
+ +
+ + + + + + + + + + + + + + + + + + + + 開始 + + + getRow(rowIndex) を呼び出す + + + + + + + + rowIndexは正しいか + + + int型かつ 0以上33以下か + + + + + + いいえ + + + + エラーを送出 + + + TypeError / + + + ValueError + + + + + + はい + + + + + + 配列を確保する + + + row = [0] × (rowIndex + 1) + + + + + + + + 基底値を設定する + + + row[0] = 1 + + + + + + + + kはrowIndex以下か + + + k = 1, 2, ..., rowIndex + + + + + + はい + + + + + + row[k] を計算する + + + row[k] = row[k-1] × (n-k+1) ÷ k + + + (n = rowIndex、// で整数のまま計算) + + + + + + 次のkへ + + + + + + いいえ(完了) + + + + + + 終了 + + + row を返す + + +
+ +
+

+ 🔎 入力例 rowIndex = 4 でのフロー追跡 +

+
    +
  1. 「開始」ノード → rowIndex = 4 を受け取る
  2. +
  3. + 「rowIndexは正しいか」ノード → + int型かつ0〜33の範囲内なので「はい」の経路へ +
  4. +
  5. + 「配列を確保する」→「基底値を設定する」→ row = [1, 0, 0, 0, 0] になる +
  6. +
  7. + 「kはrowIndex以下か」を4回「はい」で通過し、そのたびに「row[k]を計算する」でrowを更新する(紫の矢印で同じ行の中をループ) +
  8. +
  9. + k=5になった時点で「いいえ(完了)」の経路に進み、「終了」ノードで [1, 4, + 6, 4, 1] を返す +
  10. +
+
+ +

+ フローの説明:
+ このアルゴリズムには「行を積み上げるループ」が存在しません。緑の矢印で示した通り、 + 入力検証を通過した後は「配列の確保」「基底値の設定」を1回ずつ行い、 + あとは紫のループバック矢印が示す通り + k を + 1つずつ増やしながら同じ行の中だけを計算し続けます。ループが二重になっていない点が、 + 三角形全体を作る方法との決定的な違いです。 +

+
+ + +
+

+ 計算量分析 +

+ +
+

+ 📖 Big-O 記法の読み方(入力サイズ n + が大きくなるにつれて処理時間がどう増えるかの目安) +

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ nより少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間計算量空間計算量備考
三角形全体を二次元リストで構築O(n²)O(n²)直感的だが不要な行まで保持してしまう
1本のリストを右から左へ更新(in-place)O(n²)O(n)追加空間は最小だが二重ループが必要
+ 二項係数の漸化式(採用) + O(n)O(n) + 同じ行の中だけで完結するため最速 +
+
+ +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 採用した実装では、ループが + k = 1 から + rowIndex + までの1重だけで、 + 各回の計算が「1つ前の値」を使った掛け算1回・割り算1回という定数時間で終わります。 + そのため合計の時間計算量は入力(rowIndex)に比例する O(n) になります。 + 空間計算量も、出力用のリスト以外に一時配列や二次元リストを作らないため O(n) + に抑えられており、 フォローアップ条件をそのまま満たしています。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください(五十音順)。 +

+
+
+ + 型ヒント + +
+ 関数の引数や戻り値に型を注釈として書く仕組み。def getRow(self, rowIndex: int) -> List[int]: + のように書きます。Pythonは動的型付け言語なので実行時には無視されますが、pylance(VSCodeの型チェッカー)が + 実行前に誤った呼び出しを検出してくれます。 +
+
+ +
+ + 基底値 + +
+ 漸化式の出発点となる、最初から分かっている値のこと。今回は + row[0] = 1 + が基底値にあたります。 + 「rowIndex個から0個を選ぶ組み合わせは、何も選ばないという1通りしかない」という事実に対応しています。 +
+
+ +
+ + 計算量 + +
+ 入力の大きさに対して、処理にかかる時間(時間計算量)やメモリ(空間計算量)がどう増えるかの目安。 + O(1), O(n), O(n log n), O(n²) のようなBig-O記法で表します。 +
+
+ +
+ + 漸化式 + +
+ ある項の値を、1つ前の項の値から計算するための式。今回は + C(n, k) = C(n, k-1) × (n-k+1) ÷ k + という二項係数の漸化式を使い、同じ行の中だけで次の値を求めています。 +
+
+ +
+ + 二項係数 + +
+ n個の中からk個を選ぶ組み合わせの数。C(n, k) + と表記します。パスカルの三角形のrowIndex行目k番目の値は、実は + C(rowIndex, k) + そのものです。 +
+
+ +
+ + フロア除算 + +
+ Pythonの + // + 演算子。小数点以下を切り捨てて + int + を返します。Pythonの + /(真の除算)は割り切れる場合でも 常に + float + を返してしまうため、 + List[int] + という戻り値の型と合わせるために 今回は + // + を使っています。 +
+
+
+
+ +
+ LeetCode 119: Pascal's Triangle II - 初学者向け解説ページ +
+
+ + + + + + + + + + + + + + + diff --git a/public/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README_React.html b/public/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README_React.html new file mode 100644 index 00000000..95354bb0 --- /dev/null +++ b/public/Algorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README_React.html @@ -0,0 +1,1412 @@ + + + + + + LeetCode 119: Pascal's Triangle II - 二項係数の漸化式で1行を求める + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「パスカルの三角形の、指定された行だけを無駄なく作る問題」です。 +

+

+ パスカルの三角形は、各行の両端が常に + 1 で、 + それ以外の値は「1つ上の行の隣り合う2つの数の和」になっている数の並びです。 + 今回の問題では三角形全体を作る必要はなく、rowIndex + 番目(0-indexed=0から数え始める番号付け)の行だけを返せばよい、という点がポイントです。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + 三角形を上から1行ずつ積み上げる素直な方法は、二重ループが必要になり計算量が + O(rowIndex²) になってしまいます。 +
  • +
  • + フォローアップ条件「O(rowIndex) + の追加空間のみ」を満たすには、過去の行を丸ごと保持する二次元配列を作らない工夫が必要です。 +
  • +
+
+ +
+
+
+ O(rowIndex) +
+
時間計算量
+
+
+
+ O(rowIndex) +
+
追加空間計算量
+
+
+
+ binomial_recurrence +
+
採用アルゴリズム
+
+
+
+ list[int] +
+
使用データ構造
+
+
+ +
+

📥 入出力例

+
+
+
Example 1
+
rowIndex = 3
+
Output: [1,3,3,1]
+

+ 3行目(0-indexed)の両端は1、内側は1つ上の行の隣接する値の和になるため + [1,3,3,1] が正解です。 +

+
+
+
Example 2
+
rowIndex = 0
+
Output: [1]
+

+ 0行目は三角形の頂点そのものなので、要素数1の [1] が正解です。 +

+
+
+
Example 3
+
rowIndex = 1
+
Output: [1,1]
+

+ 1行目は両端の1だけで構成されるため [1,1] が正解です。 +

+
+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 代表例 + rowIndex = 4 + を使って、 配列 + row + がどのように埋まっていくかを1ステップずつ追跡します。 +

+
+
+ + +
+

+ Python実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. rowIndex の型と範囲を検証する(型ガード)
  2. +
  3. 結果を格納するリストを rowIndex + 1 個分だけ確保する
  4. +
  5. + 先頭を1に設定し、二項係数の漸化式で k=1 から rowIndex まで値を埋めていく +
  6. +
  7. 完成したリストを返す
  8. +
+
+ +
from __future__ import annotations
+
+from typing import List
+
+
+class Solution:
+    """
+    119. Pascal's Triangle II を解決するクラス
+
+    パスカルの三角形の rowIndex 行目を、二項係数の漸化式を使って
+    O(rowIndex) 時間・O(rowIndex) 追加空間で求める。
+    """
+
+    def getRow(self, rowIndex: int) -> List[int]:
+        """
+        パスカルの三角形の rowIndex 行目(0-indexed)を返す
+
+        Args:
+            rowIndex: 求めたい行番号(0 <= rowIndex <= 33)
+
+        Returns:
+            rowIndex 行目の値を格納したリスト
+
+        Raises:
+            TypeError: rowIndex が int 型でない場合
+            ValueError: rowIndex が 0〜33 の範囲外の場合
+        """
+        # 型ヒントは実行時には無視される。呼び出し元が誤った型を渡した場合に
+        # 実行時までエラーに気づけないため、isinstance() で別途検証する。
+        self._validate_input(rowIndex)
+
+        # 結果を格納するリストを先に確保する。
+        # フォローアップの「O(rowIndex)の追加空間」を満たすため、
+        # 過去の行を丸ごと保持する二次元リストは作らず、この1本のリストだけを使い回す。
+        row: List[int] = [0] * (rowIndex + 1)
+
+        # パスカルの三角形の左端は、どの行でも必ず1になる
+        # (rowIndex個からk=0個を選ぶ組み合わせは「何も選ばない」の1通りしかないため)
+        row[0] = 1
+
+        # 二項係数の漸化式 C(n, k) = C(n, k-1) * (n - k + 1) // k を使い、
+        # 1つ左の値から次の値を順番に計算していく(同じ行の中だけで完結する)
+        for k in range(1, rowIndex + 1):
+            # 掛け算を先に行ってから割り算をする。
+            # 「//」(フロア除算)を使うのは、Pythonの「/」が常にfloatを返してしまい
+            # List[int]という戻り値の型と食い違ってしまうため。
+            # 二項係数は必ず整数になる値なので、掛け算を先に行えば割り切れずに誤差が出ることはない。
+            row[k] = row[k - 1] * (rowIndex - k + 1) // k
+
+        return row
+
+    def _validate_input(self, row_index: int) -> None:
+        """入力値が問題の制約を満たすかを検証する"""
+        # isinstance() で型チェックする。
+        # bool は int のサブクラスなので、isinstance(True, int) は True になってしまう。
+        # そのため True/False が誤って渡されるケースも別途弾いておく。
+        if not isinstance(row_index, int) or isinstance(row_index, bool):
+            raise TypeError("rowIndex must be an int")
+
+        if not (0 <= row_index <= 33):
+            raise ValueError("rowIndex must be between 0 and 33")
+
+ +
+

+ ▶ 入力例 rowIndex = 3 での動作トレース +

+
+呼び出し: Solution().getRow(3)
+Step 1: _validate_input(3) → isinstance(3, int)かつboolではない → OK、0<=3<=33 → OK
+Step 2: row = [0, 0, 0, 0] を確保 → row[0] = 1 → row = [1, 0, 0, 0]
+Step 3: k=1 → row[1] = row[0] * (3-1+1) // 1 = 1 * 3 // 1 = 3 → row = [1, 3, 0, 0]
+Step 4: k=2 → row[2] = row[1] * (3-2+1) // 2 = 3 * 2 // 2 = 3 → row = [1, 3, 3, 0]
+Step 5: k=3 → row[3] = row[2] * (3-3+1) // 3 = 3 * 1 // 3 = 1 → row = [1, 3, 3, 1]
+最終結果: [1, 3, 3, 1](Example 1の出力と一致)
+
+
+ + +
+

+ 処理フローチャート +

+ +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=はい・正常フロー + 赤=エラー +
+
+

+ 紫の矢印は「同じ行の中で次の k へ戻る」ループを表します。 +

+
+ +
+ + + + + + + + + + + + + + + + + + + + 開始 + + + getRow(rowIndex) を呼び出す + + + + + + + + rowIndexは正しいか + + + int型かつ 0以上33以下か + + + + + + いいえ + + + + エラーを送出 + + + TypeError / + + + ValueError + + + + + + はい + + + + + + 配列を確保する + + + row = [0] × (rowIndex + 1) + + + + + + + + 基底値を設定する + + + row[0] = 1 + + + + + + + + kはrowIndex以下か + + + k = 1, 2, ..., rowIndex + + + + + + はい + + + + + + row[k] を計算する + + + row[k] = row[k-1] × (n-k+1) ÷ k + + + (n = rowIndex、// で整数のまま計算) + + + + + + 次のkへ + + + + + + いいえ(完了) + + + + + + 終了 + + + row を返す + + +
+ +
+

+ 🔎 入力例 rowIndex = 4 でのフロー追跡 +

+
    +
  1. 「開始」ノード → rowIndex = 4 を受け取る
  2. +
  3. + 「rowIndexは正しいか」ノード → + int型かつ0〜33の範囲内なので「はい」の経路へ +
  4. +
  5. + 「配列を確保する」→「基底値を設定する」→ row = [1, 0, 0, 0, 0] になる +
  6. +
  7. + 「kはrowIndex以下か」を4回「はい」で通過し、そのたびに「row[k]を計算する」でrowを更新する(紫の矢印で同じ行の中をループ) +
  8. +
  9. + k=5になった時点で「いいえ(完了)」の経路に進み、「終了」ノードで [1, 4, + 6, 4, 1] を返す +
  10. +
+
+ +

+ フローの説明:
+ このアルゴリズムには「行を積み上げるループ」が存在しません。緑の矢印で示した通り、 + 入力検証を通過した後は「配列の確保」「基底値の設定」を1回ずつ行い、 + あとは紫のループバック矢印が示す通り + k を + 1つずつ増やしながら同じ行の中だけを計算し続けます。ループが二重になっていない点が、 + 三角形全体を作る方法との決定的な違いです。 +

+
+ + +
+

+ 計算量分析 +

+ +
+

+ 📖 Big-O 記法の読み方(入力サイズ n + が大きくなるにつれて処理時間がどう増えるかの目安) +

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ nより少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間計算量空間計算量備考
三角形全体を二次元リストで構築O(n²)O(n²)直感的だが不要な行まで保持してしまう
1本のリストを右から左へ更新(in-place)O(n²)O(n)追加空間は最小だが二重ループが必要
+ 二項係数の漸化式(採用) + O(n)O(n) + 同じ行の中だけで完結するため最速 +
+
+ +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 採用した実装では、ループが + k = 1 から + rowIndex + までの1重だけで、 + 各回の計算が「1つ前の値」を使った掛け算1回・割り算1回という定数時間で終わります。 + そのため合計の時間計算量は入力(rowIndex)に比例する O(n) になります。 + 空間計算量も、出力用のリスト以外に一時配列や二次元リストを作らないため O(n) + に抑えられており、 フォローアップ条件をそのまま満たしています。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください(五十音順)。 +

+
+
+ + 型ヒント + +
+ 関数の引数や戻り値に型を注釈として書く仕組み。def getRow(self, rowIndex: int) -> List[int]: + のように書きます。Pythonは動的型付け言語なので実行時には無視されますが、pylance(VSCodeの型チェッカー)が + 実行前に誤った呼び出しを検出してくれます。 +
+
+ +
+ + 基底値 + +
+ 漸化式の出発点となる、最初から分かっている値のこと。今回は + row[0] = 1 + が基底値にあたります。 + 「rowIndex個から0個を選ぶ組み合わせは、何も選ばないという1通りしかない」という事実に対応しています。 +
+
+ +
+ + 計算量 + +
+ 入力の大きさに対して、処理にかかる時間(時間計算量)やメモリ(空間計算量)がどう増えるかの目安。 + O(1), O(n), O(n log n), O(n²) のようなBig-O記法で表します。 +
+
+ +
+ + 漸化式 + +
+ ある項の値を、1つ前の項の値から計算するための式。今回は + C(n, k) = C(n, k-1) × (n-k+1) ÷ k + という二項係数の漸化式を使い、同じ行の中だけで次の値を求めています。 +
+
+ +
+ + 二項係数 + +
+ n個の中からk個を選ぶ組み合わせの数。C(n, k) + と表記します。パスカルの三角形のrowIndex行目k番目の値は、実は + C(rowIndex, k) + そのものです。 +
+
+ +
+ + フロア除算 + +
+ Pythonの + // + 演算子。小数点以下を切り捨てて + int + を返します。Pythonの + /(真の除算)は割り切れる場合でも 常に + float + を返してしまうため、 + List[int] + という戻り値の型と合わせるために 今回は + // + を使っています。 +
+
+
+
+ +
+ LeetCode 119: Pascal's Triangle II - 初学者向け解説ページ +
+
+ + + + + + + + + + + + + + + diff --git a/public/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README_React.html b/public/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README_React.html new file mode 100644 index 00000000..65ec57ba --- /dev/null +++ b/public/Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README_React.html @@ -0,0 +1,822 @@ + + + + + +LeetCode 125: Valid Palindrome | 二方向ポインタで学ぶ回文判定 + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+

アルゴリズム概要

+ +
+

+ 💡 この問題を一言で言うと:「文字列から記号やスペースを除き、大文字を小文字に揃えたとき、前から読んでも後ろから読んでも同じかどうかを判定する問題」です。 +

+

+ 英語の文章では、大文字・小文字の違いやカンマ・コロン・スペースといった記号が自由に混ざります。この問題では、それらの「見た目のノイズ」を無視して、純粋に英数字(アルファベットと数字)の並びだけに注目したときに回文(前から読んでも後ろから読んでも同じ並び)になっているかを判定します。 +

+
+ +
+

⚠️ なぜ単純な方法では解けないのか

+
    +
  • 記号やスペースが文字列のどこに何個出現するか分からないため、s == s[::-1] のような一括比較では正しく判定できません。
  • +
  • 「記号を除いて小文字化した新しい文字列を作ってから比較する」という素直な方法は正解にはなりますが、文字列の長さが最大 2×10^5 になりうるため、新しい文字列を作るたびに余分なメモリを消費してしまいます。
  • +
+
+ +
+
+
O(n)
+
時間計算量
+
+
+
O(1)
+
空間計算量
+
+
+
isalnum
+
使用メソッド
+
+
+
Easy
+
難易度
+
+
+ +
+
+

s = "A man, a plan, a canal: Panama"

+

→ True

+

記号とスペースを除いて小文字化すると "amanaplanacanalpanama" となり、前から読んでも後ろから読んでも同じになるため True です。

+
+
+

s = "race a car"

+

→ False

+

クリーニングすると "raceacar" となり、逆から読むと "racaecar" で一致しないため False です。

+
+
+

s = " "(半角スペース1文字)

+

→ True

+

英数字が1つも無いため、クリーニング後は空文字列になります。空文字列は前からも後ろからも「何もない」ので、回文として扱われ True になります。

+
+
+
+ + +
+

ステップバイステップ解説

+

+ 入力例 s = "A man, a plan, a canal: Panama" を使って、二方向ポインタが実際にどう動くかを1ステップずつ確認しましょう。文字の下の数字はインデックス(何番目の文字かを示す位置番号)です。 +

+
+
+ + +
+

Python実装

+ +
+

📋 このコードの構造(先に全体像を把握しよう)

+
    +
  1. 入力が str 型かどうかを検証する(エッジケース処理を含む)
  2. +
  3. 左ポインタ left を先頭、右ポインタ right を末尾に初期化する
  4. +
  5. left < right の間、非英数字の読み飛ばし・小文字化しての比較・ポインタの移動を繰り返す
  6. +
  7. 不一致が見つかれば即座に False を、ループを抜けたら True を返す
  8. +
+
+ +
from __future__ import annotations
+from typing import Final
+
+
+class Solution:
+    """125. Valid Palindrome を解くクラス"""
+
+    _MAX_LENGTH: Final[int] = 2 * 10 ** 5
+
+    def isPalindrome(self, s: str) -> bool:
+        """
+        文字列 s が英数字だけ・小文字統一で回文かどうかを判定する
+
+        Args:
+            s: 判定対象の文字列
+
+        Returns:
+            回文であれば True、そうでなければ False
+        """
+        if not isinstance(s, str):
+            raise TypeError("Input must be a string")
+        if len(s) > self._MAX_LENGTH:
+            raise ValueError("Input length exceeds the constraint")
+
+        left: int = 0
+        right: int = len(s) - 1
+
+        while left < right:
+            if not s[left].isalnum():
+                left += 1
+                continue
+            if not s[right].isalnum():
+                right -= 1
+                continue
+            if s[left].lower() != s[right].lower():
+                return False
+            left += 1
+            right -= 1
+
+        return True
+
+ +
+

▶ 入力例 s = "A man, a plan, a canal: Panama" での動作トレース

+
入力: s = "A man, a plan, a canal: Panama"(長さ30)
+left=0, right=29
+ 0,29: 'A' と 'a' → 小文字化して比較 → 'a' == 'a' → 一致 → left=1, right=28
+ 1,28: s[1]=' ' は非英数字 → left=2
+ 2,28: 'm' と 'm' → 一致 → left=3, right=27
+ 3,27: 'a' と 'a' → 一致 → left=4, right=26
+ 4,26: 'n' と 'n' → 一致 → left=5, right=25
+ 5,25: s[5]=',' は非英数字 → left=6
+ 6,25: s[6]=' ' は非英数字 → left=7
+ 7,25: 'a' と 'a' → 一致 → left=8, right=24
+ 8,24: s[8]=' ' は非英数字 → left=9
+ 9,24: 'p' と 'P' → 小文字化 'p' == 'p' → 一致 → left=10, right=23
+10,23: s[23]=' ' は非英数字 → right=22
+10,22: s[22]=':' は非英数字 → right=21
+10,21: 'l' と 'l' → 一致 → left=11, right=20
+11,20: 'a' と 'a' → 一致 → left=12, right=19
+12,19: 'n' と 'n' → 一致 → left=13, right=18
+13,18: s[13]=',' は非英数字 → left=14
+14,18: s[14]=' ' は非英数字 → left=15
+15,18: 'a' と 'a' → 一致 → left=16, right=17
+16,17: s[16]=' ' は非英数字 → left=17
+left(17) < right(17) は成立しない → ループ終了
+出力: True ✅
+
+
+ + +
+

処理フローチャート

+ +
+

🗺️ フローチャートの読み方

+
+
+ + 楕円(緑)= 開始・終了 +
+
+ + 四角(青)= 処理ステップ +
+
+ + ひし形(黄)= 条件分岐 +
+
+ はい + いいえ + ループ +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + 開始 + + + + + + 初期化 + left = 0, right = length - 1 + + + + + + left < right ? + + はい + + + + + left が非英数字 + left += 1 (left < right へ戻る) + + + right が非英数字 + right -= 1 (left < right へ戻る) + + + + + + 小文字化して比較 + s[left] == s[right] ? + + はい + + + + + left += 1, right -= 1 + + + 戻る + + + + いいえ + + + + いいえ + + + + + False を返す + + + + True を返す + +
+ +
+

🔎 入力例 s = "A man, a plan, a canal: Panama" でのフロー追跡

+
    +
  1. 「開始」ノード → 入力 s(長さ30)を受け取る
  2. +
  3. 「初期化」ノード → left=0, right=29 に設定
  4. +
  5. 「left < right ?」ノード → 0 < 29 なので「はい」の経路へ
  6. +
  7. 「非英数字ならポインタを移動」ノード → s[0]='A' も s[29]='a' も英数字なのでそのまま次へ
  8. +
  9. 「小文字化して比較」ノード → 'a' と 'a' が一致するので「はい」の経路へ
  10. +
  11. 「left += 1, right -= 1」ノード → left=1, right=28 に更新し、再び「left < right ?」へ戻る(紫の矢印)
  12. +
  13. (中略)この後、left 側では ',' や ' ' が、right 側では ' ' や ':' がそれぞれスキップされながら、ポインタが中央へ収束していきます
  14. +
  15. 最終的に left=17, right=17 となり「left < right ?」が「いいえ」になるため、「True を返す」ノードに到達し、結果 True が返されます
  16. +
+
+ +

+ フローの説明:
+ このフローチャートは、二方向ポインタが「ループ条件のチェック」→「非英数字の読み飛ばし」→「小文字化しての比較」→「ポインタの移動」という4つの処理を繰り返しながら中央へ収束していく様子を表しています。途中で不一致が見つかれば紫のループを抜けて赤い経路で「False を返す」に到達し、一度も不一致がなければ緑の経路で「True を返す」に到達します。 +

+
+ + +
+

計算量分析

+ +
+

📖 Big-O 記法の読み方(入力サイズ n が大きくなるにつれて処理時間がどう増えるかの目安)

+
+
+
O(1)
+
常に一定
例:辞書の直接引き
+
+
+
O(n)
+
入力に比例
例:リストを1回走査
+
+
+
O(n log n)
+
n より少し多い
例:ソートアルゴリズム
+
+
+
O(n²)
+
入力の2乗
例:二重ループ総当たり
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間計算量空間計算量備考
二方向ポインタ(採用)O(n)O(1)新しい文字列を作らないため最省メモリ
フィルタ+反転比較O(n)O(n)実装は直感的だが新しい文字列を2つ生成する
+
+ +
+

🔍 なぜこの計算量になるのか

+

+ 時間計算量が O(n) になるのは、left と right という2つのポインタがそれぞれ最大でも文字列の長さ分しか動かず、各文字を高々1回ずつしか読まないためです。空間計算量が O(1) になるのは、追加で使うメモリが left と right という2つの整数変数だけであり、入力文字列の長さが変わってもこの追加メモリの量は変化しないためです。 +

+
+
+ + +
+

📖 用語集

+

このページで登場した専門用語を五十音順にまとめました。分からない言葉が出てきたときに参照してください。

+
+ +
+ + インデックス + +
+ 文字列や配列の中で「何番目の要素か」を示す位置番号のことです。多くのプログラミング言語(Pythonを含む)では 0 から数え始めるため、先頭の文字はインデックス 0、2番目の文字はインデックス 1 になります。 +
+
+ +
+ + エッジケース + +
+ 空の入力・要素が1つだけ・制約の上限に近い巨大な入力など、通常とは異なる境界的な条件のことです。普通の入力ではうまく動くのに、エッジケースだけでバグが発生することがあるため、必ず確認すべき対象です。 +
+
+ +
+ + 回文 + +
+ 前から読んでも後ろから読んでも同じ並びになる文字列や文のことです。例えば "level" や "racecar" が回文です。この問題では、記号やスペースを除き大文字小文字を無視した上で回文かどうかを判定します。 +
+
+ +
+ + 空間計算量 + +
+ アルゴリズムの処理中に使うメモリ量が、入力サイズに対してどのくらい増えるかの目安です。O(1) であれば、入力がどれだけ大きくなっても追加で使うメモリは一定のままであることを意味します。 +
+
+ +
+ + 時間計算量 + +
+ 入力サイズが大きくなるにつれて、処理にかかる時間がどのくらい増えるかの目安です。O(n) であれば、入力が2倍になると処理時間もおよそ2倍になることを意味します。 +
+
+ +
+ + 早期リターン + +
+ 条件を満たした時点ですぐに関数を終了させることです。この問題では、不一致が見つかった瞬間に残りの処理を続けず即座に False を返しており、これが早期リターンにあたります。 +
+
+ +
+ + 二方向ポインタ + +
+ 配列や文字列の両端から中央に向かって、2つの指し位置(インデックス)を同時に動かしていく手法です。新しいデータ構造を作らずに1回の走査だけで処理を終えられるため、メモリ効率と実行速度の両方に優れています。 +
+
+ +
+ + 不変条件 + +
+ アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件のことです。この問題では「left より前の英数字の並びと、right より後ろの英数字の並びを逆にしたものが常に一致している」という条件が不変条件にあたります。 +
+
+ +
+
+ +
+ LeetCode 125: Valid Palindrome | 二方向ポインタ走査による学習用解説ページ +
+
+ + + + + + + + + + + + + + + + diff --git a/public/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README_React.html b/public/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README_React.html new file mode 100644 index 00000000..78a52387 --- /dev/null +++ b/public/Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README_React.html @@ -0,0 +1,1408 @@ + + + + + + LeetCode 136. Single Number - XORで学ぶ1パス走査 + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「配列の中でペアを組んでいない、1回だけ登場する数字を見つける問題」 +

+

+ 配列 + nums + の要素はほとんどが2回ずつ登場し、1つだけ1回しか登場しない要素が混ざっています。それを効率よく見つけ出すのがゴールです。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + ハッシュテーブル(=キーと値をセットで記録できる辞書のような構造)で出現回数を数えると、配列の要素数に比例した追加メモリ(O(n))を使ってしまい、「定数空間(O(1))」という制約に違反します。 +
  • +
  • + 配列をソートしてから隣同士を比較する方法は、ソート自体にO(n log + n)の時間がかかり、「線形時間(O(n))」という制約に違反します。 +
  • +
+
+ +
+
+
O(n)
+
時間計算量
+
+
+
O(1)
+
空間計算量
+
+
+
Easy
+
難易度
+
+
+
XOR
+
キーとなる演算
+
+
+ +
+
+

nums = [2, 2, 1]

+

出力: 1

+

+ 2は2回登場して打ち消し合うため、1回しか登場しない1が答えになります。 +

+
+
+

nums = [4, 1, 2, 1, 2]

+

出力: 4

+

+ 1と2はそれぞれ2回登場して打ち消し合うため、1回しか登場しない4が答えになります。このページのステップ解説では、この例を使って動作を追跡します。 +

+
+
+
+ + +
+

+ ステップバイステップ解説 +

+

+ 入力例 + nums = [4, 1, 2, 1, 2] + を使って、result 変数がどう変化していくかを1ステップずつ確認しましょう。 +

+
+
+ + +
+

+ Python実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. nums が list かどうか、要素がすべて int かどうかを検証する
  2. +
  3. + 配列が空でないことを確認する(制約上は保証されているが、防御的に確認する) +
  4. +
  5. result を 0 で初期化し、1回のループで各要素を result にXORで重ねる
  6. +
  7. 走査が終わったら result を返す
  8. +
+
+ +
from __future__ import annotations
+
+from typing import List
+
+
+class Solution:
+    """
+    136. Single Number 解決クラス
+
+    配列内で1回だけ登場する数値を、XOR(排他的論理和)を使って
+    O(n)時間・O(1)空間で見つける。
+    """
+
+    def singleNumber(self, nums: List[int]) -> int:
+        """
+        配列内で1回だけ登場する数値を返す。
+
+        Args:
+            nums: 1回だけ登場する要素が1つ含まれる整数配列。
+                  それ以外の要素はすべてちょうど2回登場する。
+
+        Returns:
+            1回だけ登場する整数。
+
+        Raises:
+            TypeError: nums がリストでない、または要素に int 以外が含まれる場合。
+            ValueError: nums が空の場合。
+        """
+        # isinstance() で型チェックする。
+        # Python は動的型付け(=実行するまで型が確定しない仕組み)のため、
+        # 呼び出し元が誤った型を渡しても実行時まで気づけない。
+        if not isinstance(nums, list):
+            raise TypeError("Input must be a list")
+
+        # all() + ジェネレータ式で全要素が int かどうかを1行でチェックする。
+        if not all(isinstance(x, int) for x in nums):
+            raise TypeError("All elements must be integers")
+
+        # 制約上は長さ1以上が保証されているが、防御的プログラミングとして
+        # 空リストを明示的に弾いておく。
+        if len(nums) == 0:
+            raise ValueError("Input list must not be empty")
+
+        # result を「これまでXORを重ねた累積値」として使う。
+        # 初期値を 0 にするのは、0 ^ a = a という性質があるため。
+        result: int = 0
+
+        # 配列を1回だけ走査し、各要素を result に XOR で重ねていく。
+        # 同じ数字が2回現れると a ^ a = 0 で打ち消し合い、
+        # 最終的にペアのない「はぐれ者」の数字だけが result に残る。
+        for num in nums:
+            result ^= num
+
+        return result
+
+ +
+

+ ▶ 入力例 nums = [4, 1, 2, 1, 2] での動作トレース +

+
+初期状態: result = 0
+
+num = 4 → result = 0 ^ 4 = 4
+num = 1 → result = 4 ^ 1 = 5
+num = 2 → result = 5 ^ 2 = 7
+num = 1 → result = 7 ^ 1 = 6   (1が2回目 → 打ち消し合いの途中経過)
+num = 2 → result = 6 ^ 2 = 4   (2が2回目 → 打ち消し合い完了)
+
+走査終了 → result = 4 を返す
+
+
+ + +
+

+ 処理フローチャート +

+ +
+

+ 🗺️ フローチャートの読み方 +

+
+
+ + + + 楕円(緑)= 開始・終了 +
+
+ + + + 四角(青)= 処理ステップ +
+
+ + + + ひし形(黄)= 条件分岐 +
+
+ 緑=正常な進行/成功 + 赤=検証エラー +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + 開始 + + + nums を受け取る + + + + + + + + + 入力を検証 + + + 型・空をチェック + + + + + + いいえ + + + + + + 検証エラー + + + 例外を送出 + + + + + + はい + + + + + + 初期化 + + + result = 0 + + + + + + + + + 走査ループ + + + 要素は残っているか + + + + + + はい + + + + + + XOR適用 + + + result^=num + + + + + + 次の要素へ(ループ) + + + + + + いいえ(走査完了) + + + + + + 終了 + + + result を返す + + +
+ +
+

+ 🔎 入力例 nums = [4, 1, 2, 1, 2] でのフロー追跡 +

+
    +
  1. 「開始」ノード → nums = [4, 1, 2, 1, 2] を受け取る
  2. +
  3. + 「入力を検証」ひし形 → list型・全要素int・空でないため「はい」の経路へ +
  4. +
  5. 「初期化」ノード → result = 0 にセット
  6. +
  7. + 「走査ループ」ひし形 → + 要素が残っているので「はい」の経路へ、XOR適用を5回繰り返す(result は 4 + → 5 → 7 → 6 → 4 と変化) +
  8. +
  9. + 「走査ループ」ひし形 → + すべての要素を処理し終えたので「いいえ(走査完了)」の経路へ +
  10. +
  11. 「終了」ノード → result = 4 を返す
  12. +
+
+ +

+ フローの説明:
+ このアルゴリズムは「検証 → 初期化 → + ループ」というシンプルな3段構成です。ループ部分だけが配列の要素数に応じて繰り返され、それ以外は1回きりの処理です。ループが要素数分だけ回ることが、時間計算量がO(n)になる直接の理由です。 +

+
+ + +
+

+ 計算量分析 +

+ +
+

+ 📖 Big-O 記法の読み方(入力サイズ n + が大きくなるにつれて処理時間がどう増えるかの目安) +

+
+
+
O(1)
+
+ 常に一定
例:辞書の直接引き +
+
+
+
O(n)
+
+ 入力に比例
例:リストを1回走査 +
+
+
+
O(n log n)
+
+ nより少し多い
例:ソートアルゴリズム +
+
+
+
O(n²)
+
+ 入力の2乗
例:二重ループ総当たり +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間空間備考
Counterで出現回数を数えるO(n)O(n)追加メモリが制約に違反
sortedで隣同士を比較O(n log n)O(n)ソート自体が線形時間の制約に違反
+ XOR + forループ(採用) + O(n)O(1)制約に完全一致・可読性も高い
XOR + functools.reduceO(n)O(1)競技プログラミング向けの簡潔版
+
+ +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 配列を1回だけ走査するため、時間計算量はO(n)になります。XORの結果を保持する + result + という変数1つしか使わないため、空間計算量は入力サイズに関係なく常にO(1)です。ハッシュテーブルを使う方法はO(n)の追加メモリを必要とし、ソートを使う方法はO(n + log + n)の時間がかかるため、この問題の制約(線形時間・定数空間)を同時に満たせるのはXOR方式だけです。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。 +

+
+
+ + インデックス + +
+ 配列の中で各要素の位置を表す番号のこと。多くのプログラミング言語では0から数え始める。 +
+
+
+ + XOR(排他的論理和) + +
+ 2つのビットを比較し、違っていれば1、同じなら0を返す演算。同じ値同士をXORすると必ず0になる性質があり、この問題ではこの性質を使ってペアの数字を打ち消し合わせている。 +
+
+
+ + 型ヒント + +
+ 関数の引数や戻り値に型を注釈として書く仕組み。def f(x: int) -> str: + のように書く。pylanceのような静的解析ツールがバグを実行前に検出できるようになる。 +
+
+
+ + 交換法則・結合法則 + +
+ 計算の順番を入れ替えたり、まとめる位置を変えても結果が変わらない性質。XORはこの2つの法則が成り立つため、配列の要素をどの順番でXORしても最終結果は同じになる。 +
+
+
+ + 定数空間 + +
+ 入力サイズに関係なく一定量のメモリしか使わないこと。O(1)の空間計算量とも呼ばれる。 +
+
+
+ + 動的型付け + +
+ 実行するまで変数の型が確定しない仕組み。Pythonが採用している方式で、誤った型を渡しても実行時まで気づけないという特徴がある。 +
+
+
+ + ハッシュテーブル + +
+ キーと値をセットで記録できる辞書のようなデータ構造。図書館の索引カードのように、キーからすぐに値を探せる。Pythonでは + dict や + Counter + がこれに相当する。 +
+
+
+ + ビット + +
+ 0か1で表される情報の最小単位。コンピュータ内部のすべての数値はビットの並びとして表現される。 +
+
+
+ + pylance + +
+ VSCodeで使えるPythonの静的型チェックツール。型ヒントをもとに、実行前にコードの誤りを検出してくれる。 +
+
+
+ + 防御的プログラミング + +
+ 想定外の入力にも備えて、プログラムが壊れないようにあらかじめ対処を書いておく考え方。 +
+
+
+
+ +
+ LeetCode 136. Single Number — algo-beginner学習用インタラクティブ解説 +
+
+ + + + + + + + + + diff --git a/public/Algorithm/Other/leetcode/83. Remove Duplicates from Sorted List/Claude 4.6 extended/README_React.html b/public/Algorithm/Other/leetcode/83. Remove Duplicates from Sorted List/Claude 4.6 extended/README_React.html index 601ed31b..409217a0 100644 --- a/public/Algorithm/Other/leetcode/83. Remove Duplicates from Sorted List/Claude 4.6 extended/README_React.html +++ b/public/Algorithm/Other/leetcode/83. Remove Duplicates from Sorted List/Claude 4.6 extended/README_React.html @@ -1293,7 +1293,7 @@

if (stepsEl) ReactDOM.createRoot(stepsEl).render(); if (codeTabsEl) ReactDOM.createRoot(codeTabsEl).render(); - + - + - + + + + + + LeetCode 9: Palindrome Number | 数値反転比較法による解説 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/public/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README_react.html b/public/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README_react.html new file mode 100644 index 00000000..7d4982c0 --- /dev/null +++ b/public/Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README_react.html @@ -0,0 +1,1380 @@ + + + + + + Roman to Integer(LeetCode 13)解説 - ハッシュテーブル1パス走査 + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「ローマ数字の文字列を読み取り、対応する整数に変換する問題」です。 +

+

+ ローマ数字は基本的に大きい記号から小さい記号へ左から右に並べますが、IV(4)や + IX(9)のように、小さい記号を大きい記号の前に置いて引き算を表す例外パターンが6つあります。 + この問題では、その例外パターンを見落とさずに正しく数値へ変換することがポイントになります。 +

+
+ +
+

+ ⚠️ なぜ単純な方法では解けないのか +

+
    +
  • + すべての文字の値をそのまま足し算するだけでは、IV + が「1+5=6」になってしまい、正しい答え「4」にならない。 +
  • +
  • + 「今見ている文字」だけでは、足すべきか引くべきかを判断できず、「次に来る文字」との大小比較が必要になる。 +
  • +
+
+ +
+
+
+ O(n) +
+
時間計算量
+
+
+
+ O(1) +
+
空間計算量
+
+
+
+ 1 <= n <= 15 +
+
入力文字列長
+
+
+
+ dict +
+
主に使うデータ構造
+
+
+ +
+
+

+ 入力: s = "III"  + 出力: 3 +

+

+ 同じ記号Iが3つ並んでいるだけなので、単純にI+I+I=3となる。 +

+
+
+

+ 入力: s = "LVIII"  + 出力: 58 +

+

+ L=50, V=5, + III=3で、いずれも減算パターンを含まないため単純な足し算で58になる。 +

+
+
+

+ 入力: s = "MCMXCIV"  + 出力: 1994 +

+

+ M=1000, CM=900, XC=90, + IV=4で、CM・XC・IVの3つの減算パターンをすべて含む代表的な例。 +

+
+
+
+ + +
+ + +
+

+ Python実装 +

+ +
+

+ 📋 このコードの構造(先に全体像を把握しよう) +

+
    +
  1. + 入力が文字列であり、空でなく、有効な文字のみで構成されているかを検証する +
  2. +
  3. + ループの外で対応表への参照をローカル変数に束縛し、合計値totalを0で初期化する +
  4. +
  5. + 文字列を先頭から1文字ずつ走査し、次の文字と比較しながらtotalを更新する +
  6. +
  7. 走査が終わったらtotalをそのまま返す
  8. +
+
+ +
from __future__ import annotations
+
+from typing import Dict, Final, Optional
+
+
+class Solution:
+    """
+    Roman to Integer (LeetCode 13) を解決するクラス。
+
+    ローマ数字を表す文字列を受け取り、対応する整数値を返す。
+    """
+
+    # クラス変数として文字→数値の対応表を保持する。
+    # インスタンスごとに作り直す必要がないデータなので、
+    # __init__の中ではなくクラス変数として持たせることでメモリを節約できる。
+    _ROMAN_VALUES: Final[Dict[str, int]] = {
+        "I": 1,
+        "V": 5,
+        "X": 10,
+        "L": 50,
+        "C": 100,
+        "D": 500,
+        "M": 1000,
+    }
+
+    def romanToInt(self, s: str) -> int:
+        """
+        ローマ数字の文字列を整数に変換する。
+
+        Args:
+            s: 変換対象のローマ数字文字列(例: "MCMXCIV")。
+
+        Returns:
+            変換後の整数値(例: 1994)。
+
+        Raises:
+            TypeError: 入力が文字列でない場合。
+            ValueError: 入力が空、または未知の文字を含む場合。
+        """
+        # 入力検証(防御的プログラミング:制約上は保証されているが、
+        # 誤った呼び出しをされても安全に動作するようにする)
+        self._validate(s)
+
+        # ループの外で対応表への参照をローカル変数に束縛しておく。
+        # self._ROMAN_VALUES というアクセスはPythonの内部で属性検索を
+        # 伴うため、ループの外で1回だけ済ませておくと検索コストを省ける。
+        values = self._ROMAN_VALUES
+        total = 0
+        length = len(s)
+
+        # 左から右へ1文字ずつ走査する
+        for i in range(length):
+            current = values[s[i]]
+
+            # 次の文字が存在する場合のみその値を取得し、
+            # 存在しない場合はNoneとして扱う。
+            next_value: Optional[int] = values[s[i + 1]] if i + 1 < length else None
+
+            # 現在値が次の値より小さい場合は「引き算ルール」
+            # (IV, IX, XL, XC, CD, CM の6パターン)に該当する。
+            if next_value is not None and current < next_value:
+                total -= current
+            else:
+                total += current
+
+        return total
+
+    def _validate(self, data: object) -> None:
+        """型安全な入力検証を行う"""
+        if not isinstance(data, str):
+            raise TypeError("Input must be a string")
+
+        if len(data) == 0:
+            raise ValueError("Input cannot be empty")
+
+        # all() + ジェネレータ式で全文字の妥当性チェックを1行で行う。
+        if not all(char in self._ROMAN_VALUES for char in data):
+            raise ValueError("Input contains invalid roman numeral characters")
+ +
+

+ ▶ 入力例 "MCMXCIV" での動作トレース +

+
+i=0: current='M'(1000), next='C'(100)  → 1000 < 100 は偽 → 加算 → total=1000
+i=1: current='C'(100),  next='M'(1000) → 100 < 1000 は真  → 減算 → total=900
+i=2: current='M'(1000), next='X'(10)   → 1000 < 10 は偽   → 加算 → total=1900
+i=3: current='X'(10),   next='C'(100)  → 10 < 100 は真    → 減算 → total=1890
+i=4: current='C'(100),  next='I'(1)    → 100 < 1 は偽     → 加算 → total=1990
+i=5: current='I'(1),    next='V'(5)    → 1 < 5 は真       → 減算 → total=1989
+i=6: current='V'(5),    next=なし      → 無条件で加算       → total=1994
+出力: 1994 ✅
+
+
+ + +
+ + +
+

+ 計算量分析 +

+ +
+

📖 Big-O 記法の読み方

+
+
+
O(1)
+
入力サイズに関わらず
常に一定の速さ
+
+
+
O(n)
+
入力が2倍になると
処理も約2倍
+
+
+
O(n log n)
+
入力が2倍になると
処理は約2倍強
+
+
+
O(n²)
+
入力が2倍になると
処理は約4倍
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
アプローチ時間計算量空間計算量備考
+ ハッシュテーブル1パス走査(採用) + O(n)O(1) + 文字列を1回だけ走査。対応表は固定サイズ(7種類)でメモリは入力サイズに依存しない +
2文字パターンの事前置換O(n)O(n) + CM・XC・IV等を先に置換してから加算。新しい文字列を都度生成するため余分なメモリを消費する +
+
+ +
+

+ 🔍 なぜこの計算量になるのか +

+

+ 時間計算量が O(n) + になるのは、文字列の各文字をちょうど1回ずつしか見ないためです。次の文字を先読みする処理も、 + ループの中でインデックスを1つずらして参照するだけなので、追加のループを必要としません。 + 空間計算量が O(1) + になるのは、文字→数値の対応表が常に7種類固定であり、入力文字列がどれだけ長くなっても + (制約上は最大15文字ですが)対応表のサイズや追加変数の数は変わらないためです。 +

+
+
+ + +
+

+ 📖 用語集 +

+

+ このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください(五十音順)。 +

+
+
+ + インデックス + +
+ 配列や文字列の中で、要素が何番目にあるかを表す番号のこと。多くのプログラミング言語では0から数え始めるため、先頭の文字はインデックス0、2番目の文字はインデックス1となる。 +
+
+
+ + 型ヒント + +
+ 関数の引数や戻り値に、想定している型を注釈として書く仕組み。def f(x: int) -> str:のように書く。pylanceのような型チェッカーが、実行前に型の誤りを検出できるようになる。 +
+
+
+ + 計算量 + +
+ 入力の大きさに対して、処理にかかる手間(時間計算量)やメモリ量(空間計算量)がどう増えるかを表す目安。Big-O記法(O(n)など)で表現されることが多い。 +
+
+
+ + 走査 + +
+ 配列や文字列などのデータを、順番に1つずつ調べていくこと。今回の実装では、文字列を左から右へ1回だけ走査することで答えを求めている。 +
+
+
+ + ハッシュテーブル + +
+ キーから値をO(1)(一定時間)で取り出せるデータ構造。図書館の索引カードに例えると、タイトル(キー)から棚番号(値)を即座に引けるイメージ。Pythonではdictがこれに相当する。 +
+
+
+ + 不変条件 + +
+ アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件のこと。今回の実装では「ループの各時点でtotalには、それまでに処理した文字列の先頭部分を正しく変換した値が入っている」という不変条件が成り立っている。 +
+
+
+
+ +
+ LeetCode 13 · Roman to Integer · ハッシュテーブル1パス走査による解説ページ +
+
+ + + + + + + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index af22fe3c..8c1d0d7b 100644 --- a/public/index.html +++ b/public/index.html @@ -418,7 +418,7 @@

🧪 Algorithm Study Index

-

178 interactive lessons across 6 domains

+

183 interactive lessons across 6 domains

@@ -433,13 +433,13 @@

- + - +
@@ -479,6 +479,10 @@

  • 🧩LeetCode 111 - Minimum Depth of Binary Tree | BFS解説Algorithm/BinaryTree/leetcode/111. Minimum Depth of Binary Tree/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 112 – Path Sum | 再帰DFS解説Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 118 — Pascal's TriangleAlgorithm/Other/leetcode/118. Pascal's Triangle/claude sonnet 4.6 adaptive/README_React.html
  • +
  • 🧩LeetCode 119: Pascal's Triangle II - 二項係数の漸化式で1行を求めるAlgorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README_React.html
  • +
  • 🧩LeetCode 125: Valid Palindrome | 二方向ポインタで学ぶ回文判定Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README_React.html
  • +
  • 🧩LeetCode 136. Single Number - XORで学ぶ1パス走査Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README_React.html
  • +
  • 🧩LeetCode 141: Linked List Cycle - 亀と兎のアルゴリズムで解説Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/README_react.html
  • 🧩LeetCode 5: Longest Palindromic Substring - 中心展開法Algorithm/ExpandAroundCenter/leetcode/5. Longest Palindromic Substring/Claude/README.html
  • 🧩LeetCode 66: Plus One - 右から左への繰り上がり処理Algorithm/Other/leetcode/66. Plus One/Claude Sonnet 4.5/README_react.html
  • 🧩LeetCode 67: Add Binary - 二進数加算Algorithm/TwoPointers/leetcode/67. Add Binary/Claude/README_react.html
  • @@ -608,9 +612,10 @@

  • 📐HackerRank: Divisors Divisible by 2Mathematics/Fundamentals/HackerRank/Claude/Easy/Sherlock and Divisors/Sherlock_and_Divisors.html
  • 📐Halloween Party — HackerRank 解説Mathematics/Fundamentals/HackerRank/Claude/Easy/Halloween party/Halloween_party.html
  • 📐K番目順列アルゴリズム解析Mathematics/Permutation Sequence/leetcode/Claude/README.html
  • -
  • 📐LeetCode 9: Palindrome Number - 数値反転による回文判定Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 4.5/README_react.html
  • +
  • 📐LeetCode 9: Palindrome Number | 数値反転比較法による解説Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/README_react.html
  • 📐Moving Tiles - 重なり面積の時間逆算Mathematics/Fundamentals/HackerRank/Claude/Easy/moving-tiles-visualization.html
  • 📐Robot Unique Paths - 技術解説Mathematics/Combination Calculation/leetcode/62. Unique Paths/Claude/README.html
  • +
  • 📐Roman to Integer(LeetCode 13)解説 - ハッシュテーブル1パス走査Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README_react.html
  • 📐Strange Grid 解説Mathematics/Fundamentals/HackerRank/Claude/Easy/Strange Grid Again/Strange_Grid_Again.html
  • 📐Valid Number Problem - 有限状態機械アルゴリズム解説Mathematics/Finite State Machine/leetcode/65. Valid Number/Claude/README.html
  • 📐pow(x, n) アルゴリズム解析Mathematics/Exponentiation by Squaring/leetcode/50. Pow(x, n)/Claude/README.html
  • @@ -664,6 +669,10 @@

  • 🧩LeetCode 111 - Minimum Depth of Binary Tree | BFS解説Algorithm/BinaryTree/leetcode/111. Minimum Depth of Binary Tree/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 112 – Path Sum | 再帰DFS解説Algorithm/BinaryTree/leetcode/112. Path Sum/claude sonnet 4.6 adaptive/README_react.html
  • 🧩LeetCode 118 — Pascal's TriangleAlgorithm/Other/leetcode/118. Pascal's Triangle/claude sonnet 4.6 adaptive/README_React.html
  • +
  • 🧩LeetCode 119: Pascal's Triangle II - 二項係数の漸化式で1行を求めるAlgorithm/Other/leetcode/119. Pascal's Triangle II/claude sonnet 5 high/README_React.html
  • +
  • 🧩LeetCode 125: Valid Palindrome | 二方向ポインタで学ぶ回文判定Algorithm/Other/leetcode/125. Valid Palindrome/claude sonnet 5 high/README_React.html
  • +
  • 🧩LeetCode 136. Single Number - XORで学ぶ1パス走査Algorithm/Other/leetcode/136. Single Number/claude sonnet 5 high/README_React.html
  • +
  • 🧩LeetCode 141: Linked List Cycle - 亀と兎のアルゴリズムで解説Algorithm/Floyds-Tortoise-and-Hare/leetcode/141. Linked List Cycle/Claude Sonnet5/README_react.html
  • 🧩LeetCode 5: Longest Palindromic Substring - 中心展開法Algorithm/ExpandAroundCenter/leetcode/5. Longest Palindromic Substring/Claude/README.html
  • 🧩LeetCode 66: Plus One - 右から左への繰り上がり処理Algorithm/Other/leetcode/66. Plus One/Claude Sonnet 4.5/README_react.html
  • 🧩LeetCode 67: Add Binary - 二進数加算Algorithm/TwoPointers/leetcode/67. Add Binary/Claude/README_react.html
  • @@ -813,9 +822,10 @@

  • 📐HackerRank: Divisors Divisible by 2Mathematics/Fundamentals/HackerRank/Claude/Easy/Sherlock and Divisors/Sherlock_and_Divisors.html
  • 📐Halloween Party — HackerRank 解説Mathematics/Fundamentals/HackerRank/Claude/Easy/Halloween party/Halloween_party.html
  • 📐K番目順列アルゴリズム解析Mathematics/Permutation Sequence/leetcode/Claude/README.html
  • -
  • 📐LeetCode 9: Palindrome Number - 数値反転による回文判定Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 4.5/README_react.html
  • +
  • 📐LeetCode 9: Palindrome Number | 数値反転比較法による解説Mathematics/Palindrome/leetcode/9. Palindrome Number/claud sonnet 5/README_react.html
  • 📐Moving Tiles - 重なり面積の時間逆算Mathematics/Fundamentals/HackerRank/Claude/Easy/moving-tiles-visualization.html
  • 📐Robot Unique Paths - 技術解説Mathematics/Combination Calculation/leetcode/62. Unique Paths/Claude/README.html
  • +
  • 📐Roman to Integer(LeetCode 13)解説 - ハッシュテーブル1パス走査Mathematics/Roman numerals/leetcode/13. Roman to Integer/sonnet 5/README_react.html
  • 📐Strange Grid 解説Mathematics/Fundamentals/HackerRank/Claude/Easy/Strange Grid Again/Strange_Grid_Again.html
  • 📐Valid Number Problem - 有限状態機械アルゴリズム解説Mathematics/Finite State Machine/leetcode/65. Valid Number/Claude/README.html
  • 📐pow(x, n) アルゴリズム解析Mathematics/Exponentiation by Squaring/leetcode/50. Pow(x, n)/Claude/README.html
  • @@ -841,7 +851,7 @@

    🧪 - Generated on 2026-08-04 + Generated on 2026-08-06