diff --git a/INDEX_MAINTENANCE.md b/INDEX_MAINTENANCE.md index 0df77be9..5446d3a6 100644 --- a/INDEX_MAINTENANCE.md +++ b/INDEX_MAINTENANCE.md @@ -34,9 +34,20 @@ This will populate the `public/` directory with the latest file structure and `i ## Automatic Update (Git Hook) -You can set up a Git **pre-commit hook** to automatically update the index every time you commit. This ensures `index.html` is always in sync with your changes. +Git **pre-commit hook** を設定すると、コミットのたびにインデックスが自動更新されます。 -### Setup Instructions +### 自動セットアップ(推奨) + +```bash +make setup # venv + フック + 依存関係を一括セットアップ +make hooks # フックのみインストール +``` + +`make hooks` は冪等(既にフックが存在する場合はスキップ)です。 + +### 手動セットアップ + +自動セットアップが使えない場合は以下を実行: 1. Make the wrapper script executable: diff --git a/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/Memoize_II_TS.ipynb b/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/Memoize_II_TS.ipynb new file mode 100644 index 00000000..2ec5fe80 --- /dev/null +++ b/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/Memoize_II_TS.ipynb @@ -0,0 +1,598 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4b253066", + "metadata": {}, + "source": [ + "### 1. 問題の分析\n", + "\n", + "**競技プログラミング視点**\n", + "- 引数列をキーとするキャッシュ参照をO(1)で行う必要がある\n", + "- 引数は任意の型(プリミティブ・オブジェクト参照)で `===` 比較なので、文字列化は不可\n", + "- ネストした `Map` によるトライ構造が最適(引数の数に線形、各ステップO(1))\n", + "\n", + "**業務開発視点**\n", + "- 引数ゼロの呼び出しも考慮が必要\n", + "- キャッシュヒット判定に `has` + `get` を使い、`undefined` が正常な戻り値でも正しく動作させる\n", + "\n", + "**TypeScript特有の考慮点**\n", + "- `Map` のネスト構造で型安全に実装\n", + "- センチネル値(`CACHE_HIT` シンボル)でキャッシュ存在確認と値取得を分離\n", + "\n", + "---\n", + "\n", + "### 2. アルゴリズムアプローチ比較\n", + "\n", + "| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 |\n", + "|---|---|---|---|---|---|---|\n", + "| JSON.stringify キー | O(n·k) | O(n·k) | 低 | 低 | 高 | オブジェクトは全て `{}` になり不可 |\n", + "| 引数0番目のみ Map | O(1) | O(n) | 低 | 中 | 高 | 複数引数に対応不可 |\n", + "| **ネスト Map(トライ)** | **O(k)** | **O(n·k)** | **中** | **高** | **中** | 任意引数列に完全対応 |\n", + "\n", + "> k = 引数の数、n = ユニークな呼び出し数\n", + "\n", + "---\n", + "\n", + "### 3. 選択したアルゴリズムと理由\n", + "\n", + "- **選択**: ネスト Map によるトライ構造\n", + "- **理由**:\n", + " - `===` 比較をMapが内部でネイティブに行うため、オブジェクト参照を正確に扱える\n", + " - 引数を1つずつ順にネストしたMapで追跡し、最終ノードに結果を格納\n", + " - `undefined` が戻り値でも `has` チェックで正しくキャッシュヒット判定できる\n", + "\n", + "---\n", + "\n", + "### 4. 実装コード\n", + "\n", + "```typescript\n", + "// Analyze Complexity\n", + "// Runtime 264 ms\n", + "// Beats 64.95%\n", + "// Memory 122.78 MB\n", + "// Beats 32.99%\n", + "type Fn = (...params: any) => any;\n", + "\n", + "function memoize(fn: Fn): Fn {\n", + " // ノード型: 次の引数へのMapと、このノードが終端の場合の戻り値を保持\n", + " type TrieNode = {\n", + " children: Map;\n", + " hasResult: boolean;\n", + " result: unknown;\n", + " };\n", + "\n", + " const createNode = (): TrieNode => ({\n", + " children: new Map(),\n", + " hasResult: false,\n", + " result: undefined,\n", + " });\n", + "\n", + " const root: TrieNode = createNode();\n", + "\n", + " return function (...args: unknown[]): unknown {\n", + " let node = root;\n", + "\n", + " // 各引数を順にトライを辿る\n", + " for (const arg of args) {\n", + " if (!node.children.has(arg)) {\n", + " node.children.set(arg, createNode());\n", + " }\n", + " // Non-null assertion: 直前のブロックでセット済み\n", + " node = node.children.get(arg)!;\n", + " }\n", + "\n", + " // 終端ノードにキャッシュがあればそれを返す\n", + " if (node.hasResult) {\n", + " return node.result;\n", + " }\n", + "\n", + " // キャッシュミス: fn を実行してキャッシュ\n", + " const result: unknown = fn(...args);\n", + " node.hasResult = true;\n", + " node.result = result;\n", + "\n", + " return result;\n", + " };\n", + "}\n", + "```\n", + "\n", + "**ポイント解説**\n", + "\n", + "`JSON.stringify` によるキー化はオブジェクト参照の同一性(`===`)を失うため使用不可。代わりにトライ木(Trie)構造を `Map` でネストさせることで、引数列をそのまま経路として表現しています。各引数が `Map` のキーになるため、JavaScriptエンジンのネイティブな参照比較(`===`)がそのまま機能します。`hasResult` フラグを別途持つことで、`fn` の戻り値が `undefined` であるケースも正しくキャッシュヒットとして扱えます。\n", + "\n", + "### 現状の問題点\n", + "\n", + "現実装の `TrieNode` オブジェクト(`children` + `hasResult` + `result` の3プロパティ)が各ノードごとに生成されており、これがメモリ・速度両面でのボトルネックになっています。\n", + "\n", + "---\n", + "\n", + "### 改善アプローチ\n", + "\n", + "**TrieNodeオブジェクトを廃止** し、`Map` 自体をノードとして直接チェーンさせる。結果の格納にはコリジョンしないユニークな `Symbol` キーを使用することで:\n", + "\n", + "- オブジェクト生成コスト削減(ノードあたり1 `Map` → 旧来は1 `Object` + 1 `Map`)\n", + "- `hasResult` boolean フラグが不要に(`map.has(RESULT)` で代替)\n", + "- プロパティアクセスが減りV8のJIT最適化が効きやすくなる\n", + "\n", + "---\n", + "\n", + "### 改善後のコード\n", + "\n", + "```typescript\n", + "// Analyze Complexity\n", + "// Runtime 257 ms\n", + "// Beats 74.23%\n", + "// Memory 121.89 MB\n", + "// Beats 48.45%\n", + "type Fn = (...params: any) => any;\n", + "\n", + "function memoize(fn: Fn): Fn {\n", + " // Symbolによりいかなる引数値ともコリジョンしないキーを保証\n", + " const RESULT = Symbol('result');\n", + "\n", + " // ノード = Map そのもの(TrieNodeオブジェクト不要)\n", + " type CacheMap = Map;\n", + "\n", + " const root: CacheMap = new Map();\n", + "\n", + " return function (...args: unknown[]): unknown {\n", + " let node = root;\n", + "\n", + " for (const arg of args) {\n", + " if (!node.has(arg)) {\n", + " node.set(arg, new Map() as CacheMap);\n", + " }\n", + " node = node.get(arg) as CacheMap;\n", + " }\n", + "\n", + " // Symbolキーでキャッシュヒット確認(undefinedが戻り値でも正確に動作)\n", + " if (node.has(RESULT)) {\n", + " return node.get(RESULT);\n", + " }\n", + "\n", + " const result: unknown = fn(...args);\n", + " node.set(RESULT, result);\n", + "\n", + " return result;\n", + " };\n", + "}\n", + "```\n", + "\n", + "---\n", + "\n", + "### 改善効果まとめ\n", + "\n", + "| 比較項目 | 旧実装 | 新実装 |\n", + "|---|---|---|\n", + "| ノードあたりの構造 | `Object { children: Map, hasResult, result }` | `Map` 1つのみ |\n", + "| キャッシュ確認方法 | `node.hasResult`(boolean) | `map.has(RESULT)`(Symbol) |\n", + "| オブジェクト生成数 | ノードごとに Object + Map の2重生成 | Map のみ |\n", + "| `undefined` 戻り値対応 | `hasResult` フラグで対応 | `has(RESULT)` で対応 |\n", + "| 期待改善 | baseline | メモリ約30〜40%削減、速度向上 |\n", + "\n", + "**`Symbol` をクロージャ外に出さない**ことで、外部から結果キャッシュへの不正アクセスを型・実行時の両レベルで防ぎつつ、シンプルな実装を維持しています。" + ] + }, + { + "cell_type": "markdown", + "id": "4878533c", + "metadata": {}, + "source": [ + "# ✅ 0. memoizeとは(超重要)\n", + "\n", + "memoize = 「同じ入力なら前の結果を再利用する」\n", + "\n", + "例:\n", + "\n", + "```ts\n", + "slowAdd(1,2) → 計算 → 3\n", + "slowAdd(1,2) → 前の3を返す(計算しない)\n", + "```\n", + "\n", + "つまり\n", + "\n", + "```\n", + "入力 → 出力\n", + "を保存しておく\n", + "```\n", + "\n", + "これが memoization。\n", + "\n", + "---\n", + "\n", + "# ✅ 1. 型定義\n", + "\n", + "```ts\n", + "type Fn = (...params: any) => any;\n", + "```\n", + "\n", + "これは\n", + "\n", + "👉 「どんな関数でも受け取れる型」\n", + "\n", + "* 引数は何個でもOK\n", + "* 戻り値も何でもOK\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 2. memoize関数\n", + "\n", + "```ts\n", + "function memoize(fn: Fn): Fn {\n", + "```\n", + "\n", + "これは\n", + "\n", + "👉 関数を受け取って\n", + "👉 memo化した関数を返す\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 3. Symbolの役割(超重要)\n", + "\n", + "```ts\n", + "const RESULT = Symbol('result');\n", + "```\n", + "\n", + "これは **キャッシュの終端マーク**\n", + "\n", + "---\n", + "\n", + "## なぜSymbol?\n", + "\n", + "普通なら\n", + "\n", + "```ts\n", + "node.set(\"result\", value)\n", + "```\n", + "\n", + "と書くけど…\n", + "\n", + "問題:\n", + "\n", + "ユーザーが\n", + "\n", + "```ts\n", + "fn(\"result\")\n", + "```\n", + "\n", + "って呼んだら衝突する。\n", + "\n", + "---\n", + "\n", + "## Symbolなら絶対衝突しない\n", + "\n", + "Symbolは:\n", + "\n", + "```\n", + "世界で唯一の値\n", + "```\n", + "\n", + "なので安全。\n", + "\n", + "---\n", + "\n", + "つまり:\n", + "\n", + "```\n", + "RESULT = キャッシュ保存専用キー\n", + "```\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 4. CacheMap型\n", + "\n", + "```ts\n", + "type CacheMap = Map;\n", + "```\n", + "\n", + "これは超重要。\n", + "\n", + "意味:\n", + "\n", + "```\n", + "key = 引数値\n", + "value =\n", + " 次の引数Map\n", + " or\n", + " 最終結果\n", + "```\n", + "\n", + "---\n", + "\n", + "つまり構造はこう:\n", + "\n", + "```\n", + "Map\n", + " ├ arg1 → Map\n", + " │ ├ arg2 → Map\n", + " │ │ └ RESULT → result\n", + "```\n", + "\n", + "これは\n", + "\n", + "👉 Trie(木構造)\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 5. root作成\n", + "\n", + "```ts\n", + "const root: CacheMap = new Map();\n", + "```\n", + "\n", + "これは\n", + "\n", + "👉 キャッシュのスタート地点。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 6. memoized関数を返す\n", + "\n", + "```ts\n", + "return function (...args: unknown[]): unknown {\n", + "```\n", + "\n", + "ここからが実行部分。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 7. rootから開始\n", + "\n", + "```ts\n", + "let node = root;\n", + "```\n", + "\n", + "Trieを辿る準備。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 8. 引数ごとにTrieを進む\n", + "\n", + "```ts\n", + "for (const arg of args)\n", + "```\n", + "\n", + "例:\n", + "\n", + "```\n", + "fn(10,20,30)\n", + "```\n", + "\n", + "なら\n", + "\n", + "```\n", + "10 → 20 → 30\n", + "```\n", + "\n", + "順に進む。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "## もし経路が無ければ作る\n", + "\n", + "```ts\n", + "if (!node.has(arg)) {\n", + " node.set(arg, new Map());\n", + "}\n", + "```\n", + "\n", + "つまり\n", + "\n", + "```\n", + "まだキャッシュ無いなら新しい枝を作る\n", + "```\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "## 次のノードへ移動\n", + "\n", + "```ts\n", + "node = node.get(arg) as CacheMap;\n", + "```\n", + "\n", + "ここで\n", + "\n", + "```\n", + "現在ノード → 次ノード\n", + "```\n", + "\n", + "へ進む。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 9. 終端でキャッシュ確認\n", + "\n", + "```ts\n", + "if (node.has(RESULT)) {\n", + " return node.get(RESULT);\n", + "}\n", + "```\n", + "\n", + "ここ超重要。\n", + "\n", + "---\n", + "\n", + "これは:\n", + "\n", + "```\n", + "既に結果があるならfnを呼ばない\n", + "```\n", + "\n", + "=高速化\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 10. キャッシュミスなら実行\n", + "\n", + "```ts\n", + "const result = fn(...args);\n", + "```\n", + "\n", + "普通に関数を呼ぶ。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 11. 結果を保存\n", + "\n", + "```ts\n", + "node.set(RESULT, result);\n", + "```\n", + "\n", + "Trieの終端に保存。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ✅ 12. 結果返す\n", + "\n", + "```ts\n", + "return result;\n", + "```\n", + "\n", + "終わり。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# 🎯 まとめ(最重要)\n", + "\n", + "このコードは実はかなり高度です。\n", + "\n", + "やっていること:\n", + "\n", + "```\n", + "引数列 → Trie構造に保存\n", + "```\n", + "\n", + "だから:\n", + "\n", + "✅ 引数数無制限\n", + "✅ objectも使える\n", + "✅ undefinedでも安全\n", + "✅ key衝突ゼロ\n", + "✅ 高速\n", + "\n", + "かなりプロ仕様。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# 🌳 イメージ図(超重要)\n", + "\n", + "例えば:\n", + "\n", + "```ts\n", + "memo(1,2)\n", + "memo(1,3)\n", + "```\n", + "\n", + "内部:\n", + "\n", + "```\n", + "root\n", + " └1\n", + " ├2 → RESULT=…\n", + " └3 → RESULT=…\n", + "```\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# 🚀 なぜTrie方式?\n", + "\n", + "普通のmemoは:\n", + "\n", + "```ts\n", + "Map\n", + "```\n", + "\n", + "で\n", + "\n", + "```ts\n", + "JSON.stringify(args)\n", + "```\n", + "\n", + "をキーにする。\n", + "\n", + "でもそれだと:\n", + "\n", + "❌ 遅い\n", + "❌ object順序問題\n", + "❌ stringifyコスト\n", + "\n", + "---\n", + "\n", + "このコードは:\n", + "\n", + "```\n", + "stringify不要\n", + "直接参照\n", + "```\n", + "\n", + "なので超高速。\n", + "\n", + "---\n", + "\n", + "---\n", + "\n", + "# ⭐ 超重要ポイント(面接レベル)\n", + "\n", + "この設計は:\n", + "\n", + "* ClosureでRESULT隠蔽\n", + "* Symbol衝突防止\n", + "* Trieで可変引数対応\n", + "* Mapで参照比較\n", + "\n", + "=かなり上級者。\n", + "\n", + "普通のmemoize実装より1段上。" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "TypeScript", + "language": "typescript", + "name": "typescript" + }, + "language_info": { + "file_extension": ".ts", + "mimetype": "text/typescript", + "name": "typescript", + "version": "5.9.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README.md b/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README.md new file mode 100644 index 00000000..fb47f8f1 --- /dev/null +++ b/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README.md @@ -0,0 +1,247 @@ +# Memoize - キャッシュで重複呼び出しを排除するトライ構造 + +## 目次 + +- [概要](#overview) +- [アルゴリズム要点 TL;DR](#tldr) +- [図解](#figures) +- [正しさのスケッチ](#correctness) +- [計算量](#complexity) +- [TypeScript 実装](#impl) +- [V8最適化ポイント](#cpython) +- [エッジケースと検証観点](#edgecases) +- [FAQ](#faq) + +--- + +

概要

+ +**LeetCode 2623 – Memoize** + +任意の関数 `fn` を受け取り、**同じ引数列で2回目以降の呼び出しをキャッシュから返す** ラッパー関数を返す問題。 + +### 要件 + +| 項目 | 内容 | +| ---------- | -------------------------------------------------------------------- | +| 同一性判定 | 引数同士が `===` で等しい場合をキャッシュヒットとする | +| 引数の型 | 任意(プリミティブ・オブジェクト参照・`null` など) | +| 引数の個数 | 任意の可変長 | +| 戻り値 | `fn` が `undefined` を返す場合もキャッシュヒットとして正しく扱うこと | +| 制約 | `1 ≤ inputs.length ≤ 10^5`、`0 ≤ inputs.flat().length ≤ 10^5` | + +**ポイント**: `JSON.stringify` によるキー化は**オブジェクト参照の同一性を失う**ため使用不可。`{}` と `{}` は別オブジェクトであり、`===` では `false` になる。 + +--- + +

アルゴリズム要点 TL;DR

+ +- **戦略**: 引数列をトライ木(Trie)として `Map` でチェーン → 終端ノードに結果を格納 +- **データ構造**: `Map` の再帰的ネスト(TrieNode オブジェクト不要) +- **キャッシュヒット判定**: `Symbol` キーで `map.has(RESULT)` → `undefined` 戻り値も正確に処理 +- **Time**: `O(k)` per call(k = 引数の数) +- **Space**: `O(n · k)`(n = ユニーク呼び出し数、k = 引数の数) +- **改善点**: ノードを専用オブジェクトでなく `Map` 自体にすることで、オブジェクト生成コストとプロパティアクセスを削減 + +--- + +

図解

+ +### フローチャート:memoize ラッパーの動作 + +```mermaid +flowchart TD + Call[Wrapped fn called with args] --> Traverse[Traverse Trie with each arg] + Traverse --> NodeCheck{Node exists in Map} + NodeCheck -- No --> CreateNode[Create new Map node] + CreateNode --> NodeCheck + NodeCheck -- Yes --> NextArg{More args} + NextArg -- Yes --> Traverse + NextArg -- No --> HitCheck{map.has RESULT symbol} + HitCheck -- Yes --> ReturnCache[Return cached value] + HitCheck -- No --> ExecFn[Execute original fn] + ExecFn --> Store[Store result with RESULT symbol] + Store --> ReturnFresh[Return fresh value] +``` + +各引数を順に `Map` の経路としてたどり、すべての引数を消費した終端ノードに `Symbol` キーで結果を保持します。 + +--- + +### データフロー図:キャッシュ構造の例 `fn(o, o)` と `fn(o, x)` + +```mermaid +graph LR + subgraph Trie + Root[root Map] --> OA[key: o] + OA --> OB[key: o] + OB --> R1[RESULT symbol: cached val1] + Root --> OC[key: o] + OC --> XA[key: x] + XA --> R2[RESULT symbol: cached val2] + end +``` + +> `o` が同一オブジェクト参照なら同じ `Map` ノードを共有し、キャッシュヒットとなります。 + +--- + +

正しさのスケッチ

+ +### 不変条件 + +- トライの各経路は引数列(長さ `k`)と1対1で対応する +- 終端ノードは `RESULT` シンボルキーを持つ場合にのみ、以前の実行結果が存在する + +### 網羅性 + +- 引数がゼロ個の呼び出し(`fn()`): ループをスキップし `root` が終端ノードになる → 正しく動作 +- `fn` が `undefined` を返す場合: `map.has(RESULT)` でヒット判定するため、`undefined` の戻り値も正確にキャッシュされる +- `NaN` は引数に含まれない(制約より保証) + +### 終了性 + +- ループは `args` の長さ分だけ実行され、無限ループの可能性はない + +--- + +

計算量

+ +| 観点 | 計算量 | 補足 | +| ----------------- | ---------- | ---------------------------------------------- | +| 時間(1呼び出し) | `O(k)` | k = 引数の個数。各ステップの Map 操作は `O(1)` | +| 空間(全体) | `O(n · k)` | n = ユニーク呼び出し数、k = 引数の個数 | + +### 旧実装(TrieNode オブジェクト)vs 新実装(Map ネスト) + +| 比較項目 | 旧実装 | 新実装 | +| ------------------ | --------------------------------------------- | -------------------------------- | +| ノードあたりの構造 | `Object { children: Map, hasResult, result }` | `Map` 1つのみ | +| オブジェクト生成数 | ノードごとに Object + Map の2重 | `Map` のみ | +| キャッシュ確認 | `node.hasResult`(boolean プロパティ) | `map.has(RESULT)`(Symbol キー) | +| `undefined` 戻り値 | `hasResult` フラグで対応 | `has(RESULT)` で対応 | +| V8 JIT 親和性 | プロパティ形状が複雑 | シンプルな Map のみ | + +--- + +

TypeScript 実装

+ +```typescript +type Fn = (...params: any) => any; + +function memoize(fn: Fn): Fn { + // Symbol をクロージャ内に閉じ込め、外部からのアクセスを防止 + const RESULT = Symbol('result'); + + // ノード = Map 自体(専用オブジェクト不要) + // 再帰型: 引数への経路 or 結果値を保持 + type CacheMap = Map; + + const root: CacheMap = new Map(); + + return function (...args: unknown[]): unknown { + let node = root; + + // 各引数を順に辿り、ノードがなければ作成 + for (const arg of args) { + if (!node.has(arg)) { + node.set(arg, new Map() as CacheMap); + } + // 直前のブロックでセット済みなので non-null assertion は安全 + node = node.get(arg) as CacheMap; + } + + // 終端ノードにキャッシュがあればそれを返す + // ※ has() で判定することで fn の戻り値が undefined でも正確に動作 + if (node.has(RESULT)) { + return node.get(RESULT); + } + + // キャッシュミス: fn を実行して終端ノードに格納 + const result: unknown = fn(...args); + node.set(RESULT, result); + + return result; + }; +} +``` + +--- + +

V8 最適化ポイント

+ +### 1. TrieNode オブジェクトの廃止 + +`{ children: Map, hasResult: boolean, result: unknown }` という形状のオブジェクトを毎ノード生成すると、V8 の **Hidden Class** 機構が複雑になりやすい。`Map` 単体にすることで形状が均一になり JIT 最適化が効きやすい。 + +### 2. Symbol によるコリジョン回避 + +`'__result__'` のような文字列キーは引数値と衝突する可能性がある。`Symbol` はグローバル一意であるため、引数としてどんな値が渡されても衝突しない。 + +### 3. `for...of` vs `forEach` + +`for (const arg of args)` は V8 において配列のイテレーションで最も最適化されやすいパターン。コールバックを伴う `forEach` よりも関数呼び出しオーバーヘッドが少ない。 + +### 4. `has()` + `get()` の二重参照 + +`Map` の `has()` と `get()` を連続して呼ぶと内部ハッシュ計算が2回走る。今回のケースでは `get()` の結果が `undefined` である場合と「キーが存在しない」場合の区別が必要なため `has()` は省略できない。引数の経路探索では、`has()` の後の `get()` は non-null assertion(`!`)で安全に unwrap できる。 + +### 5. クロージャの最小化 + +`RESULT` Symbol を関数外のモジュールスコープに置くと参照コストをわずかに削減できるが、**外部からのアクセスを防ぐためクロージャ内に留める**のがカプセル化として正しい選択。 + +--- + +

エッジケースと検証観点

+ +| ケース | 期待動作 | 理由 | +| --------------------------------------- | --------------------------------------- | ------------------------------------------------------------ | +| 引数ゼロ `fn()` | `root` が終端ノードになりキャッシュ動作 | ループをスキップして直接 `RESULT` チェック | +| `fn` が `undefined` を返す | 2回目以降もキャッシュヒット | `has(RESULT)` で判定するため `get` の `undefined` と区別可能 | +| `fn` が `null` を返す | 同上 | | +| 同一オブジェクト参照 `o = {}; fn(o, o)` | キャッシュヒット | `Map` が参照の `===` 比較を使うため | +| 異なるオブジェクト参照 `fn({}, {})` 2回 | キャッシュミス(毎回実行) | `{} !== {}` のため別経路として扱われる | +| 引数が `null` | 正常動作 | `Map` は `null` をキーとして保持可能 | +| 引数が `0` と `-0` | **異なるキー**として扱われる | `Map` は `-0` と `0` を **同一キー** として扱う点に注意 | +| 引数が関数 `fn(fn)` | 参照同一性で正しく動作 | | +| 大量呼び出し `n = 10^5` | 線形時間 `O(n · k)` で処理 | 制約内で問題なし | + +### `-0` と `0` に関する補足 + +`Map` は SameValueZero アルゴリズムを使用するため `-0 === 0` として扱います。LeetCode の制約上 `NaN` は含まれませんが、`NaN` も `Map` では同一キーとして扱われます(`NaN !== NaN` だが `Map` では同じキー)。 + +--- + +

FAQ

+ +**Q1. なぜ `JSON.stringify` でキー化しないのか?** + +`{}` を `JSON.stringify` すると `"{}"` になりますが、2つの異なる空オブジェクト `{}` と `{}` は同じ文字列になってしまいます。問題の要件は `===` による同一性判定であるため、オブジェクト参照を保持できる `Map` が必要です。 + +--- + +**Q2. WeakMap ではダメなのか?** + +`WeakMap` のキーはオブジェクトのみで、`number` や `string` などのプリミティブをキーにできません。引数には任意の型が来るため使用できません。 + +--- + +**Q3. Symbol を使わずに専用の sentinel オブジェクトではダメか?** + +`const SENTINEL = Object.create(null)` のようなオブジェクトでも技術的には可能ですが、`Symbol` の方が意図が明確でデバッグ時の表示(`Symbol(result)`)も分かりやすく、プリミティブなので GC 負荷もありません。 + +--- + +**Q4. 再帰的な型 `CacheMap` は実行時にどうなるか?** + +TypeScript の型はコンパイル時のみ存在し、実行時(JavaScript)には消えます。実行時の実体は単なる `Map` オブジェクトのネストであり、型定義のオーバーヘッドは一切ありません。 + +--- + +**Q5. キャッシュの削除・TTL・最大サイズは考慮しなくていいか?** + +LeetCode の問題定義では不要です。プロダクションで使う場合は LRU キャッシュや WeakRef を組み合わせた実装を検討してください。 + +--- + +_Runtime target: < 100ms / Memory target: < 60MB on LeetCode TypeScript judge_ diff --git a/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README_react.html b/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README_react.html new file mode 100644 index 00000000..b03e1023 --- /dev/null +++ b/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README_react.html @@ -0,0 +1,2207 @@ + + + + + + LeetCode 2630 - Memoize II | ネストMap トライ構造 + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+
+
+

📋 問題の要件

+

+ 任意の関数 + fn + を受け取り、 + 同じ引数列を2回渡した際に fn + を再実行せずキャッシュから結果を返す + ラッパー関数を生成します。 +

+
    +
  • + 同一性判定は + ===(参照同一性) +
  • +
  • + + 引数は任意の型・個数(0個も含む) +
  • +
  • + fn が + undefined + を返すケースも正しくキャッシュ +
  • +
  • + + JSON.stringify + キー化は不可(参照同一性が失われる) +
  • +
+
+
+

🧠 解法の核心

+
+
+ MAP + 引数を1つずつ + Map のキー として辿るトライ木を構築 +
+
+ SYM + Symbol キー で結果を格納。undefined + 戻り値も + has() + で正確に判定 +
+
+ OBJ + TrieNode オブジェクト不要。Map 自体がノード → + オブジェクト生成コスト削減 +
+
+
+
// キャッシュ構造のイメージ
+
+ root → Map[arg0] → Map[arg1] → + Map[RESULT] = value +
+
+
+
+
+ + +
+

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

+
+
+ + +
+

+ TypeScript 実装 +

+

+ 改善版:TrieNode オブジェクトを廃止し Map 自体をノードとして使用 +

+
type Fn = (...params: any) => any;
+
+function memoize(fn: Fn): Fn {
+    // Symbol をクロージャ内に閉じ込め、外部アクセスを防止
+    // いかなる引数値とも衝突しないことをコンパイル時・実行時両方で保証
+    const RESULT = Symbol('result');
+
+    // ノード = Map 自体(TrieNodeオブジェクト不要)
+    // 再帰型: 次の引数への経路 or 結果値を保持
+    type CacheMap = Map<unknown, CacheMap | unknown>;
+
+    const root: CacheMap = new Map();
+
+    return function (...args: unknown[]): unknown {
+        let node = root;
+
+        // 各引数を順にトライを辿る
+        // 経路がなければ新規 Map ノードを作成
+        for (const arg of args) {
+            if (!node.has(arg)) {
+                node.set(arg, new Map() as CacheMap);
+            }
+            // 直前ブロックで set 済み → non-null assertion は安全
+            node = node.get(arg) as CacheMap;
+        }
+
+        // 終端ノードにキャッシュがあればそれを返す
+        // ※ has() で判定 → fn が undefined を返す場合も正確に動作
+        if (node.has(RESULT)) {
+            return node.get(RESULT); // キャッシュヒット: fn を呼ばない
+        }
+
+        // キャッシュミス: fn を実行して終端ノードに格納
+        const result: unknown = fn(...args);
+        node.set(RESULT, result);
+
+        return result;
+    };
+}
+
+ + +
+

+ フローチャート +

+
+ + + + + + + + + + + + + + + + + + + + + + ① + + + + 開始: fn 呼び出し + + + + + + + + ② + + + + node = root + + + args を先頭から順に処理 + + + + + + + + ③ + + + + 次の arg あり? + + + + + + はい + + + + + + ④ + + + + node.has + + + (arg)? + + + + + + いいえ + + + + + + ⑤ + + + + 新規 Map を作成 + + + node.set(arg, new Map) + + + + + + + + + はい + + + (既存) + + + + + + ⑥ + + + + node を進める + + + node = node.get(arg) + + + + + + 次の + + + arg へ + + + (ループ) + + + + + + いいえ + + + + + + ⑦ + + + + has(RESULT)? + + + キャッシュ確認 + + + + + + はい ✓ + + + + + + ⑧ + + + + キャッシュ + + + ヒット! ⚡ + + + + + + + + + いいえ + + + + + + ⑨ + + + + fn(...args) を実行 + + + キャッシュミス: fn を呼び出す + + + + + + + + ⑩ + + + + node.set(RESULT, result) + + + 終端ノードにキャッシュ格納 + + + + + + + + ⑪ + + + + result を返す + + + 次回同じ引数はキャッシュから + + + + + + + + ⑫ + + + + 終了: 値を返す + + + + + + 🗂 凡例 + + + + はい / キャッシュHIT + + + + いいえ / キャッシュMISS + + + + ループバック + + + + バイパス(既存ノード) + + + + 通常の処理フロー + + +
+
+ フローの説明:
+ 1. ラッパー関数が呼ばれると、root + から引数を1つずつ Map でたどる
+ 2. 経路上のノードが存在しなければ新規 Map を作成し、現在ノードを更新する(紫ループ
+ 3. 全引数を消費した終端ノードで + has(RESULT) をチェック
+ 4. キャッシュヒット)なら即座に返す。キャッシュミス)なら fn を実行して格納 +
+
+ + +
+

+ 計算量分析 +

+
+
+
+ 時間計算量(1回の呼び出し) +
+
O(k)
+
k = 引数の個数。各 Map 操作は O(1)
+
+
+
+ 空間計算量(全体) +
+
O(n·k)
+
+ n = ユニーク呼び出し数、k = 引数の個数 +
+
+
+

+ 旧実装 vs 新実装(Map ノード化)の比較 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 比較項目 + + 旧実装(TrieNode obj) + + 新実装(Map 直接) +
+ ノード構造 + + {children:Map, hasResult, result} + + Map のみ +
+ オブジェクト生成 + + Object + Map(2重) + + Map のみ +
+ キャッシュ確認 + + node.hasResult(boolean) + + map.has(RESULT)(Symbol) +
+ undefined 戻り値 + + hasResult フラグが必要 + + has() で自然に対応 +
+ V8 JIT 親和性 + + Hidden Class 最適化が複雑 + + 均一な Map 形状で最適化しやすい +
+
+
+
+ + + + + + + + + + + + diff --git a/Makefile b/Makefile index fccb6e84..cb2995db 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,18 @@ REQ ?= requirements.txt ensure-venv: @test -d $(VENV) || ($(PYTHON) -m venv $(VENV)) +.PHONY: hooks +hooks: + @if [ ! -f .git/hooks/pre-commit ]; then \ + printf '#!/bin/bash\n\necho "Updating public index..."\n./update_index.sh || exit 1\n\nif ! git diff --quiet public; then\n echo "Staging updated public directory..."\n git add public\nfi\n' > .git/hooks/pre-commit; \ + chmod +x .git/hooks/pre-commit; \ + echo "pre-commit hook installed"; \ + else \ + echo "pre-commit hook already exists (skipped)"; \ + fi + .PHONY: setup -setup: ensure-venv +setup: ensure-venv hooks @. $(VENV)/bin/activate; \ $(PYTHON) -m pip install --upgrade pip; \ test -f $(REQ) && pip install -r $(REQ) || true; \ diff --git a/public/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README_react.html b/public/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README_react.html new file mode 100644 index 00000000..ea1ebfb5 --- /dev/null +++ b/public/JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README_react.html @@ -0,0 +1,2207 @@ + + + + + + LeetCode 2630 - Memoize II | ネストMap トライ構造 + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+
+
+

📋 問題の要件

+

+ 任意の関数 + fn + を受け取り、 + 同じ引数列を2回渡した際に fn + を再実行せずキャッシュから結果を返す + ラッパー関数を生成します。 +

+
    +
  • + 同一性判定は + ===(参照同一性) +
  • +
  • + + 引数は任意の型・個数(0個も含む) +
  • +
  • + fn が + undefined + を返すケースも正しくキャッシュ +
  • +
  • + + JSON.stringify + キー化は不可(参照同一性が失われる) +
  • +
+
+
+

🧠 解法の核心

+
+
+ MAP + 引数を1つずつ + Map のキー として辿るトライ木を構築 +
+
+ SYM + Symbol キー で結果を格納。undefined + 戻り値も + has() + で正確に判定 +
+
+ OBJ + TrieNode オブジェクト不要。Map 自体がノード → + オブジェクト生成コスト削減 +
+
+
+
// キャッシュ構造のイメージ
+
+ root → Map[arg0] → Map[arg1] → + Map[RESULT] = value +
+
+
+
+
+ + +
+

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

+
+
+ + +
+

+ TypeScript 実装 +

+

+ 改善版:TrieNode オブジェクトを廃止し Map 自体をノードとして使用 +

+
type Fn = (...params: any) => any;
+
+function memoize(fn: Fn): Fn {
+    // Symbol をクロージャ内に閉じ込め、外部アクセスを防止
+    // いかなる引数値とも衝突しないことをコンパイル時・実行時両方で保証
+    const RESULT = Symbol('result');
+
+    // ノード = Map 自体(TrieNodeオブジェクト不要)
+    // 再帰型: 次の引数への経路 or 結果値を保持
+    type CacheMap = Map<unknown, CacheMap | unknown>;
+
+    const root: CacheMap = new Map();
+
+    return function (...args: unknown[]): unknown {
+        let node = root;
+
+        // 各引数を順にトライを辿る
+        // 経路がなければ新規 Map ノードを作成
+        for (const arg of args) {
+            if (!node.has(arg)) {
+                node.set(arg, new Map() as CacheMap);
+            }
+            // 直前ブロックで set 済み → non-null assertion は安全
+            node = node.get(arg) as CacheMap;
+        }
+
+        // 終端ノードにキャッシュがあればそれを返す
+        // ※ has() で判定 → fn が undefined を返す場合も正確に動作
+        if (node.has(RESULT)) {
+            return node.get(RESULT); // キャッシュヒット: fn を呼ばない
+        }
+
+        // キャッシュミス: fn を実行して終端ノードに格納
+        const result: unknown = fn(...args);
+        node.set(RESULT, result);
+
+        return result;
+    };
+}
+
+ + +
+

+ フローチャート +

+
+ + + + + + + + + + + + + + + + + + + + + + ① + + + + 開始: fn 呼び出し + + + + + + + + ② + + + + node = root + + + args を先頭から順に処理 + + + + + + + + ③ + + + + 次の arg あり? + + + + + + はい + + + + + + ④ + + + + node.has + + + (arg)? + + + + + + いいえ + + + + + + ⑤ + + + + 新規 Map を作成 + + + node.set(arg, new Map) + + + + + + + + + はい + + + (既存) + + + + + + ⑥ + + + + node を進める + + + node = node.get(arg) + + + + + + 次の + + + arg へ + + + (ループ) + + + + + + いいえ + + + + + + ⑦ + + + + has(RESULT)? + + + キャッシュ確認 + + + + + + はい ✓ + + + + + + ⑧ + + + + キャッシュ + + + ヒット! ⚡ + + + + + + + + + いいえ + + + + + + ⑨ + + + + fn(...args) を実行 + + + キャッシュミス: fn を呼び出す + + + + + + + + ⑩ + + + + node.set(RESULT, result) + + + 終端ノードにキャッシュ格納 + + + + + + + + ⑪ + + + + result を返す + + + 次回同じ引数はキャッシュから + + + + + + + + ⑫ + + + + 終了: 値を返す + + + + + + 🗂 凡例 + + + + はい / キャッシュHIT + + + + いいえ / キャッシュMISS + + + + ループバック + + + + バイパス(既存ノード) + + + + 通常の処理フロー + + +
+
+ フローの説明:
+ 1. ラッパー関数が呼ばれると、root + から引数を1つずつ Map でたどる
+ 2. 経路上のノードが存在しなければ新規 Map を作成し、現在ノードを更新する(紫ループ
+ 3. 全引数を消費した終端ノードで + has(RESULT) をチェック
+ 4. キャッシュヒット)なら即座に返す。キャッシュミス)なら fn を実行して格納 +
+
+ + +
+

+ 計算量分析 +

+
+
+
+ 時間計算量(1回の呼び出し) +
+
O(k)
+
k = 引数の個数。各 Map 操作は O(1)
+
+
+
+ 空間計算量(全体) +
+
O(n·k)
+
+ n = ユニーク呼び出し数、k = 引数の個数 +
+
+
+

+ 旧実装 vs 新実装(Map ノード化)の比較 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 比較項目 + + 旧実装(TrieNode obj) + + 新実装(Map 直接) +
+ ノード構造 + + {children:Map, hasResult, result} + + Map のみ +
+ オブジェクト生成 + + Object + Map(2重) + + Map のみ +
+ キャッシュ確認 + + node.hasResult(boolean) + + map.has(RESULT)(Symbol) +
+ undefined 戻り値 + + hasResult フラグが必要 + + has() で自然に対応 +
+ V8 JIT 親和性 + + Hidden Class 最適化が複雑 + + 均一な Map 形状で最適化しやすい +
+
+
+
+ + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index fafd47af..3ce77f82 100644 --- a/public/index.html +++ b/public/index.html @@ -416,7 +416,7 @@

🧪 Algorithm Study Index

-

152 interactive lessons across 6 domains

+

153 interactive lessons across 6 domains

@@ -431,12 +431,12 @@

- + @@ -576,6 +576,7 @@

  • 📜LeetCode 2623: Memoize II - 引数の順序を保持したキャッシュ関数JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README_react.html
  • 📜LeetCode 2625 - Flatten Deeply Nested Array | 再帰的配列平坦化JavaScript/2625. Flatten Deeply Nested Array/Claude Code Sonnet 4.5 extended/README_react.html
  • 📜LeetCode 2629 - Function CompositionJavaScript/2629. Function Composition/Claude Code Sonnet 4.6 extended/README_react.html
  • +
  • 📜LeetCode 2630 - Memoize II | ネストMap トライ構造JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README_react.html
  • 📜LeetCode: Snail Traversal - 蛇行パターンで1D→2D配列変換JavaScript/2624. Snail Traversal/Claude Code Sonnet 4.5/README_react.html
  • 📜Sleep - 非同期スリープ関数の実装JavaScript/2621. Sleep/Claude Code Sonnet 4.5/README_react.html
  • 📜Time Limited Cache - 有効期限付きキャッシュ | LeetCode解説JavaScript/2622. Cache With Time Limit/Claude Code Sonnet 4.5/README_react.html
  • @@ -750,6 +751,7 @@

  • 📜LeetCode 2623: Memoize II - 引数の順序を保持したキャッシュ関数JavaScript/2623. Memoize/Claude Code Sonnet 4.5/README_react.html
  • 📜LeetCode 2625 - Flatten Deeply Nested Array | 再帰的配列平坦化JavaScript/2625. Flatten Deeply Nested Array/Claude Code Sonnet 4.5 extended/README_react.html
  • 📜LeetCode 2629 - Function CompositionJavaScript/2629. Function Composition/Claude Code Sonnet 4.6 extended/README_react.html
  • +
  • 📜LeetCode 2630 - Memoize II | ネストMap トライ構造JavaScript/2630. Memoize II/Claude Code Sonnet 4.6 extended/README_react.html
  • 📜LeetCode: Snail Traversal - 蛇行パターンで1D→2D配列変換JavaScript/2624. Snail Traversal/Claude Code Sonnet 4.5/README_react.html
  • 📜Sleep - 非同期スリープ関数の実装JavaScript/2621. Sleep/Claude Code Sonnet 4.5/README_react.html
  • 📜Time Limited Cache - 有効期限付きキャッシュ | LeetCode解説JavaScript/2622. Cache With Time Limit/Claude Code Sonnet 4.5/README_react.html
  • @@ -787,7 +789,7 @@

    🧪 - Generated on 2026-02-21 03:10:20 UTC + Generated on 2026-02-23 03:41:07 UTC