Skip to content

Commit 792ecda

Browse files
committed
Data Structures: leetcode 1. Two Sum Map
1 parent 57cb6e0 commit 792ecda

6 files changed

Lines changed: 2590 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
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

Comments
 (0)