|
| 1 | +# Two Sum - ハッシュテーブル1パス探索 |
| 2 | + |
| 3 | +<h2 id="toc">目次</h2> |
| 4 | + |
| 5 | +- [概要](#overview) |
| 6 | +- [アルゴリズム要点(TL;DR)](#tldr) |
| 7 | +- [図解](#figures) |
| 8 | +- [正しさのスケッチ](#correctness) |
| 9 | +- [計算量](#complexity) |
| 10 | +- [Python実装](#impl) |
| 11 | +- [CPython最適化ポイント](#cpython) |
| 12 | +- [エッジケースと検証観点](#edgecases) |
| 13 | +- [FAQ](#faq) |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +<h2 id="overview">概要</h2> |
| 18 | + |
| 19 | +**問題**: 整数配列 `nums` と整数 `target` が与えられたとき、和が `target` になる2要素の**添字ペア**を返す。 |
| 20 | + |
| 21 | +**要件**: |
| 22 | + |
| 23 | +- 解は必ず1つ存在する(一意性保証) |
| 24 | +- 同じ要素を2回使用してはならない |
| 25 | +- 添字の順序は任意 |
| 26 | + |
| 27 | +**制約**: |
| 28 | + |
| 29 | +- `2 <= len(nums) <= 10^4` |
| 30 | +- `-10^9 <= nums[i], target <= 10^9` |
| 31 | + |
| 32 | +**Follow-up**: O(n²)未満の時間計算量で解けるか? |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +<h2 id="tldr">アルゴリズム要点(TL;DR)</h2> |
| 37 | + |
| 38 | +- **戦略**: ハッシュテーブル(`dict`)を用いた**1パス探索** |
| 39 | +- **データ構造**: `dict[int, int]` で 値→添字 を管理 |
| 40 | +- **時間計算量**: **O(n)** (配列を1回走査、各要素で平均O(1)のハッシュ操作) |
| 41 | +- **空間計算量**: **O(n)** (最悪ケースで n-1 個のエントリを保持) |
| 42 | +- **最適化**: 補数(`target - x`)の事前照会により、見つかった瞬間に即座に返却 |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +<h2 id="figures">図解</h2> |
| 47 | + |
| 48 | +## フローチャート |
| 49 | + |
| 50 | +```mermaid |
| 51 | +flowchart TD |
| 52 | + Start[Start twoSum] --> Init[Initialize empty dict seen] |
| 53 | + Init --> Loop{Enumerate nums} |
| 54 | + Loop -- For each i,x --> Calc[Compute need = target - x] |
| 55 | + Calc --> Check{Is need in seen?} |
| 56 | + Check -- Yes --> Found[Return seen_need, i] |
| 57 | + Check -- No --> Store{Is x in seen?} |
| 58 | + Store -- No --> Add[seen_x = i] |
| 59 | + Store -- Yes --> Skip[Skip duplicate] |
| 60 | + Add --> Loop |
| 61 | + Skip --> Loop |
| 62 | + Loop -- End of array --> Unreachable[Return -1,-1] |
| 63 | + Found --> End[End] |
| 64 | + Unreachable --> End |
| 65 | +``` |
| 66 | + |
| 67 | +**説明**: 配列を左から1回走査し、各要素 `x` に対して補数 `need = target - x` がハッシュに既存か確認。存在すれば即座にペアを返却。存在しなければ `x` を辞書に登録して次へ進む。 |
| 68 | + |
| 69 | +### データフロー図 |
| 70 | + |
| 71 | +```mermaid |
| 72 | +graph LR |
| 73 | + subgraph Input |
| 74 | + A[nums array] --> B[target value] |
| 75 | + end |
| 76 | + subgraph Core_Logic |
| 77 | + B --> C[Enumerate with index] |
| 78 | + C --> D[Compute complement] |
| 79 | + D --> E[Hash lookup in seen] |
| 80 | + E -- Hit --> F[Return indices] |
| 81 | + E -- Miss --> G[Register current value] |
| 82 | + end |
| 83 | + G --> C |
| 84 | + F --> H[Output pair] |
| 85 | +``` |
| 86 | + |
| 87 | +**説明**: 入力配列を走査しながら、各要素の補数をハッシュテーブルで照会。ヒット時は添字ペアを出力、ミス時は現在値を登録して次の要素へ遷移。 |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +<h2 id="correctness">正しさのスケッチ</h2> |
| 92 | + |
| 93 | +**不変条件**: |
| 94 | + |
| 95 | +- ループの各ステップで、辞書 `seen` は「現在位置より左側の要素のうち、初出のもの」の値→添字マッピングを保持 |
| 96 | + |
| 97 | +**網羅性**: |
| 98 | + |
| 99 | +- 解が必ず1つ存在するため、ペア `(i, j)` (i < j) のうち `j` に到達した時点で `nums[i]` は辞書に登録済み |
| 100 | +- 補数 `need = target - nums[j]` が `nums[i]` に一致するため、`need in seen` で検出される |
| 101 | + |
| 102 | +**基底条件**: |
| 103 | + |
| 104 | +- 配列長 >= 2、解の一意性保証により、ループ終了前に必ず `return` が実行される |
| 105 | + |
| 106 | +**終了性**: |
| 107 | + |
| 108 | +- 配列を1回走査するため、最悪でも O(n) ステップで終了 |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +<h2 id="complexity">計算量</h2> |
| 113 | + |
| 114 | +| 指標 | 計算量 | 理由 | |
| 115 | +|------------|---------|------------------------------------------| |
| 116 | +| **時間** | **O(n)** | 配列を1パス、各要素でハッシュ操作(平均O(1)) | |
| 117 | +| **空間** | **O(n)** | 最悪ケースで n-1 個の要素を辞書に格納 | |
| 118 | + |
| 119 | +**比較**: 二重ループ(O(n²))やソート+2ポインタ(O(n log n))より高速。 |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +<h2 id="impl">Python実装</h2> |
| 124 | + |
| 125 | +```python |
| 126 | +from __future__ import annotations |
| 127 | +from typing import List |
| 128 | + |
| 129 | + |
| 130 | +class Solution: |
| 131 | + def twoSum(self, nums: List[int], target: int) -> List[int]: |
| 132 | + """ |
| 133 | + ハッシュテーブル1パスでTwo Sumを解く |
| 134 | +
|
| 135 | + Args: |
| 136 | + nums: 整数配列(長さ >= 2) |
| 137 | + target: 目標和 |
| 138 | +
|
| 139 | + Returns: |
| 140 | + 和が target になる2要素の添字リスト [i, j] |
| 141 | +
|
| 142 | + Time Complexity: O(n) |
| 143 | + Space Complexity: O(n) |
| 144 | + """ |
| 145 | + seen: dict[int, int] = {} # value -> first occurrence index |
| 146 | + |
| 147 | + for i, x in enumerate(nums): |
| 148 | + need = target - x |
| 149 | + |
| 150 | + # 補数が既出か確認 |
| 151 | + if need in seen: |
| 152 | + return [seen[need], i] |
| 153 | + |
| 154 | + # 現在値を初出のみ登録(重複時は最左を保持) |
| 155 | + if x not in seen: |
| 156 | + seen[x] = i |
| 157 | + |
| 158 | + # 問題前提(解が必ず存在)により到達しないが型整合のため |
| 159 | + return [-1, -1] |
| 160 | +``` |
| 161 | + |
| 162 | +**主要ステップ**: |
| 163 | + |
| 164 | +1. **初期化**: 空の辞書 `seen` を用意 |
| 165 | +2. **走査**: `enumerate` でインデックスと値を同時取得 |
| 166 | +3. **補数計算**: `need = target - x` |
| 167 | +4. **照会**: `need in seen` で既出チェック → ヒット時は即返却 |
| 168 | +5. **登録**: ミス時は `x` を辞書に追加(初出のみ) |
| 169 | + |
| 170 | +--- |
| 171 | + |
| 172 | +<h2 id="cpython">CPython最適化ポイント</h2> |
| 173 | + |
| 174 | +### 標準実装の最適化 |
| 175 | + |
| 176 | +1. **`enumerate` の活用** |
| 177 | + - Pythonレベルでのインデックス管理を回避 |
| 178 | + - Cレベルのイテレータで高速化 |
| 179 | + |
| 180 | +2. **辞書のC実装** |
| 181 | + - CPythonの `dict` はハッシュテーブルのC実装で平均O(1) |
| 182 | + - `in` 演算子による存在確認も高速 |
| 183 | + |
| 184 | +3. **ローカル変数束縛** |
| 185 | + - ループ内で `target` や `seen` を参照するがグローバル/属性アクセスは不要 |
| 186 | + |
| 187 | +### マイクロ最適化版(上級者向け) |
| 188 | + |
| 189 | +```python |
| 190 | +from typing import List |
| 191 | + |
| 192 | + |
| 193 | +class Solution: |
| 194 | + def twoSum(self, nums: List[int], target: int) -> List[int]: |
| 195 | + """ |
| 196 | + メソッド束縛による属性解決削減版 |
| 197 | + Time: O(n), Space: O(n) |
| 198 | + """ |
| 199 | + seen: dict[int, int] = {} |
| 200 | + # 辞書メソッドをローカル束縛(属性解決オーバーヘッド削減) |
| 201 | + contains = seen.__contains__ |
| 202 | + getitem = seen.__getitem__ |
| 203 | + setitem = seen.__setitem__ |
| 204 | + |
| 205 | + for i, x in enumerate(nums): |
| 206 | + if contains(x): |
| 207 | + return [getitem(x), i] |
| 208 | + setitem(target - x, i) |
| 209 | + |
| 210 | + return [-1, -1] |
| 211 | +``` |
| 212 | + |
| 213 | +**改善点**: |
| 214 | + |
| 215 | +- `__contains__` / `__getitem__` / `__setitem__` をローカルで保持 |
| 216 | +- 属性解決(`.` 演算子)のバイトコードを削減 |
| 217 | +- 補数を辞書のキーとして登録する方式で `need` 変数を削減 |
| 218 | + |
| 219 | +**注意**: 効果は数%程度。LeetCode実行環境のノイズも大きいため、複数回実行して判断。 |
| 220 | + |
| 221 | +--- |
| 222 | + |
| 223 | +<h2 id="edgecases">エッジケースと検証観点</h2> |
| 224 | + |
| 225 | +| ケース | 入力例 | 期待出力 | 検証観点 | |
| 226 | +|--------------------|-----------------------------|-----------|------------------------| |
| 227 | +| **最小長(2要素)** | `[3, 3], 6` | `[0, 1]` | 同値ペアで正常動作 | |
| 228 | +| **負数混在** | `[-1, -2, -3, -4, -5], -8` | `[2, 4]` | 負数でもハッシュ探索が成立 | |
| 229 | +| **大きな値** | `[10^9, 10^9-1], 2*10^9-1` | `[0, 1]` | 整数オーバーフローなし | |
| 230 | +| **重複値が複数** | `[2, 5, 5, 11], 10` | `[1, 2]` | 最左のペアを優先(初出保持) | |
| 231 | +| **解が配列末尾** | `[1, 2, 3, 4], 7` | `[2, 3]` | 全走査で最後まで正しく動作 | |
| 232 | +| **0を含む** | `[0, 4, 3, 0], 0` | `[0, 3]` | 0の扱いでバグらない | |
| 233 | + |
| 234 | +**境界値**: |
| 235 | + |
| 236 | +- `len(nums) = 2` (最小長) |
| 237 | +- `nums[i] = -10^9, 10^9` (範囲端) |
| 238 | +- `target = -10^9, 10^9` |
| 239 | + |
| 240 | +**型チェック(pylance互換)**: |
| 241 | + |
| 242 | +- `nums: List[int]` 明示 |
| 243 | +- 戻り値も `List[int]` 固定 |
| 244 | + |
| 245 | +--- |
| 246 | + |
| 247 | +<h2 id="faq">FAQ</h2> |
| 248 | + |
| 249 | +### Q1: なぜソート+2ポインタではなくハッシュなのか? |
| 250 | + |
| 251 | +**A**: ソートは O(n log n) で、さらに元の添字を保持するため `(値, 添字)` ペアを生成する必要がある。ハッシュは O(n) で、添字管理も自然に行える。 |
| 252 | + |
| 253 | +### Q2: 同じ要素を2回使ってしまう心配は? |
| 254 | + |
| 255 | +**A**: ループで「先に照会→後で登録」の順序を守るため、現在の `x` は辞書に未登録。補数が `x` 自身でも、別の位置の `x` のみがヒットする。 |
| 256 | + |
| 257 | +### Q3: 重複値が複数ある場合の挙動は? |
| 258 | + |
| 259 | +**A**: `if x not in seen` により、最初の出現のみ辞書に保持。後続の同値は登録しないため、最左の添字が優先される。 |
| 260 | + |
| 261 | +### Q4: LeetCodeで実行時間にばらつきがあるのはなぜ? |
| 262 | + |
| 263 | +**A**: サーバー負荷やキャッシュ状態の影響。数回提出して中央値で判断するのが妥当。マイクロ最適化の効果は数%程度。 |
| 264 | + |
| 265 | +### Q5: 業務で使う場合の改良点は? |
| 266 | + |
| 267 | +**A**: 入力検証(型チェック、長さチェック)を追加し、`TypeError` / `ValueError` を明示的に送出。ドキュメント文字列も詳細化。 |
| 268 | + |
| 269 | +--- |
| 270 | + |
| 271 | +**まとめ**: Two Sum は**ハッシュテーブル1パス**が最適解。CPythonの `dict` のC実装を活かし、O(n)時間・O(n)空間で安定動作。Follow-upの O(n²) 未満も満たし、型安全性・可読性も両立。 |
0 commit comments