From 743e57ad8215a98424cbc065c42554d2a3a10508 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sat, 8 Aug 2026 20:07:22 +0900 Subject: [PATCH 01/26] docs(leetcode/20): add Valid Parentheses solution documents and React UI --- .../claude sonnet 5/README.md | 1018 ++++++++++ .../claude sonnet 5/README_React.html | 1781 +++++++++++++++++ .../claude sonnet 5/Valid_Parentheses_Go.md | 334 ++++ .../claude sonnet 5/Valid_Parentheses_Java.md | 278 +++ .../Valid_Parentheses_Python.md | 348 ++++ .../claude sonnet 5/Valid_Parentheses_Rust.md | 297 +++ .../Valid_Parentheses_Typescript.md | 340 ++++ .../claude sonnet 5/README_React.html | 1781 +++++++++++++++++ .../Sonnet 5/README_react.html | 1657 +++++++++++++++ public/index.html | 16 +- 10 files changed, 7844 insertions(+), 6 deletions(-) create mode 100644 Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md create mode 100644 Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html create mode 100644 Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Go.md create mode 100644 Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md create mode 100644 Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Python.md create mode 100644 Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Rust.md create mode 100644 Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md create mode 100644 public/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html create mode 100644 public/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md new file mode 100644 index 00000000..a3dccd8a --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md @@ -0,0 +1,1018 @@ +# Valid Parentheses - 括弧の対応関係をスタックで判定する(Python / TypeScript / Go / Rust) + +## 目次 + +- [概要](#overview) +- [アルゴリズム要点(TL;DR)](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [Python 実装](#impl-python) +- [TypeScript 実装](#impl-typescript) +- [Go 実装](#impl-go) +- [Rust 実装](#impl-rust) +- [言語間比較](#comparison) +- [言語別最適化ポイント](#optimization) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +> 💡 **初学者向け補足**:この問題は、一言で言うと「**開き括弧`( [ {`と閉じ括弧`) ] }`の組み合わせと順番が正しいかどうかを判定する問題**」です。 + +`'('`、`')'`、`'{'`、`'}'`、`'['`、`']'`だけからなる文字列 `s` が与えられたとき、以下の3条件をすべて満たすなら`true`、そうでなければ`false`を返します。 + +1. 開き括弧は同じ種類の閉じ括弧で閉じられなければならない +2. 開き括弧は正しい順序で閉じられなければならない +3. すべての閉じ括弧には対応する開き括弧が存在しなければならない + +この問題の難しいポイントは、**「括弧の数が合っている」ことと「括弧の対応関係が正しい」ことは別物である**という点です。たとえば`"([)]"`は開き括弧`(`が1個・`[`が1個、閉じ括弧`)`が1個・`]`が1個で**数としては合致**していますが、`(`が閉じられる前に`[`が`)`によって閉じられようとしており、**順序が破綻**しています。この「数合わせ」の誤りを避けるためには、直前に開いた括弧を記憶しておく仕組みが必要になります。 + +この記事では、Python / TypeScript / Go / Rust の4言語で本問題を解説します。結論を先に述べると、**4言語すべて同じ「スタックを使った1回走査」というアルゴリズムを採用します**。この問題はデータ構造・計算量の面で言語ごとに有利不利が生じるタイプの問題ではないため、統一したアプローチのほうが理解しやすいと判断しました。一方で、「エラーをどう表現するか」「メモリをどう確保するか」という**実装の哲学**は言語ごとに大きく異なるため、そこが本記事の主な見どころになります。 + +> 📖 **この章で登場した用語** +> +> - **制約**:入力として与えられる値の範囲や条件のこと。本問題では「文字列の長さは1以上10^4以下」「使用される文字は括弧記号のみ」など +> - **正当性**:アルゴリズムが常に正しい答えを返すことの保証 + +--- + +

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

+ +> 💡 **初学者向け補足**:TL;DR(Too Long; Didn't Read)とは「長くて読めない人向けの要約」という意味です。ここでは「なんとなくこういう手順で解くんだな」というイメージを掴んでください。詳細は後の章で説明します。 + +- **戦略**:文字列を先頭から1文字ずつ走査する。開き括弧が来たら「あとで閉じられるはずの候補」として記録し、閉じ括弧が来たら「直近に記録した開き括弧」と一致するか確認する。 +- **データ構造**:**スタック**(後入れ先出し=LIFO: Last In, First Out の構造)を使う。なぜスタックが最適かというと、「最後に開かれた括弧が最初に閉じられるべき」という問題の性質そのものが、スタックの「最後に入れたものを最初に取り出す」性質とぴったり一致するため。 +- **計算量**:Time O(n)、Space O(n)(nは文字列長)。文字列を1回だけ走査すれば十分であり、これより速くすることは理論上不可能。 +- **4言語での差異**:**アプローチ自体は4言語とも同じスタック法**。差異が出るのは「スタックの型(Python: `list[str]`、TypeScript: `string[]`、Go: `[]byte`、Rust: `Vec`)」「対応表の引き方(辞書 vs `switch` vs `match`)」「エラーの表現方法(例外 vs 判別可能ユニオン型 vs `error`戻り値 vs `Result`)」の3点です。 + +> 📖 **この章で登場した用語** +> +> - **TL;DR**:「長くて読めない人向けの要約」を意味する略語 +> - **スタック**:後入れ先出し(LIFO)の構造を持つデータ構造。最後に追加した要素を最初に取り出す +> - **LIFO(Last In, First Out)**:「最後に入れたものを最初に出す」というスタックの動作原則 + +--- + +

図解

+ +> 💡 **初学者向け補足**:図はアルゴリズムの「処理の流れ」を視覚的に示したものです。ひし形(`{}`)は条件分岐、長方形(`[]`)は処理ステップを表します。上から下へ読み進めてください。 + +### フローチャート + +この図は、文字列を1文字ずつ読みながらスタックを操作していく処理の流れを表しています。 + +```mermaid +flowchart TD + Start[Start isValid with string s] + Init[Initialize empty stack] + Loop{More characters left} + ReadChar[Read next character c] + IsOpen{c is an opening bracket} + Push[Push c onto stack] + StackEmpty{Stack is empty} + Pop[Pop top of stack into top] + Match{top matches c} + CheckEmpty{Stack is empty} + ReturnTrue[Return true] + ReturnFalse[Return false] + Start --> Init + Init --> Loop + Loop -- Yes --> ReadChar + ReadChar --> IsOpen + IsOpen -- Yes --> Push + Push --> Loop + IsOpen -- No --> StackEmpty + StackEmpty -- Yes --> ReturnFalse + StackEmpty -- No --> Pop + Pop --> Match + Match -- Yes --> Loop + Match -- No --> ReturnFalse + Loop -- No --> CheckEmpty + CheckEmpty -- Yes --> ReturnTrue + CheckEmpty -- No --> ReturnFalse +``` + +主要なノードの意味: + +- `Loop`:まだ読んでいない文字が残っているかを判定するひし形(ループの継続条件) +- `IsOpen`:読んだ文字が開き括弧かどうかを判定するひし形 +- `Push`:開き括弧をスタックに積む処理 +- `StackEmpty`(走査中):閉じ括弧が来たときにスタックが空でないかを確認する安全チェック +- `Match`:スタックから取り出した開き括弧が、今読んだ閉じ括弧と対応しているかを判定するひし形 +- `CheckEmpty`(走査後):全文字を読み終えた時点でスタックに未閉じの開き括弧が残っていないかを確認する最終チェック + +### データフロー図 + +この図は、入力文字列がどのように変換されて最終的な真偽値になるかを表しています。 + +```mermaid +graph LR + subgraph Precheck + A[Input string s] + B[Validate character set] + A --> B + end + subgraph Core + C[Stack of pending open brackets] + D[Scan characters left to right] + E[Push open or match and pop close] + B --> C + C --> D + D --> E + end + F[Final stack emptiness check] + G[Output boolean result] + E --> F + F --> G +``` + +主要な流れの説明: + +- `Input string s`から`Validate character set`へ:入力が想定した括弧文字だけで構成されているかを確認する(制約上保証されるが、防御的にチェックする言語もある) +- `Stack of pending open brackets`から`Scan characters left to right`へ:「まだ閉じられていない開き括弧」だけを保持するスタックを用意し、先頭から順に読み進める +- `Final stack emptiness check`から`Output boolean result`へ:走査が終わった時点でスタックが空なら`true`、何か残っていれば`false`として出力する + +> 💡 **代表例でのトレース**:`s = "([)]"`(要件2に違反する不正な例)を入力として、上記フローチャートをどう通過するかを示します。 +> +> ``` +> Step 1: Start → 入力 s = "([)]" +> Step 2: Init → stack = [] +> Step 3: Loop 判定 → 残り文字あり → Yes +> Step 4: ReadChar → c = '(' → IsOpen → Yes → Push → stack = ['('] +> Step 5: Loop → 残りあり → ReadChar → c = '[' → IsOpen → Yes → Push → stack = ['(', '['] +> Step 6: Loop → 残りあり → ReadChar → c = ')' → IsOpen → No +> → StackEmpty → No → Pop → top='[' , stack=['('] +> Step 7: Match 判定 → top='[' は ')' に対応しない → No → ReturnFalse +> 結果: false +> ``` + +> 📖 **この章で登場した用語** +> +> - **フローチャート**:処理の手順を図形と矢印で表したもの。ひし形=条件分岐、長方形=処理 +> - **データフロー図**:データがどのように変換・移動するかを示す図 +> - **サブグラフ**:フローチャートの中で関連する処理をグループ化したもの + +--- + +

正しさのスケッチ

+ +> 💡 **初学者向け補足**:「正しさのスケッチ」とは、アルゴリズムが常に正しい答えを返すことの根拠を整理したものです。数学的な証明ではなく「なぜ正しいと言えるか」の説明だと考えてください。 + +- **不変条件**(=処理中ずっと成り立ち続けるべき条件):スタックの中身は常に「まだ閉じられていない開き括弧」だけで構成されている。開き括弧が来たら積み、閉じ括弧が正しく一致したら取り除くという操作を守っている限り、この条件は崩れない。 +- **網羅性**(=すべてのケースをもれなく処理できているという保証):走査中の各文字は「開き括弧」「閉じ括弧」のいずれかであり(制約上)、それぞれに対応する分岐が用意されているため、処理漏れは発生しない。 +- **基底条件**:文字列の走査が最後まで終わったとき。このときスタックが空であれば「すべての開き括弧が正しく閉じられた」ことを意味し、`true`を返す根拠になる。逆に何か残っていれば「閉じられていない開き括弧がある」ことになり`false`。 +- **終了性**:文字列は有限長(最大10^4文字、制約より)であり、ループは1文字進むごとに必ず1つ前進するため、無限ループにはならず必ず有限ステップで終わる。 + +「閉じ括弧が来たのにスタックが空」というケースも見逃せません。この場合、対応する開き括弧が存在しないことが確定するため、その時点で即座に`false`を返してよいことが分かります(スタックからの取り出し操作を安全に行うための重要な分岐です)。 + +> 📖 **この章で登場した用語** +> +> - **不変条件**:アルゴリズムが正しく動くために、処理中ずっと成り立ち続けるべき条件 +> - **基底条件**:処理の終了条件。これがないと無限ループになる +> - **終了性**:アルゴリズムが必ず有限ステップで終わるという保証 +> - **網羅性**:すべてのケースをもれなく処理できているという保証 + +--- + +

計算量

+ +> 💡 **初学者向け補足**:計算量とは「入力が大きくなるにつれて、処理にかかる時間・メモリがどう増えるか」の目安です。 + +| 記法 | 意味 | 直感的なイメージ | +| ------- | ---------------------- | -------------------------- | +| `O(1)` | 入力サイズによらず一定 | 辞書で直接ページを開く | +| `O(n)` | 入力に比例して増加 | リストを端から順に読む | +| `O(n²)` | 入力の2乗で増加 | 全ペアを総当たりで確認する | + +### 4言語比較テーブル + +| 言語 | 採用アルゴリズム | Time | Space | 実装上の特徴 | +| ---------- | ---------------- | ---- | ----- | ------------------------------------------------- | +| Python | スタック法 | O(n) | O(n) | `list`をスタックに使用。`dict`でO(1)対応表引き | +| TypeScript | スタック法 | O(n) | O(n) | `string[]`をスタックに使用。型ガードで絞り込み | +| Go | スタック法 | O(n) | O(n) | `[]byte`をプリアロケーション。`switch`で分岐 | +| Rust | スタック法 | O(n) | O(n) | `Vec`を事前確保。`Result`でエラー表現 | + +すべての言語で最悪ケース(全文字が開き括弧、例:`"((((("`)ではスタックに全文字が積まれるため、空間計算量はO(n)になります。これより空間効率の良い方法(括弧の出現数だけを数えるO(1)のアプローチ)も存在しますが、[正しさのスケッチ](#correctness)で述べた「順序の検証」ができないため不採用としています。 + +**in-place / Pure の観点**:4言語とも、入力文字列自体を書き換えることはなく、新しく用意したスタックだけを使って判定します。つまりすべて**Pure**(入力を変更せず新しいデータだけを使って結果を返す)な実装です。文字列自体が非常に小さい(1文字〜10^4文字)ため、in-place化するメリットもなく、可読性を優先しています。 + +> 📖 **この章で登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **in-place**:新しいメモリを確保せず元のデータを直接書き換える操作 +> - **Pure**:入力を変更せず新しいデータを返す操作。安全だがメモリを余分に使う + +--- + +

Python 実装

+ +> 💡 **初学者向け補足**:コードを読む前に、実装の全体的な骨格を示します。 +> +> 1. 括弧の不正な組み合わせを表現する**カスタム例外クラス**(`BracketMismatchError`)を定義する +> 2. 閉じ括弧から対応する開き括弧を引く**対応表**(`dict`)をクラス定数として定義する +> 3. 業務開発版では`_validate_brackets`が例外を投げる形で検証し、`isValid`はそれを`try-except`で捕まえて`bool`に変換する +> 4. 競技プログラミング版では例外を使わず、辞書引きと`list`操作だけで最短コードにする + +```python +from __future__ import annotations + +from typing import Final + + +class BracketMismatchError(Exception): + """ + 括弧の対応関係・順序が不正な場合に送出する例外。 + bool の True/False だけでは「なぜ」不正だったのかを表現できないため、 + Exception を継承したクラスを用意し、将来のログ出力等に転用しやすくする。 + """ + + def __init__(self, message: str, char: str, position: int) -> None: + super().__init__(message) + # 追加情報として問題の文字と位置を保持しておく + self.char = char + self.position = position + + +class Solution: + """括弧の妥当性判定クラス(LeetCode 20: Valid Parentheses)""" + + # クラス定数として対応表を定義する。全インスタンスで共通のデータなので + # __init__ の中ではなくクラスレベルで1度だけ生成する。 + # Final を付けることで「再代入されない」ことを pylance に伝える。 + _PAIRS: Final[dict[str, str]] = { + ')': '(', + ']': '[', + '}': '{', + } + + def isValid(self, s: str) -> bool: + """ + 業務開発向け実装。_validate_brackets の結果を bool に変換する薄いラッパー。 + + Complexity: + Time: O(n) + Space: O(n) + """ + # Python は動的型付け言語のため、pylance の静的チェックをすり抜けて + # 誤った型が実行時に渡される可能性がある。二重の防御として有効。 + if not isinstance(s, str): + raise TypeError("Input must be a string") + + try: + self._validate_brackets(s) + except BracketMismatchError: + return False + return True + + def _validate_brackets(self, s: str) -> None: + """スタックを使って対応関係・順序を検証する内部メソッド。""" + # list をスタック(後入れ先出し構造)として使う。 + # append/pop はどちらも末尾操作で(償却)O(1)。 + stack: list[str] = [] + + # enumerate() でインデックスと文字を同時に取得する(Pythonic な書き方)。 + for position, char in enumerate(s): + if char in '([{': + # 開き括弧ならスタックに積む。 + stack.append(char) + continue + + if char in self._PAIRS: + # 閉じ括弧の場合、まずスタックが空でないか確認する。 + if not stack: + raise BracketMismatchError( + "対応する開き括弧がないまま閉じ括弧が出現しました", char, position + ) + # スタックの末尾(直前に積んだ開き括弧)を取り出す。 + top = stack.pop() + expected_open = self._PAIRS[char] + if top != expected_open: + raise BracketMismatchError( + f"'{char}' に対応する開き括弧は '{expected_open}' ですが " + f"'{top}' が見つかりました", + char, + position, + ) + continue + + # 制約上発生しないが、防御的に例外を送出する。 + raise BracketMismatchError("括弧以外の文字が含まれています", char, position) + + # 走査後にスタックに残りがあれば、閉じられていない開き括弧がある。 + if stack: + unclosed = stack[-1] + raise BracketMismatchError( + f"開き括弧 '{unclosed}' が閉じられないまま入力が終了しました", + unclosed, + len(s), + ) +``` + +競技プログラミング版は、例外を使わず「番兵値」で空スタックの`pop()`を回避する短いコードです。 + +```python +from __future__ import annotations + + +class Solution: + def isValid(self, s: str) -> bool: + """ + 競技プログラミング向け最適化実装。 + 例外処理を使わず、辞書引きと list 操作だけで完結させる。 + + Time Complexity: O(n) + Space Complexity: O(n) + """ + pairs = {')': '(', ']': '[', '}': '{'} + stack: list[str] = [] + + for char in s: + if char in pairs: + # スタックが空なら '#'(番兵、絶対に括弧と一致しないダミー値)を使う。 + # これにより空リストへの pop() で発生する IndexError を未然に回避する。 + if pairs[char] != (stack.pop() if stack else '#'): + return False + else: + stack.append(char) + + # 全て閉じられていればスタックは空になっているはず。 + return not stack +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 入力: s = "([)]" +> position=0, char='(' → '(' in '([{' → append → stack=['('] +> position=1, char='[' → '[' in '([{' → append → stack=['(', '['] +> position=2, char=')' → ')' in _PAIRS → True +> stack.pop() → top='[' , stack=['('] +> _PAIRS[')'] = '(' だが top='[' → 不一致 +> → BracketMismatchError を送出 +> isValid の戻り値: except節で捕捉 → False +> ``` + +> 📖 **この章で登場した用語** +> +> - **`from __future__ import annotations`**:型ヒントを文字列として扱うようにする宣言。循環参照や前方参照を解決できる +> - **`Final`**:`typing`モジュールの型。「この変数は再代入されない」ことをpylanceなどの型チェッカーに伝える +> - **EAFP(Easier to Ask Forgiveness than Permission)**:「まず実行してダメだったら例外で謝る」というPythonの伝統的な設計哲学 +> - **番兵(センチネル)値**:ループや条件分岐の特殊ケースを示すためのダミー値 + +--- + +

TypeScript 実装

+ +> 💡 **初学者向け補足**:Pythonの章で学んだ「スタックで対応関係を検証する」考え方をベースに、今度はTypeScriptの型システムでどう安全に書くかを見ていきます。骨格は以下の通りです。 +> +> 1. `BracketError`クラスを定義する(`Error`を継承) +> 2. 開き括弧 → 閉じ括弧の対応表を`Readonly>`型で定義する +> 3. `isOpeningBracket` / `isClosingBracket`という**型ガード関数**(=実行時に値の型を確認し、その情報をもとにTypeScriptの型を絞り込む仕組み)を用意する +> 4. `validateBrackets`が「成功か失敗か」を表す**判別可能ユニオン型**(`ValidationResult`)を返し、`isValid`はそれを`boolean`に変換する + +```typescript +// ============================================================ +// カスタムエラークラス +// ============================================================ +// なぜこのクラスが必要か: +// boolean の true/false だけでは「どこで」「なぜ」不正だったのかを表現できない。 +class BracketError extends Error { + constructor( + message: string, + public readonly char: string, + public readonly position: number, + ) { + super(message); + this.name = 'BracketError'; + } +} + +// ============================================================ +// 対応表の定義 +// ============================================================ +// Readonly を付けることで「一度定義したら変更されるべきではない」ことを +// コンパイル時に保証する(うっかり書き換えてしまう事故を防ぐ)。 +const CLOSING_BRACKETS: Readonly> = { + '(': ')', + '[': ']', + '{': '}', +}; + +// 型ガード関数:戻り値の型を `param is '(' | '[' | '{'` にすることで、 +// この関数が true を返した後、TypeScript が引数の型を自動的に絞り込んでくれる。 +function isOpeningBracket(char: string): char is '(' | '[' | '{' { + return char in CLOSING_BRACKETS; +} + +function isClosingBracket(char: string): char is ')' | ']' | '}' { + return Object.values(CLOSING_BRACKETS).includes(char); +} + +// ============================================================ +// 検証結果を表す判別可能ユニオン型 +// ============================================================ +// try-catch で例外を投げる代わりに「成功か失敗か」を型で表現することで、 +// 呼び出し元が結果を必ず確認せざるを得ない設計にする。 +type ValidationResult = + | { readonly ok: true } + | { readonly ok: false; readonly error: BracketError }; + +/** + * 括弧文字列が正しく対応しているかを検証する。 + * @param s - 検証したい文字列 + * @returns 検証結果を表すオブジェクト + * @complexity Time: O(n), Space: O(n) + */ +function validateBrackets(s: string): ValidationResult { + // string[] を「後入れ先出し」のスタックとして使う。 + const stack: string[] = []; + + // for...of + entries() で文字とインデックスを安全に取得する + // (サロゲートペア文字も1文字として正しく扱える)。 + for (const [position, char] of [...s].entries()) { + if (isOpeningBracket(char)) { + stack.push(char); + continue; + } + + if (isClosingBracket(char)) { + // stack.pop() は string | undefined を返す。 + // strict モードでは undefined チェックを省略するとコンパイルエラーになる。 + const top = stack.pop(); + + if (top === undefined) { + return { + ok: false, + error: new BracketError( + '対応する開き括弧がないまま閉じ括弧が出現しました', + char, + position, + ), + }; + } + + const expected = CLOSING_BRACKETS[top]; + if (expected !== char) { + return { + ok: false, + error: new BracketError( + `'${top}' に対応する閉じ括弧は '${expected}' ですが '${char}' が見つかりました`, + char, + position, + ), + }; + } + continue; + } + + // 制約上発生しないが、防御的に明示的なエラーを返す。 + return { + ok: false, + error: new BracketError('括弧以外の文字が含まれています', char, position), + }; + } + + if (stack.length > 0) { + const unclosed = stack[stack.length - 1] ?? ''; + return { + ok: false, + error: new BracketError( + `開き括弧 '${unclosed}' が閉じられないまま入力が終了しました`, + unclosed, + s.length, + ), + }; + } + + return { ok: true }; +} + +/** + * 括弧文字列が有効かどうかを判定する(LeetCode提出用エントリポイント)。 + * @param s - 判定対象の文字列 + * @returns 有効なら true、そうでなければ false + * @complexity Time: O(n), Space: O(n) + */ +function isValid(s: string): boolean { + return validateBrackets(s).ok; +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 入力: s = "([)]" +> position=0, char='(' → isOpeningBracket=true → push → stack=['('] +> position=1, char='[' → isOpeningBracket=true → push → stack=['(', '['] +> position=2, char=')' → isClosingBracket=true +> stack.pop() → top='[' , stack=['('] +> CLOSING_BRACKETS['['] = ']' だが char=')' → 不一致 +> → { ok: false, error: BracketError(...) } +> isValid の戻り値: false +> ``` + +> 📖 **この章で登場した用語** +> +> - **`readonly`**:配列やプロパティの値を変更できないようにする修飾子。JavaScriptには無い、TypeScript独自のコンパイル時チェック機能 +> - **型ガード**:`typeof`や独自関数(`is`構文)で実行時に値の型を確認し、その情報をもとに型を絞り込む仕組み +> - **判別可能ユニオン型**:`{ ok: true } | { ok: false, error: ... }`のように、共通のプロパティ(ここでは`ok`)の値によって型を区別できるユニオン型 +> - **Pure function(純粋関数)**:同じ入力を与えると必ず同じ出力を返し、外部の状態を変えない関数 + +--- + +

Go 実装

+ +> 💡 **初学者向け補足**:Goにはクラスが無く、`struct`とメソッドでオブジェクト指向的な設計を実現します。また例外機構(`try-catch`)も無く、エラーは戻り値として返すのがGoの慣習です。骨格は以下の通りです。 +> +> 1. `BracketError`構造体を定義し、`Error() string`メソッドで`error`インターフェースを満たす +> 2. `closingFor`ヘルパー関数で開き括弧から閉じ括弧を`switch`文で引く +> 3. `validateBrackets`が`[]byte`スタックを使い、`error`を返す形で検証する +> 4. `isValid`はその結果を`== nil`で`bool`に変換する薄いラッパーにする + +```go +package main + +import "fmt" + +// BracketError は括弧の対応関係が不正だったことを表すエラー型。 +// bool の true/false だけでは「なぜ」「どこで」不正だったかを表現できないため、 +// error インターフェースを満たす独自の型を用意する。 +type BracketError struct { + Reason string // 不正になった理由の説明文 + Char byte // 問題の原因となった文字 +} + +// Error は error インターフェースを満たすためのメソッド。 +// Go のインターフェースは「このメソッドを持っていれば自動的に実装済み」と +// みなされる暗黙的な実装であり、Java の implements 宣言は不要。 +func (e *BracketError) Error() string { + return fmt.Sprintf("%s: %q", e.Reason, e.Char) +} + +// closingFor は開き括弧に対応する閉じ括弧を返すヘルパー関数。 +// map[byte]byte ではなく switch を使う理由: +// この問題では対応関係が3種類しかなく、switch の方がマップ生成の +// コストがかからず高速かつシンプルになるため。 +func closingFor(open byte) (closing byte, ok bool) { + switch open { + case '(': + return ')', true + case '[': + return ']', true + case '{': + return '}', true + default: + return 0, false + } +} + +// validateBrackets は文字列が正しく対応した括弧かどうかを検証する。 +// +// Time Complexity: O(n) +// Space Complexity: O(n) +func validateBrackets(s string) error { + // []byte をスタックとして使う。make の第3引数で len(s) を指定し、 + // 最悪ケースでも append による再アロケーションが発生しないようにする。 + stack := make([]byte, 0, len(s)) + + // 括弧記号はすべてASCII文字(1バイト)なので、for range によるrune変換を + // 避け、バイト単位のインデックスアクセスで直接走査する方が高速。 + for i := 0; i < len(s); i++ { + c := s[i] + + switch c { + case '(', '[', '{': + stack = append(stack, c) + + case ')', ']', '}': + if len(stack) == 0 { + return &BracketError{Reason: "対応する開き括弧がない", Char: c} + } + // スタックの末尾を取り出す(pop操作)。 + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + expected, _ := closingFor(top) + if expected != c { + return &BracketError{ + Reason: fmt.Sprintf("'%c' に対応する閉じ括弧は '%c' ではない", top, expected), + Char: c, + } + } + + default: + // 制約上発生しないが、パニックさせず防御的にエラーを返す。 + return &BracketError{Reason: "括弧以外の文字が含まれている", Char: c} + } + } + + if len(stack) != 0 { + return &BracketError{Reason: "閉じられていない開き括弧が残っている", Char: stack[len(stack)-1]} + } + + return nil +} + +// isValid は LeetCode 提出用のエントリポイント(業務開発版)。 +func isValid(s string) bool { + return validateBrackets(s) == nil +} +``` + +競技プログラミング版は、エラーの詳細を持たず「対応する閉じ括弧そのもの」をスタックに積むテクニックで比較コストを減らします。 + +```go +// isValid は LeetCode 提出用のエントリポイント(競技プログラミング版)。 +// +// Time Complexity: O(n) +// Space Complexity: O(n) +func isValid(s string) bool { + stack := make([]byte, 0, len(s)) + + for i := 0; i < len(s); i++ { + switch s[i] { + case '(': + // 開き括弧が来た時点で「対応する閉じ括弧そのもの」を積んでおく。 + // これにより閉じ括弧が来たときに変換関数を介さず直接比較できる。 + stack = append(stack, ')') + case '[': + stack = append(stack, ']') + case '{': + stack = append(stack, '}') + default: + // ここに来るのは閉じ括弧のはず。 + if len(stack) == 0 || stack[len(stack)-1] != s[i] { + return false + } + stack = stack[:len(stack)-1] + } + } + + return len(stack) == 0 +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 入力: s = "([)]" +> i=0, c='(' → push ')' → stack = [')'] +> i=1, c='[' → push ']' → stack = [')', ']'] +> i=2, c=')' → default節。stack末尾=']' と s[2]=')' を比較 → 不一致 → return false +> ``` + +> 📖 **この章で登場した用語** +> +> - **`error`戻り値**:JavaやPythonの例外機構と異なり、Goはエラーを戻り値として返す。呼び出し元が`if err != nil`で必ず確認する設計になっている +> - **プリアロケーション**:`make([]byte, 0, len(s))`のように必要なメモリをあらかじめまとめて確保しておくこと。`append`による途中の再確保を防ぐ +> - **パニック**:Goで回復不能なエラーが発生した際に起きる強制終了。範囲外アクセスなどで発生する +> - **レシーバ**:メソッドが属する型のインスタンス。Javaの`this`に相当するが名前を自由に決められる + +--- + +

Rust 実装

+ +> 💡 **初学者向け補足**:Rustには所有権(=値を"誰が管理するか"をコンパイル時に決める仕組み)という、他の3言語には無い独自の概念があります。ガベージコレクタ無しでメモリの安全性を保証する代わりに、値の受け渡し方に気をつける必要があります。骨格は以下の通りです。 +> +> 1. `BracketError`という列挙型(`enum`)を定義する(失敗の種類を型で表現するため) +> 2. `validate_brackets`が`&str`(文字列への借用)を受け取り、`Vec`スタックで検証し、`Result<(), BracketError>`を返す +> 3. `Solution::is_valid`はその結果を`.is_ok()`で`bool`に変換する薄いラッパーにする + +```rust +// ============================================================ +// カスタムエラー型 +// ============================================================ +// bool の true/false だけでは「どこで」「なぜ」不正だったのかを表現できない。 +// Rust では例外を使わず、戻り値の型としてエラーを表現するのが特徴。 +#[derive(Debug, Clone, PartialEq)] +enum BracketError { + // 閉じ括弧が来たが、対応する開き括弧の種類が違った場合 + Mismatched { expected: char, found: char }, + // 閉じ括弧が来たのに対応する開き括弧が1つも積まれていない場合 + UnexpectedClose(char), + // 文字列を読み終えたのに閉じられていない開き括弧が残っている場合 + UnclosedOpen(char), +} + +impl std::fmt::Display for BracketError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Mismatched { expected, found } => { + write!(f, "'{found}' に対応する閉じ括弧は '{expected}' ではありません") + } + Self::UnexpectedClose(c) => { + write!(f, "対応する開き括弧がないまま閉じ括弧 '{c}' が出現しました") + } + Self::UnclosedOpen(c) => { + write!(f, "開き括弧 '{c}' が閉じられないまま入力が終了しました") + } + } + } +} + +impl std::error::Error for BracketError {} + +/// 開き括弧に対応する閉じ括弧を返すヘルパー関数。 +/// Option を返すことで、「括弧ではない文字」が来た場合に +/// 呼び出し元がパニックせず安全に分岐できるようにする。 +fn closing_for(open: char) -> Option { + match open { + '(' => Some(')'), + '[' => Some(']'), + '{' => Some('}'), + _ => None, + } +} + +/// 括弧文字列が正しく対応しているかを検証する。 +/// +/// # Arguments +/// * `s` - 検証したい文字列への借用(&str)。 +/// 所有権を奪わないので、呼び出し元は検証後も s を使い続けられる。 +/// +/// # Complexity +/// - Time: O(n) +/// - Space: O(n) +fn validate_brackets(s: &str) -> Result<(), BracketError> { + // Vec を「後入れ先出し」のスタックとして使う。 + // with_capacity で最悪ケースを見越し、途中の再アロケーションを防ぐ。 + let mut stack: Vec = Vec::with_capacity(s.len()); + + // s.chars() は &str を借用するだけのイテレータで所有権を奪わない。 + for c in s.chars() { + match c { + '(' | '[' | '{' => stack.push(c), + + ')' | ']' | '}' => { + // stack.pop() は Option を返す。 + // ? 演算子で None のとき即座に Err を返す。 + let top = stack.pop().ok_or(BracketError::UnexpectedClose(c))?; + + // stack に積まれるのは開き括弧のみなので None は起こり得ない。 + let expected = closing_for(top) + .expect("スタックには開き括弧のみが積まれるため必ず対応する閉じ括弧が存在する"); + + if expected != c { + return Err(BracketError::Mismatched { expected, found: c }); + } + } + + _ => unreachable!("制約により括弧文字以外は入力されない"), + } + } + + if let Some(&unclosed) = stack.last() { + return Err(BracketError::UnclosedOpen(unclosed)); + } + + Ok(()) +} + +struct Solution; + +impl Solution { + /// 括弧文字列が有効かどうかを判定する(LeetCode提出用エントリポイント)。 + /// + /// # Complexity + /// - Time: O(n) + /// - Space: O(n) + pub fn is_valid(s: String) -> bool { + // s: String(所有権を持つ)を &s として借用に変換して渡す。 + // 内部処理は読み取りだけで完結するため、無駄なコピーが発生しない。 + validate_brackets(&s).is_ok() + } +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 入力: s = "([)]" +> c='(' → push → stack = ['('] +> c='[' → push → stack = ['(', '['] +> c=')' → pop() → top='[' , stack=['('] +> closing_for('[') = ']' だが c=')' → 不一致 +> → Err(BracketError::Mismatched { expected: ']', found: ')' }) +> is_valid の戻り値: false +> ``` + +> 📖 **この章で登場した用語** +> +> - **所有権**:値を"誰が管理するか"をコンパイル時に決めるRust独自の仕組み。JavaやPythonのようなガベージコレクタなしでメモリの安全性を保証できる +> - **借用**:所有権を渡さずに値を参照する仕組み。`&T`(読み取り専用)と`&mut T`(書き込み可能)がある +> - **`Result`**:成功か失敗かを型で表現する仕組み。呼び出し元は必ず結果を確認しなければならない +> - **`?`演算子**:`Result`や`Option`がエラー・`None`だったとき、自動で呼び出し元に返す糖衣構文 +> - **ゼロコスト抽象化**:便利な高レベルな書き方(イテレータなど)をしても、手書きの低レベルコードと同等の速さになるRustの特性 + +--- + +

言語間比較

+ +> 💡 **初学者向け補足**:同じアルゴリズムでも、言語によって「メモリの扱い方」や「エラーの表現方法」が異なります。この章では4言語の実装を横並びで比較し、それぞれの設計判断の背景を振り返ります。 + +| 観点 | Python | TypeScript | Go | Rust | +| ------------------ | ------------------------------------ | -------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ | +| 採用アルゴリズム | スタック法 | スタック法 | スタック法 | スタック法 | +| スタックの実体 | `list[str]` | `string[]` | `[]byte` | `Vec` | +| メモリ確保の特徴 | 動的拡張(償却O(1)) | 動的拡張(V8内部最適化) | `make`でプリアロケーションし再アロケーションを回避 | `Vec::with_capacity`で事前確保し再アロケーションを回避 | +| エラー表現の方法 | 例外(`Exception`継承クラス) | 判別可能ユニオン型 + `Error`継承クラス | `error`戻り値(インターフェース) | `Result` + `?`演算子 | +| メモリ管理方式 | GC(参照カウント + 世代別GC) | GC(V8のGC) | GC(並行GC) | 所有権システム(GC無し) | +| 型安全性の担保方法 | pylance静的解析 + 実行時`isinstance` | コンパイル時の構造的型付け + 型ガード | 静的型付け + `go vet` | コンパイル時の所有権チェック + トレイト境界 | + +### 相違点の背景 + +Python・TypeScript・GoはいずれもGC(ガベージコレクション、=使い終わったメモリを自動で回収する仕組み)を持つ言語であり、「メモリをいつ・どう解放するか」をプログラマが意識する必要がありません。そのため、これら3言語の実装の違いは主に「速度への配慮の度合い」(Goのプリアロケーションなど)や「型システムの厳密さ」(TypeScriptの型ガード)に現れます。 + +一方Rustは所有権システム(=コンパイル時にメモリの管理者を1つに定める仕組み)を持ち、GCなしでメモリ安全性を実現します。この違いが最も顕著に表れるのが**エラー表現**です。Python/TypeScript/Goが「例外」または「戻り値でのフラグ」でエラーを表現するのに対し、Rustの`Result`は「成功した場合の値」と「失敗した場合の値」を**型レベルで両方保持する**設計になっており、呼び出し元は`match`や`?`演算子を通じて必ずどちらのケースかを意識させられます。これはRustが「実行時に予期せぬ例外で落ちる」ことを可能な限りコンパイル時に排除しようとする設計哲学の表れです。 + +**なぜ4言語で同じアルゴリズムを採用したのか**:本問題はデータ構造・計算量の観点で言語ごとに有利不利が生じるタイプの問題ではなく、「スタックを使った1回走査」が全言語で最適かつ最も自然な解法だからです。無理に言語ごとに異なるアルゴリズムを採用すると、かえって「なぜ違う解法にしたのか」という不要な疑問を読者に与えてしまいます。ただし、スタックの型・対応表の引き方・エラー表現は、各言語のイディオム(=その言語で自然とされる書き方)に忠実に従って変えています。 + +> 📖 **この章で登場した用語** +> +> - **GC(ガベージコレクション)**:使い終わったメモリを自動で回収する仕組み。Python・TypeScript(JavaScript)・Goが持つ +> - **所有権システム**:Rust独自の、コンパイル時にメモリの管理者を1つに定める仕組み。GC不要でメモリ安全性を実現する +> - **イディオム**:その言語で「自然」「慣用的」とされる書き方のパターン + +--- + +

言語別最適化ポイント

+ +> 💡 **初学者向け補足**:この章では「同じ処理でも言語ごとの書き方によって速さが変わる理由」を4言語それぞれについて、最適化前 → 最適化後 → なぜ速くなるかの3点セットで説明します。 + +### Python(CPython)最適化ポイント + +```python +# 最適化前:if/elif の連鎖で括弧の種類を判定する(遅い・冗長) +def is_open(c: str) -> bool: + if c == '(': + return True + elif c == '[': + return True + elif c == '{': + return True + return False + +# 最適化後:dict のキー検索でO(1)判定にする +pairs = {')': '(', ']': '[', '}': '{'} +# in 演算子は dict に対して平均O(1)(ハッシュテーブルによる検索) +if char in pairs: + ... +# 理由:dict のキー検索はC実装のハッシュテーブルによる平均O(1)であり、 +# if/elif の連鎖(最悪O(3)、括弧の種類数に比例)より意図が明確かつ拡張しやすい +``` + +その他、`stack.pop() if stack else '#'`のような**番兵値**を使うことで、`try-except`による例外処理コスト(Pythonの例外は「起きなければ軽い」が構築コストがゼロではない)を避けられます。 + +### TypeScript最適化ポイント + +```typescript +// 最適化前:毎回 Object.keys で対応表のキー一覧を作り直して探索する(遅い) +function isOpen(char: string): boolean { + return Object.keys({ '(': ')', '[': ']', '{': '}' }).includes(char); +} + +// 最適化後:対応表をモジュールスコープの定数にし、in 演算子で判定する +const CLOSING_BRACKETS: Readonly> = { '(': ')', '[': ']', '{': '}' }; +function isOpen(char: string): char is '(' | '[' | '{' { + return char in CLOSING_BRACKETS; +} +// 理由:Object.keys().includes() は呼び出しごとに配列を新規生成しO(n)探索になるが、 +// in 演算子はオブジェクトのプロパティ検索としてV8エンジンに最適化されており高速。 +// また型ガード(char is ...)にすることで、コンパイル時の型絞り込みという +// 追加のメリットも得られる(実行時コストはゼロ、TypeScript特有の恩恵) +``` + +### Go最適化ポイント + +```go +// 最適化前:スタックの容量を指定せず append を繰り返す(再アロケーションの可能性) +stack := []byte{} +for i := 0; i < len(s); i++ { + stack = append(stack, s[i]) +} + +// 最適化後:make の第3引数で最悪ケースの容量を事前確保する +stack := make([]byte, 0, len(s)) +for i := 0; i < len(s); i++ { + stack = append(stack, s[i]) +} +// 理由:容量を指定しない場合、append は容量超過のたびに新しい領域を確保して +// 全要素をコピーする。事前に len(s) 分を確保しておけば、 +// 最悪ケース(全て開き括弧)でもこのコピーが一切発生しない +``` + +### Rust最適化ポイント + +```rust +// 最適化前:Vec::new() で容量指定なしにスタックを作る +let mut stack: Vec = Vec::new(); + +// 最適化後:Vec::with_capacity で最悪ケースの容量を事前確保する +let mut stack: Vec = Vec::with_capacity(s.len()); +// 理由:Vec::new() は容量0からスタートし、push のたびに容量超過を判定する。 +// 容量超過が起きると、より大きな領域を確保して全要素をコピーする +// 「再アロケーション」が発生する。with_capacity で最悪ケースの +// 容量を先に確保しておけば、このコピーコストをゼロにできる +// (ゼロコスト抽象化の実践例の1つ) +``` + +> 📖 **この章で登場した用語** +> +> - **ハッシュテーブル**:キーからハッシュ値という数値を計算し、その番号の場所に値を格納・検索するデータ構造 +> - **エスケープ解析**:変数をスタック(高速・自動解放)に置くかヒープ(低速・GC管理)に置くかをコンパイラが判断する仕組み +> - **モノモーフィゼーション**:ジェネリクス関数が型ごとに専用コードへ自動展開される仕組み。動的ディスパッチより高速 +> - **再アロケーション**:配列やスライスの容量が足りなくなったとき、より大きな領域に全要素をコピーする操作 + +--- + +

エッジケースと検証観点

+ +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生します。 + +- **空文字列の場合**:制約上`1 <= s.length`のため発生しませんが、防御的に書かれたコードであれば、スタックが空のまま走査が終わり`true`を返すだけで安全に処理できます。 +- **単一の閉じ括弧(例:`")"`)の場合**:対応する開き括弧が存在しないため`false`になるべきケースです。この処理は言語によって挙動が異なります。 + - **Python**:`stack.pop()`は空リストに対して`IndexError`(存在しない要素にアクセスしようとしたときの例外)を送出するため、業務版では`if not stack:`で事前チェックし、競技版では`stack.pop() if stack else '#'`という番兵値で回避しています。 + - **TypeScript**:`Array.prototype.pop()`は空配列に対して例外を投げず`undefined`を返すため、`top === undefined`のチェックで安全に処理します。 + - **Go**:スライスに対する範囲外アクセスは**パニック**(回復不能なエラーによる強制終了)を引き起こすため、`len(stack) == 0`のチェックを先に行い、パニックが起きる前に安全に`false`を返します。 + - **Rust**:`Vec::pop()`はそもそも`Option`を返す設計になっており、空の場合は`None`が返るだけでパニックしません。`?`演算子と`ok_or()`の組み合わせで、この`None`を安全に`Err`へ変換しています。 +- **未閉じの開き括弧(例:`"((("`)の場合**:走査後もスタックに要素が残るため、4言語とも「走査後のスタックの空チェック」で`false`と判定します。 +- **括弧の種類の不一致(例:`"([)]"`)の場合**:[図解](#figures)や各実装章のトレースで示した通り、スタックから取り出した開き括弧と現在の閉じ括弧が一致しないため`false`になります。 +- **最大長10^4文字の場合**:4言語ともスタックへのプリアロケーション(Go/Rust)または効率的な動的拡張(Python/TypeScript)により、O(n)の性能が保証されます。 + +> 📖 **この章で登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件の入力 +> - **`IndexError`**:Pythonでシーケンスの範囲外インデックスにアクセスしようとしたときに発生する例外 +> - **パニック**:GoやRustで回復不能なエラーが発生した際に起きる強制終了。範囲外アクセスなどで発生する + +--- + +

FAQ

+ +**Q1. なぜ括弧の"数"を数えるだけでは不十分なのですか?** + +結論:数が合っていても順序が壊れている入力を見逃してしまうためです。 + +理由:この問題の要件2は「開き括弧は正しい順序で閉じられなければならない」というものです。数を数えるだけの方法(各括弧の出現数を記録し、最後に開き括弧の数と閉じ括弧の数が一致するか確認する方法)は、この「順序」の情報を一切保持しません。 + +補足:例えば`"([)]"`は`(`が1個・`[`が1個・`)`が1個・`]`が1個で数としては完全に一致していますが、`(`を閉じる前に`[`が`)`によって閉じられようとしており不正です。数を数える方法ではこれを`true`と誤判定してしまいます。 + +--- + +**Q2. なぜスタック(後入れ先出し)を使うのですか?別のデータ構造ではダメですか?** + +結論:「最後に開いた括弧が最初に閉じられるべき」という問題の性質が、スタックの動作原理そのものと一致するためです。 + +理由:括弧が正しくネスト(入れ子)されているとき、一番内側(=最後に開かれた)括弧が一番最初に閉じられます。スタックは「最後に入れたものを最初に取り出す」構造なので、この性質を自然に表現できます。 + +補足:キュー(先入れ先出し)を使うと、最初に開いた括弧が最初に取り出されてしまい、ネストの内側から外側へという正しい順序で照合できません。 + +--- + +**Q3. なぜ4言語とも同じアルゴリズムを採用したのですか?** + +結論:この問題はデータ構造・計算量の面で言語ごとに有利不利が生じるタイプの問題ではないためです。 + +理由:本問題の最適解は「スタックを使った1回走査、O(n)時間・O(n)空間」であり、これはPython・TypeScript・Go・Rustのいずれでも実現可能かつ最も自然な実装です。言語ごとに異なるアルゴリズムを無理に採用すると、かえって「なぜ違う解法にしたのか」という不要な疑問を生んでしまいます。 + +補足:これは全ての問題に当てはまるわけではありません。問題によっては、ある言語では標準ライブラリの特定の関数(例:Goの`container/heap`)を使う方が自然で、別の言語では手書きの実装のほうが素直、というケースも起こり得ます。今回のケースでは、[言語間比較](#comparison)章で述べた通り、スタックの型やエラー表現の哲学に違いが出ています。 + +--- + +**Q4. なぜRustだけ所有権や借用の話が頻繁に出てくるのですか?** + +結論:Rustだけがガベージコレクタ(GC)を持たず、代わりに所有権システムでメモリ安全性を保証しているためです。 + +理由:Python・TypeScript・GoはいずれもGCを持つため、「値をどう受け渡すか」を意識しなくても実行時に自動的にメモリが管理されます。一方Rustでは、値を関数に渡すたびに「所有権が移動するのか、それとも借用(参照)だけなのか」をプログラマが明示的に決める必要があり、これがコンパイル時のメモリ安全性チェックの土台になっています。 + +補足:本問題の`Solution::is_valid`が`s: String`(所有権を持つ文字列)を受け取りながら、内部では`&s`として借用に変換して`validate_brackets`に渡しているのは、この所有権システムを活かした典型的なパターンです。読み取りだけで完結する処理には所有権を渡さず借用で十分、という判断がコードに表れています。 + +--- + +**Q5. なぜGoでは`error`型を使わず`bool`を直接返すバージョンがあるのですか?** + +結論:業務開発向け(詳細なエラー情報が必要な場面)と競技プログラミング向け(速度と簡潔さが最優先の場面)で、目的が異なるためです。 + +理由:`error`戻り値を経由するバージョンは「なぜ不正なのか」を`BracketError`構造体として保持できるため、ログ出力やAPIのエラーレスポンスへの拡張がしやすい設計です。一方、LeetCodeのような制限時間内に正解を出すことが目的の場面では、エラーの詳細情報は不要であり、構造体生成のわずかなオーバーヘッドすら削りたい場面があります。 + +補足:[Go実装章](#impl-go)の競技プログラミング版では、開き括弧を積む際に「対応する閉じ括弧そのもの」をスタックに積むことで、閉じ括弧が来たときの比較を1回の等値比較だけで完結させています。 + +--- + +**Q6. スタックが空のまま閉じ括弧が来た場合、なぜクラッシュせず安全に処理できるのですか?** + +結論:4言語とも、スタックから値を取り出す前に「スタックが空でないか」を確認する分岐を必ず経由するように設計されているためです。 + +理由:もしこのチェックを省略すると、Goでは範囲外アクセスによるパニック、Pythonでは`IndexError`という形で、プログラムが想定外の形で終了してしまいます。[エッジケース](#edgecases)章で述べた通り、各言語はそれぞれ異なる方法(`if not stack`、`undefined`チェック、`len(stack) == 0`、`Option`の`None`分岐)でこの安全性を確保しています。 + +補足:この「取り出す前に空かどうか確認する」というパターンは、スタックを使うあらゆるアルゴリズムに共通する定石です。 + +> 📖 **この章で登場した用語** +> +> - **FAQ**:Frequently Asked Questions の略。よくある質問と回答のこと +> - **トレードオフ**:何かを得ると何かを失う関係。例:速さを得るとメモリが増える diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html new file mode 100644 index 00000000..b50e8936 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html @@ -0,0 +1,1781 @@ + + + + + + Valid Parentheses - LeetCode 20 解説(Python / TypeScript / Go / Rust) + + + + + + + + + + + + + + + +
+ + + + + + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「開き括弧と閉じ括弧の対応関係と順序が正しいかどうかを判定する問題」です。 +

+

+ '('')''{''}''['']' + だけからなる文字列 + s + が与えられます。すべての開き括弧が正しい種類・正しい順序で閉じられているなら + true、そうでなければ + false + を返します。 +

+
+ +
+

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

+
    +
  • + 括弧の「数」を数えるだけでは不十分です。開き括弧の数と閉じ括弧の数が一致していても、閉じる順序が壊れている入力(例:"([)]")を見逃してしまいます。 +
  • +
  • + 閉じ括弧が来たときに「一番最近開かれた、まだ閉じられていない括弧」と比較する必要があり、これには「後入れ先出し」の性質を持つデータ構造(スタック)が必要です。 +
  • +
+
+ +
+
+
+ O(n) +
+
時間計算量
+
+
+
+ O(n) +
+
空間計算量
+
+
+
+ Stack +
+
採用アプローチ
+
+
+
+ 4言語共通 +
+
アルゴリズムの統一度
+
+
+ +
+

📥 入出力例

+
+
+
s = "()[]{}"
+
出力: true
+
+ 3種類の括弧がそれぞれ正しい順序で閉じられているため +
+
+
+
s = "([)]"
+
出力: false
+
+ '(' が閉じられる前に '[' が ')' によって閉じられようとしているため +
+
+
+
+
+ + + + +
+

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

+

+ ここでは特定の言語に依存せず、「文字列を読みながらスタックを操作する」という処理の流れを、 + 代表例 + s = "([)]" + を使って1ステップずつ確認します。 + この考え方は、後の「コード(4言語)」セクションでPython / TypeScript / Go / + Rustのどの実装にも共通する土台になります。 +

+
+
+ + + + +
+

+ 実装(Python / TypeScript / Go / Rust) +

+

+ 4言語とも「開き括弧が来たら対応する閉じ括弧をスタックに積み、閉じ括弧が来たらスタック最上部と直接比較する」という同じアルゴリズムを採用しています。 + タブを切り替えて、それぞれの言語らしい書き方・エラーの扱い方の違いを見比べてみましょう。 +

+
+
+ + + + + +
+ + + + +
+

+ 計算量分析 +

+ +
+

📖 Big-O 記法の読み方

+
+
+
O(1)
+
入力サイズに関わらず
常に一定の速さ
+
+
+
O(n)
+
入力が2倍になると
処理も約2倍
+
+
+
O(n log n)
+
入力が2倍になると
処理は約2倍強
+
+
+
O(n²)
+
入力が2倍になると
処理は約4倍
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
言語 + 採用アルゴリズム + 時間計算量空間計算量実装上の特徴
Pythonスタック法O(n)O(n) + list + をスタックに、dict + で対応表引き +
+ TypeScript + スタック法O(n)O(n) + string[] + をスタックに、readonly + な対応表 +
Goスタック法O(n)O(n) + []byte + をプリアロケーション、switch + で分岐 +
Rustスタック法O(n)O(n) + Vec<char> + を事前確保、Option<char> + で安全に比較 +
+
+ +
+

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

+

+ 文字列 + s + の各文字を1回ずつしか読まないため、時間計算量は文字列長 n に比例する O(n) + になります。 + これより速くすることは、各文字を最低1回は確認する必要がある以上、理論上不可能です。 + 空間計算量については、最悪ケース(全て開き括弧、例:"(((((")ではスタックに全文字が積まれるため O(n) になります。 + 括弧の出現数だけを数えるO(1)空間のアプローチも存在しますが、順序の検証ができず不正解になるため採用していません。 +

+
+
+ + + + +
+

+ 言語間比較 +

+

+ 同じアルゴリズムでも、言語ごとに「メモリの扱い方」や「エラーの表現方法」が異なります。 + ここでは4言語の実装を横並びで比較し、それぞれの言語特有の設計判断を振り返ります。 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
観点PythonTypeScriptGoRust
+ 採用アルゴリズム + スタック法スタック法スタック法スタック法
+ スタックの実体 + list[str]string[][]byte + Vec<char> +
+ メモリ確保の特徴 + 動的拡張(償却O(1)) + 動的拡張(V8内部最適化) + + makeでプリアロケーションし再アロケーション回避 + + with_capacityで事前確保し再アロケーション回避 +
+ エラー表現の方法 + 例外(Exception)undefinedの型レベル表現 + パニックを避ける範囲チェック + + Option<T>による安全な比較 +
+ メモリ管理方式 + + GC(参照カウント+世代別GC) + GC(V8のGC)GC(並行GC) + 所有権システム(GC無し) +
+ 型安全性の担保方法 + + pylance静的解析+実行時isinstance + + コンパイル時の構造的型付け+型ガード + 静的型付け+go vet + コンパイル時の所有権チェック +
+
+ +
+

相違点の背景

+

+ Python・TypeScript・GoはいずれもGC(ガベージコレクション、=使い終わったメモリを自動で回収する仕組み)を持つ言語であり、 + メモリの解放タイミングを意識する必要がありません。一方Rustは所有権システム(=コンパイル時にメモリの管理者を1つに定める仕組み) + でGC無しにメモリ安全性を実現しており、これがエラー表現の違いに最も強く表れています。 + Python/TypeScript/Goが「例外」または「安全な値(undefinedやパニック回避チェック)」でエラーを扱うのに対し、 + RustのOption<T>は「値があるかないか」を型として持つため、 + 呼び出し側は必ずどちらのケースかを意識させられます。 + 本問題はデータ構造・計算量の面で言語ごとの有利不利が生じるタイプの問題ではないため、4言語とも同じスタック法を採用していますが、 + 対応表の型・エラーの扱い方には各言語のイディオム(=その言語で自然とされる書き方)がそれぞれ表れています。 +

+
+
+ + + + +
+

+ 📖 用語集 +

+

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

+
+
+ + イミュータブル + +
+ 一度作られたら中身を変更できない性質のこと。PythonのstrやTypeScript/JavaScriptの文字列はこの性質を持ちます。 +
+
+ +
+ + ガベージコレクション(GC) + +
+ 使い終わったメモリを自動で回収する仕組み。Python・TypeScript(JavaScript)・Goが持つ機能で、プログラマがメモリ解放のタイミングを意識しなくてよくなります。Rustはこの仕組みを持たず、代わりに所有権システムでメモリを管理します。 +
+
+ +
+ + 型ガード(TypeScript) + +
+ typeofin演算子などで実行時に値の型を確認し、その情報をもとにTypeScriptの型を絞り込む仕組みです。JavaScriptには無い、TypeScript独自のコンパイル時チェック機能です。 +
+
+ +
+ + 借用(Rust) + +
+ 所有権を渡さずに値を参照する仕組み。&T(読み取り専用)と&mut T(書き込み可能)があります。 +
+
+ +
+ + 所有権(Rust) + +
+ 値を「誰が管理するか」をコンパイル時に決めるRust独自の仕組み。JavaやPythonのようなガベージコレクタなしで、メモリの安全性をコンパイル時に保証できます。 +
+
+ +
+ + スタック + +
+ 後入れ先出し(LIFO)の構造を持つデータ構造。お皿の積み重ねのように、最後に積んだものを最初に取り出します。本問題では「まだ閉じられていない開き括弧(に対応する閉じ括弧)」を記録するために使います。 +
+
+ +
+ + スライス(Go) + +
+ Goで配列の一部(または全体)を扱うためのデータ構造。[]byteのように書き、動的にサイズを変えられます。 +
+
+ +
+ + 静的解析 + +
+ プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法。Pythonのpylance、TypeScriptのコンパイラ、Goのgo + vet、Rustのclippyがそれぞれ担っています。 +
+
+ +
+ + 償却計算量(Amortized + Complexity) + +
+ 個々の操作は時々遅くなることがあっても、多数回の操作全体で平均すると一定時間で済むという計算量の考え方。Pythonのlist.appendの再確保コストはこの考え方で「ならすとO(1)」とみなされます。 +
+
+ +
+ + パニック(Go / Rust) + +
+ GoやRustで回復不能なエラーが発生した際に起きる強制終了。スライスや配列への範囲外アクセスなどで発生します。本問題のコードでは、範囲外アクセスが起きる前に必ず空チェックを行うことで回避しています。 +
+
+ +
+ + プリアロケーション(Go / Rust) + +
+ 必要なメモリをあらかじめまとめて確保しておくこと。Goのmake([]byte, 0, len(s))やRustのVec::with_capacity(s.len())のように書くことで、途中の再確保を防ぎ高速化できます。 +
+
+ +
+ + Big-O記法 + +
+ 入力の大きさに対して、処理にかかる時間やメモリがどう増えるかを表す記法。O(n)なら「入力が2倍になると処理も約2倍になる」ことを意味します。 +
+
+ +
+ + LIFO(Last In, First Out) + +
+ 「最後に入れたものを最初に出す」という、スタックの動作原則。本問題の「一番内側の括弧から閉じる」という性質とぴったり一致します。 +
+
+ +
+ + readonly(TypeScript) + +
+ 配列やオブジェクトの値を変更できないようにする修飾子。JavaScriptには無い、TypeScript独自のコンパイル時チェック機能で、意図しない書き換えを防ぎます。 +
+
+
+
+ +
+ LeetCode 20: Valid Parentheses ― Python / TypeScript / Go / Rust 4言語解説 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Go.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Go.md new file mode 100644 index 00000000..f5b73b26 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Go.md @@ -0,0 +1,334 @@ +# 1. 問題分析結果 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「**開き括弧と閉じ括弧の"組み合わせ"と"順番"が正しいかを、後入れ先出し(=最後に積んだものを最初に取り出す)のルールで判定する問題**」です。 +> +> Goでこの問題を解く際に特に気をつけるべき点は、**「スライスをスタック代わりに使うときの再アロケーション対策」**と、**「エラーの意味を`bool`一つに潰さずに、内部設計としてどう表現するか」**の2点です。LeetCodeの関数シグネチャは`func isValid(s string) bool`で戻り値が`bool`固定ですが、業務開発では「なぜ`false`になったのか」を後から追跡できる設計にしておくと保守性が上がります。ゴルーチンやチャネルは、この問題が「1本の文字列を先頭から順番に読む」逐次処理であるため出番がありません(並行化しても速くならない典型例です)。 + +#### 競技プログラミング視点 + +- **制約分析**:文字列長は最大10^4文字(制約より)。この規模なら O(n) の線形走査であれば余裕を持って高速に処理できます。 +- **最速手法**:`[]byte`(バイトのスライス)をスタックとして使い、`make([]byte, 0, len(s))`で**プリアロケーション**(=必要なメモリをあらかじめまとめて確保しておくこと。`append`による途中の再確保を防ぎ高速化できる)しておくことで、ヒープへの再アロケーションを実質ゼロ回に抑えます。 +- **メモリ最小化**:`map[byte]byte`で対応表を持つより、`switch`文で分岐する方が、マップの生成・ハッシュ計算コストがかからず高速です。さらに競技版では「開き括弧が来たら"対応する閉じ括弧そのもの"をスタックに積む」というテクニックを使うと、閉じ括弧が来たときの照合が単純な等値比較1回で済みます。 +- **Go最適化**:`for i := 0; i < len(s); i++`でバイト単位のインデックスアクセスを使います。括弧記号はすべてASCII文字(1バイト)なので、`for range`によるrune(Unicode文字単位のイテレーション)変換のオーバーヘッドを避けられます。 + +#### 業務開発視点 + +- **型安全設計**:`byte`同士の比較だけで完結するシンプルな問題ですが、「なぜ不正なのか」を表現する独自の`error`型(`BracketError`)を定義しておくと、将来UIにエラーメッセージを出す拡張がしやすくなります。 +- **エラーハンドリング**:Goでは JavaやPythonの例外(`try-catch`/`try-except`)と違い、**エラーを戻り値として返します**。呼び出し元は`if err != nil`で必ずエラーを確認する設計になるため、エラーの握りつぶし(無視)がコードレビューで見つけやすいという利点があります。 +- **可読性**:`Solution`構造体にメソッドとして実装するか、単純な関数として実装するかは問題のテンプレート次第ですが、この問題は単純な関数形式(パターン1)が最も自然です。 + +#### Go特有分析 + +- **データ構造選択**:「順序あり・末尾への追加/削除が頻繁」という要件に最も合うのはスライス`[]byte`です。両端キューや`container/list`(双方向連結リスト)は今回オーバースペックです。 +- **標準ライブラリ活用度**:この問題は`sort`や`container/heap`のような標準ライブラリの出番はなく、素朴なスライス操作で十分です。無理に標準ライブラリを使うと逆に複雑になります。 +- **並行処理適性**:不適。文字列を先頭から順番に読み、直前の状態(スタックの中身)に依存する逐次処理のため、ゴルーチンに分割すると「途中までの対応関係」を並行して整合させる必要が生まれ、かえって複雑かつ低速になります。 +- **エスケープ解析**(=変数をスタックに置けるかヒープに置くべきかをコンパイラが自動判断する仕組み。ヒープに「逃げる」とGCの負担が増える):`stack := make([]byte, 0, len(s))`で作るスライスは、関数内で使い切って戻り値には含まれないため、コンパイラの最適化次第ではスタック上に確保される可能性があります(ただし`make`で長さが実行時に決まる場合は基本的にヒープ確保になることが多いです)。 + +> 📖 **このセクションで登場した用語** +> +> - **プリアロケーション**:必要なメモリをあらかじめまとめて確保しておくこと。`append`による途中の再確保を防ぎ高速化できる。 +> - **エスケープ解析**:変数をスタック(高速・自動解放)に置くかヒープ(低速・GC管理)に置くかをコンパイラが判断する仕組み。 +> - **`error`戻り値**:Goのエラー表現方法。例外を使わず、関数の戻り値としてエラーを明示的に返す。呼び出し元が確認を強制される。 +> - **ゴルーチン**:Goが提供する軽量スレッド。OSスレッドより格段に安く起動でき、何千個でも同時に扱える。ただし今回の問題では出番がない。 + +--- + +# 2. アルゴリズム比較表 + +> 💡 **初学者向け補足**:なぜ複数のアプローチを比較するのかというと、「動くコード」と「その状況で最善のコード」は別物だからです。特にこの問題では、**一見速そうに見えて実は不正解になるアプローチ(方法C)**があるため、比較を通じて「なぜそれがダメなのか」を理解することが重要です。 +> +> **「Go実装コスト」列**は、「スライスの再アロケーションが起きるか」「不要なヒープ逃げが発生するか」という観点で評価しています。 + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> | 記法 | 意味 | 直感的イメージ | +> | ------- | ---------------------- | ---------------------- | +> | `O(1)` | 入力サイズによらず一定 | 辞書の直引き | +> | `O(n)` | 入力に比例して増加 | リストを端から順に読む | +> | `O(n²)` | 入力の2乗で増加 | 二重ループの総当たり | + +| アプローチ | 時間計算量 | 空間計算量 | Go実装コスト | 可読性 | 標準ライブラリ活用 | 並行処理適性 | 備考 | +| ---------- | ---------- | ---------- | ------------ | ------ | ------------------ | ------------ | -------------------------------------------------- | +| 方法A | O(n) | O(n) | 低 | ★★★ | なし | 不適 | `[]byte`スタック(採用) | +| 方法B | O(1) | O(1) | 低 | ★★☆ | なし | 不適 | 括弧の**出現数**だけをカウント(誤ったアプローチ) | +| 方法C | O(n²) | O(n) | 中 | ★★☆ | `strings.Replace` | 不適 | `"()" "[]" "{}"`を空文字に繰り返し置換 | + +**Go固有の観点(スライスの再アロケーション・ヒープ逃げの有無)**: + +- **方法A**:`make([]byte, 0, len(s))`で最悪ケース(全て開き括弧)を見越した容量を事前確保しているため、走査中の`append`による再アロケーションは発生しません。文字列`s`自体はGoの文字列がイミュータブル(変更不可)なので、`s[i]`によるインデックスアクセスもコピーを伴わず安全です。 +- **方法B**:括弧の「数」だけを固定長の配列(例:`[3]int`)でカウントするので空間計算量はO(1)で最小です。しかし`"([)]"`のように**数は合っているが順序が壊れている**入力を`true`と誤判定してしまうため、要件2「正しい順序で閉じる」を満たせず**不正解**です(比較のためにあえて載せています)。 +- **方法C**:`strings.Replace`は呼び出すたびに**新しい`string`をヒープ上に確保**します(Goの文字列はイミュータブルなため、変更するたびにコピーが発生)。これを空になるまで繰り返すため、最悪ケースで O(n²) の時間計算量に加え、大量のヒープアロケーションが発生し、GC(ガベージコレクション、=使い終わったヒープメモリを自動で回収する仕組み)の負荷も増えます。 + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安。 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安。 +> - **再アロケーション**:スライスの容量が足りなくなったとき、より大きいメモリ領域に全要素をコピーする操作。 +> - **GC(ガベージコレクション)**:使い終わったヒープメモリを自動で回収する仕組み。頻繁に起動するとプログラムが一時停止する。 + +--- + +# 3. 採用アルゴリズムと根拠 + +> 💡 **初学者向け補足**:「なぜ方法Aを選び、方法B・Cを選ばなかったのか」を対比形式で説明します。 + +- **選択理由**: + - **方法Bを選ばなかった理由**:空間計算量O(1)は魅力的に見えますが、括弧が閉じられる**順序**を一切見ていないため`"([)]"`のような不正な入力を`true`と誤判定してしまいます。速さより先に「問題の要件を満たすこと」が必須です。 + - **方法Cを選ばなかった理由**:`strings.Replace`を繰り返すたびに新しい文字列がヒープに確保されるため、O(n²)の時間計算量に加えてGCの負荷が無視できません。10^4文字の入力でも実用上動きはしますが、方法Aの方が明確に優れています。 + - **方法Aを選んだ理由**:文字列を1回だけ走査するO(n)で完結し、スタック操作(`append`/スライス縮小)はGoの標準的なイディオムで安全に書けます。 +- **Go最適化戦略**: + - **プリアロケーション**:`make([]byte, 0, len(s))`で最悪ケースの容量を先に確保し、`append`による再アロケーションをゼロにする。 + - **`[]byte`スタックへの"対応する閉じ括弧"の直接格納**(競技版で採用):開き括弧が来た時点で、対応する閉じ括弧そのものをスタックに積んでおけば、閉じ括弧が来たときに`switch`や関数呼び出しを介さず、スライスの末尾の値と直接比較するだけで済みます。これにより分岐コストを1段階減らせます。 + - **バイト単位アクセス**:`for range`によるrune変換を避け、`for i := 0; i < len(s); i++`でバイト単位アクセスすることで、括弧記号(すべてASCII=1バイト文字)の走査コストを最小化します。 +- **トレードオフ**:業務版は`error`型を介するため型変換や構造体生成のわずかなオーバーヘッドがありますが、可読性・拡張性が高くなります。競技版はエラー情報を持たず`bool`一発勝負にすることで、コードがシンプルかつ最速になります。 + +> 📖 **このセクションで登場した用語** +> +> - **イディオム**:その言語で「自然」「慣用的」とされる書き方のパターン。 +> - **トレードオフ**:何かを得ると何かを失う関係。例:可読性を得ると若干の実行コストが増える。 +> - **キャッシュ効率**:CPUがデータをキャッシュから素早く読める度合い。連続したメモリ(スライス)はキャッシュに優しい。 + +--- + +# 4. 実装パターン + +> 💡 **初学者向け補足**:コード全体を示す前に、「このコードの大まかな構造(骨格)」を箇条書きで示します。 +> +> 1. 括弧の不正な組み合わせを表現する**カスタムエラー型**(`BracketError`)を定義する(Goの`error`インターフェースは「`Error() string`メソッドを持つ型」であれば何でもよく、`implements`宣言は不要) +> 2. 内部関数`validateBrackets`で`[]byte`スタックを使い、`error`を返す形で検証ロジックを実装する +> 3. LeetCode提出用の`isValid`は、その結果を`== nil`で`bool`に変換して返す + +### 使い分けの解説 + +``` +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。 +「なぜ不正なのか」をerror型として保持できるため、 +ログ出力やAPIのエラーレスポンスに転用しやすい設計です。 + +【競技プログラミング版を使う場面】 +LeetCodeの制限時間内に正解を出すことが目的のコードに向きます。 +エラーの詳細情報を持たず、開き括弧が来た時点で +対応する閉じ括弧そのものをスタックに積むことで、 +比較コストを1段階減らし実行速度を優先します。 +``` + +### 業務開発版 + +```go +package main + +import "fmt" + +// BracketError は括弧の対応関係が不正だったことを表すエラー型。 +// なぜ独自のエラー型を定義するか: +// bool の true/false だけでは「なぜ」「どこで」不正だったかを表現できない。 +// Go では例外(try-catch)を使わず、エラーを戻り値として返す設計が基本のため、 +// 型としてエラーの意味を持たせておくと呼び出し元での分岐がしやすくなる。 +type BracketError struct { + Reason string // 不正になった理由の説明文 + Char byte // 問題の原因となった文字 +} + +// Error は error インターフェースを満たすためのメソッド。 +// Go のインターフェースは「このメソッドを持っていれば自動的に実装済み」と +// みなされる暗黙的な実装であり、Java の implements 宣言は不要。 +// この Error() メソッドを持つことで BracketError は error 型として扱える。 +func (e *BracketError) Error() string { + // %q は文字を「'x'」の形でクォート付き表示するフォーマット指定子。 + return fmt.Sprintf("%s: %q", e.Reason, e.Char) +} + +// closingFor は開き括弧に対応する閉じ括弧を返すヘルパー関数。 +// map[byte]byte ではなく switch を使う理由: +// この問題では対応関係が3種類しかなく、switch の方がマップ生成・ +// ハッシュ計算のコストがかからず高速かつシンプルになるため。 +func closingFor(open byte) (closing byte, ok bool) { + switch open { + case '(': + return ')', true + case '[': + return ']', true + case '{': + return '}', true + default: + // ok=false を返すことで「開き括弧ではなかった」ことを呼び出し元に伝える。 + // null を使う言語と違い、Go では複数戻り値の第2引数として + // 「成功したかどうか」を明示的に返すのが慣習(comma-ok イディオム)。 + return 0, false + } +} + +// validateBrackets は文字列が正しく対応した括弧かどうかを検証する純粋関数。 +// error を戻り値にすることで「なぜ失敗したか」を呼び出し元に伝えられる。 +// +// Args: +// - s: 検証対象の文字列(括弧文字のみを想定) +// +// Returns: +// - 正しく対応していれば nil、そうでなければ *BracketError +// +// Time Complexity: O(n) +// Space Complexity: O(n) +func validateBrackets(s string) error { + // []byte をスタック(後入れ先出し構造)として使う。 + // make の第3引数で len(s) を指定し、最悪ケース(全て開き括弧)でも + // append による再アロケーション(メモリの取り直し)が発生しないようにする。 + stack := make([]byte, 0, len(s)) + + // 括弧記号はすべてASCII文字(1バイト)なので、 + // for range によるrune(Unicode文字単位)変換を避け、 + // バイト単位のインデックスアクセスで直接走査する方が高速。 + for i := 0; i < len(s); i++ { + c := s[i] + + switch c { + case '(', '[', '{': + // 開き括弧なら、対応する閉じ括弧が来るまでスタックに積んでおく。 + stack = append(stack, c) + + case ')', ']', '}': + // スタックが空なのに閉じ括弧が来た場合、 + // 対応する開き括弧が存在しないということなので不正。 + if len(stack) == 0 { + return &BracketError{Reason: "対応する開き括弧がない", Char: c} + } + + // スタックの末尾(=直前に積んだ開き括弧)を取り出す(pop操作)。 + // スライスの末尾を読んでから [:len-1] で縮めるのがGoでの慣用的なpop。 + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + // top(開き括弧)に対応する期待される閉じ括弧を求める。 + // stack に積まれるのは開き括弧のみなので ok は必ず true になる。 + expected, _ := closingFor(top) + if expected != c { + return &BracketError{ + Reason: fmt.Sprintf("'%c' に対応する閉じ括弧は '%c' ではない", top, expected), + Char: c, + } + } + // 一致していれば、このペアは正しく閉じられたので次のループへ進む。 + + default: + // 制約により括弧文字以外は入力されない前提だが、 + // 防御的プログラミングとしてエラーを返す(パニックさせない)。 + return &BracketError{Reason: "括弧以外の文字が含まれている", Char: c} + } + } + + // 走査し終えた時点でスタックに何か残っていれば、 + // 閉じられていない開き括弧があるということなので不正。 + if len(stack) != 0 { + return &BracketError{Reason: "閉じられていない開き括弧が残っている", Char: stack[len(stack)-1]} + } + + return nil +} + +// isValid は LeetCode 提出用のエントリポイント(業務開発版)。 +// 内部的には error を使って詳細な検証を行い、その結果を bool に変換して返す。 +// この「薄いラッパー」構造により、内部ロジックとLeetCodeのインターフェース要求を分離できる。 +func isValid(s string) bool { + return validateBrackets(s) == nil +} +``` + +### 競技プログラミング版 + +```go +// isValid は LeetCode 提出用のエントリポイント(競技プログラミング版)。 +// エラーの詳細情報を持たず bool のみを返すことで、分岐コストと +// 構造体生成のオーバーヘッドを排除し、実行速度を最優先している。 +// +// Time Complexity: O(n) +// Space Complexity: O(n) +func isValid(s string) bool { + // []byte をスタックとして使う。プリアロケーションで再アロケーションを防ぐ。 + stack := make([]byte, 0, len(s)) + + for i := 0; i < len(s); i++ { + switch s[i] { + case '(': + // 開き括弧が来た時点で「対応する閉じ括弧そのもの」を積んでおく。 + // これにより閉じ括弧が来たときに、closingFor のような + // 変換関数を介さず、直接バイト同士を比較できるようになる。 + stack = append(stack, ')') + case '[': + stack = append(stack, ']') + case '{': + stack = append(stack, '}') + default: + // ここに来るのは閉じ括弧のはず(制約上、括弧以外の文字はない)。 + // スタックが空、または末尾の期待値と一致しなければ即座に false。 + if len(stack) == 0 || stack[len(stack)-1] != s[i] { + return false + } + // 一致していればpop(スライスを1つ縮める)して次に進む。 + stack = stack[:len(stack)-1] + } + } + + // 全て閉じられていれば、スタックは空になっているはず。 + return len(stack) == 0 +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> 業務開発版で入力例 `s = "([)]"`(要件2に違反する不正な例)をトレースします。 +> +> ``` +> 初期状態: stack = [] +> i=0, c='(' → 開き括弧 → push → stack = ['('] +> i=1, c='[' → 開き括弧 → push → stack = ['(', '['] +> i=2, c=')' → 閉じ括弧。pop() → top='[' 、stack = ['('] +> closingFor('[') = ']' +> expected=']' だが c=')' → 不一致! +> → &BracketError{Reason: "'[' に対応する閉じ括弧は ']' ではない", Char: ')'} を返す +> isValid の戻り値: validateBrackets(...) != nil → false ✅(期待通り) +> ``` +> +> 競技版で同じ入力をトレースすると、より単純です。 +> +> ``` +> 初期状態: stack = [] +> i=0, s[0]='(' → stack = [')'] ← 対応する閉じ括弧そのものを積む +> i=1, s[1]='[' → stack = [')', ']'] +> i=2, s[2]=')' → default節。stack末尾=']' と s[2]=')' を比較 → 不一致 → return false ✅ +> ``` + +> 📖 **このセクションで登場した用語** +> +> - **comma-okイディオム**:`value, ok := ...`の形で「成功したかどうか」を第2戻り値として受け取るGoの慣習。マップの検索(`v, ok := m[key]`)などでも使われる。 +> - **`switch`文**:Goの分岐構文。`case`の後に`break`を書かなくても自動的に次のcaseへ流れ落ちない(他言語のfallthroughとは逆の挙動)。 +> - **`fmt.Sprintf`**:フォーマット文字列から新しい`string`を生成する関数。`%c`は文字、`%q`はクォート付き文字列を意味するフォーマット指定子。 +> - **薄いラッパー**:内部の複雑なロジックを、外部から使いやすいシンプルなインターフェースで包む設計パターン。 + +--- + +# 5. 検証 + +_テストコードは別途提供いただく前提のため、ここではエッジケースの説明のみ行います。_ + +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など境界的な入力のことです。 + +- **境界値テスト**: + - **空文字列の場合**:制約上`1 <= s.length`なので発生しませんが、防御的に`len(stack) == 0`のチェックがあるため`isValid("")`を呼んでも`true`が返り、パニックは起きません。 + - **単一の閉じ括弧(例:`")"`)の場合**:`default`節でスタックが空の状態で比較しようとするため、`len(stack) == 0`の短絡評価(=`||`の左側が`true`なら右側を評価せず即座に判定する仕組み)によって、`stack[len(stack)-1]`への範囲外アクセス(インデックスエラーによるパニック)が起きる前に安全に`false`を返します。ここが「安全性」の要です。 + - **未閉じの開き括弧(例:`"((("`)の場合**:ループ終了後も`stack`に要素が残るため、`len(stack) == 0`のチェックで`false`と判定されます。 + - **最大長10^4文字の場合**:`make([]byte, 0, len(s))`でプリアロケーション済みのため、途中の`append`で再アロケーションが発生せず、O(n)の性能が保証されます。 +- **型チェック**:`go vet ./...`では、未使用変数・到達不能コードなどが検出されますが、上記コードは`switch`の`default`節まで含めて全パターンを網羅しているため問題ありません。 +- **並行安全性**:この関数はゴルーチンを一切使わない逐次処理のため、データ競合(Race Condition)の心配はありません。 + +## 提出前チェックリスト + +- [x] `go vet ./...` でエラーなし(未使用変数・不正なフォーマット指定子なし) +- [x] エラーハンドリング:業務版は`error`戻り値を必ず`nil`チェックしている +- [x] メモリ効率:`make([]byte, 0, len(s))`でプリアロケーション済み +- [x] エッジケース:空文字列・単一閉じ括弧・未閉じ括弧を確認済み +- [x] 命名規則:レシーバなし関数のためGoの関数命名規則(`isValid`は小文字始まり、LeetCodeの慣習に合わせている)に準拠 + +> 📖 **このセクションで登場した用語** +> +> - **短絡評価**:`||`や`&&`で、左側の条件だけで結果が確定する場合に右側を評価しない仕組み。`len(stack) == 0 || stack[...]`のように、範囲外アクセスを未然に防ぐ安全策として使われる。 +> - **範囲外アクセス(out of range)**:スライスや配列の存在しないインデックスにアクセスしようとして起きるパニック。Goでは境界チェックが自動で入るため、C言語のような未定義動作にはならず、必ずパニックという形で安全に検出される。 diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md new file mode 100644 index 00000000..c68591fc --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md @@ -0,0 +1,278 @@ +# Java(OpenJDK 21)での解答 - Valid Parentheses(LeetCode 20) + +```java +class Solution { + public boolean isValid(String s) { + + } +} +``` + +--- + +## 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「**開き括弧と閉じ括弧の組み合わせと順番が正しいかどうかを判定する問題**」です。これまでのRust / Go / TypeScript / Python と全く同じアルゴリズム(スタックを使った1回走査)で解けます。 + +Javaでこの問題を解く際に特に気をつけるべき点は、**「`char`(プリミティブ型)と`Character`(ラッパークラス)のボクシング/アンボクシング(=プリミティブ型とオブジェクト型を自動的に変換する仕組み)のコスト」**と、**「スタックとしてどのクラスを選ぶか」**の2点です。Javaの`java.util.Stack`は歴史的な理由から`Vector`を継承しており、すべての操作が同期化(`synchronized`)されているため、単一スレッドで使う場合には不要なオーバーヘッドがあります。現代的なJavaでは、スタックとして`ArrayDeque`(両端キュー、=前からも後ろからも出し入れできるデータ構造)を使うのが慣習です。 + +- **競技プログラミング視点での分析** + - 実行速度を最優先とした場合、文字列を1回だけ走査するO(n)のアプローチが最速です。 + - メモリ使用量の最小化方針としては、`ArrayDeque`のようにオブジェクト(ラッパークラス`Character`)をスタックに積む方式ではなく、プリミティブ型の`char[]`配列を手動でスタックとして使う方が、ボクシング/アンボクシングのコストとオブジェクト生成のオーバーヘッドを完全に排除できます。 +- **業務開発視点での分析** + - 型安全性・保守性を重視するなら、`Stack`クラスの代わりに`Deque`インターフェース型で変数を宣言し、実装として`ArrayDeque`を使うのがJavaの慣習(インターフェースに対してプログラミングする、という原則)です。 + - `isValid`自体はLeetCode形式で`boolean`を返す必要がありますが、内部的には「なぜ不正なのか」を表現できる独自の例外クラス(`BracketMismatchException`)を用意しておくと、将来ログ出力やAPIのエラーレスポンスへの拡張がしやすくなります。 +- **Java特有の考慮点** + - **ボクシング/アンボクシング**:`char`と`Character`を比較する際、片方がプリミティブ型であれば自動的にアンボクシング(オブジェクトから値を取り出すこと)されて値同士の比較になります。これは`Character`同士を`==`で比較する場合の落とし穴(後述)を避けるための重要なポイントです。 + - **例外設計**:Javaには`RuntimeException`(実行時例外、=呼び出し元にキャッチを強制しない)と`Exception`(チェック例外、=呼び出し元に`try-catch`または`throws`宣言を強制する)の2種類があります。本問題のような「呼び出し側が毎回チェックする必要はないが、内部処理では明確にエラーを区別したい」という場面では`RuntimeException`を継承するのが自然です。 + +> 📖 **このセクションで登場した用語** +> +> - **ボクシング/アンボクシング**:`char`(プリミティブ型)と`Character`(ラッパークラス)のように、プリミティブ型とオブジェクト型の間で自動的に変換が行われる仕組み。オブジェクト生成が伴うため、大量に発生すると性能に影響する +> - **`Deque`(デック)**:前からも後ろからも要素を出し入れできるデータ構造を表すインターフェース。「両端開きの箱」のようなイメージで、スタックとしてもキューとしても使える +> - **`RuntimeException`(実行時例外)**:呼び出し元に`try-catch`を強制しない例外。プログラムのバグに近いエラーや、呼び出し側が個別に処理しなくても良いエラーに使う +> - **チェック例外(Checked Exception)**:呼び出し元に`try-catch`または`throws`宣言を強制する例外。`IOException`などが該当する + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:なぜ複数のアプローチを比較するのかというと、「動くコード」と「その状況で最善のコード」は別物だからです。特にこの問題では、**一見シンプルに見えて実は不正解になるアプローチ**があるため、比較を通じて理解を深めます。 + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) +> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループや繰り返しスキャンに多い) + +| アプローチ | 時間計算量 | 空間計算量 | Java実装コスト | 可読性 | 標準ライブラリ活用 | 備考 | +| ---------- | ---------- | ---------- | --------------- | ------ | ------------------ | ---- | +| 方法A | O(n) | O(n) | 低 | ★★★ | `Deque` / `char[]` | スタック法(採用) | +| 方法B | O(n) | O(1) | 低 | ★★☆ | なし | 括弧の**種類ごとの出現数**を数えるだけ(誤ったアプローチ) | +| 方法C | O(n²) | O(n) | 中 | ★★☆ | `String.replace` | `"()" "[]" "{}"` を空文字に繰り返し置換 | + +- **方法A**:`Deque`または`char[]`をスタックとして使い、文字列を1回だけ走査します。`ArrayDeque`を使う場合は要素の`push`/`pop`のたびに`Character`オブジェクトのボクシング/アンボクシングが発生しますが、`char[]`を手動で扱えばこれを回避できます。 +- **方法B**:括弧の「数」だけを数えれば空間計算量O(1)で済みますが、括弧が閉じられる**順序**を一切見ていないため、`"([)]"`のような不正な入力を`true`と誤判定してしまいます。速さより先に「問題の要件を満たすこと」が必須です。 +- **方法C**:`String.replace()`はJavaの`String`がイミュータブル(=一度作られたら中身を変更できない性質)であるため、呼び出すたびに新しい`String`オブジェクトを生成します。これを空になるまで繰り返すため最悪O(n²)になり、ガベージコレクション(不要になったオブジェクトのメモリを自動で回収する仕組み)の負荷も増えます。 + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **イミュータブル**:一度作られたら中身を変更できない性質。Javaの`String`はこの性質を持つ + +--- + +## 3. 選択したアルゴリズムと理由 + +> 💡 **初学者向け補足**:「なぜ方法Aを選び、方法B・Cを選ばなかったのか」を対比形式で説明します。 + +- **選択したアプローチ**: 方法A(スタックを使った線形走査) +- **理由**: + - **方法Bを選ばなかった理由**:括弧の出現数だけを数える方法は空間計算量こそO(1)ですが、順序を検証できないため`"([)]"`のような不正な入力を見逃してしまいます。 + - **方法Cを選ばなかった理由**:`String.replace()`を繰り返すたびに新しい`String`オブジェクトが生成され、最悪O(n²)になります。 + - **方法Aを選んだ理由**:文字列を1回だけ走査するO(n)で完結し、Javaの標準的なデータ構造(`Deque`または配列)で安全に実装できます。 +- **Java特有の最適化ポイント**: + - **`ArrayDeque` vs `java.util.Stack`**:`java.util.Stack`は`Vector`を継承しているため全メソッドが`synchronized`(同期化)されており、単一スレッドでの利用には不要なロックのオーバーヘッドがあります。現代的なJavaでは`Deque`インターフェースと`ArrayDeque`実装を使うのが推奨されています。 + - **ボクシングの回避(競技版)**:`char[]`配列と`int`型のインデックスを手動で管理することで、`Character`オブジェクトの生成・GC対象化を完全に避けられます。LeetCodeのような繰り返し実行される環境では、この差が積み重なって実行速度に影響します。 + - **`Map.of()`によるイミュータブルな対応表**:Java 9以降で使える`Map.of(...)`は変更不可能な(イミュータブルな)Mapを生成するため、うっかり対応表を書き換えてしまう事故をコンパイル時ではなく実行時に検出できます(書き換えようとすると`UnsupportedOperationException`が発生します)。 + +> 📖 **このセクションで登場した用語** +> +> - **`synchronized`**:複数スレッドから同時にアクセスされても安全なように、処理を1つのスレッドずつ順番に実行させる仕組み。ロックの取得・解放にオーバーヘッドが伴う +> - **`Map.of()`**:Java 9以降で使える、変更不可能な(イミュータブルな)Mapを簡潔に生成するための静的メソッド +> - **GC(ガベージコレクション)**:使い終わったオブジェクトのメモリを自動で回収する仕組み。オブジェクト生成が多いほど負荷が増える + +--- + +## 4. 実装コード + +> 💡 **初学者向け補足**:コード全体を示す前に、「このコードの大まかな構造(骨格)」を箇条書きで示します。 +> +> 1. 括弧の不正な組み合わせを表現する**カスタム例外クラス**(`BracketMismatchException`)を定義する(`RuntimeException`を継承) +> 2. 開き括弧 → 対応する閉じ括弧、の対応表を`Map.of()`でイミュータブルに定義する +> 3. 業務開発版では`validateBrackets`が例外を投げる形で検証し、`isValid`はそれを`try-catch`で捕まえて`boolean`に変換する +> 4. 競技プログラミング版では例外を使わず、`char[]`配列と`switch`文だけで最短コードにする + +### 業務開発版 + +```java +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; + +class Solution { + + /** + * 括弧の対応関係・順序が不正な場合に投げる例外。 + * RuntimeException(実行時例外)を継承することで、呼び出し元に + * try-catchや throws 宣言を強制しない設計にしている。 + * boolean の true/false だけでは「なぜ」不正だったのかを表現できないため、 + * このクラスに理由をメッセージとして持たせる。 + */ + static class BracketMismatchException extends RuntimeException { + BracketMismatchException(String message) { + super(message); + } + } + + // 開き括弧 → 対応する閉じ括弧、の対応表。 + // Map.of() で作るイミュータブルな Map は、うっかり書き換えてしまう事故を + // 実行時エラー(UnsupportedOperationException)として検出できる。 + private static final Map PAIRS = Map.of( + '(', ')', + '[', ']', + '{', '}' + ); + + public boolean isValid(String s) { + if (s == null) { + // Javaは null を許容する言語のため、防御的にチェックする。 + throw new IllegalArgumentException("Input must not be null"); + } + try { + validateBrackets(s); + } catch (BracketMismatchException e) { + // 詳細なエラー情報は例外オブジェクトの中にあるが、 + // LeetCode形式の戻り値は boolean 固定なのでここでは false に変換する。 + return false; + } + return true; + } + + /** + * スタックを使って括弧の対応関係・順序を検証する。 + * 検証に成功した場合は何も返さず正常終了し、 + * 失敗した場合は BracketMismatchException を投げる。 + */ + private void validateBrackets(String s) { + // ArrayDeque を「後入れ先出し」のスタックとして使う。 + // java.util.Stack は Vector を継承しており全メソッドが synchronized + // (同期化)されているため、単一スレッドでは ArrayDeque の方が高速。 + Deque stack = new ArrayDeque<>(); + + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (PAIRS.containsKey(c)) { + // 開き括弧なら、対応する閉じ括弧を積んでおく。 + stack.push(PAIRS.get(c)); + } else { + // 閉じ括弧のはず。スタックが空なら対応する開き括弧が無い。 + if (stack.isEmpty()) { + throw new BracketMismatchException( + "対応する開き括弧がないまま閉じ括弧 '" + c + "' が出現しました(位置 " + i + ")"); + } + // stack.pop() は Character を返す。char である c と比較する際、 + // 片方がプリミティブ型なので Java は自動的にもう片方を + // アンボクシングしてから値を比較する(安全に値比較できる)。 + char top = stack.pop(); + if (top != c) { + throw new BracketMismatchException( + "'" + c + "' に対応する開き括弧が見つかりません(位置 " + i + ")"); + } + } + } + + if (!stack.isEmpty()) { + throw new BracketMismatchException("閉じられていない開き括弧が残っています"); + } + } +} +``` + +### 競技プログラミング版 + +```java +class Solution { + public boolean isValid(String s) { + int n = s.length(); + // char[] を手動スタックとして使う。ArrayDeque と違い、 + // ボクシング/アンボクシングのコストが一切発生しないため、 + // 大量の呼び出しが発生する競技プログラミングではこちらが高速。 + char[] stack = new char[n]; + int top = 0; // スタックの次に書き込む位置(=現在の要素数) + + for (int i = 0; i < n; i++) { + char c = s.charAt(i); + switch (c) { + case '(': + stack[top++] = ')'; + break; + case '[': + stack[top++] = ']'; + break; + case '{': + stack[top++] = '}'; + break; + default: + // 閉じ括弧のはず。top == 0 ならスタックが空。 + // stack[--top] は先に top をデクリメントしてから要素を読む + // (つまり pop 相当の操作を1行で行っている)。 + if (top == 0 || stack[--top] != c) { + return false; + } + } + } + + // 全て閉じられていれば top は 0 のはず。 + return top == 0; + } +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> ``` +> 入力: s = "([)]" +> i=0, c='(' → push(')') → stack=[')'] +> i=1, c='[' → push(']') → stack=[')', ']'] +> i=2, c=')' → 閉じ括弧 → pop() → top=']' +> ']' != ')' → 不一致 → return false +> ``` + +> 💡 **ボクシング/アンボクシングを理解する**(初学者向け) +> +> **基礎**:Javaのプリミティブ型(`char`, `int`など)は、コレクション(`Deque`など)に直接格納できません。コレクションはオブジェクトしか扱えないため、`char`を`Character`という「箱」に入れる(ボクシング)必要があります。 +> ```java +> Character boxed = 'a'; // 自動的にボクシングされる +> char primitive = boxed; // 自動的にアンボクシングされる +> ``` +> +> **応用**:注意が必要なのは、`Character`同士を`==`で比較する場合です。Javaは`-128`〜`127`の範囲の値をキャッシュして使い回すため、この範囲内では`==`がたまたま`true`になりますが、範囲外では異なるオブジェクトとして扱われ`false`になることがあります。 +> ```java +> Character a = 200; +> Character b = 200; +> System.out.println(a == b); // false になりうる(キャッシュ範囲外) +> System.out.println(a.equals(b)); // true(値の比較) +> ``` +> +> **この問題での使い方**:業務開発版のコードでは`char top = stack.pop();`のように、`pop()`の戻り値(`Character`)を`char`(プリミティブ型)の変数に代入しています。この代入時点でアンボクシングされるため、その後の`top != c`は`Character`同士の参照比較ではなく、確実に値同士の比較になります。この一手間により、括弧記号がキャッシュ範囲内かどうかを気にする必要がなくなります。 + +> 📖 **このセクションで登場した用語** +> +> - **`ArrayDeque`**:配列を内部実装に使った`Deque`の実装クラス。スタックとしてもキューとしても使え、`java.util.Stack`より高速 +> - **`Map.of()`**:Java 9以降で使える、変更不可能なMapを生成する静的メソッド +> - **アンボクシング**:`Character`のようなラッパークラスのオブジェクトから、`char`のようなプリミティブ型の値を取り出す操作 +> - **`switch`文**:値に応じて処理を分岐させるJavaの構文。`case`ラベルの後に`break`を書かないと、次の`case`に処理が「フォールスルー」(流れ落ちる)する点に注意が必要 + +--- + +## 5. 検証 + +_テストコードは不要です!具体的な計測値、メモリの出力も不要です!_ + +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など境界的な入力のことです。 + +- **境界値テスト**: + - **単一の閉じ括弧(例:`")"`)の場合**:業務開発版では`stack.isEmpty()`のチェックで`BracketMismatchException`が送出されます。競技版では`top == 0`を先に確認する短絡評価(=`||`の左側がtrueなら右側を評価しない仕組み)により、`stack[--top]`への不正なアクセスを未然に回避しています。もしこのチェックを省略すると、`ArrayIndexOutOfBoundsException`(配列の範囲外アクセスによる実行時例外)が発生します。 + - **未閉じの開き括弧(例:`"((("`)の場合**:走査後もスタック(または`top`)に要素が残るため、業務開発版では`if (!stack.isEmpty())`のチェックで例外が送出され、競技版では`return top == 0`が`false`になります。 + - **括弧の種類の不一致(例:`"([)]"`)の場合**:上記のトレースで示した通り、`pop()`した値と実際の文字が一致しないため`false`になります。 + - **最大長10^4文字の場合**:`char[] stack = new char[n];`のように、あらかじめ最悪ケース(全て開き括弧)を見越した固定長配列を確保しているため、途中でのメモリ再確保が一切発生せずO(n)の性能が保証されます。 +- **null安全性**:Javaは`null`を許容する言語であるため、業務開発版では`isValid`の冒頭で`s == null`のチェックを行い、`IllegalArgumentException`(不正な引数に対する標準的な例外)を送出しています。LeetCodeの実行環境では`null`が渡されることは想定されていませんが、実務のコードとして再利用する場合にはこの防御が重要になります。 + +> 📖 **このセクションで登場した用語** +> +> - **`ArrayIndexOutOfBoundsException`**:配列の範囲外のインデックスにアクセスしようとしたときに発生する実行時例外 +> - **`IllegalArgumentException`**:メソッドに渡された引数が不正である場合に投げる、Java標準の実行時例外 +> - **短絡評価**:`||`や`&&`で、左側の条件だけで結果が確定する場合に右側を評価しない仕組み。範囲外アクセスを未然に防ぐ安全策として使われる diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Python.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Python.md new file mode 100644 index 00000000..41d3f286 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Python.md @@ -0,0 +1,348 @@ +# 1. 問題分析結果 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「**開き括弧と閉じ括弧の"組み合わせ"と"順番"が正しいかを、後入れ先出し(=最後に入れたものを最初に取り出す)のルールで判定する問題**」です。 +> +> Pythonで解く際に特に気をつけるべきCPython(=最も広く使われるPythonの実装。C言語で作られており、挙動や速度に独自の特性がある)特有の注意点は、**「`list`をスタックとして使う場合の末尾操作の計算量」**と、**「辞書(`dict`)を使った対応表引きがO(1)であることの活用」**の2点です。Pythonの`list`は末尾への追加・削除(`append`/`pop`)がO(1)ですが、これは内部的に「余裕を持った容量」を確保しているためであり、先頭操作(`insert(0, x)`や`pop(0)`)は全要素をずらす必要がありO(n)になります。今回はスタックとして末尾だけを使うため`list`で十分ですが、この違いを理解しておくことが重要です。 + +#### 競技プログラミング視点 + +- **制約分析**:文字列長は最大10^4文字(制約より)。この規模なら O(n) の線形走査を選べば十分高速に処理できます。 +- **最速手法**:`list`をスタックとして使い、辞書で「閉じ括弧 → 対応する開き括弧」の対応表を引くのが最も素直かつ高速です。競技版ではさらに、開き括弧を`in`演算子で判定するのではなく、辞書に存在するかどうかだけで開き/閉じを区別するテクニックを使います。 +- **メモリ最小化**:文字列は`str`のイテレーション(走査)自体がC実装で高速なため、余分なオブジェクト生成(例えば文字列を毎回スライスして新しい`str`を作るなど)を避けることが重要です。 +- **CPython最適化**:`dict`のキー検索はハッシュテーブル(=キーからハッシュ値という数値を計算し、その番号の場所に値を格納・検索するデータ構造)による平均O(1)のルックアップです。`if c in "([{"` のような文字列メンバーシップテストよりも、`dict`を1つ用意して`in`で判定する方が意図が明確で、かつ同程度以上に高速です。 + +#### 業務開発視点 + +- **型安全設計**:`isValid`自体はLeetCode形式で`bool`を返す必要がありますが、内部的には「なぜ不正なのか」を表現できる独自の例外クラス(`BracketMismatchError`)を定義しておくと、将来ログ出力やAPIのエラーレスポンスへの拡張がしやすくなります。 +- **エラーハンドリング**:Pythonは動的型付け言語(=変数の型が実行時に決まる言語。JavaやC++のような静的型付けと違い、コンパイル時の型チェックがない)なので、想定外の型が渡された場合に備えて`isinstance`による実行時チェックを行い、`pylance`(VSCodeの型チェッカー)による静的チェックと二重の防御を行います。 +- **可読性**:docstring(=関数やクラスの先頭に書く説明文)で計算量や例外条件を明示し、チームメンバーが後から読んでも意図が分かるようにします。 + +#### Python特有分析 + +- **データ構造選択**:「末尾への追加・削除が頻繁」という要件に対しては`list`で十分です。`collections.deque`(両端キュー。先頭操作もO(1)にできるデータ構造)は両端操作が必要な場合に有効ですが、今回は末尾操作しか使わないため`list`の方がシンプルでオーバーヘッドも小さくなります。 +- **標準ライブラリ活用度**:この問題では`heapq`(ヒープ操作用ライブラリ)や`bisect`(二分探索用ライブラリ)の出番はなく、`dict`と`list`という組み込み型だけで十分です。 +- **CPython最適化度**:`dict`のキー検索、`list.append`/`list.pop`はすべてC実装のため、Pure Python(=C拡張を使わない、素のPythonコードだけで書かれた実装)でループを書くよりも高速に動作します。 + +> 📖 **このセクションで登場した用語**(例) +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、組み込み関数の多くがC実装のため高速。 +> - **ハッシュテーブル**:キーからハッシュ値という数値を計算し、その番号の場所に値を格納・検索するデータ構造。`dict`の内部実装に使われており、平均O(1)で検索できる。 +> - **動的型付け**:変数の型が実行時に決まる言語仕様。Pythonはこれに該当し、コンパイル時の型チェックがない。 +> - **Pure Python**:C拡張を使わない、素のPythonコードだけで書かれた実装。C実装の組み込み関数より一般に低速。 + +--- + +# 2. 採用アルゴリズムと根拠 + +> 💡 **初学者向け補足**:なぜ複数のアプローチを比較するのかというと、「動くコード」と「その状況で最善のコード」は別物だからです。特にこの問題では、**一見シンプルに見えて実は不正解になるアプローチ**があるため、比較を通じて理解を深めます。 + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) +> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループや繰り返しスキャンに多い) + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ---------- | ---------- | ---------- | ----------------- | ------ | ------------------ | -------------- | ---- | +| 方法A | O(n) | O(n) | 低 | ★★★ | なし(`dict`+`list`) | 適 | スタック法(採用) | +| 方法B | O(n) | O(1) | 低 | ★★☆ | `collections.Counter` | 不適 | 括弧の**出現数**を数えるだけ(誤ったアプローチ) | +| 方法C | O(n²) | O(n) | 中 | ★★☆ | `str.replace` | 不適 | `"()" "[]" "{}"`を空文字に繰り返し置換 | + +- **選択理由**: + - **方法Bを選ばなかった理由**:`Counter`(要素の出現回数を数える辞書のサブクラス)で括弧の数だけを数えれば空間計算量O(1)で済みますが、括弧が閉じられる**順序**を一切見ていないため、`"([)]"`のような不正な入力を`True`と誤判定してしまいます。速さより先に「問題の要件を満たすこと」が必須です。 + - **方法Cを選ばなかった理由**:`str.replace`はPythonの文字列がイミュータブル(=一度作られたら中身を変更できない性質)であるため、呼び出すたびに新しい文字列オブジェクトを生成します。これを空になるまで繰り返すため最悪O(n²)になり、ガベージコレクション(不要になったオブジェクトを自動で回収する仕組み)の負荷も増えます。 + - **方法Aを選んだ理由**:文字列を1回だけ走査するO(n)で完結し、`list.append`/`list.pop`・`dict`のキー検索はいずれもCPythonのC実装による高速な操作です。 +- **Python最適化戦略**: + - **辞書によるO(1)対応表引き**:`{')': '(', ']': '[', '}': '{'}` のような辞書を用意し、`if`/`elif`の連鎖ではなく辞書引きで対応関係を判定することで、コードが簡潔かつ高速になります。 + - **`dict.get()`のデフォルト値活用**:`stack.pop() if stack else '#'`のような書き方や、`pairs.get(c)`のようにデフォルト値を使うことで、`try-except`による例外処理コストを避けられます(Pythonの例外処理は「起きなければ軽い」が「頻発すると重い」特性があるため、通常フローでの例外多用は避けるのが定石です)。 +- **トレードオフ**:業務版は型ヒントと例外クラスによる明確なエラー表現のためわずかにコード量が増えますが、可読性・拡張性が向上します。競技版はエラー情報を持たず`bool`一発勝負にすることで、コードが最小かつ最速になります。 + +> 📖 **このセクションで登場した用語** +> +> - **`Counter`**:要素の出現回数を数える`dict`のサブクラス。`collections`モジュールに含まれる。 +> - **イミュータブル**:一度作られたら中身を変更できない性質。Pythonの`str`はこの性質を持つ。 +> - **ガベージコレクション**:不要になったオブジェクトのメモリを自動で回収する仕組み。頻繁に新しいオブジェクトを作ると負荷が増える。 +> - **トレードオフ**:何かを得ると何かを失う関係。速くするとメモリが増える、など。 + +--- + +# 3. 実装パターン + +> 💡 **初学者向け補足**:コード全体を示す前に、「このコードの大まかな構造(骨格)」を箇条書きで示します。 +> +> 1. 括弧の不正な組み合わせを表現する**カスタム例外クラス**(`BracketMismatchError`)を定義する +> 2. 閉じ括弧から対応する開き括弧を引く**対応表**(`dict`)をクラス定数として定義する +> 3. 業務開発版では`_validate_brackets`が例外を投げる形で検証ロジックを実装し、`isValid`はそれを`try-except`で捕まえて`bool`に変換する +> 4. 競技プログラミング版では例外を使わず、辞書引きと`list`操作だけで最短コードにする + +``` +【業務開発版を使う場面】 +チームで長期間メンテナンスするプロダクションコードに向きます。 +「なぜ不正なのか」を例外クラスとして保持できるため、 +ログ出力やAPIのエラーレスポンスに転用しやすい設計です。 + +【競技プログラミング版を使う場面】 +LeetCodeの制限時間内に正解を出すことが目的のコードに向きます。 +例外処理を使わず辞書引きと真偽値の直接返却だけで完結させ、 +可読性よりも実行速度・コードの短さを優先します。 +``` + +### 業務開発版 + +```python +from typing import Final + + +class BracketMismatchError(Exception): + """ + 括弧の対応関係・順序が不正な場合に送出する例外。 + + なぜ独自の例外クラスを定義するか: + bool の True/False だけでは「なぜ」「どこで」不正だったのかを表現できない。 + Exception を継承したクラスを作ることで、将来的にエラーメッセージを + ログ出力やAPIレスポンスに転用しやすくなる。 + """ + + def __init__(self, message: str, char: str, position: int) -> None: + # 親クラス(Exception)のコンストラクタを呼び出し、メッセージを渡す。 + super().__init__(message) + # 追加情報として、問題の文字と位置を保持しておく。 + # 呼び出し元がログに出力したり、UIで強調表示したりする際に使える。 + self.char = char + self.position = position + + +class Solution: + """ + 括弧の妥当性判定クラス(LeetCode: Valid Parentheses) + + 競技プログラミング向けと業務開発向けの2パターンを提供する。 + """ + + # クラス定数として対応表を定義する。 + # なぜ __init__ ではなくクラス変数にするか: + # このデータは全インスタンスで共通であり、インスタンスごとに + # 作り直す必要がないため、クラスレベルで1度だけ生成する方が効率的。 + # Final を付けることで「再代入されない」ことを pylance に伝え、 + # 誤って書き換えてしまうバグを型チェックの段階で防ぐ。 + _PAIRS: Final[dict[str, str]] = { + ')': '(', + ']': '[', + '}': '{', + } + + def isValid(self, s: str) -> bool: + """ + 括弧文字列が有効かどうかを判定する(業務開発版)。 + + 内部的には _validate_brackets が詳細な検証を行い、 + 例外が送出されなければ True、送出されれば False を返す + 「薄いラッパー」として実装している。 + + Args: + s: 判定対象の文字列('(' ')' '[' ']' '{' '}' のみを含む想定) + + Returns: + 有効なら True、そうでなければ False + + Complexity: + Time: O(n) + Space: O(n) + """ + # isinstance で実行時の型チェックを行う。 + # Python は動的型付け言語のため、pylance の静的チェックをすり抜けて + # 誤った型が実行時に渡される可能性がある。二重の防御として有効。 + if not isinstance(s, str): + raise TypeError("Input must be a string") + + try: + self._validate_brackets(s) + except BracketMismatchError: + # 詳細なエラー情報は _validate_brackets の例外に含まれているが、 + # LeetCode形式の戻り値は bool 固定なので、ここでは False に変換する。 + # 実運用では logging.warning(str(e)) のようにログへ残すことが多い。 + return False + return True + + def _validate_brackets(self, s: str) -> None: + """ + 括弧の対応関係・順序を検証する内部メソッド。 + + 検証に成功した場合は何も返さず正常終了し、 + 失敗した場合は BracketMismatchError を送出する。 + 戻り値ではなく例外でエラーを表現するのは、Pythonでは + 「エラーは例外で表現する(EAFP: Easier to Ask Forgiveness than Permission)」 + という考え方が伝統的に使われてきたため。 + Rust の Result 型や Go の (value, error) とは異なるアプローチだが、 + 「呼び出し元が失敗ケースを無視できない」という目的は共通している。 + + Args: + s: 検証対象の文字列 + + Raises: + BracketMismatchError: 対応関係・順序が不正な場合 + """ + # list をスタック(後入れ先出し構造)として使う。 + # append/pop はどちらも末尾操作でO(1)(CPythonのlistは末尾に + # 余裕を持たせて確保しているため、末尾操作は平均的に高速)。 + stack: list[str] = [] + + # enumerate() でインデックスと文字を同時に取得する。 + # range(len(s)) でインデックスアクセスするより Pythonic(=Python的で慣用的)で、 + # C実装のイテレータを使うため高速。 + for position, char in enumerate(s): + if char in '([{': + # 開き括弧なら、対応する閉じ括弧が来るまでスタックに積んでおく。 + stack.append(char) + continue + + if char in self._PAIRS: + # 閉じ括弧の場合。まずスタックが空でないか確認する。 + # 空の場合、対応する開き括弧が存在しないということなので不正。 + if not stack: + raise BracketMismatchError( + "対応する開き括弧がないまま閉じ括弧が出現しました", + char, + position, + ) + + # スタックの末尾(=直前に積んだ開き括弧)を取り出す(pop操作)。 + top = stack.pop() + + # _PAIRS[char] で「この閉じ括弧に対応するはずの開き括弧」を引く。 + # dict のキー検索は平均O(1)(ハッシュテーブルによる高速アクセス)。 + expected_open = self._PAIRS[char] + + if top != expected_open: + raise BracketMismatchError( + f"'{char}' に対応する開き括弧は '{expected_open}' ですが " + f"'{top}' が見つかりました", + char, + position, + ) + # 一致していれば、このペアは正しく閉じられたので次のループへ進む。 + continue + + # 制約により括弧文字以外は入力されない前提だが、 + # 防御的プログラミング(想定外の入力にも安全に対応する設計方針)として + # 明示的に例外を送出し、意図しない挙動を防ぐ。 + raise BracketMismatchError( + "括弧以外の文字が含まれています", char, position + ) + + # 走査し終えた時点でスタックに何か残っていれば、 + # 閉じられていない開き括弧があるということなので不正。 + if stack: + unclosed = stack[-1] + raise BracketMismatchError( + f"開き括弧 '{unclosed}' が閉じられないまま入力が終了しました", + unclosed, + len(s), + ) +``` + +### 競技プログラミング版 + +```python +class Solution: + def isValid(self, s: str) -> bool: + """ + 括弧文字列が有効かどうかを判定する(競技プログラミング版)。 + + 例外処理・型チェックを省略し、辞書引きと list 操作だけで + 最短かつ最速の実装にしている。 + + Complexity: + Time: O(n) + Space: O(n) + """ + # 閉じ括弧 → 対応する開き括弧、の対応表。 + # 関数(メソッド)呼び出しのたびに辞書を作り直すのは無駄に見えるが、 + # リテラル辞書の生成はCPythonでは十分高速であり、 + # 可読性を優先してここに書いている(さらに最適化するならクラス変数化する)。 + pairs = {')': '(', ']': '[', '}': '{'} + + # list をスタックとして使う。 + stack: list[str] = [] + + for char in s: + if char in pairs: + # 閉じ括弧の場合: + # stack.pop() if stack else '#' というイディオムで、 + # 「スタックが空なら番兵文字 '#' を使う」ことで、 + # 空リストに対する pop() の例外(IndexError)を未然に回避する。 + # '#' は括弧記号に絶対に一致しないダミー値として機能する。 + if pairs[char] != (stack.pop() if stack else '#'): + return False + else: + # 開き括弧(または制約上あり得ないが未知の文字)の場合、 + # そのままスタックに積む。 + stack.append(char) + + # 全て閉じられていれば、スタックは空になっているはず。 + # not stack は「スタックが空かどうか」を確認する Pythonic な書き方。 + return not stack +``` + +> 💡 **コードの動作トレース**(初学者向け) +> 業務開発版で入力例 `s = "([)]"`(要件2に違反する不正な例)をトレースします。 +> +> ``` +> 初期状態: stack = [] +> position=0, char='(' → '(' in '([{' → True → append → stack=['('] +> position=1, char='[' → '[' in '([{' → True → append → stack=['(', '['] +> position=2, char=')' → ')' in _PAIRS → True +> stack.pop() → top='['、stack=['('] +> _PAIRS[')'] = '(' +> expected_open='(' だが top='[' → 不一致! +> → BracketMismatchError("'[' に対応する開き括弧は... ") を送出 +> isValid の戻り値: except節で捕捉 → False ✅(期待通り) +> ``` +> +> 競技版で同じ入力をトレースすると、より簡潔です。 +> +> ``` +> 初期状態: stack = [] +> char='(' → pairs に無い → append → stack=['('] +> char='[' → pairs に無い → append → stack=['(', '['] +> char=')' → pairs にある → pairs[')']='(' と stack.pop()='[' を比較 → 不一致 → return False ✅ +> ``` + +> 💡 **辞書によるO(1)対応表引き**(初学者向け) +> +> **基礎**:Pythonの`dict`は「キー」から「値」を素早く探せるデータ構造です。図書館の索引カードのように、目的のキーがあれば本棚を端から探す(=O(n))必要がなく、直接目的の場所へ辿り着けます(=平均O(1))。 +> ```python +> pairs = {')': '(', ']': '[', '}': '{'} +> print(pairs[')']) # '(' が一瞬で得られる +> ``` +> +> **応用**:`char in pairs`のように`in`演算子で辞書のキーの存在確認を行うのも、リストでの`in`検索(O(n))と違い平均O(1)で完了します。 +> +> **この問題での使い方**:閉じ括弧が来るたびに`if/elif`を3つ並べて比較するのではなく、`pairs[char]`と1行書くだけで対応する開き括弧が得られます。コードが短くなるだけでなく、括弧の種類が増えた場合にも辞書に1行追加するだけで対応できる拡張性があります。 + +> 📖 **このセクションで登場した用語** +> +> - **EAFP(Easier to Ask Forgiveness than Permission)**:「許可を求めるより、まず実行してダメだったら謝る(例外処理する)」というPythonの伝統的な設計哲学。事前に`if`でチェックする LBYL(Look Before You Leap)と対比される。 +> - **`enumerate()`**:イテラブル(リストや文字列など)から「インデックスと値のペア」を順に取り出す組み込み関数。C実装のため`range(len(x))`より高速かつPythonic。 +> - **番兵(センチネル)値**:ループや条件分岐の終端・特殊ケースを示すためのダミー値。競技版の`'#'`は「絶対に括弧と一致しない番兵」として使われている。 +> - **`Final`**:`typing`モジュールの型。「この変数は再代入されない」ことをpylanceなどの型チェッカーに伝える。 + +--- + +# 4. 検証 + +_テストコードは不要です!具体的な計測値、メモリの出力も不要です!_ + +> 💡 **初学者向け補足**:エッジケースのテストは、アルゴリズムが「ふつうの入力」だけでなく「極端な入力」でも正しく動くかを確かめるためのものです。 + +- **境界値テスト**: + - **単一の閉じ括弧(例:`")"`)の場合**:業務版では`stack`が空のため`if not stack:`で`BracketMismatchError`が送出されます。競技版では`stack.pop() if stack else '#'`により、空リストへの`pop()`で発生する`IndexError`(存在しない要素にアクセスしようとしたときの例外)を未然に回避し、安全に`False`を返します。 + - **未閉じの開き括弧(例:`"((("`)の場合**:ループ終了後も`stack`に要素が残るため、業務版では`if stack:`のチェックで例外が送出され、競技版では`return not stack`が`False`になります。 + - **最大長10^4文字の場合**:`list.append`/`list.pop`はいずれも償却O(1)(=ならすと一定時間で済む、という意味。稀に発生する再確保のコストを全体で割ると無視できるほど小さくなる)であり、`dict`のキー検索もO(1)のため、全体としてO(n)の性能が保証されます。 +- **型チェック**:pylanceによる静的解析では、`_PAIRS: Final[dict[str, str]]`のような厳密な型ヒントにより、意図しない型の値が代入された場合にエラーとして検出されます。`stack: list[str]`という宣言により、`stack.pop()`の戻り値が`str`型であることも型チェッカーに伝わります。 + +> 📖 **このセクションで登場した用語** +> +> - **`IndexError`**:リストや文字列などのシーケンスに対して、存在しないインデックスにアクセスしようとしたときに発生する例外。 +> - **償却計算量(Amortized Complexity)**:個々の操作は時々遅くなることがあっても、多数回の操作全体で平均すると一定時間で済むという計算量の考え方。`list.append`の再確保コストはこの考え方で「ならすとO(1)」とみなされる。 +> - **静的解析**:プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法。pylanceはこれをリアルタイムで行うツール。 diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Rust.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Rust.md new file mode 100644 index 00000000..a6120f2f --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Rust.md @@ -0,0 +1,297 @@ +# 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「**開き括弧と閉じ括弧の"組み合わせ"と"順番"が正しいかどうかを、後入れ先出し(=最後に置いたものを最初に取り出す)のルールで判定する問題**」です。 +> +> Rustでこの問題を解く際に特に気をつけるべき点は、**「文字列の所有権をどう扱うか」**と**「マッチング失敗時の"境界条件"をパニックさせずに処理すること」**の2点です。関数シグネチャが `s: String` となっており呼び出し元から所有権を受け取りますが、中身を読むだけなら `&str`(文字列スライス)として借用すれば十分であり、無駄なコピーを避けられます。また、閉じ括弧が来たのにスタックが空、という状況(=対応する開き括弧が存在しない)をどう安全に扱うかがRustらしい設計のポイントになります。 + +- **競技プログラミング視点での分析** + - 実行速度を最優先とした場合、文字列を1回だけ走査する線形時間 O(n) のアプローチが最速です。それ以上速くすることは(各文字を最低1回は見る必要があるため)理論上不可能です。 + - メモリ使用量の最小化方針としては、スタック(後入れ先出し構造)として `Vec` を使うのが最もシンプルですが、`char` は4バイトを占めるため、ASCII文字のみという制約(括弧記号は全てASCII)を活かして `Vec`(バイト列)にすればメモリをさらに節約できます。ヒープへの再アロケーション(=メモリ領域の再確保)回数を減らすため、`Vec::with_capacity` で事前に容量を確保するのも定石です。 +- **業務開発視点での分析** + - 型安全性・保守性を重視するなら、括弧の種類(丸・波・角)を `char` の生の比較で扱うのではなく、意味のある型(enumなど)で表現することで「間違った文字を比較してしまう」バグをコンパイル時に防げます。 + - `Result` / `Option` によるエラーハンドリング設計としては、「どの位置で」「どんな理由で」不正になったかを型で表現しておくと、将来的にエラーメッセージを表示するUIなどに拡張しやすくなります。今回のLeetCode形式では最終的に `bool` を返しますが、内部的には `Result<(), BracketError>` を返す純粋関数を用意し、それを `bool` に変換する設計にします。 +- **Rust特有の考慮点** + - **所有権・借用**:`is_valid(s: String)` はLeetCodeのシグネチャ上 `String`(所有権を持つ文字列)を受け取りますが、関数内部では中身を読むだけなので `&str` として借用すれば十分です。所有権を持つ `String` をそのまま内部関数に渡すと「ムーブ(=所有権の移動)」が発生し、以後その変数は使えなくなってしまいます。今回は最後に使うだけなので実害はありませんが、再利用したい場合は `&s` のように借用を渡すのがRustの慣習です。 + - **ライフタイム**:今回は参照をそのまま関数内で使い切って返すだけなので、明示的なライフタイム注釈 `'a` は不要です(コンパイラが自動推論してくれます=ライフタイム省略規則)。 + - **トレイト境界とモノモーフィゼーション**:この問題は `char` 型に固定された処理のため、ジェネリクス(``)は不要です。無理にジェネリックにすると、かえって可読性が落ちるため今回は使いません。 + - **イテレータアダプタ vs 命令型ループ**:`for c in s.chars()` のようなイテレータベースのループは、C言語スタイルのインデックスアクセス(`for i in 0..s.len()`)よりも安全(範囲外アクセスが起きない)で、かつコンパイラが最適化することで同等の速度が出ます(=ゼロコスト抽象化)。今回はこちらを採用します。 + +> 📖 **このセクションで登場した用語** +> +> - **所有権**:値を「誰が管理するか」をコンパイル時に決めるRust独自の仕組み。JavaやPythonのようにガベージコレクタ(=不要になったメモリを自動回収する仕組み)を使わずに、メモリの二重解放や解放後アクセスをコンパイル時に防げる。 +> - **借用**:所有権を渡さずに値を「参照」だけする仕組み。`&T`(読み取り専用の借用)と `&mut T`(書き込み可能な借用)がある。 +> - **ムーブ(move)**:所有権が別の変数に移ること。ムーブされた元の変数はそれ以降使用できなくなる(コンパイルエラーになる)。 +> - **スタック(データ構造としての意味)**:後入れ先出し(LIFO: Last In, First Out)の構造。最後に追加した要素を最初に取り出す。今回の「括弧の対応関係」の判定にぴったり合う構造。 + +--- + +# 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:なぜ複数のアプローチを比較するのかというと、「動くコード」を書くことと「その状況で最善のコード」を書くことは別物だからです。特にこの問題は一見単純ですが、誤ったアプローチ(後述の方法B)を選ぶと `"([)]"` のような**順序が壊れているケース**を見逃してしまいます。 + +| アプローチ | 時間計算量 | 空間計算量 | Rust実装コスト | 安全性 | 可読性 | 備考 | +| ---------- | ---------- | ---------- | -------------- | ------ | ------ | ---------------------------------------- | +| 方法A | O(n) | O(n) | 低 | 高 | 高 | `Vec` をスタックとして使う(採用) | +| 方法B | O(n) | O(1) | 低 | 低 | 中 | 括弧の**種類ごとの出現数**を数えるだけ(誤ったアプローチ) | +| 方法C | O(n²) | O(n) | 中 | 高 | 中 | `"()"` `"[]"` `"{}"` を空文字に繰り返し置換 | + +**Rust固有の観点(所有権・アロケーションの有無)**: + +- **方法A**:`s.chars()` は `&str`(借用)に対するイテレータなので所有権の移動は発生しません。`Vec` の `push`/`pop` はヒープへのアロケーションを伴いますが、`Vec::with_capacity(s.len())` で最悪ケースの容量を先に確保すれば、再アロケーション(=容量が足りなくなるたびにメモリを取り直す処理)はゼロ回に抑えられます。 +- **方法B**:括弧の「数」だけを `[i32; 3]` のような固定長配列(スタックに置ける、ヒープ確保不要)でカウントするので空間計算量は O(1) です。しかし `"([)]"` のように**数は合っているが順序が壊れている**入力を `true` と誤判定してしまうため、そもそも**要件2「正しい順序で閉じる」を満たせず不正解**です(比較のためにあえて載せています)。 +- **方法C**:文字列の `replace()` を空になるまで繰り返すため、1回の `replace` 走査が O(n) で、最悪 O(n) 回繰り返すため合計 O(n²) になります。さらに `replace` のたびに新しい `String` が確保される(=所有権を持つ新しい文字列がヒープに作られる)ため、メモリ効率も悪化します。 + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) +> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループや繰り返しスキャンに多い) + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさ `n` に対して、処理にかかる手間がどう増えるかの目安。 +> - **空間計算量**:処理中に使う追加メモリ量が、入力の大きさに対してどう増えるかの目安。 +> - **アロケーション**:ヒープ(動的にサイズが変わるデータを置く領域)上にメモリを確保する操作。頻繁に行うと速度が落ちる。 +> - **再アロケーション**:`Vec` などの容量が足りなくなったときに、より大きな領域を確保し直して中身をコピーする操作。事前に `with_capacity` で容量を確保すれば回避できる。 + +--- + +# 3. 選択したアルゴリズムと理由 + +> 💡 **初学者向け補足**:「なぜ方法Aを選び、方法B・Cを選ばなかったのか」を対比形式で説明します。 + +- **選択したアプローチ**: 方法A(`Vec` をスタックとして使う線形走査) +- **理由**: + - **計算量的な優位性**:方法Cの O(n²) と違い、方法Aは文字列を1回だけ走査する O(n) で完結します。入力長は最大 10,000 文字(制約より)なので、O(n²) でも実用上は動きますが、O(n) の方が明確に優れています。 + - **方法Bを選ばなかった理由**:方法Bは空間計算量こそ O(1) と優秀に見えますが、そもそも「開き括弧が閉じられる**順序**」を一切見ていないため、`"([)]"` のような不正な入力を `true` と誤判定してしまい、**問題の要件を満たしていません**。速さより先に正しさが必須です。 + - **Rustの所有権モデルとの親和性**:`s.chars()` は `&str` を借用するだけなので所有権のムーブが発生せず、関数の入出力設計がシンプルになります。またスタックへの `push`/`pop` は `Vec` の標準メソッドとして安全に(境界外アクセスによるパニックなしに)扱えます。 +- **Rust特有の最適化ポイント**: + - **ゼロコスト抽象化によるオーバーヘッドの排除**:`for c in s.chars()` というイテレータベースの書き方は、手書きの `while` ループ+インデックスアクセスと同等の機械語に最適化されます。それでいて範囲外アクセスの危険性がありません。 + - **`match` によるパターンマッチの網羅性チェック**:Rustの `match` はすべての分岐を網羅していないとコンパイルエラーになるため、「閉じ括弧の種類を1つ書き忘れる」といったバグをコンパイル時に検出できます。 + - **スタックアロケーション優先によるキャッシュ効率**:`Vec` の中身自体はヒープにありますが、事前に容量を確保しておくことで、走査中のメモリ確保コストをほぼゼロにできます。 + +> 📖 **このセクションで登場した用語** +> +> - **ゼロコスト抽象化**:便利な高レベルの書き方(イテレータなど)をしても、手書きの低レベルコードと同等の速さになるRustの特性。「抽象化のために実行時コストを払わなくてよい」という意味。 +> - **パターンマッチの網羅性**:`match` 式で全ての可能な値のパターンを書ききっているかをコンパイラがチェックしてくれる仕組み。書き漏らしがあるとコンパイルエラーになる。 +> - **キャッシュ効率**:CPUが直前に読んだメモリの近くにある値を素早く読める性質。事前に確保された連続領域(`Vec` の内部バッファ)はこの恩恵を受けやすい。 + +--- + +# 4. 実装コード + +> 💡 **初学者向け補足**:コード全体を示す前に、「このコードの大まかな構造(骨格)」を箇条書きで示します。 +> +> 1. 括弧の不正な組み合わせを表現する**カスタムエラー型**(`BracketError`)を定義する(失敗の理由を型で表現するため) +> 2. 文字列を借用(`&str`)で受け取り、`Vec` をスタックとして使う**純粋関数**(`validate_brackets`)を実装し、`Result<(), BracketError>` を返す +> 3. LeetCode形式の `Solution::is_valid` は、その結果を `.is_ok()` で `bool` に変換して返す(=内部ロジックとインターフェースを分離する設計) + +```rust +// ============================================================ +// カスタムエラー型 +// ============================================================ +// なぜこの型が必要か: +// 「どこで」「なぜ」不正だったのかを bool の true/false だけでは表現できません。 +// enum(列挙型)でエラーの種類を明示することで、将来的に +// 「エラーメッセージをユーザーに見せる」といった拡張がしやすくなります。 +// これは「try-catch で例外を投げる」言語と違い、 +// Rustでは「戻り値の型」としてエラーを表現するのが特徴です。 +#[derive(Debug, Clone, PartialEq)] +enum BracketError { + // 閉じ括弧が来たが、対応する開き括弧の種類が違った場合 + // 例: "(]" → '(' が積まれている状態で ']' が来た + Mismatched { expected: char, found: char }, + // 閉じ括弧が来たのに、対応する開き括弧が1つも積まれていない場合 + // 例: "]" だけの入力 + UnexpectedClose(char), + // 文字列を最後まで読み終えたのに、閉じられていない開き括弧が残っている場合 + // 例: "(()" → 最後に '(' が1つ余る + UnclosedOpen(char), +} + +// Display トレイト(=人間が読める文字列に変換する仕組み)を実装しておくと、 +// 将来 println!("{}", err) のような形でエラー内容を表示できるようになる。 +impl std::fmt::Display for BracketError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Mismatched { expected, found } => { + write!(f, "'{found}' に対応する閉じ括弧は '{expected}' ではありません") + } + Self::UnexpectedClose(c) => { + write!(f, "対応する開き括弧がないまま閉じ括弧 '{c}' が出現しました") + } + Self::UnclosedOpen(c) => { + write!(f, "開き括弧 '{c}' が閉じられないまま入力が終了しました") + } + } + } +} + +// std::error::Error トレイトを実装すると、他のエラー型と統一的に扱える +// (例: Box でまとめて扱う、など)。 +impl std::error::Error for BracketError {} + +// ============================================================ +// 内部ロジック(純粋関数) +// ============================================================ + +/// 開き括弧に対応する閉じ括弧を返すヘルパー関数。 +/// +/// なぜ Option を返すのか: +/// 「括弧ではない文字」が来た場合に None を返すことで、 +/// 呼び出し元がパニックせずに安全に分岐できるようにするため。 +/// null を使う言語と違い、Rustでは「値が無いかもしれない」ことを +/// 型(Option)で表現するため、呼び出し側は必ず None のケースを +/// 考慮せざるを得ません(考慮しないとコンパイルエラーになる)。 +fn closing_for(open: char) -> Option { + match open { + '(' => Some(')'), + '[' => Some(']'), + '{' => Some('}'), + _ => None, + } +} + +/// 括弧文字列が正しく対応しているかを検証する。 +/// +/// # Arguments +/// * `s` - 検証したい文字列への借用(&str)。 +/// 所有権を奪わないので、呼び出し元は検証後も s を使い続けられる。 +/// +/// # Returns +/// 正しく対応していれば `Ok(())`。 +/// +/// # Errors +/// 対応関係・順序が崩れている場合に `BracketError` を返す。 +/// +/// # Complexity +/// - Time: O(n) (文字列を1回だけ走査するため) +/// - Space: O(n) (最悪ケースで全ての文字が開き括弧の場合、スタックがnサイズになる) +fn validate_brackets(s: &str) -> Result<(), BracketError> { + // Vec を「後入れ先出し」のスタックとして使う。 + // with_capacity で最悪ケース(全部開き括弧)を見越して容量を確保し、 + // 途中での再アロケーション(メモリの取り直し)を防ぐ。 + let mut stack: Vec = Vec::with_capacity(s.len()); + + // s.chars() は &str に対するイテレータで、所有権を奪わずに + // 1文字ずつ走査できる。インデックスでのアクセス(s[i]のような書き方)は + // Rustでは許されておらず(UTF-8のバイト境界問題があるため)、 + // このイテレータ経由のアクセスが安全かつ慣用的な方法。 + for c in s.chars() { + match c { + // 開き括弧なら、対応する閉じ括弧が来るまで「積んでおく」。 + '(' | '[' | '{' => stack.push(c), + + // 閉じ括弧が来た場合の処理。 + ')' | ']' | '}' => { + // stack.pop() は Option を返す。 + // スタックが空なら None、要素があれば Some(最後に積んだ値) が返る。 + // これにより「空のスタックに対する誤ったアクセス」による + // パニック(実行時クラッシュ)を型レベルで防いでいる。 + let top = stack.pop().ok_or(BracketError::UnexpectedClose(c))?; + + // top(直前に積んだ開き括弧)に対応する閉じ括弧を計算する。 + // closing_for は Option を返すが、top は必ず開き括弧なので + // ここで None になることは論理的にありえない + // (push される値は '(' '[' '{' のいずれかだけのため)。 + let expected = closing_for(top) + .expect("スタックには開き括弧のみが積まれるため必ず対応する閉じ括弧が存在する"); + + // 期待する閉じ括弧と、実際に来た閉じ括弧が一致するか確認。 + if expected != c { + return Err(BracketError::Mismatched { expected, found: c }); + } + // 一致していれば、この開き括弧・閉じ括弧のペアは正しく閉じられた + // ことになるので、次のループへ進む。 + } + + // 制約(constraints)により括弧記号以外の文字は入力されない前提。 + // それでも match は全パターンを網羅する必要があるため、 + // 到達しないはずの分岐として unreachable! を置いておく。 + // これは「起きないはずのバグ」をパニックで検出するための安全弁。 + _ => unreachable!("制約により括弧文字以外は入力されない"), + } + } + + // 最後まで走査し終えた時点で、スタックに何か残っていれば + // 「閉じられていない開き括弧がある」ということなので不正。 + // stack.last() は Option<&char> を返すので、コピーして値として扱う。 + if let Some(&unclosed) = stack.last() { + return Err(BracketError::UnclosedOpen(unclosed)); + } + + Ok(()) +} + +// ============================================================ +// LeetCode形式のインターフェース +// ============================================================ + +struct Solution; + +impl Solution { + /// 括弧文字列が有効かどうかを判定する。 + /// + /// LeetCodeのシグネチャ上 `s: String`(所有権を持つ文字列)を受け取るが、 + /// 内部の検証処理は読み取りだけで完結するため、`&s` として借用を渡す。 + /// これにより validate_brackets 側で無駄な文字列コピーが発生しない。 + /// + /// # Complexity + /// - Time: O(n) + /// - Space: O(n) + pub fn is_valid(s: String) -> bool { + // Result<(), BracketError> を bool に変換するだけの薄いラッパー。 + // is_ok() は Ok(_) なら true、Err(_) なら false を返す。 + validate_brackets(&s).is_ok() + } +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> 入力例 `s = "([)]"`(要件2に違反する不正な例)でトレースします。 +> +> ``` +> 初期状態: stack = [] +> Step 1: c='(' → 開き括弧なので push → stack = ['('] +> Step 2: c='[' → 開き括弧なので push → stack = ['(', '['] +> Step 3: c=')' → 閉じ括弧。stack.pop() → Some('[') が top になる +> closing_for('[') = Some(']') +> expected=']' だが found=')' → 一致しない! +> → Err(BracketError::Mismatched { expected: ']', found: ')' }) を返す +> 結果: is_valid("([)]") = false ✅(期待通り) +> ``` +> +> 正常系の例 `s = "([])"` も見てみましょう。 +> +> ``` +> 初期状態: stack = [] +> Step 1: c='(' → push → stack = ['('] +> Step 2: c='[' → push → stack = ['(', '['] +> Step 3: c=']' → pop() → Some('[')、closing_for('[')=']'、一致 → OK → stack = ['('] +> Step 4: c=')' → pop() → Some('(')、closing_for('(')=')'、一致 → OK → stack = [] +> ループ終了後: stack.last() = None → 未閉じの括弧なし +> 結果: is_valid("([])") = true ✅(期待通り) +> ``` + +> 💡 **`?` 演算子の動きを図で理解する**(初学者向け) +> `stack.pop().ok_or(BracketError::UnexpectedClose(c))?` の `?` は、以下の処理と等価です: +> +> ``` +> pop() が None を返した場合 +> → ok_or(...) が Err(BracketError::UnexpectedClose(c)) に変換 +> → ? がその Err をそのまま関数の戻り値として即座に返す(関数を抜ける) +> pop() が Some(x) を返した場合 +> → ok_or(...) が Ok(x) に変換 +> → ? が x を取り出して、変数 top に代入し、処理を続ける +> ``` +> +> これにより、`match` でいちいち分岐を書かなくても、エラーの伝播(=呼び出し元に自動的にエラーを渡すこと)が1行で書けます。`try-catch` を持つ言語との違いは、**エラーが「例外」という特別な制御フローではなく、戻り値の型として明示される**点です。関数のシグネチャ(`Result<(), BracketError>`)を見ただけで「この関数は失敗しうる」ことが一目で分かります。 + +> 📖 **このセクションで登場した用語** +> +> - **`&str` と `String`**:`String` は所有権を持つ・伸縮可能な文字列。`&str` はその文字列(の一部)への借用(参照)。関数が読み取りしかしないなら `&str` を受け取るのがRustの慣習。 +> - **`Option`**:値が「あるかもしれないし、無いかもしれない」ことを表す型。`Some(値)` か `None` のどちらか。他言語の `null` と違い、中身を使う前に必ず「本当に値があるか」をコンパイラがチェックさせる。 +> - **`ok_or()`**:`Option` を `Result` に変換するメソッド。`None` だった場合に使うエラー値を指定できる。 +> - **`?` 演算子**:`Result` や `Option` がエラー・`None` だったとき、自動的に呼び出し元へ返す糖衣構文(=書き方を簡潔にするための構文糖)。 +> - **`unreachable!`**:「論理的に絶対に到達しないはず」の分岐に置くマクロ。もし実際に到達してしまった場合はパニック(プログラムの異常終了)してバグの存在を知らせる。 +> - **`expect("理由")`**:`Option`/`Result` の中身を取り出すが、もし失敗ケースだった場合は指定したメッセージ付きでパニックする。「なぜここでは失敗しないと確信しているか」を書き残すためのドキュメントも兼ねる。 diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md new file mode 100644 index 00000000..5ecc9ee6 --- /dev/null +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md @@ -0,0 +1,340 @@ +# 1. 問題の分析 + +> 💡 **初学者向け補足**:この問題は、一言で言うと「**開き括弧と閉じ括弧の"組み合わせ"と"順番"が正しいかを、後入れ先出し(=最後に置いたものを最初に取り出す)のルールで判定する問題**」です。 + +- **競技プログラミング視点での分析** + - 実行速度を最優先とした場合、文字列を1回だけ走査する O(n) のアプローチが最速です。文字を1回ずつ見る以上、これより速くすることは理論上不可能です。 + - メモリ使用量の最小化方針としては、配列(`string[]`)をスタック(後入れ先出し構造)として使うのがシンプルです。TypeScript(=JavaScriptに型を付けたプログラミング言語)の配列は動的にサイズが変わるため、事前に容量を確保する仕組みはありませんが、`push`/`pop`はどちらもO(1)の操作なので気にする必要はありません。 +- **業務開発視点での分析** + - 型安全性・保守性を重視するなら、「開き括弧」「閉じ括弧」という概念を生の`string`型のまま扱うのではなく、意味のある型(**ユニオン型**=複数の型のいずれか1つであることを表す型)で表現することで、「間違った文字を比較してしまう」バグをコンパイル時に防げます。 + - エラーハンドリングとしては、`isValid`自体はLeetCode形式で`boolean`を返す必要がありますが、内部的には「なぜ不正なのか」を判別できる設計にしておくと、将来的にエラー内容をユーザーに見せるUIなどに拡張しやすくなります。 +- **TypeScript特有の考慮点** + - **型推論**(=型を明示しなくてもTypeScriptが自動で型を判断してくれる機能)を活用し、変数の型注釈を最小限にして可読性を保ちます。 + - **ジェネリクス**は今回不要です。この問題は`string`型に固定された処理であり、無理にジェネリック化すると逆に可読性が落ちます。 + - **型ガード**(=実行時に値の型を絞り込むための判定処理。`typeof`や`Array.isArray`などで行う)を使い、`unknown`型の入力が来ても安全に扱えるようにします。ただしLeetCode形式では引数の型が`string`で固定されているため、今回は主に「文字が期待した種類の記号か」を判定する用途で使います。 + +> 📖 **このセクションで登場した用語**(例) +> +> - **後入れ先出し(LIFO)**:最後に追加した要素を最初に取り出すデータ構造の性質。今回の「括弧の対応関係」の判定にぴったり合う。 +> - **ユニオン型**:`'(' | '[' | '{'`のように「複数の型のうちいずれか1つ」であることを表すTypeScriptの型。 +> - **型推論**:型を明示しなくてもTypeScriptが自動で型を判断してくれる機能。 +> - **型ガード**:実行時に値の型を絞り込むための判定処理。`typeof x === 'string'`のような形で書く。 + +--- + +# 2. アルゴリズムアプローチ比較 + +> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べることで、なぜ特定の方法を選ぶべきかが明確になります。特にこの問題は、**一見メモリ効率が良さそうでも実は不正解になるアプローチ**があるため、比較が重要です。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +| ---------- | ---------- | ---------- | ------------ | -------- | ------ | ---------------------------------------------------------- | +| 方法A | O(n) | O(n) | 低 | 高 | 高 | `string[]`をスタックとして使う(採用) | +| 方法B | O(n) | O(1) | 低 | 中 | 中 | 括弧の**種類ごとの出現数**を数えるだけ(誤ったアプローチ) | +| 方法C | O(n²) | O(n) | 中 | 中 | 中 | `"()" "[]" "{}"`を空文字に繰り返し置換 | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形) +> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループや繰り返しスキャンに多い) + +**補足(各アプローチの詳細)**: + +- **方法A**:`push`/`pop`はどちらも配列の末尾に対する操作でO(1)です。文字列を1回だけ走査するのでトータルO(n)。型安全性の面でも、スタックの中身を`('(' | '[' | '{')[]`のような限定した型にすることで、誤った値が紛れ込むのをコンパイル時に防げます。 +- **方法B**:括弧の「数」だけを数えるので空間計算量はO(1)です。しかし`"([)]"`のように**数は合っているが順序が壊れている**入力を`true`と誤判定してしまうため、要件2「正しい順序で閉じる」を満たせず**不正解**です(比較のためにあえて載せています)。 +- **方法C**:`String.prototype.replace()`は呼び出すたびに**新しい文字列**を生成します。JavaScript/TypeScriptの文字列はイミュータブル(=一度作られたら中身を変更できない性質)なので、置換のたびにメモリ上に新しい文字列が作られます。これを空になるまで繰り返すため、最悪ケースでO(n²)になり、メモリ効率も悪化します。 + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して、処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **イミュータブル**:一度作られたら中身を変更できない性質。TypeScript/JavaScriptの文字列(`string`)はこの性質を持つ + +--- + +# 3. 選択したアルゴリズムと理由 + +> 💡 **初学者向け補足**:「なぜ方法Aを選び、方法B・Cを選ばなかったのか」を対比形式で説明します。 + +- **選択したアプローチ**: 方法A(`string[]`をスタックとして使う線形走査) +- **理由**: + - **計算量的な優位性**:方法Cの O(n²) と違い、方法Aは文字列を1回だけ走査する O(n) で完結します。 + - **方法Bを選ばなかった理由**:方法Bは空間計算量こそO(1)と優秀に見えますが、括弧が閉じられる**順序**を一切見ていないため、`"([)]"`のような不正な入力を`true`と誤判定してしまい、問題の要件を満たしていません。速さより先に「正しさ」が必須です。 + - **TypeScript環境での型安全性**:スタックの型を`('(' | '[' | '{')[]`のように限定することで、「本来入るはずのない値がスタックに紛れ込む」というバグをコンパイル時に検出できます。`Map`で対応表を持つ設計にすれば、`readonly`修飾子と組み合わせて「実行中に対応表が書き換えられてしまう」事故も防げます。 +- **TypeScript特有の最適化ポイント**: + - **コンパイル時の型チェックによるエラー防止**:括弧の対応表を`Readonly>`型で定義することで、対応表への意図しない代入をコンパイルエラーとして検出できます。 + - **型推論による開発効率向上**:`const stack: string[] = []`のように必要な箇所だけ型注釈を書き、あとはTypeScriptの型推論に任せることでコードがすっきりします。 + - **`readonly`の活用**:入力の`string`自体はもともとイミュータブルですが、対応表などの補助データ構造に`readonly`を付けることで「意図せぬ書き換え」を防止します。 + +> 📖 **このセクションで登場した用語** +> +> - **型安全性**:プログラムの実行前(コンパイル時)に、型の不一致によるバグを検出できる性質 +> - **`Readonly`**:オブジェクトの全プロパティを読み取り専用にするTypeScriptのユーティリティ型 +> - **`Record`**:キーの型`K`と値の型`V`を指定してオブジェクトの型を表現するユーティリティ型 + +--- + +# 4. 実装コード + +> 💡 **初学者向け補足**:コード全体を示す前に、「このコードの大まかな構造(骨格)」を箇条書きで示します。 +> +> 1. 括弧の不正な組み合わせを表現する**カスタムエラークラス**(`BracketError`)を定義する +> 2. 開き括弧と閉じ括弧の対応表を`Readonly>`型で定義する +> 3. 入力文字列を1文字ずつ走査し、スタックを使って対応関係と順序を検証する`validateBrackets`関数を実装する(型レベルでのエラー表現として、成功時と失敗時を区別できる戻り値にする) +> 4. LeetCode提出用の`isValid`は、その結果を`boolean`に変換して返す薄いラッパーにする + +```typescript +// ============================================================ +// カスタムエラークラス +// ============================================================ +// なぜこのクラスが必要か: +// boolean の true/false だけでは「どこで」「なぜ」不正だったのかを表現できない。 +// Error を継承したクラスを作ることで、将来的にエラーメッセージを +// ログ出力やUI表示に転用しやすくなる。 +class BracketError extends Error { + // readonly を付けることで、生成後にこれらのプロパティが + // 書き換えられてしまう事故を防ぐ(意図しない代入をコンパイルエラーにする) + constructor( + message: string, + public readonly char: string, + public readonly position: number, + ) { + super(message); + // クラス名を正しく設定する(Errorを継承する際のTypeScript/JSの定石) + this.name = 'BracketError'; + } +} + +// ============================================================ +// 対応表の定義 +// ============================================================ +// なぜ Readonly> を使うのか: +// この対応表は「一度定義したら変更されるべきではない」データ。 +// readonly を付けることで、うっかり CLOSING_BRACKETS.foo = 'x' のような +// 代入をしてしまった場合にコンパイルエラーとして検出できる。 +// Map ではなくオブジェクトリテラルを使う理由: +// キーが3種類だけの固定集合であり、オブジェクトの方が可読性が高いため。 +const CLOSING_BRACKETS: Readonly> = { + '(': ')', + '[': ']', + '{': '}', +}; + +// 「開き括弧かどうか」を判定するための型ガード関数。 +// 戻り値の型を `param is '(' | '[' | '{'` にすることで、 +// この関数が true を返した後、TypeScriptが param の型を +// 自動的に絞り込んでくれる(型の絞り込み=Type Narrowing)。 +function isOpeningBracket(char: string): char is '(' | '[' | '{' { + // Object.prototype.hasOwnProperty を直接呼ぶより、 + // in 演算子を使う方がシンプルで意図が明確になる + return char in CLOSING_BRACKETS; +} + +// 「閉じ括弧かどうか」を判定する型ガード関数。 +// CLOSING_BRACKETS の値(Object.values)に含まれるかどうかで判定する。 +function isClosingBracket(char: string): char is ')' | ']' | '}' { + return Object.values(CLOSING_BRACKETS).includes(char); +} + +// ============================================================ +// 検証結果を表す型(型レベルでのエラー表現) +// ============================================================ +// なぜ union 型で結果を表現するのか: +// try-catch で例外を投げる代わりに、「成功か失敗か」を型で表現することで、 +// 呼び出し元が結果を必ず確認せざるを得ない設計にできる。 +// これは Rust の Result や Go の (value, error) の考え方に近い。 +type ValidationResult = + | { readonly ok: true } + | { readonly ok: false; readonly error: BracketError }; + +// ============================================================ +// メインロジック(純粋関数) +// ============================================================ + +/** + * 括弧文字列が正しく対応しているかを検証する。 + * + * @param s - 検証したい文字列 + * @returns 検証結果を表すオブジェクト(成功: {ok: true}、失敗: {ok: false, error}) + * @complexity Time: O(n), Space: O(n) + */ +function validateBrackets(s: string): ValidationResult { + // string[] を「後入れ先出し」のスタックとして使う。 + // TypeScriptの配列は push/pop がどちらもO(1)であり、 + // Rustの Vec::with_capacity のような事前確保の仕組みはないが、 + // V8エンジン(Node.jsが使うJavaScriptエンジン)が内部的に + // 効率よくメモリを管理してくれるため、通常は気にしなくてよい。 + const stack: string[] = []; + + // for...of ループで1文字ずつ走査する。 + // index が必要な場合は entries() を使うことで、 + // 文字とインデックスの両方を安全に取得できる。 + for (const [position, char] of [...s].entries()) { + if (isOpeningBracket(char)) { + // 開き括弧なら、対応する閉じ括弧が来るまでスタックに積んでおく + stack.push(char); + continue; + } + + if (isClosingBracket(char)) { + // stack.pop() は string | undefined を返す。 + // TypeScriptの配列の pop() は「空かもしれない」ことを + // 型レベルで表現しており(strictモード時)、 + // undefined チェックを省略するとコンパイルエラーになる。 + // これは null を使う言語と違い、「値が無いかもしれない」ことを + // 型システムが強制的に意識させてくれる仕組み。 + const top = stack.pop(); + + if (top === undefined) { + // 対応する開き括弧が存在しないまま閉じ括弧が来た場合 + return { + ok: false, + error: new BracketError( + `対応する開き括弧がないまま閉じ括弧が出現しました`, + char, + position, + ), + }; + } + + // top(開き括弧)に対応する閉じ括弧を対応表から引く。 + // isOpeningBracket の型ガードにより、top は必ず + // CLOSING_BRACKETS のキーであることが保証されているため、 + // 非nullアサーション(!)を使わずとも安全にアクセスできる。 + const expected = CLOSING_BRACKETS[top]; + + if (expected !== char) { + // 期待する閉じ括弧と実際に来た文字が異なる場合 + return { + ok: false, + error: new BracketError( + `'${top}' に対応する閉じ括弧は '${expected}' ですが '${char}' が見つかりました`, + char, + position, + ), + }; + } + // 一致していれば、このペアは正しく閉じられたので次のループへ進む + continue; + } + + // 制約により括弧文字以外は入力されない前提だが、 + // 防御的プログラミング(=想定外の入力にも安全に対応する設計方針)として + // 明示的にエラーを返し、意図しない挙動を防ぐ。 + return { + ok: false, + error: new BracketError(`括弧以外の文字が含まれています`, char, position), + }; + } + + // 走査し終えた時点でスタックに何か残っていれば、 + // 閉じられていない開き括弧があるということなので不正。 + if (stack.length > 0) { + // スタックの末尾(最後に残った未閉じの開き括弧)を取得する。 + // ここでの ?? は「もし undefined だったら」の場合の + // フォールバック値を指定する Nullish coalescing 演算子だが、 + // stack.length > 0 が保証されているため実際には発生しない。 + const unclosed = stack[stack.length - 1] ?? ''; + return { + ok: false, + error: new BracketError( + `開き括弧 '${unclosed}' が閉じられないまま入力が終了しました`, + unclosed, + s.length, + ), + }; + } + + return { ok: true }; +} + +// ============================================================ +// LeetCode提出用のエントリポイント +// ============================================================ + +/** + * 括弧文字列が有効かどうかを判定する。 + * + * @param s - 判定対象の文字列('(' ')' '[' ']' '{' '}' のみを含む) + * @returns 有効なら true、そうでなければ false + * @complexity Time: O(n), Space: O(n) + */ +function isValid(s: string): boolean { + // ValidationResult の ok プロパティだけを見て boolean に変換する + // 薄いラッパー。内部の詳細なエラー情報はここでは使わないが、 + // 将来ログ出力や詳細なエラーメッセージ表示が必要になった場合、 + // validateBrackets を直接呼び出すことで拡張できる設計にしてある。 + return validateBrackets(s).ok; +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> 入力例 `s = "([)]"`(要件2に違反する不正な例)でトレースします。 +> +> ``` +> 初期状態: stack = [] +> Step 1: position=0, char='(' → isOpeningBracket('(')=true → push → stack=['('] +> Step 2: position=1, char='[' → isOpeningBracket('[')=true → push → stack=['(', '['] +> Step 3: position=2, char=')' → isClosingBracket(')')=true +> stack.pop() → top='[' 、stack=['('] +> CLOSING_BRACKETS['['] = ']' +> expected=']' だが char=')' → 不一致! +> → { ok: false, error: BracketError(...) } を返す +> isValid の戻り値: false ✅(期待通り) +> ``` +> +> 正常系の例 `s = "([])"` も見てみましょう。 +> +> ``` +> 初期状態: stack = [] +> Step 1: '(' → push → stack=['('] +> Step 2: '[' → push → stack=['(', '['] +> Step 3: ']' → pop() → top='['、expected=']'、char=']' → 一致 → stack=['('] +> Step 4: ')' → pop() → top='('、expected=')'、char=')' → 一致 → stack=[] +> ループ終了後: stack.length === 0 → 未閉じの括弧なし +> isValid の戻り値: true ✅(期待通り) +> ``` + +> 💡 **型ガードによる型の絞り込み(Type Narrowing)を理解する**(初学者向け) +> +> **基礎**:TypeScriptでは、`if`文の中で特定の判定を行うと、その後の処理でTypeScriptが変数の型を自動的に絞り込んでくれます。 +> +> ```typescript +> function example(value: string | number) { +> if (typeof value === 'string') { +> // ここでは TypeScript が value を string 型だと認識してくれる +> console.log(value.toUpperCase()); +> } +> } +> ``` +> +> **応用**:自作の関数でも、戻り値の型に`param is Type`という特殊な書き方(**型述語**=関数が特定の型であることを保証する構文)をすることで、同じ絞り込みの恩恵を受けられます。 +> +> **この問題での使い方**:`isOpeningBracket(char): char is '(' | '[' | '{'`と書くことで、`if (isOpeningBracket(char))`の中では、TypeScriptが`char`を`'(' | '[' | '{'`型として扱ってくれます。これにより、`stack.push(char)`のように、スタックの型と矛盾しない安全な代入ができます。 + +> 📖 **このセクションで登場した用語** +> +> - **型ガード**:実行時の判定によって、その後のコードでTypeScriptに変数の型を絞り込ませる仕組み。`typeof`や独自関数(`is`構文)で実現できる。 +> - **型述語(Type Predicate)**:`param is Type`という戻り値の型注釈。関数がその型であることを保証し、呼び出し元での型の絞り込みを可能にする。 +> - **Type Narrowing(型の絞り込み)**:条件分岐によって、変数の型がより具体的な型に絞り込まれるTypeScriptの機能。 +> - **Nullish coalescing演算子(`??`)**:左辺が`null`または`undefined`の場合にのみ右辺の値を使う演算子。`||`と違い、`0`や`''`のような falsy な値では右辺に切り替わらない。 +> - **防御的プログラミング**:想定外の入力やエラーが来ても、プログラムが安全に振る舞うように設計する方針。 + +--- + +# 5. 検証 + +_テストコードは不要のため、ここではエッジケースの説明のみ行います。_ + +> 💡 **初学者向け補足**:エッジケースとは「入力が空・最小値・最大値・重複あり」など境界的な入力のことです。 + +- **単一の閉じ括弧(例:`")"`)の場合**:`stack.pop()`が`undefined`を返すため、`top === undefined`の分岐で安全に`{ ok: false, ... }`を返します。TypeScriptの`strict`モードでは、`pop()`の戻り値が`string | undefined`型として扱われるため、このチェックを省略するとコンパイルエラーになります。これにより「配列が空なのに要素を取り出そうとする」バグをコンパイル時に検出できる設計になっています。 +- **未閉じの開き括弧(例:`"((("`)の場合**:ループ終了後も`stack.length > 0`となるため、最後のチェックで不正と判定されます。 +- **括弧以外の文字が混入した場合**:制約上発生しませんが、`isOpeningBracket`にも`isClosingBracket`にも該当しない文字が来た場合、`default`相当の分岐で明示的にエラーを返すため、想定外の挙動(無限ループや`undefined`の伝播)を防げます。 +- **サロゲートペア文字を含む場合の注意**:この問題では発生しませんが、`[...s].entries()`のようにスプレッド構文で文字列を展開すると、絵文字などの**サロゲートペア**(=2つのUTF-16コードユニットで1文字を表す仕組み)も正しく1文字として扱えます。単純な`for (let i = 0; i < s.length; i++)`によるインデックスアクセスだと、サロゲートペアが分断されてしまう可能性があるため、今回のように`for...of`や`entries()`を使うのがTypeScript/JavaScriptでの安全な慣習です。 + +> 📖 **このセクションで登場した用語** +> +> - **`strict`モード**:TypeScriptコンパイラの厳格な型チェックを有効にする設定。`null`/`undefined`の扱いなどがより厳密になる。 +> - **サロゲートペア**:UTF-16で1つの文字を2つのコードユニットで表現する仕組み。絵文字など一部の文字で使われる。 +> - **スプレッド構文(`...`)**:配列や文字列の要素を展開する構文。`[...s]`で文字列を1文字ずつの配列に変換できる。 diff --git a/public/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html b/public/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html new file mode 100644 index 00000000..33890547 --- /dev/null +++ b/public/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html @@ -0,0 +1,1781 @@ + + + + + + Valid Parentheses - LeetCode 20 解説(Python / TypeScript / Go / Rust) + + + + + + + + + + + + + + + +
+ + + + + + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「開き括弧と閉じ括弧の対応関係と順序が正しいかどうかを判定する問題」です。 +

+

+ '('')''{''}''['']' + だけからなる文字列 + s + が与えられます。すべての開き括弧が正しい種類・正しい順序で閉じられているなら + true、そうでなければ + false + を返します。 +

+
+ +
+

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

+
    +
  • + 括弧の「数」を数えるだけでは不十分です。開き括弧の数と閉じ括弧の数が一致していても、閉じる順序が壊れている入力(例:"([)]")を見逃してしまいます。 +
  • +
  • + 閉じ括弧が来たときに「一番最近開かれた、まだ閉じられていない括弧」と比較する必要があり、これには「後入れ先出し」の性質を持つデータ構造(スタック)が必要です。 +
  • +
+
+ +
+
+
+ O(n) +
+
時間計算量
+
+
+
+ O(n) +
+
空間計算量
+
+
+
+ Stack +
+
採用アプローチ
+
+
+
+ 4言語共通 +
+
アルゴリズムの統一度
+
+
+ +
+

📥 入出力例

+
+
+
s = "()[]{}"
+
出力: true
+
+ 3種類の括弧がそれぞれ正しい順序で閉じられているため +
+
+
+
s = "([)]"
+
出力: false
+
+ '(' が閉じられる前に '[' が ')' によって閉じられようとしているため +
+
+
+
+
+ + + + +
+

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

+

+ ここでは特定の言語に依存せず、「文字列を読みながらスタックを操作する」という処理の流れを、 + 代表例 + s = "([)]" + を使って1ステップずつ確認します。 + この考え方は、後の「コード(4言語)」セクションでPython / TypeScript / Go / + Rustのどの実装にも共通する土台になります。 +

+
+
+ + + + +
+

+ 実装(Python / TypeScript / Go / Rust) +

+

+ 4言語とも「開き括弧が来たら対応する閉じ括弧をスタックに積み、閉じ括弧が来たらスタック最上部と直接比較する」という同じアルゴリズムを採用しています。 + タブを切り替えて、それぞれの言語らしい書き方・エラーの扱い方の違いを見比べてみましょう。 +

+
+
+ + + + + +
+ + + + +
+

+ 計算量分析 +

+ +
+

📖 Big-O 記法の読み方

+
+
+
O(1)
+
入力サイズに関わらず
常に一定の速さ
+
+
+
O(n)
+
入力が2倍になると
処理も約2倍
+
+
+
O(n log n)
+
入力が2倍になると
処理は約2倍強
+
+
+
O(n²)
+
入力が2倍になると
処理は約4倍
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
言語 + 採用アルゴリズム + 時間計算量空間計算量実装上の特徴
Pythonスタック法O(n)O(n) + list + をスタックに、dict + で対応表引き +
+ TypeScript + スタック法O(n)O(n) + string[] + をスタックに、readonly + な対応表 +
Goスタック法O(n)O(n) + []byte + をプリアロケーション、switch + で分岐 +
Rustスタック法O(n)O(n) + Vec<char> + を事前確保、Option<char> + で安全に比較 +
+
+ +
+

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

+

+ 文字列 + s + の各文字を1回ずつしか読まないため、時間計算量は文字列長 n に比例する O(n) + になります。 + これより速くすることは、各文字を最低1回は確認する必要がある以上、理論上不可能です。 + 空間計算量については、最悪ケース(全て開き括弧、例:"(((((")ではスタックに全文字が積まれるため O(n) になります。 + 括弧の出現数だけを数えるO(1)空間のアプローチも存在しますが、順序の検証ができず不正解になるため採用していません。 +

+
+
+ + + + +
+

+ 言語間比較 +

+

+ 同じアルゴリズムでも、言語ごとに「メモリの扱い方」や「エラーの表現方法」が異なります。 + ここでは4言語の実装を横並びで比較し、それぞれの言語特有の設計判断を振り返ります。 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
観点PythonTypeScriptGoRust
+ 採用アルゴリズム + スタック法スタック法スタック法スタック法
+ スタックの実体 + list[str]string[][]byte + Vec<char> +
+ メモリ確保の特徴 + 動的拡張(償却O(1)) + 動的拡張(V8内部最適化) + + makeでプリアロケーションし再アロケーション回避 + + with_capacityで事前確保し再アロケーション回避 +
+ エラー表現の方法 + 例外(Exception)undefinedの型レベル表現 + パニックを避ける範囲チェック + + Option<T>による安全な比較 +
+ メモリ管理方式 + + GC(参照カウント+世代別GC) + GC(V8のGC)GC(並行GC) + 所有権システム(GC無し) +
+ 型安全性の担保方法 + + pylance静的解析+実行時isinstance + + コンパイル時の構造的型付け+型ガード + 静的型付け+go vet + コンパイル時の所有権チェック +
+
+ +
+

相違点の背景

+

+ Python・TypeScript・GoはいずれもGC(ガベージコレクション、=使い終わったメモリを自動で回収する仕組み)を持つ言語であり、 + メモリの解放タイミングを意識する必要がありません。一方Rustは所有権システム(=コンパイル時にメモリの管理者を1つに定める仕組み) + でGC無しにメモリ安全性を実現しており、これがエラー表現の違いに最も強く表れています。 + Python/TypeScript/Goが「例外」または「安全な値(undefinedやパニック回避チェック)」でエラーを扱うのに対し、 + RustのOption<T>は「値があるかないか」を型として持つため、 + 呼び出し側は必ずどちらのケースかを意識させられます。 + 本問題はデータ構造・計算量の面で言語ごとの有利不利が生じるタイプの問題ではないため、4言語とも同じスタック法を採用していますが、 + 対応表の型・エラーの扱い方には各言語のイディオム(=その言語で自然とされる書き方)がそれぞれ表れています。 +

+
+
+ + + + +
+

+ 📖 用語集 +

+

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

+
+
+ + イミュータブル + +
+ 一度作られたら中身を変更できない性質のこと。PythonのstrやTypeScript/JavaScriptの文字列はこの性質を持ちます。 +
+
+ +
+ + ガベージコレクション(GC) + +
+ 使い終わったメモリを自動で回収する仕組み。Python・TypeScript(JavaScript)・Goが持つ機能で、プログラマがメモリ解放のタイミングを意識しなくてよくなります。Rustはこの仕組みを持たず、代わりに所有権システムでメモリを管理します。 +
+
+ +
+ + 型ガード(TypeScript) + +
+ typeofin演算子などで実行時に値の型を確認し、その情報をもとにTypeScriptの型を絞り込む仕組みです。JavaScriptには無い、TypeScript独自のコンパイル時チェック機能です。 +
+
+ +
+ + 借用(Rust) + +
+ 所有権を渡さずに値を参照する仕組み。&T(読み取り専用)と&mut T(書き込み可能)があります。 +
+
+ +
+ + 所有権(Rust) + +
+ 値を「誰が管理するか」をコンパイル時に決めるRust独自の仕組み。JavaやPythonのようなガベージコレクタなしで、メモリの安全性をコンパイル時に保証できます。 +
+
+ +
+ + スタック + +
+ 後入れ先出し(LIFO)の構造を持つデータ構造。お皿の積み重ねのように、最後に積んだものを最初に取り出します。本問題では「まだ閉じられていない開き括弧(に対応する閉じ括弧)」を記録するために使います。 +
+
+ +
+ + スライス(Go) + +
+ Goで配列の一部(または全体)を扱うためのデータ構造。[]byteのように書き、動的にサイズを変えられます。 +
+
+ +
+ + 静的解析 + +
+ プログラムを実行せずに、コードを読むだけでバグや型エラーを検出する手法。Pythonのpylance、TypeScriptのコンパイラ、Goのgo + vet、Rustのclippyがそれぞれ担っています。 +
+
+ +
+ + 償却計算量(Amortized + Complexity) + +
+ 個々の操作は時々遅くなることがあっても、多数回の操作全体で平均すると一定時間で済むという計算量の考え方。Pythonのlist.appendの再確保コストはこの考え方で「ならすとO(1)」とみなされます。 +
+
+ +
+ + パニック(Go / Rust) + +
+ GoやRustで回復不能なエラーが発生した際に起きる強制終了。スライスや配列への範囲外アクセスなどで発生します。本問題のコードでは、範囲外アクセスが起きる前に必ず空チェックを行うことで回避しています。 +
+
+ +
+ + プリアロケーション(Go / Rust) + +
+ 必要なメモリをあらかじめまとめて確保しておくこと。Goのmake([]byte, 0, len(s))やRustのVec::with_capacity(s.len())のように書くことで、途中の再確保を防ぎ高速化できます。 +
+
+ +
+ + Big-O記法 + +
+ 入力の大きさに対して、処理にかかる時間やメモリがどう増えるかを表す記法。O(n)なら「入力が2倍になると処理も約2倍になる」ことを意味します。 +
+
+ +
+ + LIFO(Last In, First Out) + +
+ 「最後に入れたものを最初に出す」という、スタックの動作原則。本問題の「一番内側の括弧から閉じる」という性質とぴったり一致します。 +
+
+ +
+ + readonly(TypeScript) + +
+ 配列やオブジェクトの値を変更できないようにする修飾子。JavaScriptには無い、TypeScript独自のコンパイル時チェック機能で、意図しない書き換えを防ぎます。 +
+
+
+
+ +
+ LeetCode 20: Valid Parentheses ― Python / TypeScript / Go / Rust 4言語解説 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html b/public/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html new file mode 100644 index 00000000..905cbc03 --- /dev/null +++ b/public/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html @@ -0,0 +1,1657 @@ + + + + + + + LeetCode 14: Longest Common Prefix - 水平走査法による解説(Python/TypeScript/Go/Rust) + + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +
+

+ 💡 + この問題を一言で言うと:「複数の文字列を先頭から見比べて、全員に共通する最長の接頭辞を取り出す問題」です。 +

+

+ 文字列の配列 strs + が与えられたとき、すべての文字列に共通する最長の接頭辞(先頭部分)を返します。 + 共通する接頭辞が1文字も無ければ、空文字列 "" を返します。 +

+
+ +
+

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

+
    +
  • + 共通接頭辞の長さは、文字列の本数が増えるほど短くなっていく可能性があるため、候補をどう縮めていくかを丁寧に管理する必要があります。 +
  • +
  • + 空文字列や要素数1件などの境界的な入力(エッジケース)を見落とすと、実装が一見正しく動いているように見えても、特定の入力でだけ誤った結果を返してしまいます。 +
  • +
+
+ +
+
+
+ O(S) +
+
時間計算量
+
+
+
+ O(1) +
+
空間計算量
+
+
+
+ 最大200件 各200文字 +
+
入力サイズの制約
+
+
+
+ 水平走査 +
+
採用アプローチ
+
+
+ +
+

入出力例

+
+
+

例1

+

+ strs = [flower, flow, flight] +

+

出力: fl

+

+ なぜ正解か:3つの文字列すべてが f と l + で始まっており、3文字目でそれぞれ o / o / i と分かれるため、 + 共通しているのは先頭2文字の fl までです。 +

+
+
+

例2

+

+ strs = [dog, racecar, car] +

+

+ 出力: (空文字列) +

+

+ なぜ正解か:dog, racecar, car + はそもそも先頭の1文字目からすべて異なる(d, r, c)ため、 + 共通する接頭辞は1文字も存在しません。 +

+
+
+
+
+ + +
+

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

+

+ 代表例 strs = [flower, flow, flight] + を使って、水平走査アルゴリズムがどのように動くかを1ステップずつ確認しましょう。 + 左のリストからステップを選ぶか、「再生」ボタンで自動的に進めることができます。 +

+
+
+ + +
+

+ 実装(Python / TypeScript / Go / Rust) +

+

+ 同じアルゴリズムでも、言語によって書き方や最適化の方向性が変わります。 + タブを切り替えて、4言語それぞれの実装・動作トレース・言語特有のポイントを見比べてみましょう。 +

+
+
+ + +
+

+ 処理フローチャート +

+
+
+ + +
+

+ 計算量分析 +

+ +
+

📖 Big-O 記法の読み方

+
+
+
O(1)
+
入力サイズに関わらず
常に一定の速さ
+
+
+
O(n)
+
入力が2倍になると
処理も約2倍
+
+
+
O(S)
+
全文字数Sに
比例して増加
+
+
+
O(n²)
+
入力が2倍になると
処理は約4倍
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
言語 + 採用アルゴリズム + 時間計算量空間計算量候補の縮め方
Python垂直走査(zip + set)O(S)O(1) + 一致した文字をリストに集め join で結合 +
+ TypeScript + 水平走査(startsWith)O(S)O(1) + slice で新しい文字列に再代入 +
Go + 水平走査(strings.HasPrefix) + O(S)O(1) + スライス式でコピーせず範囲だけ変更 +
Rust水平走査(starts_with)O(S)O(1) + usize の長さのみ更新し文字列は最後に1回だけ生成 +
+
+ +
+

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

+

+ どの言語の実装も、最悪の場合で全文字列の全文字を最大1回ずつ調べる構造になっているため、時間計算量は + O(S)(Sは全文字列の合計文字数)です。 + 空間計算量については、候補の文字列(または候補の長さを表す整数)以外に追加のデータ構造を使わないため、出力用の文字列を除けば + O(1) になります。 +

+
+
+ + +
+

+ 言語間比較 +

+

+ 同じアルゴリズムでも、言語ごとに「メモリの扱い方」や「エラーの表現方法」が異なります。 + ここでは4言語の実装を横並びで比較し、それぞれの言語特有の設計判断を振り返ります。 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
観点PythonTypeScriptGoRust
+ メモリ確保の特徴 + + join で最後に1回だけ結合 + + slice のたびに新しい文字列 + スライス式はコピーなし + 整数更新のみ、最後に1回だけ確保 +
+ エラー表現 + + TypeError / ValueError の例外 + + TypeError / RangeError の例外 + + error 戻り値(提出版は省略) + + Result型が定石だが戻り値固定のため空文字列で表現 +
+ メモリ管理方式 + + GC(ガベージコレクション) + + GC(ガベージコレクション) + + GC(ガベージコレクション) + + 所有権システム(GC不要) +
+ 型安全性の担保方法 + 型ヒント + pylancereadonly + 型ガード明示的な型宣言 + go vet + 所有権システム + コンパイラチェック +
+
+ +
+

相違点の背景

+

+ Python・TypeScript・GoはGC(ガベージコレクション、=使い終わったメモリを自動で回収する仕組み)付きの言語であるため、 + 文字列の縮小操作で新しいオブジェクトが生成されてもGCが後始末をしてくれます。 + 一方Rustにはガベージコレクタが無く、コンパイル時に所有権を追跡する仕組みがあるため、 + 「新しい文字列を作らず整数だけを更新する」という、より低レベルなメモリ効率を意識した設計が自然に選ばれます。 +

+
+
+ + +
+

+ 📖 用語集 +

+

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

+
+
+ + イミュータブル + +
+ 一度作成したら内容を変更できない性質のことです。Python・TypeScript(JavaScript)・Go・Rustのいずれの言語でも文字列はイミュータブルであり、 + 「候補を1文字縮める」処理は実際には新しい範囲を指す文字列(またはスライス)を作り直していることになります。 +
+
+ +
+ + インデックスエラー + +
+ 配列や文字列の範囲外の要素にアクセスしようとしたときに発生するエラーです。 + 例えば空の配列に対して strs[0] + にアクセスすると発生する可能性があるため、多くの実装で最初に空配列のチェックを行います。 +
+
+ +
+ + エッジケース + +
+ 空の配列・要素数1件・全要素が完全一致など、通常とは異なる境界的な条件の入力のことです。 + エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生することがあります。 +
+
+ +
+ + 型ヒント + +
+ 関数の引数や戻り値に型を注釈として書く仕組みです。Pythonでは + List[str] + のように書き、pylance(VSCodeの型チェッカー)が実行前に型の誤りを検出できるようにします。 +
+
+ +
+ + 計算量 + +
+ 入力の大きさに対して、処理にかかる手間(時間計算量)や使用するメモリ量(空間計算量)がどう増えるかの目安です。 + Big-O記法(O(n) など)で表され、この問題では時間計算量 O(S)、空間計算量 + O(1) になります。 +
+
+ +
+ + 借用 + +
+ Rust特有の仕組みで、値の所有権を渡さずにその値を参照することです。 + &str + のように書くことで、Vec<String> + の所有権を保持したまま中身を安全に読み取れます。 +
+
+ +
+ + 垂直走査 + +
+ 複数の文字列を「同じ位置(インデックス)」ごとに縦方向に比較していく方法です。 + Pythonの実装では zip と set + という組み込み関数を組み合わせてこの走査を実現しています。 +
+
+ +
+ + 水平走査 + +
+ 1つの文字列(先頭の文字列)を基準の候補として、他の文字列と丸ごと比較しながら候補を縮めていく方法です。 + TypeScript・Go・Rustの実装ではこちらを採用しています。 +
+
+ +
+ + スライス + +
+ 文字列や配列の一部分(範囲)だけを、コピーを作らずに参照する操作です。 + Go の + s[:i] + や Rust の + &s[..n] + がこれにあたります。 +
+
+ +
+ + 早期終了 + +
+ それ以上調べても答えが変わらないと分かった時点で処理を打ち切ることです。 + この問題では候補が空文字列になった時点で、共通接頭辞が無いと確定するため即座に処理を終えます。 +
+
+ +
+ + 所有権 + +
+ 値を"誰が管理するか"をコンパイル時に決めるRust独自の仕組みです。 + ガベージコレクタを使わずにメモリを安全に管理できる一方、値を渡す際に「借用」か「移動」かを意識する必要があります。 +
+
+ +
+ + パニック + +
+ GoやRustで回復不能なエラーが発生した際に起きる強制終了です。 + 範囲外のインデックスやスライスへアクセスしようとした場合などに発生することがあるため、事前の入力検証が重要になります。 +
+
+
+
+ +
+ LeetCode 14: Longest Common Prefix の解説ページ(Python / TypeScript / Go / Rust + 実装比較) +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index 8c1d0d7b..f078b3c7 100644 --- a/public/index.html +++ b/public/index.html @@ -418,7 +418,7 @@

🧪 Algorithm Study Index

-

183 interactive lessons across 6 domains

+

185 interactive lessons across 6 domains

@@ -433,9 +433,9 @@

- + @@ -483,6 +483,7 @@

  • 🧩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 14: Longest Common Prefix - 水平走査法による解説(Python/TypeScript/Go/Rust)Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/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
  • @@ -516,14 +517,15 @@

  • 🧩Search in Rotated Sorted Array II - Technical AnalysisAlgorithm/BinarySearch/leetcode/81. Search in Rotated Sorted Array II/Claude/README.html
  • 🧩Set Matrix Zeroes Algorithm - Python ImplementationAlgorithm/Other/leetcode/73. Set Matrix Zeroes/Claude/README.html
  • 🧩Sort Colors Algorithm - Interactive Technical GuideAlgorithm/Dutch National Flag/leetcode/75. Sort Colors/Claude/README.html
  • -
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • +
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • 🧩Subsets II - 反復的拡張法による重複排除 | アルゴリズム解説Algorithm/Other/leetcode/90. Subsets II/Claude/README.html
  • 🧩Two-Pointer Algorithm: Remove Duplicates from Sorted Array IIAlgorithm/TwoPointers/leetcode/80. Remove Duplicates from Sorted Array II/Claude/README.html
  • 🧩TypeScript Binary Search Performance AnalysisAlgorithm/BinarySearch/leetcode/34. Find First and Last Position of Element in Sorted Array/READEME-typescript.html
  • 🧩Unique BSTs II - 分割統治+区間メモ化Algorithm/BinarySearch/leetcode/95. Unique Binary Search Trees II/Claude/README.html
  • 🧩Unique Paths II - Dynamic Programming TutorialAlgorithm/DynamicProgramming/leetcode/63. Unique Paths II/Claude/README.html
  • 🧩Unique Permutations Algorithm AnalysisAlgorithm/Backtracking/leetcode/47. Permutations II/Claude/README.html
  • +
  • 🧩Valid Parentheses - LeetCode 20 解説(Python / TypeScript / Go / Rust)Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html
  • 🧩myAtoi Algorithm AnalysisAlgorithm/Other/leetcode/8. String to Integer (atoi)/README.html
  • 🧩りんご購入DPアルゴリズム解析Algorithm/DynamicProgramming/other/To achieve the lowest price/to achieve the lowest price 1/Claude/README.html
  • 🧩りんご購入アルゴリズム詳細解析Algorithm/DynamicProgramming/other/To achieve the lowest price/to achieve the lowest price 3/Claude/README.html
  • @@ -673,6 +675,7 @@

  • 🧩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 14: Longest Common Prefix - 水平走査法による解説(Python/TypeScript/Go/Rust)Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/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
  • @@ -706,14 +709,15 @@

  • 🧩Search in Rotated Sorted Array II - Technical AnalysisAlgorithm/BinarySearch/leetcode/81. Search in Rotated Sorted Array II/Claude/README.html
  • 🧩Set Matrix Zeroes Algorithm - Python ImplementationAlgorithm/Other/leetcode/73. Set Matrix Zeroes/Claude/README.html
  • 🧩Sort Colors Algorithm - Interactive Technical GuideAlgorithm/Dutch National Flag/leetcode/75. Sort Colors/Claude/README.html
  • -
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • +
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • 🧩Subsets II - 反復的拡張法による重複排除 | アルゴリズム解説Algorithm/Other/leetcode/90. Subsets II/Claude/README.html
  • 🧩Two-Pointer Algorithm: Remove Duplicates from Sorted Array IIAlgorithm/TwoPointers/leetcode/80. Remove Duplicates from Sorted Array II/Claude/README.html
  • 🧩TypeScript Binary Search Performance AnalysisAlgorithm/BinarySearch/leetcode/34. Find First and Last Position of Element in Sorted Array/READEME-typescript.html
  • 🧩Unique BSTs II - 分割統治+区間メモ化Algorithm/BinarySearch/leetcode/95. Unique Binary Search Trees II/Claude/README.html
  • 🧩Unique Paths II - Dynamic Programming TutorialAlgorithm/DynamicProgramming/leetcode/63. Unique Paths II/Claude/README.html
  • 🧩Unique Permutations Algorithm AnalysisAlgorithm/Backtracking/leetcode/47. Permutations II/Claude/README.html
  • +
  • 🧩Valid Parentheses - LeetCode 20 解説(Python / TypeScript / Go / Rust)Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README_React.html
  • 🧩myAtoi Algorithm AnalysisAlgorithm/Other/leetcode/8. String to Integer (atoi)/README.html
  • 🧩りんご購入DPアルゴリズム解析Algorithm/DynamicProgramming/other/To achieve the lowest price/to achieve the lowest price 1/Claude/README.html
  • 🧩りんご購入アルゴリズム詳細解析Algorithm/DynamicProgramming/other/To achieve the lowest price/to achieve the lowest price 3/Claude/README.html
  • @@ -851,7 +855,7 @@

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

    + アルゴリズム概要 +

    + +
    +

    + 💡 + この問題を一言で言うと:「複数の文字列を先頭から見比べて、全員に共通する最長の接頭辞を取り出す問題」です。 +

    +

    + 文字列の配列 strs + が与えられたとき、すべての文字列に共通する最長の接頭辞(先頭部分)を返します。 + 共通する接頭辞が1文字も無ければ、空文字列 "" を返します。 +

    +
    + +
    +

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

    +
      +
    • + 共通接頭辞の長さは、文字列の本数が増えるほど短くなっていく可能性があるため、候補をどう縮めていくかを丁寧に管理する必要があります。 +
    • +
    • + 空文字列や要素数1件などの境界的な入力(エッジケース)を見落とすと、実装が一見正しく動いているように見えても、特定の入力でだけ誤った結果を返してしまいます。 +
    • +
    +
    + +
    +
    +
    + O(S) +
    +
    時間計算量
    +
    +
    +
    + O(1) +
    +
    空間計算量
    +
    +
    +
    + 最大200件 各200文字 +
    +
    入力サイズの制約
    +
    +
    +
    + 水平走査 +
    +
    採用アプローチ
    +
    +
    + +
    +

    入出力例

    +
    +
    +

    例1

    +

    + strs = [flower, flow, flight] +

    +

    出力: fl

    +

    + なぜ正解か:3つの文字列すべてが f と l + で始まっており、3文字目でそれぞれ o / o / i と分かれるため、 + 共通しているのは先頭2文字の fl までです。 +

    +
    +
    +

    例2

    +

    + strs = [dog, racecar, car] +

    +

    + 出力: (空文字列) +

    +

    + なぜ正解か:dog, racecar, car + はそもそも先頭の1文字目からすべて異なる(d, r, c)ため、 + 共通する接頭辞は1文字も存在しません。 +

    +
    +
    +
    +
    + + +
    +

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

    +

    + 代表例 strs = [flower, flow, flight] + を使って、水平走査アルゴリズムがどのように動くかを1ステップずつ確認しましょう。 + 左のリストからステップを選ぶか、「再生」ボタンで自動的に進めることができます。 +

    +
    +
    + + +
    +

    + 実装(Python / TypeScript / Go / Rust) +

    +

    + 同じアルゴリズムでも、言語によって書き方や最適化の方向性が変わります。 + タブを切り替えて、4言語それぞれの実装・動作トレース・言語特有のポイントを見比べてみましょう。 +

    +
    +
    + + +
    +

    + 処理フローチャート +

    +
    +
    + + +
    +

    + 計算量分析 +

    + +
    +

    📖 Big-O 記法の読み方

    +
    +
    +
    O(1)
    +
    入力サイズに関わらず
    常に一定の速さ
    +
    +
    +
    O(n)
    +
    入力が2倍になると
    処理も約2倍
    +
    +
    +
    O(S)
    +
    全文字数Sに
    比例して増加
    +
    +
    +
    O(n²)
    +
    入力が2倍になると
    処理は約4倍
    +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    言語 + 採用アルゴリズム + 時間計算量空間計算量候補の縮め方
    Python垂直走査(zip + set)O(S)O(1) + 一致した文字をリストに集め join で結合 +
    + TypeScript + 水平走査(startsWith)O(S)O(1) + slice で新しい文字列に再代入 +
    Go + 水平走査(strings.HasPrefix) + O(S)O(1) + スライス式でコピーせず範囲だけ変更 +
    Rust水平走査(starts_with)O(S)O(1) + usize の長さのみ更新し文字列は最後に1回だけ生成 +
    +
    + +
    +

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

    +

    + どの言語の実装も、最悪の場合で全文字列の全文字を最大1回ずつ調べる構造になっているため、時間計算量は + O(S)(Sは全文字列の合計文字数)です。 + 空間計算量については、候補の文字列(または候補の長さを表す整数)以外に追加のデータ構造を使わないため、出力用の文字列を除けば + O(1) になります。 +

    +
    +
    + + +
    +

    + 言語間比較 +

    +

    + 同じアルゴリズムでも、言語ごとに「メモリの扱い方」や「エラーの表現方法」が異なります。 + ここでは4言語の実装を横並びで比較し、それぞれの言語特有の設計判断を振り返ります。 +

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    観点PythonTypeScriptGoRust
    + メモリ確保の特徴 + + join で最後に1回だけ結合 + + slice のたびに新しい文字列 + スライス式はコピーなし + 整数更新のみ、最後に1回だけ確保 +
    + エラー表現 + + TypeError / ValueError の例外 + + TypeError / RangeError の例外 + + error 戻り値(提出版は省略) + + Result型が定石だが戻り値固定のため空文字列で表現 +
    + メモリ管理方式 + + GC(ガベージコレクション) + + GC(ガベージコレクション) + + GC(ガベージコレクション) + + 所有権システム(GC不要) +
    + 型安全性の担保方法 + 型ヒント + pylancereadonly + 型ガード明示的な型宣言 + go vet + 所有権システム + コンパイラチェック +
    +
    + +
    +

    相違点の背景

    +

    + Python・TypeScript・GoはGC(ガベージコレクション、=使い終わったメモリを自動で回収する仕組み)付きの言語であるため、 + 文字列の縮小操作で新しいオブジェクトが生成されてもGCが後始末をしてくれます。 + 一方Rustにはガベージコレクタが無く、コンパイル時に所有権を追跡する仕組みがあるため、 + 「新しい文字列を作らず整数だけを更新する」という、より低レベルなメモリ効率を意識した設計が自然に選ばれます。 +

    +
    +
    + + +
    +

    + 📖 用語集 +

    +

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

    +
    +
    + + イミュータブル + +
    + 一度作成したら内容を変更できない性質のことです。Python・TypeScript(JavaScript)・Go・Rustのいずれの言語でも文字列はイミュータブルであり、 + 「候補を1文字縮める」処理は実際には新しい範囲を指す文字列(またはスライス)を作り直していることになります。 +
    +
    + +
    + + インデックスエラー + +
    + 配列や文字列の範囲外の要素にアクセスしようとしたときに発生するエラーです。 + 例えば空の配列に対して strs[0] + にアクセスすると発生する可能性があるため、多くの実装で最初に空配列のチェックを行います。 +
    +
    + +
    + + エッジケース + +
    + 空の配列・要素数1件・全要素が完全一致など、通常とは異なる境界的な条件の入力のことです。 + エッジケースを見落とすと、普通のテストは通るのに特定の入力でだけバグが発生することがあります。 +
    +
    + +
    + + 型ヒント + +
    + 関数の引数や戻り値に型を注釈として書く仕組みです。Pythonでは + List[str] + のように書き、pylance(VSCodeの型チェッカー)が実行前に型の誤りを検出できるようにします。 +
    +
    + +
    + + 計算量 + +
    + 入力の大きさに対して、処理にかかる手間(時間計算量)や使用するメモリ量(空間計算量)がどう増えるかの目安です。 + Big-O記法(O(n) など)で表され、この問題では時間計算量 O(S)、空間計算量 + O(1) になります。 +
    +
    + +
    + + 借用 + +
    + Rust特有の仕組みで、値の所有権を渡さずにその値を参照することです。 + &str + のように書くことで、Vec<String> + の所有権を保持したまま中身を安全に読み取れます。 +
    +
    + +
    + + 垂直走査 + +
    + 複数の文字列を「同じ位置(インデックス)」ごとに縦方向に比較していく方法です。 + Pythonの実装では zip と set + という組み込み関数を組み合わせてこの走査を実現しています。 +
    +
    + +
    + + 水平走査 + +
    + 1つの文字列(先頭の文字列)を基準の候補として、他の文字列と丸ごと比較しながら候補を縮めていく方法です。 + TypeScript・Go・Rustの実装ではこちらを採用しています。 +
    +
    + +
    + + スライス + +
    + 文字列や配列の一部分(範囲)だけを、コピーを作らずに参照する操作です。 + Go の + s[:i] + や Rust の + &s[..n] + がこれにあたります。 +
    +
    + +
    + + 早期終了 + +
    + それ以上調べても答えが変わらないと分かった時点で処理を打ち切ることです。 + この問題では候補が空文字列になった時点で、共通接頭辞が無いと確定するため即座に処理を終えます。 +
    +
    + +
    + + 所有権 + +
    + 値を"誰が管理するか"をコンパイル時に決めるRust独自の仕組みです。 + ガベージコレクタを使わずにメモリを安全に管理できる一方、値を渡す際に「借用」か「移動」かを意識する必要があります。 +
    +
    + +
    + + パニック + +
    + GoやRustで回復不能なエラーが発生した際に起きる強制終了です。 + 範囲外のインデックスやスライスへアクセスしようとした場合などに発生することがあるため、事前の入力検証が重要になります。 +
    +
    +
    +
    + +
    + LeetCode 14: Longest Common Prefix の解説ページ(Python / TypeScript / Go / Rust + 実装比較) +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4bd81300e0a5b78b6310bac16aba3d50b16f9376 Mon Sep 17 00:00:00 2001 From: myoshi2891 <96483039+myoshi2891@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:08:16 +0000 Subject: [PATCH 03/26] build: auto-generate public directory --- public/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index f078b3c7..e5358672 100644 --- a/public/index.html +++ b/public/index.html @@ -517,8 +517,8 @@

  • 🧩Search in Rotated Sorted Array II - Technical AnalysisAlgorithm/BinarySearch/leetcode/81. Search in Rotated Sorted Array II/Claude/README.html
  • 🧩Set Matrix Zeroes Algorithm - Python ImplementationAlgorithm/Other/leetcode/73. Set Matrix Zeroes/Claude/README.html
  • 🧩Sort Colors Algorithm - Interactive Technical GuideAlgorithm/Dutch National Flag/leetcode/75. Sort Colors/Claude/README.html
  • -
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • +
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • 🧩Subsets II - 反復的拡張法による重複排除 | アルゴリズム解説Algorithm/Other/leetcode/90. Subsets II/Claude/README.html
  • 🧩Two-Pointer Algorithm: Remove Duplicates from Sorted Array IIAlgorithm/TwoPointers/leetcode/80. Remove Duplicates from Sorted Array II/Claude/README.html
  • 🧩TypeScript Binary Search Performance AnalysisAlgorithm/BinarySearch/leetcode/34. Find First and Last Position of Element in Sorted Array/READEME-typescript.html
  • @@ -709,8 +709,8 @@

  • 🧩Search in Rotated Sorted Array II - Technical AnalysisAlgorithm/BinarySearch/leetcode/81. Search in Rotated Sorted Array II/Claude/README.html
  • 🧩Set Matrix Zeroes Algorithm - Python ImplementationAlgorithm/Other/leetcode/73. Set Matrix Zeroes/Claude/README.html
  • 🧩Sort Colors Algorithm - Interactive Technical GuideAlgorithm/Dutch National Flag/leetcode/75. Sort Colors/Claude/README.html
  • -
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • +
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • 🧩Subsets II - 反復的拡張法による重複排除 | アルゴリズム解説Algorithm/Other/leetcode/90. Subsets II/Claude/README.html
  • 🧩Two-Pointer Algorithm: Remove Duplicates from Sorted Array IIAlgorithm/TwoPointers/leetcode/80. Remove Duplicates from Sorted Array II/Claude/README.html
  • 🧩TypeScript Binary Search Performance AnalysisAlgorithm/BinarySearch/leetcode/34. Find First and Last Position of Element in Sorted Array/READEME-typescript.html
  • From e793a49bf28e6666e397b5863229cdf0e4f9fce0 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sat, 8 Aug 2026 22:42:33 +0900 Subject: [PATCH 04/26] docs(leetcode/20): fix Java Character caching explanation, align Rust BracketError invalid char handling, and narrow TS bracket types --- .../claude sonnet 5/README.md | 26 +++++++++++++------ .../claude sonnet 5/Valid_Parentheses_Java.md | 11 ++++---- .../claude sonnet 5/Valid_Parentheses_Rust.md | 12 +++++---- .../Valid_Parentheses_Typescript.md | 22 +++++++++------- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md index a3dccd8a..c0dc2fba 100644 --- a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md @@ -397,20 +397,25 @@ class BracketError extends Error { // ============================================================ // Readonly を付けることで「一度定義したら変更されるべきではない」ことを // コンパイル時に保証する(うっかり書き換えてしまう事故を防ぐ)。 -const CLOSING_BRACKETS: Readonly> = { +type OpeningBracket = '(' | '[' | '{'; +type ClosingBracket = ')' | ']' | '}'; + +const CLOSING_BRACKETS: Readonly> = { '(': ')', '[': ']', '{': '}', }; -// 型ガード関数:戻り値の型を `param is '(' | '[' | '{'` にすることで、 +const CLOSING_BRACKET_LIST: readonly ClosingBracket[] = [')', ']', '}']; + +// 型ガード関数:戻り値の型を `char is OpeningBracket` にすることで、 // この関数が true を返した後、TypeScript が引数の型を自動的に絞り込んでくれる。 -function isOpeningBracket(char: string): char is '(' | '[' | '{' { +function isOpeningBracket(char: string): char is OpeningBracket { return char in CLOSING_BRACKETS; } -function isClosingBracket(char: string): char is ')' | ']' | '}' { - return Object.values(CLOSING_BRACKETS).includes(char); +function isClosingBracket(char: string): char is ClosingBracket { + return (CLOSING_BRACKET_LIST as readonly string[]).includes(char); } // ============================================================ @@ -429,8 +434,8 @@ type ValidationResult = * @complexity Time: O(n), Space: O(n) */ function validateBrackets(s: string): ValidationResult { - // string[] を「後入れ先出し」のスタックとして使う。 - const stack: string[] = []; + // OpeningBracket[] を「後入れ先出し」のスタックとして使う。 + const stack: OpeningBracket[] = []; // for...of + entries() で文字とインデックスを安全に取得する // (サロゲートペア文字も1文字として正しく扱える)。 @@ -697,6 +702,8 @@ enum BracketError { UnexpectedClose(char), // 文字列を読み終えたのに閉じられていない開き括弧が残っている場合 UnclosedOpen(char), + // 括弧以外の不正な文字が出現した場合 + InvalidCharacter(char), } impl std::fmt::Display for BracketError { @@ -711,6 +718,9 @@ impl std::fmt::Display for BracketError { Self::UnclosedOpen(c) => { write!(f, "開き括弧 '{c}' が閉じられないまま入力が終了しました") } + Self::InvalidCharacter(c) => { + write!(f, "不正な文字 '{c}' が入力されました") + } } } } @@ -762,7 +772,7 @@ fn validate_brackets(s: &str) -> Result<(), BracketError> { } } - _ => unreachable!("制約により括弧文字以外は入力されない"), + _ => return Err(BracketError::InvalidCharacter(c)), } } diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md index c68591fc..88fbe728 100644 --- a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md @@ -239,12 +239,13 @@ class Solution { > char primitive = boxed; // 自動的にアンボクシングされる > ``` > -> **応用**:注意が必要なのは、`Character`同士を`==`で比較する場合です。Javaは`-128`〜`127`の範囲の値をキャッシュして使い回すため、この範囲内では`==`がたまたま`true`になりますが、範囲外では異なるオブジェクトとして扱われ`false`になることがあります。 +> **応用**:注意が必要なのは、`Character`同士を`==`で比較する場合です。Javaは`\u0000`〜`\u007f`(0〜127、ASCII文字範囲)の値をキャッシュして使い回すため(※`Integer`の`-128`〜`127`とは範囲が異なります)、この範囲内では`==`がたまたま`true`になりますが、範囲外では異なるオブジェクトとして扱われ`false`になることがあります。そのため、オブジェクト比較では`equals()`を使うか、`char`へアンボクシングして値比較を行う必要があります。 > ```java -> Character a = 200; -> Character b = 200; -> System.out.println(a == b); // false になりうる(キャッシュ範囲外) -> System.out.println(a.equals(b)); // true(値の比較) +> Character a = '\u0080'; +> Character b = '\u0080'; +> System.out.println(a == b); // false(キャッシュ範囲外のため別参照) +> System.out.println(a.equals(b)); // true(equals による値比較) +> System.out.println((char)a == (char)b); // true(アンボクシングによる値比較) > ``` > > **この問題での使い方**:業務開発版のコードでは`char top = stack.pop();`のように、`pop()`の戻り値(`Character`)を`char`(プリミティブ型)の変数に代入しています。この代入時点でアンボクシングされるため、その後の`top != c`は`Character`同士の参照比較ではなく、確実に値同士の比較になります。この一手間により、括弧記号がキャッシュ範囲内かどうかを気にする必要がなくなります。 diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Rust.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Rust.md index a6120f2f..48440c32 100644 --- a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Rust.md +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Rust.md @@ -107,6 +107,8 @@ enum BracketError { // 文字列を最後まで読み終えたのに、閉じられていない開き括弧が残っている場合 // 例: "(()" → 最後に '(' が1つ余る UnclosedOpen(char), + // 括弧以外の不正な文字が出現した場合 + InvalidCharacter(char), } // Display トレイト(=人間が読める文字列に変換する仕組み)を実装しておくと、 @@ -123,6 +125,9 @@ impl std::fmt::Display for BracketError { Self::UnclosedOpen(c) => { write!(f, "開き括弧 '{c}' が閉じられないまま入力が終了しました") } + Self::InvalidCharacter(c) => { + write!(f, "不正な文字 '{c}' が入力されました") + } } } } @@ -205,11 +210,8 @@ fn validate_brackets(s: &str) -> Result<(), BracketError> { // ことになるので、次のループへ進む。 } - // 制約(constraints)により括弧記号以外の文字は入力されない前提。 - // それでも match は全パターンを網羅する必要があるため、 - // 到達しないはずの分岐として unreachable! を置いておく。 - // これは「起きないはずのバグ」をパニックで検出するための安全弁。 - _ => unreachable!("制約により括弧文字以外は入力されない"), + // 括弧記号以外の文字が入力された場合は不正な文字エラーを返す。 + _ => return Err(BracketError::InvalidCharacter(c)), } } diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md index 5ecc9ee6..f48f894f 100644 --- a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md @@ -114,26 +114,30 @@ class BracketError extends Error { // 代入をしてしまった場合にコンパイルエラーとして検出できる。 // Map ではなくオブジェクトリテラルを使う理由: // キーが3種類だけの固定集合であり、オブジェクトの方が可読性が高いため。 -const CLOSING_BRACKETS: Readonly> = { +type OpeningBracket = '(' | '[' | '{'; +type ClosingBracket = ')' | ']' | '}'; + +const CLOSING_BRACKETS: Readonly> = { '(': ')', '[': ']', '{': '}', }; +const CLOSING_BRACKET_LIST: readonly ClosingBracket[] = [')', ']', '}']; + // 「開き括弧かどうか」を判定するための型ガード関数。 -// 戻り値の型を `param is '(' | '[' | '{'` にすることで、 -// この関数が true を返した後、TypeScriptが param の型を +// 戻り値の型を `char is OpeningBracket` にすることで、 +// この関数が true を返した後、TypeScriptが char の型を // 自動的に絞り込んでくれる(型の絞り込み=Type Narrowing)。 -function isOpeningBracket(char: string): char is '(' | '[' | '{' { +function isOpeningBracket(char: string): char is OpeningBracket { // Object.prototype.hasOwnProperty を直接呼ぶより、 // in 演算子を使う方がシンプルで意図が明確になる return char in CLOSING_BRACKETS; } // 「閉じ括弧かどうか」を判定する型ガード関数。 -// CLOSING_BRACKETS の値(Object.values)に含まれるかどうかで判定する。 -function isClosingBracket(char: string): char is ')' | ']' | '}' { - return Object.values(CLOSING_BRACKETS).includes(char); +function isClosingBracket(char: string): char is ClosingBracket { + return (CLOSING_BRACKET_LIST as readonly string[]).includes(char); } // ============================================================ @@ -159,12 +163,12 @@ type ValidationResult = * @complexity Time: O(n), Space: O(n) */ function validateBrackets(s: string): ValidationResult { - // string[] を「後入れ先出し」のスタックとして使う。 + // OpeningBracket[] を「後入れ先出し」のスタックとして使う。 // TypeScriptの配列は push/pop がどちらもO(1)であり、 // Rustの Vec::with_capacity のような事前確保の仕組みはないが、 // V8エンジン(Node.jsが使うJavaScriptエンジン)が内部的に // 効率よくメモリを管理してくれるため、通常は気にしなくてよい。 - const stack: string[] = []; + const stack: OpeningBracket[] = []; // for...of ループで1文字ずつ走査する。 // index が必要な場合は entries() を使うことで、 From bb7bd2a57b0cd9b877c13a44a1d6ce05c208080c Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sat, 8 Aug 2026 22:43:26 +0900 Subject: [PATCH 05/26] docs(leetcode/14): update Go/TS/Python explanations, remove unreachable React hook guard, and regenerate public html --- .../Sonnet 5/Longest_Common_Prefix_Go.md | 4 ++-- .../Sonnet 5/Longest_Common_Prefix_Python.md | 2 +- .../Sonnet 5/Longest_Common_Prefix_Typescript.md | 2 +- .../leetcode/14. Longest Common Prefix/Sonnet 5/README.md | 8 ++++---- .../14. Longest Common Prefix/Sonnet 5/README_react.html | 5 ----- .../14. Longest Common Prefix/Sonnet 5/README_react.html | 5 ----- 6 files changed, 8 insertions(+), 18 deletions(-) diff --git a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Go.md b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Go.md index 66ebe6a5..369d4742 100644 --- a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Go.md +++ b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Go.md @@ -180,7 +180,7 @@ func (s *Solution) LongestCommonPrefixCompetitive(strs []string) string { prefix := strs[0] // 2番目以降の文字列を順番にチェックする。 - // range はインデックスと値の両方を返すが、値(文字列)だけ使うので i は使わない。 + // 3部構成の for ループ(初期化・条件・更新)を使い、インデックス i で strs[i] にアクセスする。 for i := 1; i < len(strs); i++ { current := strs[i] @@ -303,7 +303,7 @@ func (s *Solution) mainAlgorithm(strs []string) string { - **境界値テスト**(実際のテストコードは別途提供とのことなので、確認すべき代表ケースのみ挙げます) - `[]string{"a"}` → 要素1件:`isEdgeCase`が拾って`"a"`をそのまま返す。空スライスの場合との違いに注意(空スライスは`validateInput`でerrorになる)。 - - `[]string{"", "abc"}` → 空文字列が混じっている:`prefix = ""`になるので1回目のループ内`for`がすぐ`prefix == ""`条件を満たし早期リターンする。空文字列を先頭に置いた場合、`prefix`が最初から空なので、そもそも比較ループに入る前に`""`を返す形になる(`strings.HasPrefix(anything, "")`は常にtrueだが、初期値が空である以上結果も空)。 + - `[]string{"", "abc"}` → 空文字列が混じっている:先頭要素により `prefix` が `""` から始まり、`strings.HasPrefix(current, "")` は常に `true` となるため、`prefix` を縮めるループに入らず処理が進み、最終的に `""` が返る。 - `[]string{"abc", "abc", "abc"}` → 全員完全一致:`prefix`は一度も縮まらず`"abc"`のまま返る。 - `[]string{"dog", "racecar", "car"}` → 先頭文字から不一致:`prefix`が空文字列になるまで縮み`""`が返る。 - `nil`(`nil`スライス)を渡した場合:`len(nil) == 0`となるため`validateInput`が`errors.New`でエラーを返す。`nil`スライスの場合に`strs[0]`へ直接アクセスするとパニック(インデックス範囲外)になるため、検証を先に行うことがGoでは特に重要。 diff --git a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Python.md b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Python.md index d849ff22..1511851d 100644 --- a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Python.md +++ b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Python.md @@ -235,7 +235,7 @@ LeetCodeやAtCoderなど、制限時間内に正解を出すことが目的の - `["abc", "abc", "abc"]` → 全員完全一致 → 最短文字列そのもの`"abc"`が返る - `["dog", "racecar", "car"]` → 先頭文字から不一致 → `""` - 200個・各200文字の文字列がすべて一致 → 計算量O(S)なので最大4万文字でも一瞬 -- **型チェック**: pylance対応の型ヒット(`List[str] -> str`、内部変数`List[str]`)により、静的解析の段階で「文字列以外の要素が混入していないか」を検出できる。実行時には`_validate_input`が`isinstance`チェックでダメ押しする。 +- **型チェック**: pylance対応の型ヒント(`List[str] -> str`、内部変数`List[str]`)により、静的解析の段階で「文字列以外の要素が混入していないか」を検出できる。実行時には`_validate_input`が`isinstance`チェックでダメ押しする。 > 📖 **このセクションで登場した用語** > diff --git a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Typescript.md b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Typescript.md index 4d0c5298..07ffc7fc 100644 --- a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Typescript.md +++ b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/Longest_Common_Prefix_Typescript.md @@ -204,7 +204,7 @@ function longestCommonPrefix(strs: readonly string[]): string { - **境界値テスト**(実際にテストコードは書きませんが、以下のケースで正しく動作することを確認すべきです) - `["a"]` → 要素1件 → エッジケース処理で`"a"`をそのまま返す - - `["", "abc"]` → 空文字列が混じっている → 1回目のループで`prefix`が即座に`""`になり早期終了 + - `["", "abc"]` → 空文字列が混じっている → 先頭要素により `prefix` が `""` から始まり、`startsWith("")` が `true` となるため `prefix` を縮めるループに入らず処理が進み、最終的に `""` が返る - `["abc", "abc", "abc"]` → 全員完全一致 → `prefix`は一度も縮まらず`"abc"`のまま返る - `["dog", "racecar", "car"]` → 先頭文字から不一致 → `""` - 200個・各200文字の文字列がすべて一致 → 計算量O(S)なので最大4万文字でも一瞬で終わる diff --git a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README.md b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README.md index e0ddc865..853c3acb 100644 --- a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README.md +++ b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README.md @@ -578,13 +578,13 @@ prefix = "".join(prefix_chars) 補足:TypeScript版やGo版でも実は近い発想(`slice` によるコピーなしの範囲操作)を使っていますが、Rustでは所有権の制約がより厳格なため、整数管理という形でその発想がさらに徹底されています。 -**Q3. なぜ4言語とも「要素数1件」のケースをわざわざ特別扱いしているのですか? ループに任せればよいのでは?** +**Q3. なぜ Python・TypeScript・Rust の3言語では「要素数1件」のケースを明示的にチェックしているのですか?** -結論:要素数1件のケースもループに任せることは可能ですが、明示的に分岐させることでコードの意図が読み手に伝わりやすくなるため、あえて分けています。 +結論:Python・TypeScript・Rust の3言語では、要素数1件のケースを明示的にアーリーリターンさせることで不要な処理をスキップし、コードの意図を明確にしています。一方、Go言語実装では `1 < len(strs)` のループ条件により、要素数1件の場合はループに入らずそのまま `prefix`(`strs[0]`)を返す構造となっています。 -理由:例えばGo版やRust版で「2番目以降の文字列」をループで処理する構造にしていると、要素が1件しかない場合はそもそもループの中身が一度も実行されません。この場合、先頭の文字列がそのまま返る動作自体は正しいのですが、「なぜ正しいのか」がコードを読むだけでは伝わりにくくなります。 +理由:Python・TypeScript・Rust の実装では、要素数1件のケースで即座に先頭要素を返すことで無用なイテレーションやオブジェクト生成を抑え、エッジケース処理の意図を明示しています。 -補足:可読性を重視する業務開発の文脈では、「暗黙的に正しく動く」コードより「明示的に意図を示す」コードの方が、後から読む人にとって親切です。 +補足:Go言語実装でも `len(strs) == 0` の境界値チェック後に `prefix := strs[0]` とし、`for i := 1; i < len(strs); i++` の3部構成ループに入ります。要素が1件の場合は条件式が偽となりループの中身が一度も実行されずに `prefix` が返るため、各言語の設計スタイルに合致した正確な動作となります。 **Q4. `startsWith` や `HasPrefix` を使わずに、1文字ずつ比較するループを自分で書いてはダメなのですか?** diff --git a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html index 328b7b5f..0e95c8ce 100644 --- a/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html +++ b/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html @@ -888,11 +888,6 @@

    const timerRef = useRef(null); useEffect(() => { if (isPlaying) { - if (activeStep > stepsData.length) { - setIsPlaying(false); - setActiveStep(1); - return; - } timerRef.current = setTimeout(() => { if (activeStep === stepsData.length) { setActiveStep(1); diff --git a/public/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html b/public/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html index 905cbc03..2cdde611 100644 --- a/public/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html +++ b/public/Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html @@ -888,11 +888,6 @@

    const timerRef = useRef(null); useEffect(() => { if (isPlaying) { - if (activeStep > stepsData.length) { - setIsPlaying(false); - setActiveStep(1); - return; - } timerRef.current = setTimeout(() => { if (activeStep === stepsData.length) { setActiveStep(1); From 4b29aa442c545cdfac4567eee0c52b3207b25c2d Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sun, 9 Aug 2026 00:11:16 +0900 Subject: [PATCH 06/26] docs(leetcode/20): fix markdownlint MD031 and clarify non-deterministic Character reference comparison --- .../claude sonnet 5/Valid_Parentheses_Java.md | 5 +++-- public/index.html | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md index 88fbe728..69c731fc 100644 --- a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Java.md @@ -239,11 +239,12 @@ class Solution { > char primitive = boxed; // 自動的にアンボクシングされる > ``` > -> **応用**:注意が必要なのは、`Character`同士を`==`で比較する場合です。Javaは`\u0000`〜`\u007f`(0〜127、ASCII文字範囲)の値をキャッシュして使い回すため(※`Integer`の`-128`〜`127`とは範囲が異なります)、この範囲内では`==`がたまたま`true`になりますが、範囲外では異なるオブジェクトとして扱われ`false`になることがあります。そのため、オブジェクト比較では`equals()`を使うか、`char`へアンボクシングして値比較を行う必要があります。 +> **応用**:注意が必要なのは、`Character`同士を`==`で比較する場合です。Javaは`\u0000`〜`\u007f`(0〜127、ASCII文字範囲)の値をキャッシュして使い回すため(※`Integer`の`-128`〜`127`とは範囲が異なります)、この範囲内では`==`が常に`true`になりますが、範囲外では参照同一性が保証されず、環境やボクシングのされ方によって`true`または`false`になり得ます。そのため、確実に値を比較するには`equals()`を使うか、`char`へアンボクシングして比較を行う必要があります。 +> > ```java > Character a = '\u0080'; > Character b = '\u0080'; -> System.out.println(a == b); // false(キャッシュ範囲外のため別参照) +> System.out.println(a == b); // true または false(参照同一性が保証されないため依存) > System.out.println(a.equals(b)); // true(equals による値比較) > System.out.println((char)a == (char)b); // true(アンボクシングによる値比較) > ``` diff --git a/public/index.html b/public/index.html index e5358672..f078b3c7 100644 --- a/public/index.html +++ b/public/index.html @@ -517,8 +517,8 @@

  • 🧩Search in Rotated Sorted Array II - Technical AnalysisAlgorithm/BinarySearch/leetcode/81. Search in Rotated Sorted Array II/Claude/README.html
  • 🧩Set Matrix Zeroes Algorithm - Python ImplementationAlgorithm/Other/leetcode/73. Set Matrix Zeroes/Claude/README.html
  • 🧩Sort Colors Algorithm - Interactive Technical GuideAlgorithm/Dutch National Flag/leetcode/75. Sort Colors/Claude/README.html
  • -
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • +
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • 🧩Subsets II - 反復的拡張法による重複排除 | アルゴリズム解説Algorithm/Other/leetcode/90. Subsets II/Claude/README.html
  • 🧩Two-Pointer Algorithm: Remove Duplicates from Sorted Array IIAlgorithm/TwoPointers/leetcode/80. Remove Duplicates from Sorted Array II/Claude/README.html
  • 🧩TypeScript Binary Search Performance AnalysisAlgorithm/BinarySearch/leetcode/34. Find First and Last Position of Element in Sorted Array/READEME-typescript.html
  • @@ -709,8 +709,8 @@

  • 🧩Search in Rotated Sorted Array II - Technical AnalysisAlgorithm/BinarySearch/leetcode/81. Search in Rotated Sorted Array II/Claude/README.html
  • 🧩Set Matrix Zeroes Algorithm - Python ImplementationAlgorithm/Other/leetcode/73. Set Matrix Zeroes/Claude/README.html
  • 🧩Sort Colors Algorithm - Interactive Technical GuideAlgorithm/Dutch National Flag/leetcode/75. Sort Colors/Claude/README.html
  • -
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • +
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • 🧩Subsets II - 反復的拡張法による重複排除 | アルゴリズム解説Algorithm/Other/leetcode/90. Subsets II/Claude/README.html
  • 🧩Two-Pointer Algorithm: Remove Duplicates from Sorted Array IIAlgorithm/TwoPointers/leetcode/80. Remove Duplicates from Sorted Array II/Claude/README.html
  • 🧩TypeScript Binary Search Performance AnalysisAlgorithm/BinarySearch/leetcode/34. Find First and Last Position of Element in Sorted Array/READEME-typescript.html
  • From 74c3986401a2695e3ed7ec584f8527aa21b64406 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sun, 9 Aug 2026 00:11:36 +0900 Subject: [PATCH 07/26] docs(leetcode/20): use Object.prototype.hasOwnProperty.call in TypeScript isOpeningBracket to prevent prototype property matches --- .../20. Valid Parentheses/claude sonnet 5/README.md | 2 +- .../claude sonnet 5/Valid_Parentheses_Typescript.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md index c0dc2fba..eb7ff4ca 100644 --- a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/README.md @@ -411,7 +411,7 @@ const CLOSING_BRACKET_LIST: readonly ClosingBracket[] = [')', ']', '}']; // 型ガード関数:戻り値の型を `char is OpeningBracket` にすることで、 // この関数が true を返した後、TypeScript が引数の型を自動的に絞り込んでくれる。 function isOpeningBracket(char: string): char is OpeningBracket { - return char in CLOSING_BRACKETS; + return Object.prototype.hasOwnProperty.call(CLOSING_BRACKETS, char); } function isClosingBracket(char: string): char is ClosingBracket { diff --git a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md index f48f894f..77d71fac 100644 --- a/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md +++ b/Algorithm/BinarySearch/leetcode/20. Valid Parentheses/claude sonnet 5/Valid_Parentheses_Typescript.md @@ -130,9 +130,9 @@ const CLOSING_BRACKET_LIST: readonly ClosingBracket[] = [')', ']', '}']; // この関数が true を返した後、TypeScriptが char の型を // 自動的に絞り込んでくれる(型の絞り込み=Type Narrowing)。 function isOpeningBracket(char: string): char is OpeningBracket { - // Object.prototype.hasOwnProperty を直接呼ぶより、 - // in 演算子を使う方がシンプルで意図が明確になる - return char in CLOSING_BRACKETS; + // in 演算子ではなく hasOwnProperty を使い、 + // プロトタイプ鎖の継承プロパティ(toString等)の誤判定を防ぐ + return Object.prototype.hasOwnProperty.call(CLOSING_BRACKETS, char); } // 「閉じ括弧かどうか」を判定する型ガード関数。 From a04d0d99a6456a0af35f274e7fe737c713a4d88f Mon Sep 17 00:00:00 2001 From: myoshi2891 <96483039+myoshi2891@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:12:54 +0000 Subject: [PATCH 08/26] build: auto-generate public directory --- public/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index f078b3c7..e5358672 100644 --- a/public/index.html +++ b/public/index.html @@ -517,8 +517,8 @@

  • 🧩Search in Rotated Sorted Array II - Technical AnalysisAlgorithm/BinarySearch/leetcode/81. Search in Rotated Sorted Array II/Claude/README.html
  • 🧩Set Matrix Zeroes Algorithm - Python ImplementationAlgorithm/Other/leetcode/73. Set Matrix Zeroes/Claude/README.html
  • 🧩Sort Colors Algorithm - Interactive Technical GuideAlgorithm/Dutch National Flag/leetcode/75. Sort Colors/Claude/README.html
  • -
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • +
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • 🧩Subsets II - 反復的拡張法による重複排除 | アルゴリズム解説Algorithm/Other/leetcode/90. Subsets II/Claude/README.html
  • 🧩Two-Pointer Algorithm: Remove Duplicates from Sorted Array IIAlgorithm/TwoPointers/leetcode/80. Remove Duplicates from Sorted Array II/Claude/README.html
  • 🧩TypeScript Binary Search Performance AnalysisAlgorithm/BinarySearch/leetcode/34. Find First and Last Position of Element in Sorted Array/READEME-typescript.html
  • @@ -709,8 +709,8 @@

  • 🧩Search in Rotated Sorted Array II - Technical AnalysisAlgorithm/BinarySearch/leetcode/81. Search in Rotated Sorted Array II/Claude/README.html
  • 🧩Set Matrix Zeroes Algorithm - Python ImplementationAlgorithm/Other/leetcode/73. Set Matrix Zeroes/Claude/README.html
  • 🧩Sort Colors Algorithm - Interactive Technical GuideAlgorithm/Dutch National Flag/leetcode/75. Sort Colors/Claude/README.html
  • -
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/54. Spiral Matrix/Claude/README.html
  • +
  • 🧩Spiral Matrix Algorithm AnalysisAlgorithm/Other/leetcode/59. Spiral Matrix II/Claude/README.html
  • 🧩Subsets II - 反復的拡張法による重複排除 | アルゴリズム解説Algorithm/Other/leetcode/90. Subsets II/Claude/README.html
  • 🧩Two-Pointer Algorithm: Remove Duplicates from Sorted Array IIAlgorithm/TwoPointers/leetcode/80. Remove Duplicates from Sorted Array II/Claude/README.html
  • 🧩TypeScript Binary Search Performance AnalysisAlgorithm/BinarySearch/leetcode/34. Find First and Last Position of Element in Sorted Array/READEME-typescript.html
  • From d8fe90dc3c7c8078473dda7ead65cd794a481256 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Sun, 9 Aug 2026 13:55:32 +0900 Subject: [PATCH 09/26] docs(leetcode/21): add Sonnet 5 solutions and documentation (Go, Java, Python, Rust, TypeScript) --- .../Sonnet 5/Merge-Two-Sorted-Lists-Go.md | 284 +++++++++++++++++ .../Sonnet 5/Merge-Two-Sorted-Lists-Java.md | 203 ++++++++++++ .../Sonnet 5/Merge-Two-Sorted-Lists-Python.md | 251 +++++++++++++++ .../Sonnet 5/Merge-Two-Sorted-Lists-Rust.md | 294 ++++++++++++++++++ .../Merge-Two-Sorted-Lists-Typescript.md | 218 +++++++++++++ public/index.html | 2 +- 6 files changed, 1251 insertions(+), 1 deletion(-) create mode 100644 DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Go.md create mode 100644 DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Java.md create mode 100644 DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Python.md create mode 100644 DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Rust.md create mode 100644 DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Typescript.md diff --git a/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Go.md b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Go.md new file mode 100644 index 00000000..2b6d5ae1 --- /dev/null +++ b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Go.md @@ -0,0 +1,284 @@ +# LeetCode 21: Merge Two Sorted Lists(Go版) + +## 1. 問題分析結果 + +> 💡 この問題は、一言で言うと「すでに整列された2つの連結リストを、新しいノードを作らずに1つの整列済みリストへ“縫い合わせる”問題」です。 + +**Goで解く際に特に気をつけるべき点**を先にまとめます。この問題最大の落とし穴は、**「`list1` や `list2` が `nil` であることは異常なエラー値ではなく、空リストを表す正常な状態」**という点です。Goでは「呼び出し元が必ずエラーを確認する」という `error` 戻り値の文化がありますが、この問題のシグネチャは `func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode` であり `error` を返す余地がありません。つまり `nil` チェックは「エラー処理」ではなく「アルゴリズムのロジックの一部(終端条件)」として扱う必要があります。また、ゴルーチン(=Goが提供する軽量スレッド)の出番は一切ありません。ノード数は最大50件と小さく、かつ「小さい方を選んでつなぐ」という処理は本質的に逐次的(前のノードを見ないと次の判断ができない)なため、並列化してもオーバーヘッド(=余分にかかる処理コスト)のほうが大きくなります。スライスのアロケーション戦略についても、この問題は連結リストのポインタ操作のみで完結し、スライス(`[]T`)を一切使わないため、プリアロケーションの出番もありません。 + +- **競技プログラミング視点**: 実行速度・メモリ効率最優先 +- **業務開発視点**: 可読性・保守性・型安全性・エラーハンドリング重視 +- **Go特有考慮**: ゴルーチン、チャネル、スライス最適化、エスケープ解析 + +#### 競技プログラミング視点 + +- **制約分析**: ノード数は最大50件×2リストなので、どんなアルゴリズムを選んでも実行時間の心配はほぼありません。ここでは「制約に守られて何でも通る」問題ではなく「正しい設計を学ぶための」問題として扱います。 +- **最速手法**: 各ノードを1回ずつしか見ない反復(ループ)処理が理論上も実装上も最速。時間計算量は O(n+m)(n, mはそれぞれのリスト長)。 +- **メモリ最小化**: 新しいノードを一切生成せず、既存ノードの `Next` フィールドだけを付け替える「スプライシング(splicing、つなぎ替え)」で空間計算量 O(1) を実現します。 +- **Go最適化**: `*ListNode` はすでにポインタなので、関数の引数として渡す際に構造体全体がコピーされることはありません(ポインタという「住所」だけがコピーされる)。これはGoにおける大きな構造体の受け渡しコストを避ける典型的なパターンです。 + +#### 業務開発視点 + +- **型安全設計**: `*ListNode`(ポインタ型)であることそのものが「値が存在するかもしれないし `nil` かもしれない」ことを表現しています。GoにはTypeScriptの `Optional` のような専用構文はありませんが、ポインタ型は本質的に同じ役割(nil許容)を担います。 +- **エラーハンドリング**: 前述の通り、`nil` は異常値ではなく正常な空リストなので、`error` を返したり `panic` を起こしたりする必要はありません。むしろ、`nil` チェックを怠って `list1.Val` にいきなりアクセスすると `nil` ポインタ参照によるパニック(=Goで回復不能なエラーが発生した際に起きる強制終了)が起きるため、**アクセス前に必ず `nil` チェックを行う**という点こそが本質的な安全対策です。 +- **可読性**: ダミーノード(番兵ノード)パターンを使うことで、「リストが空のときの特別扱い」の分岐を書かずに済み、コード全体がシンプルになります。 + +#### Go特有分析 + +- **データ構造選択**: スライス(`[]T`)やマップ(`map[K]V`)は使いません。この問題は自前の連結ノード構造(`*ListNode`)のポインタを直接つなぎ替える問題であり、Go標準コレクションへの変換はメモリ面でも計算量面でも不利になるためです。 +- **標準ライブラリ活用度**: `sort` や `container/heap` を持ち出すこともできますが、単純な2本のポインタ比較で事足りるため、標準ライブラリへの依存はむしろ過剰設計になります。 +- **並行処理適性**: 前述の通り、逐次依存性が強いアルゴリズムのため並行処理には向きません。 +- **エスケープ解析**: 今回のアルゴリズムは新しいノードを1つも生成しない(`&ListNode{}` のような `new` 相当の操作を行わない)ため、ヒープへの新規アロケーションは基本的に発生しません。唯一生成するのはダミーノード1つだけで、これは関数のスコープを超えて戻り値(`dummy.Next` を辿った先)から間接的に参照され続けるため、コンパイラのエスケープ解析(=変数をスタックに置けるかヒープに置くべきかをコンパイラが自動判断する仕組み)によって**ヒープに配置されます**(スタックに置くと関数終了後に消えてしまい、返したポインタが無効になるため)。 + +> 📖 **このセクションで登場した用語** +> +> - **エスケープ解析**:変数をスタック(高速・自動解放)に置くかヒープ(低速・GC管理)に置くかをコンパイラが判断する仕組み +> - **アロケーション**:ヒープ上にメモリを新たに確保する操作。頻繁に行うとGCが頻発して処理が遅くなる +> - **パニック**:Goで回復不能なエラーが発生した際に起きる強制終了。`nil` ポインタへのアクセスなどで発生する +> - **制約分析**:入力サイズの上限から「どのくらいの計算量まで許容されるか」を逆算すること + +--- + +## 2. アルゴリズム比較表 + +> 💡 同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて最適なものを選びます。「Go実装コスト」列は、「新規ノードのアロケーションが起きるか」「不要なヒープ逃げが発生するか」という観点で評価しています。 + +> 💡 **Big-O記法の読み方**(初学者向け) +> | 記法 | 意味 | 直感的イメージ | +> |------|------|--------------| +> | `O(1)` | 入力サイズによらず一定 | 辞書の直引き | +> | `O(n)` | 入力に比例して増加 | リストを端から順に読む | +> | `O(n log n)` | nより少し多く増加 | ソートアルゴリズムの典型 | +> | `O(n²)` | 入力の2乗で増加 | 二重ループの総当たり | + +| アプローチ | 時間計算量 | 空間計算量 | Go実装コスト | 可読性 | 標準ライブラリ活用 | 並行処理適性 | 備考 | +| --------------------------------------------- | ---------------- | ---------- | ------------ | ------ | ------------------ | ------------ | ---------------------------------------------------------------------------- | +| 方法A:反復(ダミーヘッド + 2ポインタ) | O(n+m) | O(1) | 低 | ★★★ | 不要 | 不適 | 推奨。新規アロケーションはダミーノード1個のみ | +| 方法B:再帰(先頭同士を比較して自己呼び出し) | O(n+m) | O(n+m) | 低 | ★★★ | 不要 | 不適 | コールスタックがリスト長に比例して積まれる | +| 方法C:両リストをスライスに集めて`sort.Slice` | O((n+m)log(n+m)) | O(n+m) | 中 | ★★☆ | `sort` | 不適 | すでにソート済みという前提を無駄にし、スライスの再アロケーションも発生しうる | + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **再アロケーション**:スライスの容量が足りなくなったとき、より大きいメモリ領域に全要素をコピーする操作 +> - **コールスタック**:関数呼び出しの「呼び出し履歴」を積んでおくメモリ領域。再帰が深くなるとここに積まれる分だけメモリを消費する + +--- + +## 2. 採用アルゴリズムと根拠 + +- **選択理由**:方法Aは方法Bと同じ O(n+m) の時間計算量を持ちながら、空間計算量は O(1) で済みます。方法B(再帰)はコールスタックにO(n+m)を消費するため、入力サイズが大きくなった場合に「スタックオーバーフロー」に近い問題(Goでは実際にはゴルーチンのスタックは自動拡張されるため即座にクラッシュはしませんが、無駄なメモリ消費であることに変わりはありません)を抱えます。方法Cは `sort.Slice` が内部で効率的にソートしてくれるとしても、「すでにソート済み」という問題の前提を捨てており、時間・空間の両方で無駄があります。 +- **Go最適化戦略**: + - **エスケープ解析への影響**:方法Aではダミーノード1個だけがヒープに逃げます(関数の戻り値として参照され続けるため)。方法Cでは中間スライスもヒープに逃げる可能性が高く、ヒープ確保の総量が増えます。 + - **スライスアロケーション回数**:方法A・Bはスライスを一切使わないためアロケーション回数はゼロ(ダミーノード生成の1回を除く)。方法Cはスライスへの `append` によって再アロケーションが発生する可能性があります。 + - **標準ライブラリとの親和性**:`sort` パッケージは「配列的なコレクションをソートする」場面に強みがありますが、今回のように「2つのソート済み系列をマージする」処理には過剰です。 +- **トレードオフ**:方法Bのほうがコード行数はわずかに短くなりますが、再帰は「入力サイズが将来大きくなる可能性」に対して脆弱です。今回は保守性・堅牢性を優先し、反復(方法A)を採用します。 + +> 📖 **このセクションで登場した用語** +> +> - **モノモーフィゼーション**:ジェネリクス関数が型ごとに専用コードへ自動展開される仕組み。今回は `ListNode` が具体的な型なのでジェネリクスは不要 +> - **トレードオフ**:何かを得ると何かを失う関係。例:速さを得るとメモリが増える +> - **キャッシュ効率**:CPUがデータをキャッシュから素早く読める度合い。連結リストはスライスと違いメモリ上で連続していないため、キャッシュ効率という観点では本来スライスに劣るが、この問題では入力自体が連結リストなので選択の余地がない + +--- + +## 3. 実装パターン + +> 💡 コード全体の骨格: +> +> 1. 結果リストの先頭を仮に指し示す「ダミーノード」を用意する +> 2. `tail`(=これまでにつなぎ終えた部分の末尾を指すポインタ)をダミーノードに合わせておく +> 3. `list1` と `list2` の先頭同士を比較し、小さい方を `tail` の後ろにつなげ、つないだ方のリストと `tail` を1つ進める +> 4. どちらかが `nil` になるまで3を繰り返す +> 5. 片方が尽きたら、残った方(すでにソート済み)をそのまま丸ごとつなげる +> 6. `dummy.Next`(ダミーノードの次)が本当の答えの先頭なので、それを返す + +**【業務開発版を使う場面】** +チームで長期間メンテナンスするプロダクションコードに向きます。`Solution` 構造体にメソッドとして実装し、`nil` が正常な状態であることをコメントで明示することで、後から読んだ人が「なぜここでエラーを返さないのか」を迷わないようにしています。 + +```go +package main + +// Definition for singly-linked list. +// type ListNode struct { +// Val int +// Next *ListNode +// } +type ListNode struct { + Val int + Next *ListNode +} + +// Solution 構造体(LeetCode形式) +// Goにはクラスがないためstructとメソッドを組み合わせる。 +type Solution struct{} + +// MergeTwoLists は2つのソート済み連結リストを1つのソート済み連結リストにマージする(業務開発版)。 +// +// 重要:list1 / list2 が nil であることは異常値ではなく、 +// 「空のリストが渡された」という正常な状態を表す。 +// そのため error は返さず、panic も起こさない。 +// +// 新しいノードは1つ(ダミーノード)を除いて一切作成せず、既存ノードの +// Next フィールドをつなぎ替える(スプライシング)ことでマージするため、 +// 実質的な空間計算量は O(1) となる。 +// +// Time Complexity: O(n + m) +// Space Complexity: O(1) +func (s *Solution) MergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode { + // ダミーノード(番兵ノード)を用意する。 + // このノード自体には意味のある値は入れない(Valは0で仮置き)。 + // なぜダミーノードを使うか: + // 「結果リストがまだ空のとき、先頭をどう扱うか」という + // 特別な分岐を一切書かずに済むため。 + // このノードは関数終了後も dummy.Next を辿って参照され続けるので、 + // エスケープ解析によりヒープに配置される。 + dummy := &ListNode{Val: 0} + + // tail は「これまでにつなぎ終えた部分の末尾ノード」を指すポインタ。 + // 最初はダミーノード自身を指しておく。 + tail := dummy + + // list1、list2 の両方に まだノードが残っている(nilでない)間、 + // 先頭同士の値を比較して小さい方を tail の後ろにつなげる。 + for list1 != nil && list2 != nil { + if list1.Val <= list2.Val { + // list1の先頭値のほうが小さい(同値含む)ので + // list1の現在ノードを結果リストの末尾につなげる + tail.Next = list1 + // list1を1つ次に進める(つないだノードは処理済みなので進める) + list1 = list1.Next + } else { + // list2の先頭値のほうが小さいので list2側をつなげる + tail.Next = list2 + list2 = list2.Next + } + // tailを、たった今つないだノードの位置まで進める + tail = tail.Next + } + + // ループを抜けた時点で、list1 か list2 のどちらか(または両方)が + // nil(尽きた状態)になっている。 + // 残っている側はすでにソート済みなので、1つずつ比較する必要はなく、 + // そのまま丸ごと tail の後ろにつなげてよい。 + if list1 != nil { + tail.Next = list1 + } else { + tail.Next = list2 + } + + // dummyノード自体はデータを持たない仮のノードなので、 + // 実際の答えの先頭は dummy.Next である + return dummy.Next +} +``` + +**【競技プログラミング版を使う場面】** +LeetCodeの制限時間内に正解を出すことが目的のコードに向きます。コメントを最小限にし、`if list1 != nil { ... } else { ... }` の代わりに三項演算子のない Go でも簡潔に書けるパターンを使っています(Goには三項演算子がないため、この程度の分岐が最短表現です)。 + +```go +package main + +func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode { + // ダミーヘッド + 2ポインタ。O(n+m) / O(1)。 + dummy := &ListNode{} + tail := dummy + for list1 != nil && list2 != nil { + if list1.Val <= list2.Val { + tail.Next, list1 = list1, list1.Next + } else { + tail.Next, list2 = list2, list2.Next + } + tail = tail.Next + } + // 残った方(nilでない方)をまとめてつなぐ。 + // Goには三項演算子がないため、この if 文が最短表現。 + if list1 != nil { + tail.Next = list1 + } else { + tail.Next = list2 + } + return dummy.Next +} +``` + +> 💡 **業務開発版と競技版の違いのポイント**: +> 競技版では `tail.Next, list1 = list1, list1.Next` のように**多重代入**(=複数の変数へ同時に値を代入するGoの構文)を使い、右辺がすべて評価されてから左辺に代入されるという性質を利用して1行にまとめています。業務開発版ではあえて2行に分けて書くことで、「まずつなぐ」「次に進める」という処理の意図を読み手に明示しています。どちらも動作は同一です。 + +> 💡 **コードの動作トレース**(初学者向け) +> +> 入力:`list1 = [1,2,4]`、`list2 = [1,3,4]` +> +> ``` +> 初期状態: dummy -> (何も繋がっていない) +> tail = dummy +> list1 = [1,2,4], list2 = [1,3,4] +> +> Step 1: list1.Val(1) <= list2.Val(1) → true → list1の"1"をつなぐ +> dummy -> 1(list1由来) +> list1 は次の "2" へ、tail はつないだ "1" へ移動 +> +> Step 2: list1.Val(2) vs list2.Val(1) → list2の"1"のほうが小さい → list2をつなぐ +> dummy -> 1 -> 1(list2由来) +> list2 は次の "3" へ +> +> Step 3: list1.Val(2) <= list2.Val(3) → list1の"2"をつなぐ +> dummy -> 1 -> 1 -> 2 +> list1 は次の "4" へ +> +> Step 4: list1.Val(4) vs list2.Val(3) → list2の"3"のほうが小さい → list2をつなぐ +> dummy -> 1 -> 1 -> 2 -> 3 +> list2 は次の "4" へ +> +> Step 5: list1.Val(4) <= list2.Val(4) → list1の"4"をつなぐ(同値はlist1優先) +> dummy -> 1 -> 1 -> 2 -> 3 -> 4 +> list1 は nil になる(尽きた) +> +> Step 6: for条件 (list1 != nil && list2 != nil) が false → ループ終了 +> list1 == nil なので、残っている list2(= [4])を丸ごとつなぐ +> dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 +> +> Step 7: dummy.Next を返す → [1,1,2,3,4,4](期待通りの出力) +> ``` + +> 📖 **このセクションで登場した用語** +> +> - **多重代入**:`a, b = b, a` のように複数の変数へ同時に値を代入するGoの構文。右辺はすべて評価されてから左辺へ代入されるため、一時変数を使わずに値の入れ替えができる +> - **レシーバ**:メソッドが属する型のインスタンス。`func (s *Solution) MergeTwoLists(...)` の `s` の部分 +> - **ポインタレシーバ `*T`**:今回の `*Solution` のように、メソッドのレシーバに `*` を付けること。ただし今回は `Solution` 自体は空の構造体で状態を持たないため、値レシーバでも動作は変わらない(LeetCodeの慣習に合わせてポインタレシーバを使用) + +--- + +## 4. 検証 + +> 💡 エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。境界値の確認は、アルゴリズムが"ふつうの入力"だけでなく"極端な入力"でも正しく動くかを確かめるために重要です。 + +- **境界値テスト(頭の中でのトレース確認)**: + - `list1=nil, list2=nil`(Example 2):**なぜ問題になりうるか**:もし `nil` チェックを怠って `list1.Val` にいきなりアクセスしていたら、Goの `nil` ポインタ参照によりパニックが発生していた。今回のコードでは `for` 条件が最初から偽(両方 `nil`)になるため即座にループを抜け、`tail.Next = list2`(`nil`)となり、`dummy.Next` も `nil` のまま。結果は `nil`(空リスト)で正しい。 + - `list1=nil, list2=[0]`(Example 3):**なぜ問題になりうるか**:`list1` が `nil` の状態で `list1.Val` を比較しようとするとパニックになる。今回のコードでは `for` 条件が最初から偽(`list1` が `nil`)になるため即座にループを抜け、`list2`(`[0]`)がそのままつながる。結果は `[0]` で正しい。 + - 片方が他方より大幅に長い場合(例:`list1=[1]`, `list2=[2,3,4,5]`):**なぜ問題になりうるか**:ループを抜けた後も1件ずつ比較を続けようとすると、すでに `nil` になった側の `.Val` にアクセスしてパニックになる。今回のコードは `list1 != nil` の分岐で「残っている側を丸ごとつなぐ」ため、この問題は起きない。 + - 同値が並ぶ場合(例:`list1=[1,1]`, `list2=[1,1]`):`<=` の比較により、同値のときは常に `list1` 側が先に採用される。安定した挙動になっており、順序の破綻はない。 + +- **型チェック(`go vet` / `golangci-lint` 対応)**:`*ListNode` というポインタ型を明示的に使用し、`nil` 比較は `!= nil` / `== nil` の明示的な形で記述しているため、`go vet` の未使用変数・到達不能コードチェックに引っかかる箇所はありません。関数・メソッドの引数と戻り値の型もすべて明示しています。 +- **並行安全性**:ゴルーチンを一切使用していないため、データ競合(Race Condition)は原理的に発生しません。`go test -race` を実行する必要もありません。 + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること +> - **`go vet`**:コンパイルは通るが怪しいコード(未使用変数、到達不能コードなど)を検出する静的解析ツール +> - **データ競合(Race Condition)**:複数ゴルーチンが同じメモリを同時に読み書きするとき結果が不定になる問題 + +--- + +## 5. 提出前チェックリスト + +- [x] `go vet ./...` でエラーなし(`nil` 比較・型はすべて明示的) +- [x] エラーハンドリング:本問題では `nil` が正常値であるため `error` 戻り値は不要と判断(コメントで明示) +- [x] メモリ効率:新規アロケーションはダミーノード1個のみ。スライス等の一時オブジェクトは使用していない +- [x] 並行安全性:ゴルーチンを使用していないため対象外 +- [x] エッジケース:空リスト同士、片方が空、長さの異なるリスト、同値のケースをすべてトレースで確認済み +- [x] 命名規則:`MergeTwoLists`(エクスポート、業務版)/ `mergeTwoLists`(非エクスポート、競技版・LeetCode提出用)、レシーバ名 `s` + +**計算量まとめ**:時間計算量 O(n+m)、空間計算量 O(1)(n, m はそれぞれ list1, list2 のノード数。ダミーノード1個分の定数アロケーションを除く)。 diff --git a/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Java.md b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Java.md new file mode 100644 index 00000000..63da43fe --- /dev/null +++ b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Java.md @@ -0,0 +1,203 @@ +# LeetCode 21: Merge Two Sorted Lists + +## 1. 問題の分析 + +> 💡 この問題は、一言で言うと「すでに整列された2つの連結リストを、新しいノードを作らずに1つの整列済みリストへ“縫い合わせる”問題」です。 + +**競技プログラミング視点での分析** + +- 実行速度を最優先するなら、各ノードを1回ずつ見るだけで済む**反復(ループ)処理**が最速。O(n+m) の1パスで完結し、余計な比較や再帰呼び出しのオーバーヘッドがない。 +- メモリ使用量の最小化方針としては、**新しいノードを一切作らず、既存ノードの `next` 参照だけを付け替える**(この操作を「スプライシング(splicing、つなぎ替え)」と呼ぶ)。配列に変換してからソートするような方法は、リストがすでにソート済みという情報を無駄にしてしまう。 + +**業務開発視点での分析** + +- 型安全性の面では、この問題は `ListNode.val` が `int` に固定されているLeetCode標準シグネチャなので、ジェネリクス(=型をパラメータとして受け取れる仕組み)を導入する必要は**ありません**。もし「任意の `Comparable` な値を持つノードをマージする汎用ライブラリ」を作るなら `>` を使う場面ですが、今回はメソッドシグネチャが固定されているため、無理に汎用化するとかえって可読性が落ちます。 +- エラーハンドリングで注意すべき点:**`list1` や `list2` が `null` であることは「異常な入力」ではなく「空のリストを表す正常な状態」**です。Example 2 (`list1 = [], list2 = []`) がまさにそれです。テンプレートでよくある「`null` なら即 `IllegalArgumentException`」という防御的プログラミングをそのまま適用すると、この問題では**正しい入力を誤ってエラー扱いしてしまう**ので注意が必要です。 + +**Java特有の考慮点** + +- 静的型付けにより、`list1.val` は必ず `int` であることがコンパイル時に保証されており、実行時の型チェックは不要。 +- `ListNode` はJava標準の `java.*` に属するクラスではなく、問題側で定義されたクラスなので、`Optional` を挟むよりも「`null` はリストの終端(または空リスト)を意味する」という連結リストの一般的な慣習に従うほうが、他のJavaコードとの一貫性が高く読みやすくなります。 + +> 📖 **このセクションで登場した用語** +> +> - **スプライシング(splicing)**:新しいノードを作らず、既存のノードの参照(`next`)をつなぎ替えることでデータ構造を組み替える操作。メモリのコピーが発生しないため効率的。 +> - **`NullPointerException`**:`null` の参照に対してメソッド呼び出しやフィールドアクセスを行おうとした際に発生する実行時例外。今回は `list1.val` にアクセスする前に必ず `list1 != null` を確認することで防ぐ。 +> - **防御的プログラミング**:呼び出し元が誤った値を渡してもプログラムが壊れないように、事前に入力を検証する設計方針。ただし今回のように「`null` = 正常な空リスト」の場合は過剰な検証がバグのもとになる。 + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ問題でも解き方は複数あります。「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」、そしてJavaらしい実装コストを比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | Java実装コスト | 型安全性 | 可読性 | 備考 | +| --------------------------------------------- | ----------------- | ---------- | -------------- | -------- | ------ | ------------------------------------------ | +| 方法A:反復(ダミーヘッド + 二本のポインタ) | O(n+m) | O(1) | 低 | 高 | 高 | 新規ノード生成なし。推奨 | +| 方法B:再帰(先頭同士を比較して再帰呼び出し) | O(n+m) | O(n+m) | 低 | 高 | 高 | コールスタックがリスト長に比例して積まれる | +| 方法C:全ノードを配列/リストに集めてソート | O((n+m) log(n+m)) | O(n+m) | 中 | 中 | 中 | すでにソート済みという前提を無駄にする | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の手間で済む(最速・最小) +> - `O(n+m)`:2つの入力の合計サイズに比例して手間が増える(線形) +> - `O((n+m) log(n+m))`:ソートに典型的な計算量。入力が増えるとやや急激に手間が増える + +> 📖 **このセクションで登場した用語** +> +> - **再帰(recursion)**:関数が自分自身を呼び出す処理方式。「小さい問題に分解して、その答えを組み合わせる」考え方に向いている。 +> - **コールスタック**:関数呼び出しの「呼び出し履歴」を積んでおくメモリ領域。再帰が深くなるとここに積まれる分だけメモリを消費し、深すぎると `StackOverflowError` が起きる。 + +--- + +## 3. 選択したアルゴリズムと理由 + +> 💡 選んだ理由を、他の方法と対比しながら説明します。 + +- **選択したアプローチ**:方法A(ダミーヘッドを使った反復処理) +- **理由**: + - **計算量的な優位性**:方法B(再帰)と同じ O(n+m) の時間計算量でありながら、空間計算量は O(1) で済む(方法Bはコールスタックに O(n+m) 消費する)。 + - **Java環境での安全性**:制約上ノード数は最大50なので方法Bでも `StackOverflowError` の危険は事実上ないが、実務コードとして「入力サイズに依存してスタックが伸びる」設計は将来的な拡張(例:ノード数が数万に増える)に対して脆い。方法Aはその制約自体が存在しない。 + - **保守性・可読性**:ループ1本で完結し、条件分岐もシンプル。デバッガでも1行ずつ追いやすい。 + - **方法Cを選ばなかった理由**:わざわざ配列に集めてソートするのは、「すでにソート済み」という問題の前提を無視した回り道であり、時間計算量も余分な O(log(n+m)) がかかるうえ、空間計算量も O(n+m) と余計にメモリを使う。 +- **Java特有の最適化ポイント**: + - `ListNode` はプリミティブ型 `int` を直接フィールドに持つ設計(`Integer` へのボクシングが発生しない)ので、比較 `list1.val <= list2.val` は最初からボクシングコストなしで行える。 + - ダミーノード(番兵ノード)パターンを使うことで、「リストが空の場合の特別扱い」という条件分岐(`if (head == null) { ... }` のような処理)を1つも書かずに済み、コンパイル時にもコードパスが単純になる。 + +> 📖 **このセクションで登場した用語** +> +> - **ダミーヘッド(番兵ノード)**:リストの先頭に、実際のデータを持たない仮のノードを1つ置いておくテクニック。「リストが空かどうか」で処理を分岐させる必要がなくなる。 +> - **ボクシング**:`int` のようなプリミティブ型を `Integer` のようなラッパークラスのオブジェクトに変換すること。オブジェクト生成のオーバーヘッドが発生するため、大量のデータを扱う場面では避けたい。今回の `ListNode.val` は `int` のままなので発生しない。 + +--- + +## 4. 実装コード + +> 💡 コード全体の骨格: +> +> 1. 結果リストの先頭を仮に指し示す「ダミーノード」を用意する +> 2. `list1` と `list2` の先頭同士を比較し、小さい方を結果リストの末尾につなげる。つないだ方のリストを1つ進める +> 3. どちらかが尽きるまで2を繰り返す +> 4. 片方が尽きたら、残った方(すでにソート済み)をそのまま丸ごとつなげる +> 5. ダミーノードの次(`dummy.next`)が本当の答えの先頭なので、それを返す + +```java +/** + * Definition for singly-linked list. + * public class ListNode { + * int val; + * ListNode next; + * ListNode() {} + * ListNode(int val) { this.val = val; } + * ListNode(int val, ListNode next) { this.val = val; this.next = next; } + * } + */ +class Solution { + + /** + * 2つのソート済み連結リストを1つのソート済み連結リストにマージする。 + * + * 新しいノードは一切作成せず、既存ノードの next 参照をつなぎ替える + * (スプライシング)ことでマージするため、空間計算量は O(1) となる。 + * + * @param list1 1つ目のソート済み連結リストの先頭ノード。 + * null の場合は「空のリスト」を表す(異常値ではない) + * @param list2 2つ目のソート済み連結リストの先頭ノード。 + * null の場合は「空のリスト」を表す(異常値ではない) + * @return マージ後のソート済み連結リストの先頭ノード + * (両方が空リストの場合は null を返す) + */ + public ListNode mergeTwoLists(ListNode list1, ListNode list2) { + // ダミーノード(番兵ノード)を用意する。 + // このノード自体には意味のある値は入れない(値は0で仮置き)。 + // こうすることで「結果リストがまだ空のとき、先頭をどう扱うか」 + // という特別な分岐を書かずに済む。 + ListNode dummy = new ListNode(0); + + // tail は「これまでにつなぎ終えた部分の末尾ノード」を指す変数。 + // 最初はダミーノード自身を指しておく。 + ListNode tail = dummy; + + // list1、list2 の両方にまだノードが残っている間、 + // 先頭同士の値を比較して小さい方を tail の後ろにつなげる。 + while (list1 != null && list2 != null) { + if (list1.val <= list2.val) { + // list1の先頭値のほうが小さい(同値含む)ので、 + // list1の現在ノードを結果リストの末尾につなげる + tail.next = list1; + // list1を1つ次に進める(つないだノードは処理済みなので進める) + list1 = list1.next; + } else { + // list2の先頭値のほうが小さいので、list2側をつなげる + tail.next = list2; + list2 = list2.next; + } + // tailを、たった今つないだノードの位置まで進める + tail = tail.next; + } + + // ループを抜けた時点で、list1 か list2 のどちらか(または両方)が + // null(尽きた状態)になっている。 + // 残っている側はすでにソート済みなので、1つずつ比較する必要はなく、 + // そのまま丸ごと tail の後ろにつなげてよい。 + // 三項演算子:list1が残っていればlist1を、そうでなければlist2をつなげる + tail.next = (list1 != null) ? list1 : list2; + + // dummyノード自体はデータを持たない仮のノードなので、 + // 実際の答えの先頭は dummy.next である + return dummy.next; + } +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> 入力:`list1 = [1,2,4]`、`list2 = [1,3,4]` +> +> ``` +> 初期状態: dummy -> (何も繋がっていない) +> tail = dummy +> list1 = [1,2,4], list2 = [1,3,4] +> +> Step 1: list1.val(1) <= list2.val(1) は true → list1の"1"をつなぐ +> 結果: dummy -> 1(from list1) +> list1は次の "2" に進む、tailは今つないだ "1" に進む +> +> Step 2: list1.val(2) vs list2.val(1) → list2の"1"のほうが小さい → list2をつなぐ +> 結果: dummy -> 1 -> 1(from list2) +> list2は次の "3" に進む、tailは今つないだ "1" に進む +> +> Step 3: list1.val(2) <= list2.val(3) → list1の"2"をつなぐ +> 結果: dummy -> 1 -> 1 -> 2 +> list1は次の "4" に進む +> +> Step 4: list1.val(4) vs list2.val(3) → list2の"3"のほうが小さい → list2をつなぐ +> 結果: dummy -> 1 -> 1 -> 2 -> 3 +> list2は次の "4" に進む +> +> Step 5: list1.val(4) <= list2.val(4) → list1の"4"をつなぐ(同値はlist1優先) +> 結果: dummy -> 1 -> 1 -> 2 -> 3 -> 4 +> list1はnullになる(尽きた) +> +> Step 6: while条件 (list1 != null && list2 != null) が false になりループ終了 +> list1 == null なので、残っている list2(= [4])を丸ごとつなぐ +> 結果: dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 +> +> Step 7: dummy.next を返す → [1,1,2,3,4,4](期待通りの出力) +> ``` + +> 📖 **このセクションで登場した用語** +> +> - **三項演算子(`条件 ? A : B`)**:`if-else` を1行で書くための構文。「条件がtrueならA、falseならB」という値を返す。今回は「list1が残っていればlist1を、そうでなければlist2を」という選択に使っている。 +> - **`this.next`(連結リストの `next` 参照)**:あるノードから次のノードへの「つながり」を表すフィールド。これを付け替えることでリストの構造そのものを変更できる。 + +--- + +## 5. 制約条件の確認 + +- 外部ライブラリは使用していません(`ListNode` 以外は言語組み込みの機能のみ)。 +- 追加のノードを1つも生成していないため、メモリ使用量は入力サイズによらず一定(O(1))です。 +- `-Xlint:all` で警告が出るような未チェックキャストや生型(raw type)の使用はありません。 +- クラス名 `Solution`・`ListNode` はパスカルケース、変数名 `list1`/`list2`/`dummy`/`tail` はキャメルケース(またはそれに準じる慣習的な命名)です。 + +**計算量まとめ**:時間計算量 O(n+m)、空間計算量 O(1)(n, m はそれぞれ list1, list2 のノード数)。 diff --git a/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Python.md b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Python.md new file mode 100644 index 00000000..01e69552 --- /dev/null +++ b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Python.md @@ -0,0 +1,251 @@ +# LeetCode 21: Merge Two Sorted Lists(Python版) + +## 1. 問題分析結果 + +> 💡 この問題は、一言で言うと「すでに整列された2つの連結リストを、新しいノードを作らずに1つの整列済みリストへ“縫い合わせる”問題」です。 + +**CPython特有の注意点を先にまとめます**:この問題は再帰で書くと非常に短くコンパクトになりますが、CPythonの再帰にはデフォルトで約1000回という呼び出し回数の上限(`sys.getrecursionlimit()`)があります。今回は制約上ノード数が最大50なので問題になりませんが、「短く書けるから」という理由だけで安易に再帰を選ぶと、入力サイズが変わったときに `RecursionError`(=再帰の呼び出し回数が上限を超えたときに発生する例外)を引き起こすリスクがある、という点は覚えておくべきです。 + +### 競技プログラミング視点 + +- **制約分析**:ノード数は最大50件×2リストなので、どんなアルゴリズムを選んでも実行時間の心配はほぼありません。ここでは「制約に守られて何でも通る」問題ではなく「正しい設計を学ぶための」問題として扱います。 +- **最速手法**:各ノードを1回ずつしか見ない反復(ループ)処理が理論上も実装上も最速です。時間計算量は O(n+m)(n, mはそれぞれのリストの長さ)。 +- **メモリ最小化**:新しいノードを一切生成せず、既存ノードの `next` 参照だけを付け替える「スプライシング(splicing、つなぎ替え)」で空間計算量 O(1) を実現します。 +- **CPython最適化**:この問題はリストや辞書のような組み込みコレクションを扱うわけではなく、自前の連結ノード構造を1つずつ処理する問題なので、`sum()` や内包表記のようなC実装の恩恵をそのまま活かせる場面は限定的です。最大の最適化は「余計なオブジェクトを作らないこと」そのものです。 + +#### 業務開発視点 + +- **型安全設計**:`Optional[ListNode]`(= `ListNode` または `None` のどちらかであることを表す型ヒント)を使い、「リストが空である」という状態を型レベルで明示します。 +- **エラーハンドリング**:この問題における最大の注意点は、**「`list1` や `list2` が `None` であることは異常値ではなく、空リストを表す正常な状態」**という点です。テンプレートによくある「入力が空ならエラー」というパターンをそのまま当てはめると、Example 2(`list1=[], list2=[]`)のような正しい入力を誤って拒否してしまいます。 +- **可読性**:ダミーノード(番兵ノード)パターンを使うことで、「リストが空のときの特別扱い」の分岐を書かずに済み、コード全体がシンプルになります。 + +#### Python特有分析 + +- **データ構造選択**:`list`(Python標準のリスト型)や `deque`(両端キュー)は使いません。この問題は自前の連結リスト構造(`ListNode`)を直接操作する問題であり、Python標準コレクションへの変換はメモリ面でも計算量面でも不利になるためです。 +- **標準ライブラリ活用度**:`heapq` や `collections` を持ち出すこともできますが、単純な2本のポインタ比較で事足りるため、標準ライブラリへの依存はむしろ過剰設計(オーバーエンジニアリング)になります。 +- **CPython最適化度**:この問題の最適化の本質は「アルゴリズム自体を O(n+m) に保つこと」であり、組み込み関数の活用余地は小さいです。 + +> 📖 **このセクションで登場した用語** +> +> - **CPython**:最も広く使われるPythonの実装。C言語で書かれており、組み込み関数の多くがC実装のため高速 +> - **`RecursionError`**:再帰関数の呼び出しが深くなりすぎて、Pythonの再帰呼び出し上限(デフォルト約1000回)を超えたときに発生する例外 +> - **スプライシング(splicing)**:新しいノードを作らず、既存ノードの参照をつなぎ替えることでデータ構造を組み替える操作 +> - **`Optional[ListNode]`**:`ListNode` または `None` のどちらかであることを表す型ヒント。`Union[ListNode, None]` の短縮形 + +--- + +## 2. 採用アルゴリズムと根拠 + +> 💡 同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」、そしてPythonらしい実装コストを比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 | +| ------------------------------------------------- | ---------------- | ---------- | ---------------- | ------ | ------------------ | ------------- | ------------------------------------------ | +| 方法A:反復(ダミーヘッド + 2ポインタ) | O(n+m) | O(1) | 低 | ★★★ | 不要 | 適 | 推奨。再帰呼び出しのオーバーヘッドがない | +| 方法B:再帰(先頭同士を比較して自己呼び出し) | O(n+m) | O(n+m) | 低 | ★★★ | 不要 | 不適 | コードは短いがコールスタックを消費 | +| 方法C:両リストを`list`に集めて`sorted()`でソート | O((n+m)log(n+m)) | O(n+m) | 中 | ★★☆ | `sorted()` | 適(C実装) | すでにソート済みという前提を無視した回り道 | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n+m)`:2つの入力の合計サイズに比例して処理が増える(線形) +> - `O((n+m) log(n+m))`:ソートに典型的な計算量。入力が増えるとやや急激に手間が増える + +- **選択理由**:方法Aは方法Bと同じ O(n+m) の時間計算量を持ちながら、空間計算量は O(1) で済みます(方法Bはコールスタック=関数呼び出しの履歴を積んでおくメモリ領域にO(n+m)を消費します)。方法Cは `sorted()` がC実装で高速だとしても、「すでにソート済み」という問題の前提を捨ててしまっており、時間・空間の両方で無駄があります。 +- **Python最適化戦略**:ダミーヘッド(番兵ノード)を使い、`while` ループ中で `<=` による単純比較のみを行います。オブジェクト生成を一切行わないため、ガベージコレクション(=使われなくなったオブジェクトを自動的に片付ける仕組み)の負荷も最小限に抑えられます。 +- **トレードオフ**:方法Bのほうがコード行数はわずかに短くなりますが、再帰は「入力サイズが将来大きくなる可能性」に対して脆弱です。今回は保守性・堅牢性を優先し、反復(方法A)を採用します。 + +> 📖 **このセクションで登場した用語** +> +> - **コールスタック**:関数呼び出しの「呼び出し履歴」を積んでおくメモリ領域。再帰が深くなるとここに積まれる分だけメモリを消費する +> - **ガベージコレクション(GC)**:もう使われなくなったオブジェクトのメモリを自動的に解放してくれるPythonの仕組み。オブジェクト生成を減らすほどGCの負荷も減る +> - **ダミーヘッド(番兵ノード)**:リストの先頭に、実データを持たない仮のノードを1つ置いておくテクニック。「リストが空かどうか」で処理を分岐させる必要がなくなる + +--- + +## 3. 実装パターン + +> 💡 コード全体の骨格: +> +> 1. 結果リストの先頭を仮に指し示す「ダミーノード」を用意する +> 2. `tail`(=これまでにつなぎ終えた部分の末尾を指す変数)をダミーノードに合わせておく +> 3. `list1` と `list2` の先頭同士を比較し、小さい方を `tail` の後ろにつなげ、つないだ方のリストと `tail` を1つ進める +> 4. どちらかが尽きるまで3を繰り返す +> 5. 片方が尽きたら、残った方(すでにソート済み)をそのまま丸ごとつなげる +> 6. `dummy.next`(ダミーノードの次)が本当の答えの先頭なので、それを返す + +**【業務開発版を使う場面】** +チームで長期間メンテナンスするプロダクションコードに向きます。入力が本当に `None`(空リスト)であっても正しく動作することをコメントで明示し、後から読んだ人が「なぜここで `None` チェックをしていないのか/しているのか」を迷わないようにしています。 + +```python3 +from typing import Optional + + +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next +class ListNode: + """LeetCode標準の連結リストノード定義(コメントアウトされたものと同一)。""" + + def __init__(self, val: int = 0, next: "Optional[ListNode]" = None) -> None: + self.val: int = val + self.next: Optional["ListNode"] = next + + +class Solution: + """LeetCode 21: Merge Two Sorted Lists 解決クラス""" + + def mergeTwoLists( + self, list1: Optional[ListNode], list2: Optional[ListNode] + ) -> Optional[ListNode]: + """ + 2つのソート済み連結リストを1つのソート済み連結リストにマージする(業務開発版)。 + + 重要:list1 / list2 が None であることは異常値ではなく、 + 「空のリストが渡された」という正常な状態を表す。 + そのため ValueError 等は送出しない。 + + 新しいノードは一切作成せず、既存ノードの next 参照をつなぎ替える + (スプライシング)ことでマージするため、空間計算量は O(1) となる。 + + Args: + list1: 1つ目のソート済み連結リストの先頭ノード(空リストなら None) + list2: 2つ目のソート済み連結リストの先頭ノード(空リストなら None) + + Returns: + マージ後のソート済み連結リストの先頭ノード + (両方が空リストの場合は None) + """ + # ダミーノード(番兵ノード)を用意する。 + # 実データとしての意味は持たせず、値は0で仮置きする。 + # これにより「結果リストがまだ空のときの特別扱い」を + # 一切書かずに済む。 + dummy: ListNode = ListNode() + + # tail は「これまでにつなぎ終えた部分の末尾ノード」を指す変数。 + # 最初はダミーノード自身を指しておく。 + tail: ListNode = dummy + + # list1、list2 の両方にまだノードが残っている間、 + # 先頭同士の値を比較して小さい方を tail の後ろにつなげる。 + # Python は動的型付け言語だが、型ヒントを付けているため + # pylance が list1 / list2 の None チェック漏れを検出してくれる。 + while list1 is not None and list2 is not None: + if list1.val <= list2.val: + # list1の先頭値のほうが小さい(同値含む)ので + # list1の現在ノードを結果リストの末尾につなげる + tail.next = list1 + # list1を1つ次に進める + list1 = list1.next + else: + # list2の先頭値のほうが小さいので list2側をつなげる + tail.next = list2 + list2 = list2.next + # tailを、たった今つないだノードの位置まで進める + tail = tail.next + + # ループを抜けた時点で、list1 か list2 のどちらか(または両方)が + # None(尽きた状態)になっている。 + # 残っている側はすでにソート済みなので、そのまま丸ごと + # tail の後ろにつなげてよい(一方が None なら None がそのままつながるだけ)。 + tail.next = list1 if list1 is not None else list2 + + # dummyノード自体はデータを持たない仮のノードなので、 + # 実際の答えの先頭は dummy.next である + return dummy.next +``` + +**【競技プログラミング版を使う場面】** +LeetCodeやAtCoderなど、制限時間内に正解を出すことが目的のコードに向きます。docstringやコメントを最小限にし、変数名も短くして、書く手間と読む手間の両方を削っています。 + +```python3 +from typing import Optional + + +class Solution: + def mergeTwoLists( + self, list1: Optional[ListNode], list2: Optional[ListNode] + ) -> Optional[ListNode]: + # ダミーヘッド + 2ポインタ。O(n+m) / O(1)。 + dummy = tail = ListNode() + while list1 and list2: + if list1.val <= list2.val: + tail.next, list1 = list1, list1.next + else: + tail.next, list2 = list2, list2.next + tail = tail.next + tail.next = list1 or list2 + return dummy.next +``` + +> 💡 **業務開発版と競技版の違いのポイント**: +> 競技版では `tail.next, list1 = list1, list1.next` のようにタプル代入(=複数の変数へ同時に値を代入する書き方)を使い、代入を1行にまとめています。また `list1 and list2` は `list1 is not None and list2 is not None` の短縮形として使っていますが、これは `ListNode` に `__bool__` や `__len__` が定義されていないため、`None` かどうかだけで真偽が決まる(=空でないオブジェクトは常に真)という前提に依存しています。業務開発版では意図を明確にするためあえて `is not None` と書いています。 + +> 💡 **コードの動作トレース**(初学者向け) +> +> 入力:`list1 = [1,2,4]`、`list2 = [1,3,4]` +> +> ``` +> 初期状態: dummy -> (何も繋がっていない) +> tail = dummy +> list1 = [1,2,4], list2 = [1,3,4] +> +> Step 1: list1.val(1) <= list2.val(1) → True → list1の"1"をつなぐ +> dummy -> 1(list1由来) +> list1 は次の "2" へ、tail はつないだ "1" へ移動 +> +> Step 2: list1.val(2) vs list2.val(1) → list2の"1"のほうが小さい → list2をつなぐ +> dummy -> 1 -> 1(list2由来) +> list2 は次の "3" へ +> +> Step 3: list1.val(2) <= list2.val(3) → list1の"2"をつなぐ +> dummy -> 1 -> 1 -> 2 +> list1 は次の "4" へ +> +> Step 4: list1.val(4) vs list2.val(3) → list2の"3"のほうが小さい → list2をつなぐ +> dummy -> 1 -> 1 -> 2 -> 3 +> list2 は次の "4" へ +> +> Step 5: list1.val(4) <= list2.val(4) → list1の"4"をつなぐ(同値はlist1優先) +> dummy -> 1 -> 1 -> 2 -> 3 -> 4 +> list1 は None になる(尽きた) +> +> Step 6: while条件 (list1 is not None and list2 is not None) が False → ループ終了 +> list1 が None なので、残っている list2(= [4])を丸ごとつなぐ +> dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 +> +> Step 7: dummy.next を返す → [1,1,2,3,4,4](期待通りの出力) +> ``` + +> 💡 **型ヒントとpylanceの関係(初学者向け)** +> `Optional[ListNode]` という型ヒントを書いておくことで、pylance(VSCodeの型チェッカー)が「`list1.val` にアクセスする前に `None` チェックをしていない」といったミスを実行前に警告してくれます。今回のコードでは `while list1 is not None and list2 is not None:` というガード(=安全確認のための条件分岐)の内側でのみ `list1.val` にアクセスしているため、pylanceは「ここでは `list1` が `None` ではないと保証されている」と型を絞り込み(=型の絞り込み。英語で narrowing)、エラーを出しません。 + +> 📖 **このセクションで登場した用語** +> +> - **タプル代入**:`a, b = b, a` のように複数の変数へ同時に値を代入するPythonの書き方。一時変数を使わずに値の入れ替えができる +> - **`__bool__` / 真偽値判定**:Pythonのオブジェクトが `if obj:` のように書かれたときに真か偽かを判定する仕組み。`__bool__` が定義されていない場合、`None` 以外は基本的に真として扱われる +> - **型の絞り込み(narrowing)**:`if x is not None:` のような条件分岐の内側で、型チェッカーが「この範囲内では `x` は `None` ではない」と自動的に判断してくれる仕組み +> - **docstring**:関数やクラスの先頭に書く説明文。`"""三重クォート"""` で囲む + +--- + +## 4. 検証 + +> 💡 エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。エッジケースの確認は、アルゴリズムが"ふつうの入力"だけでなく"極端な入力"でも正しく動くかを確かめるために重要です。 + +- **境界値テスト(頭の中でのトレース確認)**: + - `list1=[], list2=[]`(Example 2):`dummy` は最初から誰ともつながらず、`while` ループの条件が最初から偽(両方 `None`)になるため即座に終了。`tail.next = list1 if list1 is not None else list2` は `None if None is not None else None` → `None` となり、`dummy.next` も `None` のまま。結果は `[]` で正しい。 + - `list1=[], list2=[0]`(Example 3):`while` ループの条件が最初から偽(`list1` が `None`)になるため即座に終了。`tail.next = list2`(`[0]`)がそのままつながる。結果は `[0]` で正しい。 + - 片方が他方より大幅に長い場合(例:`list1=[1]`, `list2=[2,3,4,5]`):`list1` の1件を消費した時点で `list1` が `None` になり、ループを抜けて `list2` の残り全部(`[2,3,4,5]`)が丸ごとつながる。個別に比較し直す必要がないことが確認できる。 + - 同値が並ぶ場合(例:`list1=[1,1]`, `list2=[1,1]`):`<=` の比較により、同値のときは常に `list1` 側が先に採用される。安定した挙動になっており、順序の破綻はない。 + +- **型チェック(pylance対応)**:全ての引数・戻り値・ローカル変数に型ヒント(`Optional[ListNode]`、`ListNode`)を付与しており、`None` の可能性がある箇所は `is not None` ガードの内側でのみアクセスしているため、pylanceの厳格モード(strict mode)でも警告は発生しません。 + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること +> - **strict mode(厳格モード)**:pylanceの型チェックをより厳しく行う設定。未使用変数や `Any` 型の暗黙的な使用なども検出対象になる + +**計算量まとめ**:時間計算量 O(n+m)、空間計算量 O(1)(n, m はそれぞれ list1, list2 のノード数)。 diff --git a/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Rust.md b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Rust.md new file mode 100644 index 00000000..0fba488a --- /dev/null +++ b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Rust.md @@ -0,0 +1,294 @@ +# LeetCode 21: Merge Two Sorted Lists(Rust版) + +## 1. 問題の分析 + +> 💡 この問題は、一言で言うと「すでに整列された2つの連結リストを、新しいノードを作らずに1つの整列済みリストへ“縫い合わせる”問題」です。 + +**Rustで解く際に特に気をつけるべき点**を先にまとめます。Rustの `ListNode` は `next: Option>` という形で、次のノードを**所有(owns)**しています。JavaやTypeScript、Goでは「ポインタ/参照をつなぎ替える」だけで済みましたが、Rustでは**1つの値は同時に1箇所からしか所有できない**という所有権(=値を"誰が管理するか"をコンパイル時に決めるRust独自の仕組み)のルールがあるため、「`list1` の次のノードを取り出して `tail` につなぐ」という操作は、実は**値の"移動"(move)**そのものです。単純に `tail.next = list1.next`(参照のコピー)のようなことはできず、`Option::take()` という「箱の中身を取り出して、元の箱には `None` を残す」操作が中心的な役割を果たします。また、この問題は `Result` を使うようなエラーが発生する余地がなく、`None` は「空リスト」を表す正常な値であるため、エラーハンドリングの主眼は「パニックさせないこと」よりもむしろ「**所有権を正しく受け渡すこと**」そのものにあります。 + +**競技プログラミング視点での分析** + +- 実行速度を最優先するなら、各ノードを1回ずつ見るだけで済む処理が最速。時間計算量は O(n+m)。 +- メモリ使用量の最小化方針としては、**新しい `Box` を1つも生成せず、既存の `Box` を移動(move)させるだけ**でマージを完了させます。ヒープアロケーション(=ヒープ上にメモリを新たに確保する操作)はゼロ回に抑えられます。 + +**業務開発視点での分析** + +- 型安全性の面では、`Option>` という型そのものが「ノードが存在するか(`Some`)、リストが終端/空か(`None`)」をコンパイル時に表現しています。これはJavaやTypeScriptの `null` チェック忘れによる実行時エラーを、コンパイラが構造的に防いでくれる仕組みです。 +- エラーハンドリングとして `Result` を使う必要はありません。関数シグネチャ自体が `Option>` を返すよう固定されており、`None` は正常な戻り値の一部だからです。 + +**Rust特有の考慮点** + +- 所有権・借用・ライフタイムの設計方針:この問題では引数の `list1` / `list2` を**借用(`&`)ではなく所有権ごと受け取っています**(`Option>` であり `&Option>` ではない)。これは「渡された2つのリストを解体して、新しい1つのリストに再構成する」という処理の性質上、元のリストをそのまま残しておく必要がなく、むしろ**所有権を完全にもらって自由に組み替えたほうが効率的**だからです。 +- トレイト境界とモノモーフィゼーションによる最適化:今回は `ListNode` という具体的な型を扱うため、ジェネリクスやトレイト境界(=ジェネリクス型に「この機能が必要」と制約をつける仕組み)は不要です。 +- イテレータアダプタ vs 命令型ループの選択基準:`.map()` や `.fold()` のようなイテレータアダプタは「連続したコレクションを順に処理する」場面に強みがありますが、連結リストの所有権を1ノードずつ移動させながら別の構造に組み替える今回の処理は、`while let` を使った明示的なループ(または再帰)のほうが所有権の流れを追いやすく適しています。 + +> 📖 **このセクションで登場した用語**(例) +> +> - **所有権**:値を"誰が管理するか"をコンパイル時に決めるRust独自の仕組み。1つの値は同時に1箇所からしか所有できない +> - **移動(move)**:ある変数が持つ値の所有権を、別の変数へ渡す操作。移動元の変数はその後使えなくなる +> - **借用**:所有権を渡さずに値を参照する仕組み。`&T`(読み取り専用)と`&mut T`(書き込み可能)がある +> - **`Option`**:値があるかないかを型で表現する列挙型。`Some(値)` か `None` のどちらか。他言語の `null` と違い、コンパイラが「中身を確認せずに使うこと」を許さない +> - **ヒープアロケーション**:`Box::new(...)` のように、ヒープ上にメモリを新たに確保する操作。今回のアルゴリズムでは新規のアロケーションを一切行わない + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」、そしてRust固有の観点(所有権の移動が発生するか・アロケーションが必要か)を比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | Rust実装コスト | 安全性 | 可読性 | 備考 | +| ------------------------------------------ | ---------------- | ------------------------ | -------------- | ------ | ------ | --------------------------------------------------------- | +| 方法A:反復(`take()` + 可変参照の再借用) | O(n+m) | O(1)(補助領域) | 中 | 高 | 中 | `unsafe`不要。所有権の流れがやや複雑 | +| 方法B:再帰(`match`でパターンマッチ) | O(n+m) | O(n+m)(コールスタック) | 低 | 高 | 高 | コードは短く直感的。所有権の移動が自然に書ける | +| 方法C:両方を`Vec`に集めてソート | O((n+m)log(n+m)) | O(n+m) | 中 | 高 | 中 | `Box`の中身を都度取り出す必要がありアロケーションも増える | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n+m)`:2つの入力の合計サイズに比例して手間が増える(線形) +> - `O((n+m) log(n+m))`:ソートに典型的な計算量。入力が増えるとやや急激に手間が増える + +**Rust固有の観点からの補足**: + +- 方法A(反復)は、`list1.take()` によって `list1` から中身を"奪い"、その所有権を新しい構造につなぎ込みます。新規の `Box` は1つも作らないため、ヒープアロケーションはゼロ回です。ただし「今つないだノードの次に、また次をつなげる」ために `tail` という**可変参照(`&mut`)の再借用**を毎回行う必要があり、借用チェッカー(=`&T`と`&mut T`の使い方をコンパイル時にチェックする仕組み)との付き合い方にやや習熟が必要です。 +- 方法B(再帰)は、`match (list1, list2) { ... }` のパターンマッチで所有権の移動が非常に自然に書けます。ただし、再帰1回につきコールスタック(=関数呼び出しの履歴を積んでおくメモリ領域)を1段消費するため、空間計算量は O(n+m) になります。 +- 方法C(ソート)は、リストの中身を一度 `Vec` に取り出す際に `Box` の中身を1つずつ移動させる必要があり、実装がかえって複雑になるうえ、「すでにソート済み」という前提を活かせません。 + +> 📖 **このセクションで登場した用語** +> +> - **借用チェッカー**:`&T`(共有参照)と`&mut T`(排他参照)が同時に競合しないかをコンパイル時にチェックするRustコンパイラの仕組み。データ競合をコンパイル時に排除できる +> - **再借用(reborrow)**:すでに借用しているものから、さらに一時的に借用し直す操作。今回は `tail` を毎ループ更新する際に発生する +> - **コールスタック**:関数呼び出しの「呼び出し履歴」を積んでおくメモリ領域。再帰が深くなるとここに積まれる分だけメモリを消費する + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**:方法A(反復・`take()`による所有権移動 + 可変参照の再借用) +- **理由**: + - **計算量的な優位性**:方法Bと同じ O(n+m) の時間計算量を持ちながら、空間計算量は補助領域 O(1) で済みます(方法Bはコールスタックに O(n+m) を消費します)。今回の制約(ノード数最大50)では方法Bでもスタックオーバーフローの危険はまずありませんが、業務コードとして「入力サイズに比例してスタックが伸びる」設計は将来の拡張性に対して脆いという判断です。 + - **Rustの所有権モデルとの親和性**:`Option::take()` は「箱の中身を取り出し、元の箱には `None` を残す」というRust特有の操作で、所有権を安全に移動させながら**二重解放(同じメモリを2回解放してしまうバグ)やダングリングポインタ(=すでに無効になったメモリを指し続けるポインタ)を、コンパイラが構造的に防いでくれます**。これはC++で同様のコードを書く場合と比べた、所有権モデルの大きな恩恵です。 + - **保守性・可読性**:反復1本で完結し、`unsafe` ブロックを一切使わないため、コンパイラの安全性保証をフルに活かせます。 +- **Rust特有の最適化ポイント**: + - ゼロコスト抽象化(=便利な書き方をしても、手書きの低レベルコードと同じ速さになる性質)の観点から、`Option>` の `match` や `take()` はいずれもコンパイル後には単純なポインタ操作とヌルチェックに近いコードへ最適化されます。 + - スタックアロケーション優先の観点では、今回のアルゴリズムはヒープアロケーションを一切行わないため、「新しいノードのために `Box::new` を呼ぶ」というコストがそもそも発生しません。 + +> 📖 **このセクションで登場した用語** +> +> - **ゼロコスト抽象化**:便利な高レベルな書き方(`Option`や`match`など)をしても、手書きの低レベルコードと同等の速さになるRustの特性 +> - **二重解放(double free)**:同じメモリ領域を誤って2回解放してしまうバグ。C/C++では手動で気をつける必要があるが、Rustは所有権システムでコンパイル時に防ぐ +> - **ダングリングポインタ**:すでに解放された(無効になった)メモリを指し続けているポインタ。Rustの借用チェッカーがコンパイル時に防ぐ + +--- + +## 4. 実装コード + +> 💡 コード全体の骨格: +> +> 1. 結果リストの先頭を仮に指し示す「ダミーノード」を `Box` として1つだけ確保する +> 2. `tail` という可変参照を使って「今つなぎ終えた部分の末尾」を追いかける +> 3. `list1` と `list2` の先頭同士を比較し、値が小さいほうを `take()` で取り出して `tail.next` につなぐ。取り出したノードの `next` を新しい `list1`(または `list2`)とする +> 4. どちらかが `None` になるまで3を繰り返す +> 5. 残った方(すでにソート済み)を丸ごと `tail.next` につなぐ +> 6. `dummy.next` が本当の答えの先頭なので、それを `Option` ごと返す + +```rust +// Definition for singly-linked list. +// #[derive(PartialEq, Eq, Clone, Debug)] +// pub struct ListNode { +// pub val: i32, +// pub next: Option> +// } +// +// impl ListNode { +// #[inline] +// fn new(val: i32) -> Self { +// ListNode { +// next: None, +// val +// } +// } +// } +struct Solution; + +impl Solution { + /// 2つのソート済み連結リストを1つのソート済み連結リストにマージする。 + /// + /// 新しい `Box`(ヒープ上のノード)は「ダミーノード」1つを除いて + /// 一切生成しない。既存ノードの所有権を `Option::take()` によって + /// 移動させ、そのままつなぎ替える(スプライシング)ことでマージする。 + /// + /// 重要:`list1` / `list2` が `None` であることはエラーではなく、 + /// 「空のリストが渡された」という正常な状態を表す。 + /// そのため `Result` は使わず、`panic!` も発生しない。 + /// + /// # Arguments + /// * `list1` - 1つ目のソート済み連結リストの先頭(所有権ごと受け取る) + /// * `list2` - 2つ目のソート済み連結リストの先頭(所有権ごと受け取る) + /// + /// # Returns + /// マージ後のソート済み連結リストの先頭(両方が空リストなら `None`) + /// + /// # Complexity + /// - Time: O(n + m) + /// - Space: O(1)(補助領域のみ。新規ヒープ確保はダミーノード1個分) + pub fn merge_two_lists( + mut list1: Option>, + mut list2: Option>, + ) -> Option> { + // ダミーノード(番兵ノード)を1つだけヒープに確保する。 + // このノード自体には意味のある値は入れない(値は0で仮置き)。 + // なぜダミーノードを使うか: + // 「結果リストがまだ空のとき、先頭をどう扱うか」という + // 特別な分岐を一切書かずに済むため。 + let mut dummy = Box::new(ListNode::new(0)); + + // tail は「これまでにつなぎ終えた部分の末尾ノード」への + // 可変参照(&mut)。所有権は dummy 側に残したまま、 + // "今どこまでつなぎ終えたか"を指し示すためだけに使う。 + // + // なぜ Box そのものを持ち回さず &mut にするか: + // dummy の所有権自体は最後に dummy.next として返す必要があるため、 + // ループの中では「借用」だけを使って末尾を指し示す。 + let mut tail = &mut dummy; + + // list1、list2 の両方にまだノードが残っている(Someである)間、 + // 先頭同士の値を比較して小さい方を tail の後ろにつなげる。 + while list1.is_some() && list2.is_some() { + // as_ref().unwrap() で中身を「覗き見る」だけ(所有権は奪わない)。 + // is_some() で存在を確認済みなので unwrap() が失敗することはない。 + let take_from_list1 = list1.as_ref().unwrap().val <= list2.as_ref().unwrap().val; + + if take_from_list1 { + // list1.take() で list1 の中身(Box)を"奪う"。 + // 奪った後、元の list1 には None が自動的に残る。 + // これが Rust における「移動」の安全な実現方法。 + let mut node = list1.take().unwrap(); + + // node の next フィールドも take() で奪い、 + // それを新しい list1(=「まだつないでいない残りの部分」)とする。 + // node.next には None が残るので、後で tail につなぐときに + // 誤って残りのリストごと持って行ってしまう事故が起きない。 + list1 = node.next.take(); + + // node(中身を空にした1ノード分の Box)を tail の後ろにつなぐ。 + // ここで node の所有権は tail.next(=dummy構造の内部)へ移動する。 + tail.next = Some(node); + } else { + // list2 側でも同様の操作を行う + let mut node = list2.take().unwrap(); + list2 = node.next.take(); + tail.next = Some(node); + } + + // tail を、たった今つないだノードの位置まで進める。 + // as_mut() で Option> から &mut Box を取り出し、 + // unwrap() で中身にアクセスする(直前で Some を代入したばかりなので安全)。 + // これは「再借用(reborrow)」と呼ばれる操作:tail という借用から、 + // さらに一段先の借用を作り直している。 + tail = tail.next.as_mut().unwrap(); + } + + // ループを抜けた時点で、list1 か list2 のどちらか(または両方)が + // None(尽きた状態)になっている。 + // 残っている側はすでにソート済みなので、1つずつ比較する必要はなく、 + // そのまま丸ごと tail.next につなげてよい。 + // Option::or() は「自分が None なら相手を返す」というメソッド。 + // list1 が Some ならそれを、None なら list2(Some でも None でも)を返す。 + tail.next = list1.or(list2); + + // dummy はローカル変数だが、その中身(dummy.next 以下のチェーン)を + // 丸ごと戻り値として返す。dummy 自身(値0のノード)は + // ここで破棄され、dummy.next 以降の所有権だけが呼び出し元に渡る。 + dummy.next + } +} +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> 入力:`list1 = [1,2,4]`、`list2 = [1,3,4]` +> +> ``` +> 初期状態: dummy = Box(val:0, next:None) +> tail = &mut dummy +> list1 = Some([1,2,4]), list2 = Some([1,3,4]) +> +> Step 1: list1.val(1) <= list2.val(1) → true +> node = list1.take().unwrap() → node=[1] (元のlist1由来)、list1は一旦None +> list1 = node.next.take() → list1 = Some([2,4])、nodeのnextはNone +> tail.next = Some(node) → dummy -> 1 +> tail = tail.next.as_mut().unwrap() → tailは今つないだ"1"を指す +> +> Step 2: list1.val(2) vs list2.val(1) → list2の"1"のほうが小さい +> node = list2.take().unwrap() → node=[1] (list2由来) +> list2 = node.next.take() → list2 = Some([3,4]) +> tail.next = Some(node) → dummy -> 1 -> 1 +> tail は今つないだ"1(list2由来)"を指す +> +> Step 3: list1.val(2) <= list2.val(3) → list1の"2"をつなぐ +> list1 = Some([4]) +> dummy -> 1 -> 1 -> 2 +> +> Step 4: list1.val(4) vs list2.val(3) → list2の"3"のほうが小さい +> list2 = Some([4]) +> dummy -> 1 -> 1 -> 2 -> 3 +> +> Step 5: list1.val(4) <= list2.val(4) → list1の"4"をつなぐ(同値はlist1優先) +> list1 = None(nodeのnextがNoneだったため) +> dummy -> 1 -> 1 -> 2 -> 3 -> 4 +> +> Step 6: while条件 (list1.is_some() && list2.is_some()) が false → ループ終了 +> list1 = None なので、tail.next = list1.or(list2) → list2(=Some([4]))がつながる +> dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 +> +> Step 7: dummy.next を返す → [1,1,2,3,4,4](期待通りの出力) +> ``` + +> 💡 **`take()` の動きを図で理解する**(初学者向け) +> `Option::take()` は以下の処理と等価です: +> +> ``` +> 実行前: list1 = Some(Box(値)) +> take() 実行 → 戻り値: Some(Box(値)) +> list1 の中身: None (空の箱が残る) +> ``` +> +> これにより、「値を取り出しつつ、元の変数を安全に空にする」という操作を、コンパイラのチェックを通しながら行えます。`std::mem::replace(&mut list1, None)` と本質的に同じ処理ですが、`take()` はそれを読みやすく糖衣構文化(=複雑な処理を短く書けるようにした構文)したものです。 + +> 📖 **このセクションで登場した用語** +> +> - **`Option::take()`**:`Option` の中身を取り出しつつ、元の変数には `None` を残すメソッド。所有権を安全に"奪う"ための標準的な手段 +> - **`Option::or()`**:自分が `None` のとき、代わりに引数の値を返すメソッド。`list1.or(list2)` は「`list1`があればそれを、なければ`list2`を」という意味 +> - **再借用(reborrow)**:すでに借用している `&mut` 参照から、さらに一段先の `&mut` 参照を作り直す操作。`tail = tail.next.as_mut().unwrap()` がその例 +> - **`as_ref()` / `as_mut()`**:`Option` の中身を「奪わずに」参照として取り出すメソッド。それぞれ `Option<&T>` / `Option<&mut T>` を返す + +--- + +## 5. 検証 + +> 💡 エッジケースとは「入力が空・最小値・最大値・重複あり」など、通常とは異なる境界的な入力のことです。境界値の確認は、アルゴリズムが"ふつうの入力"だけでなく"極端な入力"でも正しく動くかを確かめるために重要です。 + +- **境界値テスト(頭の中でのトレース確認)**: + - `list1=None, list2=None`(Example 2):`while` の条件 `list1.is_some() && list2.is_some()` が最初から偽になるため即座にループを抜ける。`tail.next = list1.or(list2)` は `None.or(None)` → `None`。`dummy.next` も `None` のまま。結果は `None`(空リスト)で正しい。 + - `list1=None, list2=Some([0])`(Example 3):ループの条件が最初から偽(`list1` が `None`)になるため即座に終了。`tail.next = None.or(Some([0]))` → `Some([0])` がそのままつながる。結果は `[0]` で正しい。 + - 片方が他方より大幅に長い場合(例:`list1=[1]`, `list2=[2,3,4,5]`):`list1` の1件を消費した時点で `list1` が `None` になり、ループを抜けて `list2` の残り全部が `or()` によって丸ごとつながる。個別に取り出し直す必要がないことが確認できる。 + - 同値が並ぶ場合(例:`list1=[1,1]`, `list2=[1,1]`):`<=` の比較により、同値のときは常に `list1` 側が先に採用される。所有権の移動順序も含めて挙動は安定している。 + +- **所有権・借用に関するチェック**:このコードは `unsafe` ブロックを1つも使用していません。`take()` による所有権の移動と、`as_mut()` による再借用のみで安全にリストを組み替えており、コンパイラの借用チェッカーが二重解放やダングリング参照の可能性をコンパイル時に排除しています。`#![deny(clippy::all)]` 相当のコードクオリティとしても、不要な `clone()` や `unwrap()` の濫用(`is_some()` で存在確認済みの `unwrap()` のみ使用)を避けています。 + +> 📖 **このセクションで登場した用語** +> +> - **エッジケース**:空のリスト・要素1つ・最大サイズ入力など、境界的な条件のこと +> - **境界値テスト**:エッジケースに対してもアルゴリズムが正しく動くかを確かめること +> - **`clippy`**:Rustの静的解析ツール。非効率なコードパターンやより良い書き方を提案してくれる + +--- + +## 6. 制約条件の確認 + +- 外部クレートは使用していません(`std` の `Box` と `Option` のみ)。 +- `unsafe` ブロックは1つも使用していません。 +- 新規のヒープアロケーションはダミーノード1個分のみで、それ以外は既存ノードの所有権移動(`take()`)のみで完結しています。 + +**計算量まとめ**:時間計算量 O(n+m)、空間計算量 O(1)(補助領域のみ。n, m はそれぞれ list1, list2 のノード数)。 diff --git a/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Typescript.md b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Typescript.md new file mode 100644 index 00000000..20c418e0 --- /dev/null +++ b/DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-Typescript.md @@ -0,0 +1,218 @@ +# LeetCode 21: Merge Two Sorted Lists(TypeScript版) + +## 1. 問題の分析 + +> 💡 この問題は、一言で言うと「すでに整列された2つの連結リストを、新しいノードを作らずに1つの整列済みリストへ“縫い合わせる”問題」です。 + +**競技プログラミング視点での分析** +- 実行速度を最優先するなら、各ノードを1回ずつ見るだけで済む**反復(ループ)処理**が最速。時間計算量は O(n+m)(n, mはそれぞれのリスト長)で、再帰呼び出しのようなオーバーヘッド(=余分にかかる処理コスト)がない。 +- メモリ使用量の最小化方針としては、**新しいノードを一切作らず、既存ノードの `next` 参照だけを付け替える**(この操作を「スプライシング(splicing、つなぎ替え)」と呼ぶ)。配列に変換してからソートし直すような方法は、リストがすでにソート済みという情報を無駄にしてしまう。 + +**業務開発視点での分析** +- 型安全性の面では、`ListNode` は問題側で固定されたクラスなので、ジェネリクス(=型を後から自由に差し込める仕組み)を導入する必要は**ありません**。この問題は `number` 型の `val` に固定されており、無理に汎用化するとかえって型が複雑になり可読性が落ちます。 +- エラーハンドリングで最も注意すべき点:**`list1` や `list2` が `null` であることは「異常な入力」ではなく「空のリストを表す正常な状態」**です。Example 2 (`list1 = [], list2 = []`) がまさにそれです。テンプレートによくある「`null` なら即 `TypeError`」という防御的プログラミングをそのまま当てはめると、この問題では**正しい入力を誤ってエラー扱いしてしまう**ので注意が必要です。 + +**TypeScript特有の考慮点** +- 型推論(=型を明示しなくてもTypeScriptが自動で型を判断してくれる機能)により、`list1.val` は必ず `number` であることがコンパイル時に保証されます。 +- 型ガード(=実行時のチェックとコンパイル時の型の絞り込みを同時に行う仕組み)として `list1 !== null` を使うと、その条件分岐の内側ではTypeScriptが自動的に `list1` の型を `ListNode`(`null` を除いた型)に絞り込んでくれます。JavaScriptにはこの「コンパイラが型を追跡する」という概念自体が存在せず、実行時に `undefined` や `null` へのアクセスでクラッシュして初めて気づくことが多いのに対し、TypeScriptでは**コードを書いている最中(エディタ上)**にエラーとして警告してくれる点が大きな違いです。 + +> 📖 **このセクションで登場した用語**(例) +> +> - **型推論**:型を明示しなくてもTypeScriptが自動で型を判断してくれる機能 +> - **null安全性**:`null`や`undefined`による予期しないクラッシュを防ぐ仕組み +> - **型ガード**:`if (x !== null)` のような条件分岐によって、その内側でTypeScriptに「`x`は`null`ではない」と伝え、型を絞り込ませる仕組み +> - **オーバーヘッド**:ある処理を実現するために余分にかかる時間やメモリのコスト + +--- + +## 2. アルゴリズムアプローチ比較 + +> 💡 同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」、そしてTypeScriptらしい実装コストを比べて最適なものを選びます。 + +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | +|---|---|---|---|---|---|---| +| 方法A:反復(ダミーヘッド + 2ポインタ) | O(n+m) | O(1) | 低 | 高 | 高 | 新規ノード生成なし。推奨 | +| 方法B:再帰(先頭同士を比較して自己呼び出し) | O(n+m) | O(n+m) | 低 | 高 | 高 | コールスタックがリスト長に比例して積まれる | +| 方法C:両リストを配列に集めてソート | O((n+m) log(n+m)) | O(n+m) | 中 | 中 | 中 | すでにソート済みという前提を無駄にする | + +> 💡 **Big-O記法の読み方**(初学者向け) +> +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小) +> - `O(n+m)`:2つの入力の合計サイズに比例して手間が増える(線形) +> - `O((n+m) log(n+m))`:ソートに典型的な計算量。入力が増えるとやや急激に手間が増える + +> 📖 **このセクションで登場した用語** +> +> - **時間計算量**:入力の大きさに対して、処理にかかる手間がどう増えるかの目安 +> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安 +> - **コールスタック**:関数呼び出しの「呼び出し履歴」を積んでおくメモリ領域。再帰が深くなるとここに積まれる分だけメモリを消費し、深すぎると `RangeError: Maximum call stack size exceeded` が起きる + +--- + +## 3. 選択したアルゴリズムと理由 + +- **選択したアプローチ**:方法A(ダミーヘッドを使った反復処理) +- **理由**: + - **計算量的な優位性**:方法B(再帰)と同じ O(n+m) の時間計算量でありながら、空間計算量は O(1) で済む(方法Bはコールスタックに O(n+m) 消費する)。 + - **TypeScript環境での型安全性**:`while` ループの条件を `list1 !== null && list2 !== null` とすることで、ループの内側ではTypeScriptが `list1.val` や `list2.val` へのアクセスを「`null` かもしれないプロパティへのアクセス」としてエラー扱いせず、安全にコンパイルが通る。 + - **保守性・可読性**:ループ1本で完結し、条件分岐もシンプル。デバッガでも1行ずつ追いやすい。 + - **方法Cを選ばなかった理由**:わざわざ配列に集めてソートするのは、「すでにソート済み」という問題の前提を無視した回り道であり、時間計算量も余分な O(log(n+m)) がかかるうえ、空間計算量も O(n+m) と余計にメモリを使う。 +- **TypeScript特有の最適化ポイント**: + - コンパイル時の型チェックにより、「`list1.val` に `undefined` を代入してしまう」といったミスをコードを書いている最中に検出できる。 + - `readonly` は今回のノードのように「途中で `next` を書き換える必要があるデータ構造」には適用しない(`next` を `readonly` にしてしまうとスプライシング自体ができなくなる)。イミュータブル(変更不可)にすべき部分とミュータブル(変更可能)であるべき部分を型定義レベルで正しく見極めることが、この問題における型安全性の要点。 + +> 📖 **このセクションで登場した用語** +> +> - **ジェネリクス**:型を後から自由に差し込める仕組み。今回の問題は `val: number` に固定されているため使用しない +> - **コンパイル時**:TypeScriptのコードをJavaScriptに変換する段階。ここでエラーを検出できると実行時のバグを防げる +> - **イミュータブル / ミュータブル**:イミュータブルは「一度作ったら値を変更できない」性質、ミュータブルは「後から値を変更できる」性質。連結リストの `next` 参照は今回のアルゴリズムではミュータブルである必要がある + +--- + +## 4. 実装コード + +> 💡 コード全体の骨格: +> 1. 結果リストの先頭を仮に指し示す「ダミーノード」を用意する +> 2. `list1` と `list2` の先頭同士を比較し、小さい方を結果リストの末尾につなげる。つないだ方のリストを1つ進める +> 3. どちらかが尽きるまで2を繰り返す +> 4. 片方が尽きたら、残った方(すでにソート済み)をそのまま丸ごとつなげる +> 5. ダミーノードの次(`dummy.next`)が本当の答えの先頭なので、それを返す + +```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) + * } + * } + */ +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; + } +} + +/** + * 2つのソート済み連結リストを1つのソート済み連結リストにマージする。 + * + * 新しいノードは一切作成せず、既存ノードの next 参照をつなぎ替える + * (スプライシング)ことでマージするため、空間計算量は O(1) となる。 + * + * 重要:list1 / list2 が null であることは異常値ではなく、 + * 「空のリストが渡された」という正常な状態を表す。 + * そのため TypeError 等は送出しない。 + * + * @param list1 - 1つ目のソート済み連結リストの先頭ノード(空リストなら null) + * @param list2 - 2つ目のソート済み連結リストの先頭ノード(空リストなら null) + * @returns マージ後のソート済み連結リストの先頭ノード(両方が空リストの場合は null) + * @complexity Time: O(n + m), Space: O(1) + */ +function mergeTwoLists(list1: ListNode | null, list2: ListNode | null): ListNode | null { + // ダミーノード(番兵ノード)を用意する。 + // このノード自体には意味のある値は入れない(値は0で仮置き)。 + // こうすることで「結果リストがまだ空のとき、先頭をどう扱うか」 + // という特別な分岐を書かずに済む。 + const dummy: ListNode = new ListNode(0); + + // tail は「これまでにつなぎ終えた部分の末尾ノード」を指す変数。 + // let で宣言する理由:tail.next への再代入だけでなく、 + // tail 自体もループのたびに「次のノード」を指すように変わっていくため。 + let tail: ListNode = dummy; + + // list1、list2 の両方にまだノードが残っている間、 + // 先頭同士の値を比較して小さい方を tail の後ろにつなげる。 + // この条件式が型ガードとして働き、ループ内では TypeScript が + // list1・list2 を「null ではない ListNode」として扱ってくれる。 + while (list1 !== null && list2 !== null) { + if (list1.val <= list2.val) { + // list1の先頭値のほうが小さい(同値含む)ので、 + // list1の現在ノードを結果リストの末尾につなげる + tail.next = list1; + // list1を1つ次に進める(つないだノードは処理済みなので進める) + list1 = list1.next; + } else { + // list2の先頭値のほうが小さいので、list2側をつなげる + tail.next = list2; + list2 = list2.next; + } + // tailを、たった今つないだノードの位置まで進める + // tail.next は必ず ListNode(直前の if/else でつないだノード)なので + // TypeScript の型的にも安全に代入できる + tail = tail.next; + } + + // ループを抜けた時点で、list1 か list2 のどちらか(または両方)が + // null(尽きた状態)になっている。 + // 残っている側はすでにソート済みなので、1つずつ比較する必要はなく、 + // そのまま丸ごと tail の後ろにつなげてよい。 + // 三項演算子:list1が残っていればlist1を、そうでなければlist2をつなげる + tail.next = list1 !== null ? list1 : list2; + + // dummyノード自体はデータを持たない仮のノードなので、 + // 実際の答えの先頭は dummy.next である + return dummy.next; +} + +export { mergeTwoLists, ListNode }; +``` + +> 💡 **コードの動作トレース**(初学者向け) +> +> 入力:`list1 = [1,2,4]`、`list2 = [1,3,4]` +> +> ``` +> 初期状態: dummy -> (何も繋がっていない) +> tail = dummy +> list1 = [1,2,4], list2 = [1,3,4] +> +> Step 1: list1.val(1) <= list2.val(1) → true → list1の"1"をつなぐ +> 結果: dummy -> 1(from list1) +> list1は次の "2" に進む、tailは今つないだ "1" に進む +> +> Step 2: list1.val(2) vs list2.val(1) → list2の"1"のほうが小さい → list2をつなぐ +> 結果: dummy -> 1 -> 1(from list2) +> list2は次の "3" に進む、tailは今つないだ "1" に進む +> +> Step 3: list1.val(2) <= list2.val(3) → list1の"2"をつなぐ +> 結果: dummy -> 1 -> 1 -> 2 +> list1は次の "4" に進む +> +> Step 4: list1.val(4) vs list2.val(3) → list2の"3"のほうが小さい → list2をつなぐ +> 結果: dummy -> 1 -> 1 -> 2 -> 3 +> list2は次の "4" に進む +> +> Step 5: list1.val(4) <= list2.val(4) → list1の"4"をつなぐ(同値はlist1優先) +> 結果: dummy -> 1 -> 1 -> 2 -> 3 -> 4 +> list1はnullになる(尽きた) +> +> Step 6: while条件 (list1 !== null && list2 !== null) が false になりループ終了 +> list1 === null なので、残っている list2(= [4])を丸ごとつなぐ +> 結果: dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4 +> +> Step 7: dummy.next を返す → [1,1,2,3,4,4](期待通りの出力) +> ``` + +> 📖 **このセクションで登場した用語** +> +> - **readonly**:変数の値を変更できないようにする修飾子。今回の `tail` や `next` は書き換えが必要なため `let` のまま使用している(不変にすべき値とすべきでない値の見極めが重要) +> - **TypeError / RangeError**:エラーの種類。`TypeError`は型が不正な場合、`RangeError`は値の範囲が不正な場合に使う。今回は正常な `null` 入力に対してこれらを投げないよう注意している +> - **Pure function(純粋関数)**:同じ入力を与えると必ず同じ出力を返し、外部の状態を変えない関数。`mergeTwoLists` はリスト自体(ノードの `next`)を書き換えるためオブジェクト内部への副作用はあるが、外部のグローバル変数などは一切変更しない +> - **三項演算子(`条件 ? A : B`)**:`if-else` を1行で書くための構文。「条件がtrueならA、falseならB」という値を返す + +--- + +## 5. 制約条件の確認 + +- 外部ライブラリは使用していません(Node.js標準機能・言語組み込み機能のみ)。 +- ESM形式(`export` 文)で出力しています。 +- 追加のノードを1つも生成していないため、メモリ使用量は入力サイズによらず一定(O(1))です。 +- TypeScript strict mode(`strictNullChecks` を含む)でも、`while (list1 !== null && list2 !== null)` による型ガードのおかげでコンパイルエラーは発生しません。 + +**計算量まとめ**:時間計算量 O(n+m)、空間計算量 O(1)(n, m はそれぞれ list1, list2 のノード数)。 diff --git a/public/index.html b/public/index.html index f078b3c7..d9b55f6e 100644 --- a/public/index.html +++ b/public/index.html @@ -855,7 +855,7 @@

    🧪 - Generated on 2026-08-08 + Generated on 2026-08-09
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + diff --git a/public/Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/README_React.html b/public/Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/README_React.html new file mode 100644 index 00000000..57616b2b --- /dev/null +++ b/public/Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/README_React.html @@ -0,0 +1,2215 @@ + + + + + + LeetCode 26: Remove Duplicates from Sorted Array — 11言語解説 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + diff --git a/public/index.html b/public/index.html index e4efbc4d..3620b69c 100644 --- a/public/index.html +++ b/public/index.html @@ -418,7 +418,7 @@

    🧪 Algorithm Study Index

    -

    185 interactive lessons across 6 domains

    +

    186 interactive lessons across 6 domains

    @@ -433,9 +433,9 @@

    - + @@ -484,6 +484,7 @@

  • 🧩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 14: Longest Common Prefix - 水平走査法による解説(Python/TypeScript/Go/Rust)Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html
  • +
  • 🧩LeetCode 26: Remove Duplicates from Sorted Array — 11言語解説Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/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
  • @@ -676,6 +677,7 @@

  • 🧩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 14: Longest Common Prefix - 水平走査法による解説(Python/TypeScript/Go/Rust)Algorithm/Other/leetcode/14. Longest Common Prefix/Sonnet 5/README_react.html
  • +
  • 🧩LeetCode 26: Remove Duplicates from Sorted Array — 11言語解説Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/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
  • @@ -855,7 +857,7 @@

    🧪 - Generated on 2026-08-10 + Generated on 2026-08-11