Skip to content

Commit 1d9db06

Browse files
committed
feat: add Claude 4.6 extended solution for LeetCode 83 (Remove Duplicates from Sorted List)
1 parent ba3b28e commit 1d9db06

7 files changed

Lines changed: 3475 additions & 4 deletions

File tree

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
# Remove Duplicates from Sorted List - In-place Pointer Traversal
2+
3+
## 目次(Table of Contents)
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+
### 問題要約
20+
21+
**LeetCode #83 — Remove Duplicates from Sorted List**
22+
23+
ソート済み単方向連結リストの先頭ノード `head` を受け取り、**各要素がちょうど 1 回だけ現れる**ようにリストをインプレースで変更して返す。
24+
25+
### 要件
26+
27+
| 項目 | 内容 |
28+
| ---------- | ---------------------------------------------------- |
29+
| 入力 | `head: Optional[ListNode]`(ソート済み連結リスト) |
30+
| 出力 | `Optional[ListNode]`(重複除去済み、同じ先頭ノード) |
31+
| ノード数 | 0 〜 300 |
32+
| 値の範囲 | −100 〜 100 |
33+
| ソート保証 | 昇順ソート済み(重複は必ず**隣接**する) |
34+
| 安定性 | 元の相対順序を保持 |
35+
| 副作用 | `next` ポインタをインプレースで変更(Pure ではない) |
36+
37+
---
38+
39+
<h2 id="tldr">アルゴリズム要点(TL;DR)</h2>
40+
41+
- **戦略**: ソート済みであることを活かし、**隣接する重複を 1 パスで除去**する
42+
- **データ構造**: `current` ポインタ 1 本のみ(追加構造なし)
43+
- **操作**: `current.val == current.next.val` の間 `current.next` を読み飛ばし続け、異なった時点で `current` を前進
44+
- **計算量**: Time `O(n)` / Space `O(1)`
45+
- **メモリ**: スタック変数 `current``nxt` のみ。新規ノード生成ゼロ
46+
- **CPython 最適化**: `current.next` の繰り返し属性アクセスをローカル変数 `nxt` にキャッシュして `LOAD_ATTR` を削減
47+
48+
---
49+
50+
<h2 id="figures">図解</h2>
51+
52+
### フローチャート
53+
54+
```mermaid
55+
flowchart TD
56+
Start[Start deleteDuplicates] --> Guard{head is None or head.next is None}
57+
Guard -- Yes --> RetHead[Return head as is]
58+
Guard -- No --> Init[current = head]
59+
Init --> LoopCond{current.next is not None}
60+
LoopCond -- No --> RetHead2[Return head]
61+
LoopCond -- Yes --> Cache[nxt = current.next]
62+
Cache --> Dup{current.val == nxt.val}
63+
Dup -- Yes --> Skip[current.next = nxt.next]
64+
Skip --> LoopCond
65+
Dup -- No --> Advance[current = nxt]
66+
Advance --> LoopCond
67+
```
68+
69+
> `current` は重複が続く限り**移動しない**点がポイント。`nxt` をスキップしても同値が連続する可能性があるため、読み飛ばし後に再度チェックする。
70+
71+
---
72+
73+
### データフロー図(具体例)
74+
75+
```mermaid
76+
graph LR
77+
subgraph Input
78+
N1[1] --> N2[1]
79+
N2 --> N3[2]
80+
N3 --> N4[3]
81+
N4 --> N5[3]
82+
N5 --> NullA[None]
83+
end
84+
subgraph Step1
85+
S1_1[1] --> S1_3[2]
86+
S1_3 --> S1_4[3]
87+
S1_4 --> S1_5[3]
88+
S1_5 --> NullB[None]
89+
end
90+
subgraph Step2
91+
S2_1[1] --> S2_3[2]
92+
S2_3 --> S2_4[3]
93+
S2_4 --> NullC[None]
94+
end
95+
subgraph Output
96+
O1[1] --> O3[2]
97+
O3 --> O4[3]
98+
O4 --> NullD[None]
99+
end
100+
Input -- skip dup 1 --> Step1
101+
Step1 -- advance cur --> Step2
102+
Step2 -- skip dup 3 --> Output
103+
```
104+
105+
> 各ステップで `current.next` の接続先を付け替えることで、中間ノードをリストから切り離す。切り離されたノードは Python の GC が自動回収する。
106+
107+
---
108+
109+
### ASCII ポインタ操作図
110+
111+
```
112+
【初期状態】
113+
[1] → [1] → [2] → [3] → [3] → None
114+
115+
cur
116+
117+
Step 1: cur.val(1) == nxt.val(1) → nxt をスキップ
118+
cur.next = nxt.next
119+
[1] ──────→ [2] → [3] → [3] → None
120+
121+
cur ※ cur は移動しない
122+
123+
Step 2: cur.val(1) != nxt.val(2) → cur を前進
124+
[1] → [2] → [3] → [3] → None
125+
126+
cur
127+
128+
Step 3: cur.val(2) != nxt.val(3) → cur を前進
129+
[1] → [2] → [3] → [3] → None
130+
131+
cur
132+
133+
Step 4: cur.val(3) == nxt.val(3) → nxt をスキップ
134+
[1] → [2] → [3] → None
135+
136+
cur
137+
138+
Step 5: cur.next is None → ループ終了
139+
出力: [1] → [2] → [3] → None ✅
140+
```
141+
142+
---
143+
144+
<h2 id="correctness">正しさのスケッチ</h2>
145+
146+
### 不変条件
147+
148+
> ループの各反復開始時点で、`head` から `current`(含む)までの部分列には重複がない。
149+
150+
| 条件 | 説明 |
151+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
152+
| **初期化** | `current = head` の時点で部分列は長さ 1 → 自明に重複なし |
153+
| **維持** | `current.val == nxt.val` なら `nxt` をスキップ(不変条件を保ちつつ次を確認)。異なれば `current` を前進(不変条件は維持される) |
154+
| **終了** | `current.next is None` でループを抜けると、リスト全体で不変条件が成立 |
155+
156+
### 網羅性
157+
158+
- 重複を検出するたびに `next` を付け替え → 検出漏れなし(ソート済みなので隣接比較で十分)
159+
- `current` が前進するのは値が異なる場合のみ → 3連続以上の重複も正しく処理される
160+
161+
### 終了性
162+
163+
- `current` は前進するか、`next` ポインタが短縮されるかのいずれか
164+
- リストは有限長 → 必ず `current.next is None` に到達してループ終了
165+
166+
---
167+
168+
<h2 id="complexity">計算量</h2>
169+
170+
| 指標 || 理由 |
171+
| -------------- | ------ | ------------------------------------------------------- |
172+
| **時間計算量** | `O(n)` | 各ノードを最大 1 回しか走査しない |
173+
| **空間計算量** | `O(1)` | スタック変数 `current` / `nxt` のみ。ヒープ追加確保ゼロ |
174+
175+
### in-place vs Pure 比較
176+
177+
| 方式 | Time | Space | 特徴 |
178+
| ---------------------- | ---- | ----- | ----------------------------------------- |
179+
| **in-place(本実装)** | O(n) | O(1) | 元のノードを再利用。ヒープ確保なし |
180+
| 配列変換+再構築 | O(n) | O(n) | 可読性高いが不要なオブジェクト生成あり |
181+
| 再帰 | O(n) | O(n) | コール スタック消費。n ≤ 300 なら許容範囲 |
182+
183+
---
184+
185+
<h2 id="impl">Python 実装</h2>
186+
187+
```python
188+
from __future__ import annotations
189+
190+
from typing import Optional, TYPE_CHECKING
191+
192+
if TYPE_CHECKING:
193+
# pylance / mypy 用の型スタブ(実行時には評価されない)
194+
class ListNode:
195+
val: int
196+
next: Optional[ListNode]
197+
198+
def __init__(
199+
self, val: int = 0, next: Optional[ListNode] = None
200+
) -> None: ...
201+
202+
try:
203+
# LeetCode 実行環境では ListNode が既に定義済み → そのまま利用
204+
ListNode # type: ignore[used-before-def]
205+
except NameError:
206+
# ローカル実行用の最小フォールバック
207+
class ListNode: # type: ignore[no-redef]
208+
__slots__ = ("val", "next")
209+
210+
def __init__(
211+
self, val: int = 0, next: Optional[ListNode] = None
212+
) -> None:
213+
self.val = val
214+
self.next = next
215+
216+
217+
class Solution:
218+
"""
219+
LeetCode #83 — Remove Duplicates from Sorted List
220+
221+
業務開発版(型安全・pylance 対応)と
222+
競技プログラミング版(最速・最小)の 2 パターンを提供。
223+
"""
224+
225+
# ------------------------------------------------------------------ #
226+
# 業務開発版 ── 型安全・可読性・pylance 対応 #
227+
# ------------------------------------------------------------------ #
228+
def deleteDuplicates(
229+
self, head: Optional[ListNode]
230+
) -> Optional[ListNode]:
231+
"""
232+
ソート済み連結リストの重複ノードをインプレースで削除する(業務開発版)
233+
234+
Args:
235+
head: 連結リストの先頭ノード(空リストの場合は None)
236+
237+
Returns:
238+
重複を除いたソート済み連結リストの先頭ノード
239+
240+
Time Complexity: O(n) ─ 各ノードを最大 1 回走査
241+
Space Complexity: O(1) ─ ポインタ変数のみ、追加メモリなし
242+
"""
243+
# ── ガード節 ────────────────────────────────────────────────────
244+
# 空リスト or ノードが 1 つ → 重複なし、そのまま返す
245+
if head is None or head.next is None:
246+
return head
247+
248+
# ── 1 ポインタ走査(インプレース) ────────────────────────────
249+
current: ListNode = head
250+
251+
while current.next is not None:
252+
# LOAD_ATTR 削減のため current.next をローカル変数にキャッシュ
253+
nxt: ListNode = current.next
254+
255+
if current.val == nxt.val:
256+
# 重複検出 → nxt をスキップ(current は移動しない)
257+
# 次のノードも同値の可能性があるため current は据え置き
258+
current.next = nxt.next
259+
else:
260+
# 値が異なる → current を 1 つ前進
261+
current = nxt
262+
263+
return head
264+
265+
# ------------------------------------------------------------------ #
266+
# 競技プログラミング版 ── 最速・型チェック省略 #
267+
# ------------------------------------------------------------------ #
268+
def deleteDuplicates_competitive(
269+
self, head: Optional[ListNode]
270+
) -> Optional[ListNode]:
271+
"""
272+
競技プログラミング向け最適化実装
273+
274+
- エラーハンドリング省略
275+
- ローカル変数キャッシュで LOAD_ATTR を削減
276+
- CPython の属性参照コストを最小化
277+
278+
Time Complexity: O(n)
279+
Space Complexity: O(1)
280+
"""
281+
cur = head
282+
while cur and cur.next:
283+
nxt = cur.next
284+
if cur.val == nxt.val:
285+
cur.next = nxt.next # スキップ(cur は移動しない)
286+
else:
287+
cur = nxt # 前進
288+
return head
289+
```
290+
291+
---
292+
293+
<h2 id="cpython">CPython 最適化ポイント</h2>
294+
295+
### 属性アクセスのキャッシュ
296+
297+
```python
298+
# ❌ 遅い: 毎回 LOAD_ATTR が 2 回発生
299+
while current.next is not None:
300+
if current.val == current.next.val:
301+
current.next = current.next.next
302+
303+
# ✅ 速い: nxt にキャッシュして LOAD_ATTR を削減
304+
while current.next is not None:
305+
nxt = current.next # ← ここで 1 回だけ LOAD_ATTR
306+
if current.val == nxt.val:
307+
current.next = nxt.next
308+
```
309+
310+
| テクニック | 効果 | 本問題への適用 |
311+
| ------------------------ | -------------------------------------------------- | ----------------------- |
312+
| ローカル変数キャッシュ | `LOAD_ATTR``LOAD_FAST`(約 2 倍高速) | `nxt = current.next`|
313+
| `while cur and cur.next` | `None` チェックを CPython の truthiness で短絡評価 | 競技版で適用 ✅ |
314+
| スライス回避 | 不要なリストコピーを避ける | 本問題は不要 — |
315+
| `lru_cache` | 再帰的メモ化 | 本問題は不要 — |
316+
| `bisect` | ソート済み配列への二分探索 | 本問題は不要 — |
317+
318+
### なぜ配列変換しないか
319+
320+
```python
321+
# ❌ 配列変換+再構築(O(n) 追加メモリ)
322+
vals = []
323+
cur = head
324+
while cur:
325+
if not vals or vals[-1] != cur.val:
326+
vals.append(cur.val)
327+
cur = cur.next
328+
# → 新規 ListNode を n 個生成するコストが発生
329+
```
330+
331+
ポインタ付け替えのみなら新規オブジェクト生成ゼロで、GC 負荷も最小になる。
332+
333+
---
334+
335+
<h2 id="edgecases">エッジケースと検証観点</h2>
336+
337+
| ケース | 入力 | 期待出力 | 対処 |
338+
| ------------ | ----------------------- | ------------ | ----------------------------------- |
339+
| 空リスト | `head = None` | `None` | ガード節で即時 return |
340+
| ノード 1 つ | `[5]` | `[5]` | `head.next is None` で即時 return |
341+
| 全て同値 | `[3, 3, 3]` | `[3]` | inner while が連続スキップ |
342+
| 全て異なる | `[1, 2, 3]` | `[1, 2, 3]` | 重複検出なし、そのまま return |
343+
| 2 ノード重複 | `[1, 1]` | `[1]` | 1 回スキップして終了 |
344+
| 先頭のみ重複 | `[1, 1, 2, 3]` | `[1, 2, 3]` | Step 1 でスキップ |
345+
| 末尾のみ重複 | `[1, 2, 3, 3]` | `[1, 2, 3]` | 最終ステップでスキップ |
346+
| 最大制約 | n = 300, 全値 −100〜100 | 重複除去済み | O(n) で問題なし |
347+
| 負の値を含む | `[-3, -3, 0, 1, 1]` | `[-3, 0, 1]` | `==` 比較なので値の正負に依存しない |
348+
349+
---
350+
351+
<h2 id="faq">FAQ</h2>
352+
353+
**Q1. `current` を重複スキップ時に前進させない理由は?**
354+
355+
> ソート済みリストで 3 つ以上同値が連続する場合(例: `[1, 1, 1]`)、1 回スキップしても次も同値の可能性がある。`current` を動かさず再チェックすることで、連続する全重複を正しく除去できる。
356+
357+
**Q2. スキップされたノード(旧 `next`)のメモリはどうなる?**
358+
359+
> Python は参照カウント方式の GC を持つ。`current.next = nxt.next``nxt` への参照が消えると、`nxt` の参照カウントが 0 になり即座に解放される(CPython の場合)。
360+
361+
**Q3. なぜ再帰で実装しないのか?**
362+
363+
> 再帰版は可読性が高いが、呼び出しスタックを `O(n)` 消費する。本問題は `n ≤ 300` なので実用上問題ないが、反復版の方がスタックオーバーフローリスクがなく、空間計算量も `O(1)` と優れるため反復を選択した。
364+
365+
**Q4. `head.next is None` のガードは本当に必要か?**
366+
367+
> 厳密にはなくても動作する(`while current.next is not None` が即座にスキップされるため)。しかし「ノードが 1 つ以下なら変更不要」という意図を明示することで可読性と保守性が向上するため、明示的に記述している。
368+
369+
**Q5. 競技プログラミング版と業務開発版の実行速度差は?**
370+
371+
> `n ≤ 300` の小規模入力では測定誤差レベルの差しか生じない。大規模入力(n が数万〜数十万)では `LOAD_ATTR` のキャッシュ効果が現れ始めるが、本問題の制約では本質的な差はない。業務コードでは可読性・型安全性を優先した実装を推奨する。

0 commit comments

Comments
 (0)