Skip to content

Commit a28e5de

Browse files
committed
docs(leetcode): add multi-language explanations for LeetCode 26 Remove Duplicates from Sorted Array
1 parent e2f0975 commit a28e5de

11 files changed

Lines changed: 2502 additions & 0 deletions

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-C#.md

Lines changed: 193 additions & 0 deletions
Large diffs are not rendered by default.

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-C++.md

Lines changed: 201 additions & 0 deletions
Large diffs are not rendered by default.

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-C.md

Lines changed: 207 additions & 0 deletions
Large diffs are not rendered by default.

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-Dart.md

Lines changed: 183 additions & 0 deletions
Large diffs are not rendered by default.

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-Go.md

Lines changed: 316 additions & 0 deletions
Large diffs are not rendered by default.

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-Java.md

Lines changed: 194 additions & 0 deletions
Large diffs are not rendered by default.

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-Kotlin.md

Lines changed: 190 additions & 0 deletions
Large diffs are not rendered by default.

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-Python.md

Lines changed: 338 additions & 0 deletions
Large diffs are not rendered by default.

Algorithm/TwoPointers/leetcode/26. Remove Duplicates from Sorted Array/Sonnet 5/Remove-Duplicates-from-Sorted-Array-Rust.md

Lines changed: 310 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# LeetCode解答フォーマット
2+
3+
---
4+
5+
## 1. 問題の分析
6+
7+
> 💡 **初学者向け補足**:この問題は、一言で言うと「すでに並び替え済みの配列から、同じ値が連続している部分をひとつにまとめて、前方に詰め直す問題」です。配列自体を直接書き換える(in-place=その場で操作する)必要がある点がポイントです。
8+
9+
### 競技プログラミング視点での分析
10+
11+
- 配列はすでにソート済みなので、**同じ値は必ず隣り合っている**という性質を利用できます。これにより、余計な探索(例えば全要素同士を比較する)をせず、**配列を1回だけ左から右へなめる(1パス)だけ**で処理が完結します。
12+
- 実行速度を最優先するなら、時間計算量 O(n)(nは配列の要素数)が理論上の最速ラインです。全要素を最低1回は確認する必要があるため、これより速くはなりません。
13+
- メモリ使用量の最小化方針としては、新しい配列やSet(=重複を許さない集合を表すデータ構造)を作らず、**与えられた配列そのものを書き換える**ことで、追加メモリを定数個の変数(インデックス用の整数など)だけに抑えます。これが空間計算量 O(1) を実現する鍵です。
14+
15+
### 業務開発視点での分析
16+
17+
- この問題では境界条件(配列が空、要素が1個だけ、全要素が同じ値、全要素がバラバラ等)を正しく処理できるかが保守性・堅牢性の観点で重要です。
18+
- Swiftでは配列は「値型(`struct`)(=代入時に値そのものがコピーされる型。詳しくは後述)」ですが、この問題のシグネチャは `inout` (イン・アウト。=関数の外にある変数を直接書き換えられるようにする仕組み) を使って参照渡しのように振る舞わせているため、呼び出し元の配列を安全に直接変更できます。
19+
- エラーハンドリングについては、この問題は「`nums.length >= 1`」という制約が保証されているため例外を投げる(`throws`)必要はありませんが、念のため空配列に対しても安全に動くようガード(`guard`)を入れておくと、将来的な仕様変更にも耐えられる堅牢なコードになります。
20+
21+
### Swift特有の考慮点
22+
23+
- **値型(`struct`)と参照型(`class`)の使い分け**:Swiftの`Array`は値型です。Javaの配列やC++の生ポインタ配列のように「複数の変数が同じメモリ領域を指す」ということは基本的に起きません。しかし今回は`inout`を使うことで、まるで参照渡しのように、呼び出し元の配列を直接編集できます。これにより新しい配列をコピーして返す必要がなくなり、メモリ効率が良くなります。
24+
- **ジェネリクス・プロトコル境界**:今回のシグネチャは`Int`型固定ですが、もし汎用化するなら`<T: Comparable>`(=「大小比較ができる型」という制約付きの型パラメータ)を使うことで、`Int`以外の型(`Double``String`など)にも対応できる汎用的な実装が可能です。
25+
- **Optionalバインディングによるnull安全性**:今回のアルゴリズムでは配列の添字アクセスのみを使い、`Optional`型(=値があるかもしれないし、ないかもしれないことを表す型。`T?`と書く)は登場しません。ただし、範囲外アクセスによるクラッシュを防ぐため、ループの範囲指定を正確に行う必要があります。
26+
27+
> 📖 **このセクションで登場した用語**
28+
>
29+
> - **in-place(インプレース)**:新しい配列を作らず、与えられた配列そのものを直接書き換える操作方式。
30+
> - **`inout`**:Swiftで、関数の引数を「呼び出し元の変数を直接書き換えられる形」で受け取るためのキーワード。C++の参照渡し(`&`)に近い概念。
31+
> - **値型(`struct`**:代入時に値そのものがコピーされる型。SwiftのArray・Int・Stringなどが該当。
32+
> - **1パスアルゴリズム**:配列やリストを先頭から末尾まで1回だけ走査して処理を完了させるアルゴリズムのこと。
33+
34+
---
35+
36+
## 2. アルゴリズムアプローチ比較
37+
38+
> 💡 **初学者向け補足**:同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」を比べて、この問題の制約(in-place・O(1)空間)に最も適したものを選びます。
39+
40+
| アプローチ | 時間計算量 | 空間計算量 | Swift実装コスト | 型安全性 | 可読性 | 備考 |
41+
| ---------------------------------------- | ---------- | ---------- | --------------- | -------- | ------ | ------------------------------------------------------------ |
42+
| **A. Two-Pointer(低速・高速ポインタ)** | O(n) | O(1) |||| ソート済みという前提を最大限活用。in-place要件を満たす最適解 |
43+
| B. Setを使った重複除去 | O(n) | O(n) |||| Setは順序を保証しないため、別途ソートし直す必要があり非効率 |
44+
| C. 新配列にfilterして結果を返す | O(n) | O(n) |||| シンプルだが「in-place」というこの問題の要件そのものに反する |
45+
46+
> 💡 **Big-O記法の読み方**(初学者向け)
47+
>
48+
> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリで済む(最速・最小)
49+
> - `O(n)`:入力が2倍になると、処理も約2倍になる(線形)
50+
> - `O(n²)`:入力が2倍になると、処理は約4倍になる(二重ループに多い、今回は該当なし)
51+
52+
**Swift特有の値型コピーコストについて**:方法B・Cでは新しい`Array`(Setから配列への変換、あるいは`filter`の結果)を生成するため、要素数分のメモリ確保とコピーが発生します。SwiftのArrayはCopy-on-Write(COW。=実際に変更が加えられるまではコピーを作らず、参照を共有し続ける最適化)を採用していますが、それでも「新しい配列を作る」という行為自体はO(n)のメモリ確保を伴います。一方、方法Aは既存の配列を直接上書きするだけなので、この追加コストが一切発生しません。
53+
54+
> 📖 **このセクションで登場した用語**
55+
>
56+
> - **時間計算量**:入力の大きさに対して、処理にかかる手間がどう増えるかの目安。
57+
> - **空間計算量**:処理中に使うメモリ量がどう増えるかの目安。
58+
> - **Copy-on-Write(COW)**:SwiftのArrayなどが採用する最適化。変更が実際に発生するまでコピーを作らない仕組み。
59+
60+
---
61+
62+
## 3. 選択したアルゴリズムと理由
63+
64+
> 💡 **初学者向け補足**:他の方法と比較しながら、なぜ二重ポインタ法を選んだのかを説明します。
65+
66+
- **選択したアプローチ**: A. Two-Pointer(slow / fast の2つのインデックスを使う方法)
67+
- **理由**:
68+
- **計算量的な優位性**:Set(方法B)や新配列生成(方法C)はどちらもO(n)の追加メモリが必要ですが、Two-Pointer法はO(1)の追加メモリ(`slow`という整数変数ひとつ)だけで済みます。**この問題は「in-placeで行うこと」自体が明示的な要件**なので、O(n)空間を使う方法B・Cはそもそも要件を満たしません。
69+
- **Swift環境での型安全性**`Int`同士の比較演算子`!=`はコンパイル時に型チェックされるため、誤った型同士を比較してしまうミスがそもそも起こり得ません。
70+
- **保守性・可読性の観点**:ループはひとつだけで、条件分岐も「値が変わったかどうか」のシンプルな1行のみです。将来的にコードを読む人にとっても意図が追いやすい構造です。
71+
- **Swift特有の最適化ポイント**:
72+
- `inout`引数を使うことで、配列のコピーを一切発生させずに呼び出し元の配列を直接書き換えられます。
73+
- `guard`文による早期リターンで、空配列という特殊ケース(今回の制約上は起こりませんが、防御的プログラミングとして)を先に弾き、メインロジックのネストを浅く保っています。
74+
75+
> 📖 **このセクションで登場した用語**
76+
>
77+
> - **Two-Pointer(二重ポインタ)法**:配列上で2つの添字(インデックス)を異なる速度・役割で動かしながら処理を進めるアルゴリズムパターン。
78+
> - **防御的プログラミング**:想定外の入力(今回で言えば空配列など)が来ても安全に動作するよう、あらかじめ検証コードを入れておく設計方針。
79+
80+
---
81+
82+
## 4. 実装コード
83+
84+
> 💡 **初学者向け補足**:コード全体を示す前に、大まかな構造(骨格)を示します。
85+
>
86+
> 1. まず配列が空でないかを確認する(空なら結果は0個なのでそのまま`0`を返す)
87+
> 2. 「まだユニークな要素を書き込んでよい位置」を指す`slow`ポインタを用意する
88+
> 3. `fast`ポインタで配列を先頭から末尾まで1つずつなめていき、`slow`が指す値と違う値が見つかるたびに、その値を`slow`の次の位置に書き込む
89+
> 4. 最後に`slow + 1`(=ユニークな要素の個数)を返す
90+
91+
```swift
92+
class Solution {
93+
/// ソート済み配列から重複要素を取り除き、前方にユニークな値だけを詰め直す
94+
/// - Parameter nums: 非減少順(同じ値の連続を含む昇順)にソートされた整数配列。
95+
/// `inout` により、この関数は呼び出し元の配列を直接書き換える。
96+
/// - Returns: 重複を除いたあとのユニークな要素数 k
97+
func removeDuplicates(_ nums: inout [Int]) -> Int {
98+
// 配列が空の場合、ユニークな要素は0個なのでそのまま0を返す。
99+
// (この問題の制約では nums.length >= 1 が保証されているが、
100+
// 防御的プログラミングとして安全側に倒しておく)
101+
guard !nums.isEmpty else {
102+
return 0
103+
}
104+
105+
// slow は「最後に確定したユニークな値」が置かれているインデックス。
106+
// 最初はインデックス0の要素(nums[0])が唯一の確定済みユニーク値なので0から始める。
107+
var slow = 0
108+
109+
// fast はこれから調べていく値を指すインデックス。
110+
// インデックス1から末尾まで、配列を1回だけなめる(1パス)。
111+
for fast in 1..<nums.count {
112+
// 配列はソート済みなので、同じ値は必ず隣接している。
113+
// fast が指す値が、直近の確定済みユニーク値(nums[slow])と異なるなら、
114+
// それは「新しく見つかったユニークな値」ということになる。
115+
if nums[fast] != nums[slow] {
116+
// slow をひとつ進めて「次にユニーク値を書き込む位置」を確保する
117+
slow += 1
118+
// 新しく見つかったユニーク値を、確定済み領域の直後(nums[slow])に書き込む
119+
nums[slow] = nums[fast]
120+
}
121+
// nums[fast] == nums[slow] の場合は「すでに確認済みの値の重複」なので、
122+
// 何もせずに fast だけを次に進める(何もしないことがそのまま「重複を無視する」処理になる)
123+
}
124+
125+
// slow は0始まりのインデックスなので、要素の個数に直すには+1する
126+
return slow + 1
127+
}
128+
}
129+
```
130+
131+
> 💡 **コードの動作トレース**(初学者向け)
132+
>
133+
> 具体例:`nums = [0,0,1,1,1,2,2,3,3,4]` で実際にトレースしてみます。
134+
>
135+
> ```
136+
> 初期状態: nums = [0,0,1,1,1,2,2,3,3,4]、slow = 0
137+
>
138+
> fast=1: nums[1]=0, nums[slow]=nums[0]=0 → 同じ値なので何もしない
139+
> fast=2: nums[2]=1, nums[slow]=nums[0]=0 → 値が違う!
140+
> slow を 1 に進め、nums[1] = nums[2] = 1 を書き込む
141+
> → nums = [0,1,1,1,1,2,2,3,3,4]
142+
> fast=3: nums[3]=1, nums[slow]=nums[1]=1 → 同じ値なので何もしない
143+
> fast=4: nums[4]=1, nums[slow]=nums[1]=1 → 同じ値なので何もしない
144+
> fast=5: nums[5]=2, nums[slow]=nums[1]=1 → 値が違う!
145+
> slow を 2 に進め、nums[2] = nums[5] = 2 を書き込む
146+
> → nums = [0,1,2,1,1,2,2,3,3,4]
147+
> fast=6: nums[6]=2, nums[slow]=nums[2]=2 → 同じ値なので何もしない
148+
> fast=7: nums[7]=3, nums[slow]=nums[2]=2 → 値が違う!
149+
> slow を 3 に進め、nums[3] = nums[7] = 3 を書き込む
150+
> → nums = [0,1,2,3,1,2,2,3,3,4]
151+
> fast=8: nums[8]=3, nums[slow]=nums[3]=3 → 同じ値なので何もしない
152+
> fast=9: nums[9]=4, nums[slow]=nums[3]=3 → 値が違う!
153+
> slow を 4 に進め、nums[4] = nums[9] = 4 を書き込む
154+
> → nums = [0,1,2,3,4,2,2,3,3,4]
155+
>
156+
> ループ終了。 return slow + 1 = 4 + 1 = 5
157+
>
158+
> → k = 5、先頭5要素は [0,1,2,3,4] となり、期待される出力と一致する
159+
> ```
160+
>
161+
> インデックス5以降の値(`[2,2,3,3,4]`の名残)は、問題文の指示どおり「気にしなくてよい部分」としてそのまま残っていますが、これは正解として扱われます(Custom Judgeは先頭k個しか見ないため)。
162+
163+
> 📖 **このセクションで登場した用語**
164+
>
165+
> - **`guard` 文**:条件を満たさない場合に早期リターンする構文。`if`と異なり、`guard`のスコープを抜けたあとは「条件が満たされている」ことがコンパイラにも保証されるため、その後のコードで余計なnilチェックが不要になる。
166+
> - **`1..<nums.count`**:Swiftの範囲演算子(Range Operator)のひとつ。「1以上、`nums.count`未満」を意味する半開区間(=終端を含まない範囲)。終端を含めたい場合は`...`(クローズド・レンジ)を使う。
167+
> - **添字(インデックス)アクセス `nums[i]`**:配列の`i`番目(0始まり)の要素にアクセスする構文。範囲外の`i`を指定すると実行時にクラッシュするため、ループ範囲の指定を正確に行うことが重要。
168+
169+
---
170+
171+
## 補足:計算量まとめ
172+
173+
- **時間計算量**: O(n) — 配列を1回だけ走査するため(n = `nums.count`)
174+
- **空間計算量**: O(1) — 追加で使うメモリは`slow`という整数変数ひとつのみ(入力配列自体は書き換えているだけで、新規のメモリ確保は行っていない)

0 commit comments

Comments
 (0)