Skip to content

Commit e516040

Browse files
committed
docs: LeetCode 21および26の解説ドキュメントの計算量表記と記述の推敲
1 parent 9a8c169 commit e516040

9 files changed

Lines changed: 32 additions & 30 deletions

File tree

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@
88
>
99
> **Goで解く際に特に気をつけるべき点**:この問題はエラーハンドリング設計・ゴルーチンの必要性・スライスのアロケーション戦略という3つの観点で見ると非常にシンプルです。まず、Goのスライスは関数に渡されるとスライスヘッダ(ポインタ・長さ・容量)がコピーされますが、ポインタが指す先の配列本体は呼び出し元と共有されているため、`nums[i] = x`のような書き込みは呼び出し元のスライスにもそのまま反映されます。次に、この問題は単純な1パスの逐次処理でありデータ依存関係(前の要素の処理結果が次の判定に影響する)があるため、ゴルーチン(=Goが提供する軽量スレッド)による並列化には向きません。最後に、この問題は新しいスライスを一切生成しないため、`make`によるプリアロケーション(=必要なメモリをあらかじめまとめて確保しておくこと)も不要です。
1010
11-
#### 競技プログラミング視点
11+
### 競技プログラミング視点
1212

1313
- **制約分析**: LeetCodeの制約は`1 <= nums.length <= 3 * 10^4`程度であり、O(n)アルゴリズムであれば余裕を持って制限時間内に収まります。O(n log n)やO(n²)を使う必要は全くありません。
1414
- **最速手法**: ソート済みという性質を使ったTwo-Pointer(二重ポインタ)法がBig-O最適(O(n))かつGo実装最適(追加ヒープアロケーション0回)の両方を満たします。
1515
- **メモリ最小化**: 新しいスライスや`map`を一切生成せず、`int`型のローカル変数1個だけで完結させます。プリアロケーションも不要(そもそも新しいスライスを作らないため)です。
1616
- **Go最適化**: 組み込み関数(`len()`)以外は特に標準ライブラリを使わず、シンプルなforループのみで実装します。
1717

18-
#### 業務開発視点
18+
### 業務開発視点
1919

2020
- **型安全設計**: `[]int`という明示的な型のスライスを直接扱うため、型に関する曖昧さは生じません。
2121
- **エラーハンドリング**: この問題は`nums.length >= 1`という制約が保証されていますが、業務開発視点では`len(nums) == 0`のケースをガードしておくと、将来的な呼び出し規約の変更にも耐えられます。Goには例外機構がないため、今回は「異常系」ではなく「正当な入力の一種」として扱い、`error`は返さずそのまま`0`を返す設計にします(空スライスはこの問題の文脈では契約違反ではないため)。
2222
- **可読性**: ループはひとつだけで、コメントを各行に付けることで意図(なぜそうするか)を明確にします。
2323

24-
#### Go特有分析
24+
### Go特有分析
2525

2626
- **データ構造選択**: この問題は「順序ありでインデックスアクセスが必要」という性質を持つため、スライス`[]int`がそのまま最適です。`map`(キー検索)や`channel`(ゴルーチン間通信)は不要です。
2727
- **標準ライブラリ活用度**: 実は標準ライブラリの`slices.Compact`(Go 1.21+で追加)がまさにこの問題と同種の処理を行いますが、今回はアルゴリズムの内部動作を理解する教育的価値を重視し、手動でTwo-Pointerを実装します。
@@ -94,27 +94,27 @@ for fast := 1; fast < len(nums); fast++ {
9494
// O(1) 空間・ヒープアロケーション0回で同じ結果が得られる
9595
```
9696

97-
#### Goコンパイラ最適化
97+
### Goコンパイラ最適化
9898

9999
- **組み込み関数活用**: `len(nums)`のみを使用。`make()``append()`は今回は不要(新しいスライスを作らないため)。
100100
- **スライス操作**: in-place操作のみ。プリアロケーションも該当なし(元のスライスの容量をそのまま使う)。
101101
- **インライン化**: このロジックは単純な比較・代入のみなので、コンパイラが呼び出し元にインライン展開しやすい形をしています。
102102
- **標準ライブラリ**: 今回は使用しません(`sort`は前提条件としてすでにソート済みのため不要、`container/heap`も不要)。
103103

104-
#### データ構造選択指針
104+
### データ構造選択指針
105105

106106
| 状況 | 選ぶべき構造 | 理由 |
107107
|---|---|---|
108108
| 順序あり・インデックスアクセス | スライス `[]int`(今回採用) | 連続メモリでキャッシュ効率が高く、ソート済みの隣接比較に最適 |
109109
| キーで素早く検索したい | マップ `map[K]V`(今回不採用) | O(1)のルックアップだが、ハッシュ計算コストとヒープ逃げが発生する |
110110

111-
#### メモリ最適化
111+
### メモリ最適化
112112

113113
- **プリアロケーション**: 該当なし。新しいスライスを一切生成しないため`make([]T, 0, cap)`は不要です。
114114
- **エスケープ解析**: `slow``fast`はどちらもスタックに割り当てられます(関数外に参照が漏れないため)。
115115
- **ポインタ使用**: `nums []int`はすでにスライスヘッダの受け渡しであり、大きな構造体のポインタ渡しのような追加の最適化余地はありません。
116116

117-
#### 並行処理最適化
117+
### 並行処理最適化
118118

119119
> 💡 **初学者向け補足**
120120
> 1. **なぜ並行処理が必要か**:この問題では不要です。`slow`ポインタの位置は「それまでのすべての要素の処理結果」に依存する逐次的な状態であり、チャンクに分割して独立に処理することができません。

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@
88
>
99
> **Pythonで解く際に特に気をつけるべきCPython特有の注意点**:この問題は`nums`というリストそのものを直接書き換える(in-place=新しいオブジェクトを作らず、既存のオブジェクトを直接書き換える操作)ことが要件です。Pythonのリストはミュータブル(=生成後に中身を変更できるオブジェクト)なオブジェクトであり、関数に渡されるとリストへの「参照」が渡されます(C言語のポインタ渡しに近い挙動)。そのため、関数内で`nums[i] = x`のように要素を書き換えると、呼び出し元のリストも同時に書き換わります。もう1つの注意点は、`nums.count(value)`や`nums.index(value)`のようなリストメソッドを安易にループ内で使うと、それぞれがO(n)の走査を行うため、全体の計算量がO(n²)に悪化してしまう点です。今回は単純な添字アクセス(`nums[i]`)のみで完結させ、この落とし穴を避けます。
1010
11-
#### 競技プログラミング視点
11+
### 競技プログラミング視点
1212

1313
- **制約分析**: LeetCodeの制約は`1 <= nums.length <= 3 * 10^4`程度であり、O(n)アルゴリズムであれば余裕を持って制限時間内に収まります。O(n log n)以上の計算量は不要です。
1414
- **最速手法**: ソート済みという性質を使ったTwo-Pointer(二重ポインタ)法がBig-O最適(O(n))です。CPythonではPure Pythonのforループはインタープリタのオーバーヘッドが大きいため、組み込み関数で置き換えられる部分がないか検討しますが、この問題は「前の要素の処理結果に依存する逐次処理」であり、`sum()``map()`のような組み込み関数に単純に置き換えることはできません。
1515
- **メモリ最小化**: 新しいリストや`set`を一切生成せず、`int`型の変数1個だけで完結させるインプレース操作(=新しいオブジェクトを作らず、既存のオブジェクトを直接書き換える操作。メモリ効率が良い)を採用します。
1616
- **CPython最適化**: 組み込みの`len()`のみを使用します。`enumerate()`は「現在の要素の値」しか提供せず、今回は「1つ前に確定した位置」も同時に参照する必要があるため、素直な`range()`ベースのループのほうが適しています。
1717

18-
#### 業務開発視点
18+
### 業務開発視点
1919

2020
- **型安全設計**: 引数`nums: List[int]`、戻り値`-> int`という型ヒント(=関数の引数や戻り値に型を注釈として書く仕組み)をそのまま活かし、pylance(=VSCodeで使えるPythonの静的型チェックツール。実行前にバグを検出できる)が型の不整合を実行前に検出できるようにします。
2121
- **エラーハンドリング**: 想定される例外として、①`nums``None`または`list`以外の型で渡されるケース(`TypeError`)、②`nums`が空リストのケース(この問題の文脈では正当な入力として`0`を返す)を考慮します。Pythonは動的型付け言語(=変数の型が実行時まで確定しない言語)のため、呼び出し元が誤った型を渡しても実行時までエラーに気づけません。型ヒント + pylanceによる静的チェックと、実行時の`isinstance`チェックを組み合わせることで、コンパイル言語(Java/C++など)に近い安全性を実現します。
2222
- **可読性**: docstring(=関数やクラスの先頭に書く説明文。`"""三重クォート"""`で囲む)・行コメント・意味の明確な変数名(`slowPointer`ではなく`slow`のような簡潔だが文脈で意味が通る名前)を徹底します。
2323

24-
#### Python特有分析
24+
### Python特有分析
2525

2626
- **データ構造選択**: この問題は「順序ありでインデックスアクセスが必要」という性質を持つため、`list`(配列)がそのまま最適です。`set`(メンバーシップテスト用)や`dict`(キー値ペア用)、`collections.deque`(両端操作用)は今回の処理には不要です。
2727
- **標準ライブラリ活用度**: `itertools.groupby`を使うと「連続する同じ値をグループ化する」処理を宣言的に書けますが、内部でイテレータオブジェクトを生成するオーバーヘッドがあり、単純なTwo-Pointerより低速になりがちです。今回は標準ライブラリに頼らず手動実装します。

DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-C#.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
## 1. 問題の分析
44

5-
> 💡 この問題は、一言で言うと「すでに整列された2つの連結リストを、新しいノードを作らずに1つの整列済みリストへ“縫い合わせる”問題」です。
5+
> 💡 この問題は、一言で言うと「すでに整列された2つの連結リストを、ダミーノード1個を除き新しいリストノードを作らずに1つの整列済みリストへ“縫い合わせる”問題」です。
66
77
**C#で解く際に特に気をつけるべき点**を先にまとめます。今回与えられたシグネチャでは `ListNode next` のように `?` が付いていません。つまりこのコードは**null許容参照型(nullable reference types)が有効化されていない、従来のC#の書き方**です。C#8.0以降では `#nullable enable` を有効にすると `ListNode?` のように書かないと `null` を代入できなくなりますが、LeetCode標準のテンプレートはこの機能を使わない設計になっています。そのため、**コンパイラは `list1` や `list2` が `null` かどうかを静的にはチェックしてくれません**。JavaやDartのような「型が絞り込まれる」機能に頼れない以上、**アクセス前に必ず自分で `null` チェックを書く**という規律がこの問題では特に重要になります。また、`ListNode` は `class`(参照型)として定義されているため、C#の `struct`(値型)とは異なり、変数に代入すると**参照(オブジェクトへの住所)がコピーされる**だけで、ノードの中身そのものが複製されることはありません。この性質のおかげで、JavaやGoと同様に「既存ノードの `next` をつなぎ替えるだけ」というアルゴリズムがそのまま成立します。
88

99
**競技プログラミング視点での分析**
1010

1111
- 実行速度を最優先するなら、各ノードを1回ずつ見るだけで済む**反復(ループ)処理**が最速。時間計算量は O(n+m)(n, mはそれぞれのリスト長)。
12-
- メモリ使用量の最小化方針としては、**新しいノードを一切作らず、既存ノードの `next` フィールドだけを付け替える**(この操作を「スプライシング(splicing、つなぎ替え)」と呼ぶ)。C#は.NETのガベージコレクション(=使われなくなったオブジェクトを自動的に片付ける仕組み)を持つ言語なので手動でのメモリ解放は不要ですが、新規オブジェクトを作らないほうがGCの負荷が減り高速という点は他のGC言語(Java、Dart等)と共通です。
12+
- メモリ使用量の最小化方針としては、**ダミーノード1個を除き新しいリストノードを作らず、既存ノードの `next` フィールドだけを付け替える**(この操作を「スプライシング(splicing、つなぎ替え)」と呼ぶ)。C#は.NETのガベージコレクション(=使われなくなったオブジェクトを自動的に片付ける仕組み)を持つ言語なので手動でのメモリ解放は不要ですが、新規オブジェクトを作らないほうがGCの負荷が減り高速という点は他のGC言語(Java、Dart等)と共通です。
1313

1414
**業務開発視点での分析**
1515

@@ -36,7 +36,7 @@
3636
3737
| アプローチ | 時間計算量 | 空間計算量 | C#実装コスト | 型安全性 | 可読性 | 備考 |
3838
| --------------------------------------------- | ----------------- | ---------- | ------------ | -------- | ------ | ------------------------------------------ |
39-
| 方法A:反復(ダミーヘッド + 2ポインタ) | O(n+m) | O(1) |||| 新規ノード生成なし。推奨 |
39+
| 方法A:反復(ダミーヘッド + 2ポインタ) | O(n+m) | O(1) |||| ダミーノード1個(ListNode(0))のみ作成。推奨 |
4040
| 方法B:再帰(先頭同士を比較して自己呼び出し) | O(n+m) | O(n+m) |||| コールスタックがリスト長に比例して積まれる |
4141
| 方法C:両リストを`List<int>`に集めて`Sort()` | O((n+m) log(n+m)) | O(n+m) |||| すでにソート済みという前提を無駄にする |
4242

@@ -61,7 +61,7 @@
6161
- **計算量的な優位性**:方法B(再帰)と同じ O(n+m) の時間計算量でありながら、空間計算量は O(1) で済む(方法Bはコールスタックに O(n+m) 消費する)。今回の制約(最大50ノード)では方法Bでも問題になりませんが、業務コードとしては入力サイズに依存する設計は将来的な拡張に対して脆いです。
6262
- **C#環境での安全性**`while` ループの条件を `list1 != null && list2 != null` とすることで、ループの内側では `list1.val` へのアクセス前に必ず `null` チェックが済んでいることが保証される。
6363
- **保守性・可読性**:ループ1本で完結し、条件分岐もシンプル。デバッガでも1行ずつ追いやすい。
64-
- **方法Cを選ばなかった理由**:わざわざ `List<int>` に集めてソートするのは、「すでにソート済み」という問題の前提を無視した回り道であり、時間計算量も余分な O(log(n+m)) がかかるうえ、空間計算量も O(n+m) と余計にメモリを使う。
64+
- **方法Cを選ばなかった理由**:わざわざ `List<int>` に集めてソートするのは、「すでにソート済み」という問題の前提を無視した回り道であり、要素の収集と `List<int>.Sort()` によるソートを含めた全体の時間計算量が O((n+m) log(n+m)) となるうえ、空間計算量も O(n+m) と余計にメモリを使う。
6565
- **C#特有の最適化ポイント**
6666
- `ListNode` は参照型なので、比較 `list1.val <= list2.val``int`(値型)同士の単純な比較であり、ボクシング(=`int` のようなプリミティブ型を `object` のようなラッパーオブジェクトに変換すること)は一切発生しない。
6767
- 新規オブジェクト生成はダミーノード1個のみに抑えており、ガベージコレクタの負荷を最小限にしている。
@@ -112,7 +112,7 @@ public class Solution {
112112
/// 「空のリストが渡された」という正常な状態を表す。
113113
/// そのため ArgumentNullException 等は送出しない。
114114
///
115-
/// 新しいノードは一切作成せず、既存ノードの next 参照を
115+
/// ダミーノード1個(ListNode(0))を除き新規リストノードは作成せず、既存ノードの next 参照を
116116
/// つなぎ替える(スプライシング)ことでマージするため、
117117
/// 追加のオブジェクト生成はダミーノード1個のみで済む(空間計算量 O(1))。
118118
/// </summary>

DataStructures/LinkedLists/leetcode/21. Merge Two Sorted Lists/Sonnet 5/Merge-Two-Sorted-Lists-C++.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
- **計算量的な優位性**:方法B(再帰)と同じ O(n+m) の時間計算量でありながら、空間計算量は O(1) で済む(方法Bはコールスタックに O(n+m) 消費する)。
6969
- **C++環境でのメモリ安全性**`new` を一切呼ばないため、`delete` の呼び忘れによるメモリリークや、二重解放(同じメモリを2回解放してしまうバグ)のリスクがそもそも発生しません。C++では「メモリ管理はプログラマの責任」であるからこそ、**そもそもアロケーションを行わない設計**が最も安全な選択肢になります。
7070
- **保守性・可読性**:ループ1本で完結し、条件分岐もシンプル。デバッガでも1行ずつ追いやすい。
71-
- **方法Cを選ばなかった理由**:わざわざ `std::vector` に集めてソートするのは、「すでにソート済み」という問題の前提を無視した回り道であり、時間計算量も余分な O(log(n+m)) がかかるうえ、空間計算量も O(n+m) と余計にメモリを使う。
71+
- **方法Cを選ばなかった理由**:わざわざ `std::vector` に集めてソートするのは、「すでにソート済み」という問題の前提を無視した回り道であり、要素の収集と `std::sort` によるソートを含めた全体の時間計算量が O((n+m) log(n+m)) となるうえ、空間計算量も O(n+m) と余計にメモリを使う。
7272
- **C++特有の最適化ポイント**
7373
- ヒープアロケーションを一切行わないため、`malloc`/`new` に伴うメモリ確保のオーバーヘッドやヒープの断片化(フラグメンテーション)を回避できる。
7474
- `list1->val <= list2->val` のような比較は `int` 同士の単純な比較であり、値のコピーやボクシングのようなコストは一切発生しない(C++には元々そうした概念がない)。

0 commit comments

Comments
 (0)