|
| 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