Skip to content

Commit d8fe90d

Browse files
committed
docs(leetcode/21): add Sonnet 5 solutions and documentation (Go, Java, Python, Rust, TypeScript)
1 parent 74c3986 commit d8fe90d

6 files changed

Lines changed: 1251 additions & 1 deletion

File tree

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

Lines changed: 284 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# LeetCode 21: Merge Two Sorted Lists
2+
3+
## 1. 問題の分析
4+
5+
> 💡 この問題は、一言で言うと「すでに整列された2つの連結リストを、新しいノードを作らずに1つの整列済みリストへ“縫い合わせる”問題」です。
6+
7+
**競技プログラミング視点での分析**
8+
9+
- 実行速度を最優先するなら、各ノードを1回ずつ見るだけで済む**反復(ループ)処理**が最速。O(n+m) の1パスで完結し、余計な比較や再帰呼び出しのオーバーヘッドがない。
10+
- メモリ使用量の最小化方針としては、**新しいノードを一切作らず、既存ノードの `next` 参照だけを付け替える**(この操作を「スプライシング(splicing、つなぎ替え)」と呼ぶ)。配列に変換してからソートするような方法は、リストがすでにソート済みという情報を無駄にしてしまう。
11+
12+
**業務開発視点での分析**
13+
14+
- 型安全性の面では、この問題は `ListNode.val``int` に固定されているLeetCode標準シグネチャなので、ジェネリクス(=型をパラメータとして受け取れる仕組み)を導入する必要は**ありません**。もし「任意の `Comparable` な値を持つノードをマージする汎用ライブラリ」を作るなら `<T extends Comparable<T>>` を使う場面ですが、今回はメソッドシグネチャが固定されているため、無理に汎用化するとかえって可読性が落ちます。
15+
- エラーハンドリングで注意すべき点:**`list1``list2``null` であることは「異常な入力」ではなく「空のリストを表す正常な状態」**です。Example 2 (`list1 = [], list2 = []`) がまさにそれです。テンプレートでよくある「`null` なら即 `IllegalArgumentException`」という防御的プログラミングをそのまま適用すると、この問題では**正しい入力を誤ってエラー扱いしてしまう**ので注意が必要です。
16+
17+
**Java特有の考慮点**
18+
19+
- 静的型付けにより、`list1.val` は必ず `int` であることがコンパイル時に保証されており、実行時の型チェックは不要。
20+
- `ListNode` はJava標準の `java.*` に属するクラスではなく、問題側で定義されたクラスなので、`Optional<T>` を挟むよりも「`null` はリストの終端(または空リスト)を意味する」という連結リストの一般的な慣習に従うほうが、他のJavaコードとの一貫性が高く読みやすくなります。
21+
22+
> 📖 **このセクションで登場した用語**
23+
>
24+
> - **スプライシング(splicing)**:新しいノードを作らず、既存のノードの参照(`next`)をつなぎ替えることでデータ構造を組み替える操作。メモリのコピーが発生しないため効率的。
25+
> - **`NullPointerException`**`null` の参照に対してメソッド呼び出しやフィールドアクセスを行おうとした際に発生する実行時例外。今回は `list1.val` にアクセスする前に必ず `list1 != null` を確認することで防ぐ。
26+
> - **防御的プログラミング**:呼び出し元が誤った値を渡してもプログラムが壊れないように、事前に入力を検証する設計方針。ただし今回のように「`null` = 正常な空リスト」の場合は過剰な検証がバグのもとになる。
27+
28+
---
29+
30+
## 2. アルゴリズムアプローチ比較
31+
32+
> 💡 同じ問題でも解き方は複数あります。「速さ(時間計算量)」と「メモリの使いやすさ(空間計算量)」、そしてJavaらしい実装コストを比べて最適なものを選びます。
33+
34+
| アプローチ | 時間計算量 | 空間計算量 | Java実装コスト | 型安全性 | 可読性 | 備考 |
35+
| --------------------------------------------- | ----------------- | ---------- | -------------- | -------- | ------ | ------------------------------------------ |
36+
| 方法A:反復(ダミーヘッド + 二本のポインタ) | O(n+m) | O(1) |||| 新規ノード生成なし。推奨 |
37+
| 方法B:再帰(先頭同士を比較して再帰呼び出し) | O(n+m) | O(n+m) |||| コールスタックがリスト長に比例して積まれる |
38+
| 方法C:全ノードを配列/リストに集めてソート | O((n+m) log(n+m)) | O(n+m) |||| すでにソート済みという前提を無駄にする |
39+
40+
> 💡 **Big-O記法の読み方**(初学者向け)
41+
>
42+
> - `O(1)`:入力の大きさに関わらず、常に一定の手間で済む(最速・最小)
43+
> - `O(n+m)`:2つの入力の合計サイズに比例して手間が増える(線形)
44+
> - `O((n+m) log(n+m))`:ソートに典型的な計算量。入力が増えるとやや急激に手間が増える
45+
46+
> 📖 **このセクションで登場した用語**
47+
>
48+
> - **再帰(recursion)**:関数が自分自身を呼び出す処理方式。「小さい問題に分解して、その答えを組み合わせる」考え方に向いている。
49+
> - **コールスタック**:関数呼び出しの「呼び出し履歴」を積んでおくメモリ領域。再帰が深くなるとここに積まれる分だけメモリを消費し、深すぎると `StackOverflowError` が起きる。
50+
51+
---
52+
53+
## 3. 選択したアルゴリズムと理由
54+
55+
> 💡 選んだ理由を、他の方法と対比しながら説明します。
56+
57+
- **選択したアプローチ**:方法A(ダミーヘッドを使った反復処理)
58+
- **理由**
59+
- **計算量的な優位性**:方法B(再帰)と同じ O(n+m) の時間計算量でありながら、空間計算量は O(1) で済む(方法Bはコールスタックに O(n+m) 消費する)。
60+
- **Java環境での安全性**:制約上ノード数は最大50なので方法Bでも `StackOverflowError` の危険は事実上ないが、実務コードとして「入力サイズに依存してスタックが伸びる」設計は将来的な拡張(例:ノード数が数万に増える)に対して脆い。方法Aはその制約自体が存在しない。
61+
- **保守性・可読性**:ループ1本で完結し、条件分岐もシンプル。デバッガでも1行ずつ追いやすい。
62+
- **方法Cを選ばなかった理由**:わざわざ配列に集めてソートするのは、「すでにソート済み」という問題の前提を無視した回り道であり、時間計算量も余分な O(log(n+m)) がかかるうえ、空間計算量も O(n+m) と余計にメモリを使う。
63+
- **Java特有の最適化ポイント**
64+
- `ListNode` はプリミティブ型 `int` を直接フィールドに持つ設計(`Integer` へのボクシングが発生しない)ので、比較 `list1.val <= list2.val` は最初からボクシングコストなしで行える。
65+
- ダミーノード(番兵ノード)パターンを使うことで、「リストが空の場合の特別扱い」という条件分岐(`if (head == null) { ... }` のような処理)を1つも書かずに済み、コンパイル時にもコードパスが単純になる。
66+
67+
> 📖 **このセクションで登場した用語**
68+
>
69+
> - **ダミーヘッド(番兵ノード)**:リストの先頭に、実際のデータを持たない仮のノードを1つ置いておくテクニック。「リストが空かどうか」で処理を分岐させる必要がなくなる。
70+
> - **ボクシング**`int` のようなプリミティブ型を `Integer` のようなラッパークラスのオブジェクトに変換すること。オブジェクト生成のオーバーヘッドが発生するため、大量のデータを扱う場面では避けたい。今回の `ListNode.val``int` のままなので発生しない。
71+
72+
---
73+
74+
## 4. 実装コード
75+
76+
> 💡 コード全体の骨格:
77+
>
78+
> 1. 結果リストの先頭を仮に指し示す「ダミーノード」を用意する
79+
> 2. `list1``list2` の先頭同士を比較し、小さい方を結果リストの末尾につなげる。つないだ方のリストを1つ進める
80+
> 3. どちらかが尽きるまで2を繰り返す
81+
> 4. 片方が尽きたら、残った方(すでにソート済み)をそのまま丸ごとつなげる
82+
> 5. ダミーノードの次(`dummy.next`)が本当の答えの先頭なので、それを返す
83+
84+
```java
85+
/**
86+
* Definition for singly-linked list.
87+
* public class ListNode {
88+
* int val;
89+
* ListNode next;
90+
* ListNode() {}
91+
* ListNode(int val) { this.val = val; }
92+
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
93+
* }
94+
*/
95+
class Solution {
96+
97+
/**
98+
* 2つのソート済み連結リストを1つのソート済み連結リストにマージする。
99+
*
100+
* 新しいノードは一切作成せず、既存ノードの next 参照をつなぎ替える
101+
* (スプライシング)ことでマージするため、空間計算量は O(1) となる。
102+
*
103+
* @param list1 1つ目のソート済み連結リストの先頭ノード。
104+
* null の場合は「空のリスト」を表す(異常値ではない)
105+
* @param list2 2つ目のソート済み連結リストの先頭ノード。
106+
* null の場合は「空のリスト」を表す(異常値ではない)
107+
* @return マージ後のソート済み連結リストの先頭ノード
108+
* (両方が空リストの場合は null を返す)
109+
*/
110+
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
111+
// ダミーノード(番兵ノード)を用意する。
112+
// このノード自体には意味のある値は入れない(値は0で仮置き)。
113+
// こうすることで「結果リストがまだ空のとき、先頭をどう扱うか」
114+
// という特別な分岐を書かずに済む。
115+
ListNode dummy = new ListNode(0);
116+
117+
// tail は「これまでにつなぎ終えた部分の末尾ノード」を指す変数。
118+
// 最初はダミーノード自身を指しておく。
119+
ListNode tail = dummy;
120+
121+
// list1、list2 の両方にまだノードが残っている間、
122+
// 先頭同士の値を比較して小さい方を tail の後ろにつなげる。
123+
while (list1 != null && list2 != null) {
124+
if (list1.val <= list2.val) {
125+
// list1の先頭値のほうが小さい(同値含む)ので、
126+
// list1の現在ノードを結果リストの末尾につなげる
127+
tail.next = list1;
128+
// list1を1つ次に進める(つないだノードは処理済みなので進める)
129+
list1 = list1.next;
130+
} else {
131+
// list2の先頭値のほうが小さいので、list2側をつなげる
132+
tail.next = list2;
133+
list2 = list2.next;
134+
}
135+
// tailを、たった今つないだノードの位置まで進める
136+
tail = tail.next;
137+
}
138+
139+
// ループを抜けた時点で、list1 か list2 のどちらか(または両方)が
140+
// null(尽きた状態)になっている。
141+
// 残っている側はすでにソート済みなので、1つずつ比較する必要はなく、
142+
// そのまま丸ごと tail の後ろにつなげてよい。
143+
// 三項演算子:list1が残っていればlist1を、そうでなければlist2をつなげる
144+
tail.next = (list1 != null) ? list1 : list2;
145+
146+
// dummyノード自体はデータを持たない仮のノードなので、
147+
// 実際の答えの先頭は dummy.next である
148+
return dummy.next;
149+
}
150+
}
151+
```
152+
153+
> 💡 **コードの動作トレース**(初学者向け)
154+
>
155+
> 入力:`list1 = [1,2,4]``list2 = [1,3,4]`
156+
>
157+
> ```
158+
> 初期状態: dummy -> (何も繋がっていない)
159+
> tail = dummy
160+
> list1 = [1,2,4], list2 = [1,3,4]
161+
>
162+
> Step 1: list1.val(1) <= list2.val(1) は true → list1の"1"をつなぐ
163+
> 結果: dummy -> 1(from list1)
164+
> list1は次の "2" に進む、tailは今つないだ "1" に進む
165+
>
166+
> Step 2: list1.val(2) vs list2.val(1) → list2の"1"のほうが小さい → list2をつなぐ
167+
> 結果: dummy -> 1 -> 1(from list2)
168+
> list2は次の "3" に進む、tailは今つないだ "1" に進む
169+
>
170+
> Step 3: list1.val(2) <= list2.val(3) → list1の"2"をつなぐ
171+
> 結果: dummy -> 1 -> 1 -> 2
172+
> list1は次の "4" に進む
173+
>
174+
> Step 4: list1.val(4) vs list2.val(3) → list2の"3"のほうが小さい → list2をつなぐ
175+
> 結果: dummy -> 1 -> 1 -> 2 -> 3
176+
> list2は次の "4" に進む
177+
>
178+
> Step 5: list1.val(4) <= list2.val(4) → list1の"4"をつなぐ(同値はlist1優先)
179+
> 結果: dummy -> 1 -> 1 -> 2 -> 3 -> 4
180+
> list1はnullになる(尽きた)
181+
>
182+
> Step 6: while条件 (list1 != null && list2 != null) が false になりループ終了
183+
> list1 == null なので、残っている list2(= [4])を丸ごとつなぐ
184+
> 結果: dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4
185+
>
186+
> Step 7: dummy.next を返す → [1,1,2,3,4,4](期待通りの出力)
187+
> ```
188+
189+
> 📖 **このセクションで登場した用語**
190+
>
191+
> - **三項演算子(`条件 ? A : B`)**:`if-else` を1行で書くための構文。「条件がtrueならA、falseならB」という値を返す。今回は「list1が残っていればlist1を、そうでなければlist2を」という選択に使っている。
192+
> - **`this.next`(連結リストの `next` 参照)**:あるノードから次のノードへの「つながり」を表すフィールド。これを付け替えることでリストの構造そのものを変更できる。
193+
194+
---
195+
196+
## 5. 制約条件の確認
197+
198+
- 外部ライブラリは使用していません(`ListNode` 以外は言語組み込みの機能のみ)。
199+
- 追加のノードを1つも生成していないため、メモリ使用量は入力サイズによらず一定(O(1))です。
200+
- `-Xlint:all` で警告が出るような未チェックキャストや生型(raw type)の使用はありません。
201+
- クラス名 `Solution`・`ListNode` はパスカルケース、変数名 `list1`/`list2`/`dummy`/`tail` はキャメルケース(またはそれに準じる慣習的な命名)です。
202+
203+
**計算量まとめ**:時間計算量 O(n+m)、空間計算量 O(1)(n, m はそれぞれ list1, list2 のノード数)。

0 commit comments

Comments
 (0)