Skip to content

Commit deeaaab

Browse files
committed
Add LeetCode 88. Merge Sorted Array (Claude 4.6 Sonnet) implementations in Rust, Python, and TypeScript
1 parent df08bc8 commit deeaaab

7 files changed

Lines changed: 3539 additions & 4 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
## 1. 問題分析結果
2+
3+
### 競技プログラミング視点
4+
5+
`nums1` の末尾から逆順に埋める3ポインタ法が最適。CPython では `list` のインデックスアクセスは O(1) の C 実装であり、追加リスト生成ゼロ・ヒープアロケーションゼロで O(m+n) を達成できます。
6+
7+
### 業務開発視点
8+
9+
LeetCode のシグネチャは `None` 返却の破壊的操作。Pylance 対応のため `List[int]` 型ヒントを厳密に付与し、`m`/`n``int` として明示します。エッジケース(`n == 0`)は早期リターンで明示的に処理します。
10+
11+
### Python特有分析
12+
13+
- `list.__setitem__` は C レイヤーで動作し、純粋な Python ループでも高速
14+
- `nums1[k], i, j, k` のローカル変数参照はグローバル参照より高速(LOAD_FAST)
15+
- 競技版では `while` + インデックス直接操作が最速(イテレータ生成コスト不要)
16+
- `nums1[:j+1] = nums2[:j+1]` のスライス代入は C の `memmove` 相当で残余コピーを一括処理可能
17+
18+
---
19+
20+
## 2. アルゴリズムアプローチ比較
21+
22+
| アプローチ | 時間計算量 | 空間計算量 | Python実装コスト | 可読性 | 標準ライブラリ活用 | CPython最適化 | 備考 |
23+
| ---------------------- | ---------------- | ---------- | ---------------- | ------ | ------------------ | ------------------- | ------------------------- |
24+
| 後ろから3ポインタ | O(m+n) | O(1) || ★★★ | なし || 追加アロケーションなし ✅ |
25+
| コピー後 `list.sort()` | O((m+n)log(m+n)) | O(1) | 最低 | ★★★ | なし | 適(Timsort C実装) | 競技次善策 |
26+
| `heapq.merge` + 展開 | O(m+n) | O(m+n) || ★★☆ | heapq || 一時リスト生成が発生 |
27+
28+
---
29+
30+
## 3. 採用アルゴリズムと根拠
31+
32+
- **選択**: 後ろからの3ポインタマージ(業務版・競技版ともに同一戦略)
33+
- **Python最適化戦略**: 残余コピーをスライス代入 `nums1[:j+1] = nums2[:j+1]` に置換することで、CPython 内部の `memmove` が発動し逐次代入より高速
34+
- **トレードオフ**: スライス代入は一時オブジェクトを1個生成するが定数サイズであり実質 O(1) の追加コスト
35+
36+
---
37+
38+
## 4. 実装パターン
39+
40+
```python
41+
# Runtime 0 ms
42+
# Beats 100.00%
43+
# Memory 19.42 MB
44+
# Beats 35.99%
45+
from typing import List
46+
47+
class Solution:
48+
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
49+
"""
50+
2つのソート済み配列を nums1 にインプレースでマージする。
51+
後ろから走査する3ポインタ法により O(m+n) 時間・O(1) 追加空間を実現。
52+
53+
不変条件: k >= i が常に成立するため nums1 の未処理要素を上書きしない。
54+
初期値 k - i = n >= 0 が各イテレーションで保持される。
55+
56+
Args:
57+
nums1: マージ先リスト(長さ m+n、末尾 n 要素は 0 で埋められている)
58+
m: nums1 の有効要素数
59+
nums2: マージ元リスト(長さ n)
60+
n: nums2 の要素数
61+
62+
Time Complexity: O(m + n)
63+
Space Complexity: O(1) ※スライス代入の一時オブジェクト除く
64+
"""
65+
# ── 業務開発版 ─────────────────────────────────────────────
66+
# n == 0 のとき nums1 は既に完成しているため早期リターン
67+
if n == 0:
68+
return
69+
70+
i: int = m - 1 # nums1 有効末尾ポインタ
71+
j: int = n - 1 # nums2 末尾ポインタ
72+
k: int = m + n - 1 # 書き込み位置ポインタ
73+
74+
# どちらかが尽きるまで大きい方を末尾に書き込む
75+
while i >= 0 and j >= 0:
76+
if nums1[i] >= nums2[j]:
77+
nums1[k] = nums1[i]
78+
i -= 1
79+
else:
80+
nums1[k] = nums2[j]
81+
j -= 1
82+
k -= 1
83+
84+
# nums2 の残余要素を一括スライス代入(CPython の memmove 相当で高速)
85+
# nums1 の残余は既に正しい位置にあるため操作不要
86+
if j >= 0:
87+
nums1[: j + 1] = nums2[: j + 1]
88+
89+
def merge_competitive(
90+
self, nums1: List[int], m: int, nums2: List[int], n: int
91+
) -> None:
92+
"""
93+
競技プログラミング向け最適化実装。
94+
ローカル変数へのキャッシュで LOAD_FAST を最大活用。
95+
96+
Time Complexity: O(m + n)
97+
Space Complexity: O(1)
98+
"""
99+
# ローカル変数へのバインドで属性ルックアップを排除
100+
i, j, k = m - 1, n - 1, m + n - 1
101+
102+
while i >= 0 and j >= 0:
103+
if nums1[i] >= nums2[j]:
104+
nums1[k] = nums1[i]
105+
i -= 1
106+
else:
107+
nums1[k] = nums2[j]
108+
j -= 1
109+
k -= 1
110+
111+
if j >= 0:
112+
nums1[: j + 1] = nums2[: j + 1]
113+
```
114+
115+
---
116+
117+
## Python特有の追加考慮事項
118+
119+
**スライス代入 `nums1[:j+1] = nums2[:j+1]` の内部動作**
120+
121+
CPython の `list_ass_slice``memmove` で連続メモリを一括コピーします。逐次 `while` ループに比べてインタープリタのディスパッチコストを排除でき、残余要素が多いほど効果的です。
122+
123+
**`LOAD_FAST` 最適化(競技版)**
124+
125+
Python のバイトコードにおいて、メソッド内のローカル変数アクセス (`LOAD_FAST`) はグローバル変数アクセス (`LOAD_GLOBAL`) より約2倍高速です。`nums1`, `nums2` は引数として自動的にローカルスコープに入るため、競技版では追加のキャッシュは不要。カウンタ変数 `i`, `j`, `k` も同様です。
126+
127+
**Pylance 型安全性**
128+
129+
`List[int]``from typing import List` で明示し、戻り値を `None` と宣言することで Pylance の strict モードでも警告ゼロを維持します。Python 3.9 以降では `list[int]` の組み込み記法も使用可能ですが、LeetCode の CPython 3.11 環境では `from typing import List` が最も互換性の高い記述です。
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
## 1. 問題の分析
2+
3+
**競技プログラミング視点での分析**
4+
5+
`nums1`の末尾から逆順に埋めていく「後ろからマージ」戦略が最適です。追加アロケーションなしでインプレース処理でき、時間 O(m+n)・空間 O(1) が実現できます。前から埋めると上書きが発生するため、後ろからスキャンするのが核心です。
6+
7+
**業務開発視点での分析**
8+
9+
LeetCode の関数シグネチャは `&mut Vec<i32>` の受け渡しを強制しますが、内部では `usize` インデックスと `saturating_sub` を活用し、符号なし整数のアンダーフローをコンパイル時に潰します。`i32 → usize` の変換は `as usize` で確実に行います。
10+
11+
**Rust特有の考慮点**
12+
13+
- `i32` 引数 `m`, `n` は LeetCode の制約上 `0..=200` に収まるため、`as usize` キャストは安全
14+
- `checked_sub``saturating_sub` でインデックス計算のパニックを防止
15+
- 借用は `&mut Vec<i32>` のスライス操作に集約し、ライフタイムを単純化
16+
17+
---
18+
19+
## 2. アルゴリズムアプローチ比較
20+
21+
| アプローチ | 時間計算量 | 空間計算量 | Rust実装コスト | 安全性 | 可読性 | 備考 |
22+
| --------------------------- | ---------------- | ---------- | -------------- | ------ | ------ | ------------------------- |
23+
| 後ろからマージ(3ポインタ) | O(m+n) | O(1) |||| 追加アロケーションなし ✅ |
24+
| 前からコピー後ソート | O((m+n)log(m+n)) | O(1) |||| 標準の `.sort()` 利用可 |
25+
| 一時バッファにコピー | O(m+n) | O(m) |||| Vec への clone が発生 |
26+
27+
---
28+
29+
## 3. 選択したアルゴリズムと理由
30+
31+
- **選択**: 後ろからの 3 ポインタマージ
32+
- **理由**: O(m+n) の時間・O(1) の追加空間。末尾から書き込むことで`nums1`の有効領域を上書きせずマージできる。Rustの所有権モデルと完全に親和的(`split_at_mut` 不要、単純なインデックスアクセス)。
33+
- **最適化ポイント**: `usize` 算術のみでスタック完結、ヒープアロケーションゼロ、ループ本体は比較1回+代入1回のみ
34+
35+
---
36+
37+
以下に図解でアルゴリズムの動作を示します:
38+
39+
![alt text](image.png)
40+
41+
## 4. 実装コード
42+
43+
```rust
44+
// Runtime 0 ms
45+
// Beats 100.00%
46+
// Memory 2.29 MB
47+
// Beats 25.52%
48+
impl Solution {
49+
pub fn merge(nums1: &mut Vec<i32>, m: i32, nums2: &mut Vec<i32>, n: i32) {
50+
// i32 → usize への安全なキャスト(制約: 0 <= m, n <= 200)
51+
let mut i = m as usize; // nums1 の有効要素末尾(排他)
52+
let mut j = n as usize; // nums2 の末尾(排他)
53+
let mut k = (m + n) as usize; // 書き込み位置末尾(排他)
54+
55+
// 3ポインタを後ろから前へ動かしながらマージ
56+
//
57+
// 不変条件: k >= i が常に成立するため nums1 の未処理要素を上書きしない
58+
//
59+
// # Complexity
60+
// - Time: O(m + n) — 各要素をちょうど1回だけ参照・書き込み
61+
// - Space: O(1) — 追加ヒープアロケーションなし
62+
while i > 0 && j > 0 {
63+
k -= 1;
64+
if nums1[i - 1] >= nums2[j - 1] {
65+
nums1[k] = nums1[i - 1];
66+
i -= 1;
67+
} else {
68+
nums1[k] = nums2[j - 1];
69+
j -= 1;
70+
}
71+
}
72+
73+
// nums2 の残余要素を nums1 の先頭へコピー
74+
// (nums1 の残余は既に正しい位置にあるのでコピー不要)
75+
while j > 0 {
76+
k -= 1;
77+
nums1[k] = nums2[j - 1];
78+
j -= 1;
79+
}
80+
}
81+
}
82+
```
83+
84+
---
85+
86+
## Rust 固有の最適化観点
87+
88+
**`usize` 算術によるゼロコスト安全性**
89+
90+
LeetCode の引数は `i32` ですが、配列インデックスは `usize` が必要です。制約上 `0 <= m, n <= 200` かつ `1 <= m + n <= 200` が保証されるため `as usize` キャストは常に安全。`i -= 1` 等のデクリメントは `i > 0` ガード後にのみ実行されるため、`usize` アンダーフロー(パニック)は構造的に排除されています。
91+
92+
**借用規則との親和性**
93+
94+
`nums1``nums2` を同時に `&mut` で保持しますが、それぞれ独立した `Vec` への借用であるため借用チェッカーを通過します。`split_at_mut` 等の特別な操作は不要です。
95+
96+
**ループ本体の最小化**
97+
98+
比較1回・代入1回・デクリメント1回のみ。イテレータ抽象化より命令型ループが適切なケースで、コンパイラは確実にインライン展開・SIMD 最適化を適用できます。
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
## 1. 問題の分析
2+
3+
**競技プログラミング視点での分析**
4+
5+
Rustの回答と同様に、後ろからの3ポインタマージが最適解です。TypeScriptでは `number` 型が IEEE 754 倍精度浮動小数点のため整数演算に余分なオーバーヘッドはなく、インデックス操作はV8エンジンがSMI(Small Integer)最適化を適用します。
6+
7+
**業務開発視点での分析**
8+
9+
LeetCode のシグネチャは `void` 返却の破壊的操作ですが、TypeScript の `strict` モードと `readonly` による境界を意識した実装で保守性を担保します。インデックス境界違反は実行時ではなくロジック上で構造的に防止します。
10+
11+
**TypeScript特有の考慮点**
12+
13+
- `number` 型インデックスの減算は `0` ガード後にのみ実行し、負数インデックスアクセスを防止
14+
- `as const` は不要(可変配列操作のため)
15+
- V8 の Packed SMI Arrays 最適化を維持するため、配列の型を混在させない
16+
17+
---
18+
19+
## 2. アルゴリズムアプローチ比較
20+
21+
| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 |
22+
| --------------------------- | ---------------- | ---------- | ------------ | -------- | ------ | -------------------------- |
23+
| 後ろからマージ(3ポインタ) | O(m+n) | O(1) |||| 追加アロケーションなし ✅ |
24+
| コピー後 `.sort()` | O((m+n)log(m+n)) | O(1) | 最低 || 最高 | 比較関数必須・競技では次善 |
25+
| 一時バッファにコピー | O(m+n) | O(m) |||| スプレッド構文でヒープ確保 |
26+
27+
---
28+
29+
## 3. 選択したアルゴリズムと理由
30+
31+
- **選択**: 後ろからの3ポインタマージ
32+
- **理由**: O(m+n) 時間・O(1) 追加空間。末尾書き込みにより有効要素の上書きが構造的に不可能。TypeScript の `number` 型インデックスは整数として扱われ、V8 の SMI 最適化が全体に適用される
33+
- **TypeScript特有の最適化ポイント**: 変数を `let` で宣言し型推論に委ねることでコンパイラの最適化ヒントを最大化。`const` で宣言すると再代入不可になるため、カウンタ変数は `let` が適切
34+
35+
---
36+
37+
## 4. 実装コード
38+
39+
```typescript
40+
// Runtime 0 ms
41+
// Beats 100.00%
42+
// Memory 54.72 MB
43+
// Beats 93.68%
44+
45+
/**
46+
* 2つのソート済み配列を nums1 にインプレースでマージする。
47+
* 後ろから走査する3ポインタ法により、追加アロケーションなしで O(m+n) を実現。
48+
*
49+
* @param nums1 - マージ先配列(長さ m+n、末尾 n 要素は 0 で埋められている)
50+
* @param m - nums1 の有効要素数
51+
* @param nums2 - マージ元配列(長さ n)
52+
* @param n - nums2 の要素数
53+
*
54+
* @complexity Time: O(m + n), Space: O(1)
55+
*
56+
* 不変条件: k >= i が常に成立するため nums1 の未処理要素を上書きしない
57+
* 証明: 各イテレーションで k と (i または j) が同時に 1 減少する。
58+
* 初期値 k = m + n - 1, i = m - 1 より k - i = n >= 0 が保持される。
59+
*/
60+
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
61+
// 各ポインタは「次に参照すべき要素のインデックス」を表す(0-based)
62+
let i: number = m - 1; // nums1 有効末尾
63+
let j: number = n - 1; // nums2 末尾
64+
let k: number = m + n - 1; // 書き込み位置末尾
65+
66+
// どちらかの配列が空になるまで大きい方を末尾から書き込む
67+
while (i >= 0 && j >= 0) {
68+
if (nums1[i] >= nums2[j]) {
69+
nums1[k] = nums1[i];
70+
i--;
71+
} else {
72+
nums1[k] = nums2[j];
73+
j--;
74+
}
75+
k--;
76+
}
77+
78+
// nums2 の残余要素をコピー
79+
// (nums1 の残余は既に正しい位置にあるため操作不要)
80+
while (j >= 0) {
81+
nums1[k] = nums2[j];
82+
j--;
83+
k--;
84+
}
85+
}
86+
```
87+
88+
---
89+
90+
## TypeScript固有の最適化観点
91+
92+
**V8 エンジンと配列最適化**
93+
94+
`nums1``number[]` として渡されるため、V8 は内部的に `PACKED_SMI_ELEMENTS` または `PACKED_DOUBLE_ELEMENTS` として最適化されます。要素の書き込みを `nums1[k] = value` の単純代入に統一することで、この最適化が維持され `HOLEY_ELEMENTS` への降格を防ぎます。
95+
96+
**型安全インデックス管理**
97+
98+
`i >= 0` / `j >= 0` のガードにより、TypeScript の `strict: true` 環境でも `number` 型のインデックスが負にならないことが実行フロー上保証されます。`noUncheckedIndexedAccess` を有効にした場合でも、ガード後の `nums1[i]``number` として安全にアクセスできます。
99+
100+
**`while` vs イテレータ**
101+
102+
`.reduce()``.forEach()` はコールバック関数のクロージャ生成コストが発生します。本問題のようなインプレース操作かつ複数ポインタを連動させるケースでは、命令型 `while` ループが最も直接的で V8 の JIT 最適化が効きやすい選択です。

0 commit comments

Comments
 (0)