【LeetCode 刷题笔记】:合并两个有序数组——掌握三指针原地合并的核心思想
题目来源:第 88 题(Merge Sorted Array)
难度:Easy → Medium
标签:数组、双指针、三指针、原地操作
Hot 100 必刷指数:⭐⭐⭐⭐⭐
在 LeetCode 的数组类高频题中,「合并两个有序数组」是一道看似简单却极具教学意义的经典题。它不仅考察你对有序结构的理解,更深入检验你对空间优化与指针移动策略的掌握。
本文将带你:
- 理解题目陷阱:
nums1已预留空间但含“垃圾值”; - 分析朴素解法为何浪费空间;
- 揭秘三指针从后往前合并的巧妙思路;
- 手把手实现 O(1) 额外空间、O(m+n) 时间 的最优解。
🔹 1. 题目解析:不只是“合并”,而是“原地合并”
📌 题目简述
给你两个非递减整数数组 nums1 和 nums2,以及两个整数 m 和 n,分别表示 nums1 和 nums2 中的有效元素个数。
nums1的长度为m + n,其中前m个元素是有效数据,后n个位置为 0(预留空间)。- 要求:将
nums2合并到nums1中,使nums1成为一个非递减的有序数组。 - 关键限制:必须原地修改
nums1,不能返回新数组。
💡 示例:
nums1 = [1,2,3,0,0,0], m = 3 nums2 = [2,5,6], n = 3 // 合并后:nums1 = [1,2,2,3,5,6]
🔸 2. 朴素解法:双指针从前向后(但需额外空间)
✅ 思路
- 使用两个指针
i、j分别指向nums1和nums2的开头; - 比较
nums1[i]与nums2[j],将较小者放入新数组; - 最终将新数组拷贝回
nums1。
❌ 问题:空间复杂度 O(m+n)
function merge(nums1, m, nums2, n) {
let i = 0, j = 0;
const merged = [];
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) merged.push(nums1[i++]);
else merged.push(nums2[j++]);
}
while (i < m) merged.push(nums1[i++]);
while (j < n) merged.push(nums2[j++]);
for (let k = 0; k < merged.length; k++) {
nums1[k] = merged[k];
}
}
- 优点:逻辑清晰,易于理解;
- 缺点:违背“原地”要求的精神,且 LeetCode 明确希望你利用
nums1的尾部空间。
💡 小结:从前向后合并 → 需要 O(n) 额外空间
🔧 3. 关键洞察:为什么“从后往前”能省空间?
🤔 核心观察
nums1的后 n 个位置是空的(0) ,不会被覆盖;- 如果我们从大到小合并(即从两个数组的末尾开始),那么写入的位置(
nums1的尾部)始终是安全的,不会覆盖未处理的有效元素。
✅ 三指针策略
i = m - 1:指向nums1有效部分的最后一个元素;j = n - 1:指向nums2的最后一个元素;k = m + n - 1:指向nums1的最后一个位置(写入位);
每次比较 nums1[i] 和 nums2[j],将较大者写入 nums1[k],然后对应指针左移。
💡 为什么是“较大者”?因为我们从后往前构建降序填充,最终整体仍是升序!
🔥 4. 三指针原地合并:最优解实现
💻 完整代码(带详细注释)
function merge(nums1, m, nums2, n) {
// 三指针:i 指向 nums1 有效尾,j 指向 nums2 尾,k 指向 nums1 总尾
let i = m - 1;
let j = n - 1;
let k = m + n - 1;
// 从后往前合并,直到其中一个数组耗尽
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k] = nums1[i]; // nums1[i] 更大,放后面
i--;
} else {
nums1[k] = nums2[j]; // nums2[j] 更大或相等,放后面
j--;
}
k--;
}
// 如果 nums2 还有剩余(说明 nums1 已空),直接复制
while (j >= 0) {
nums1[k] = nums2[j];
j--;
k--;
}
// 注意:如果 nums1 有剩余,无需操作,因为它们已在正确位置!
}
🧪 测试用例
let nums1 = [1,2,3,0,0,0], m = 3;
let nums2 = [2,5,6], n = 3;
merge(nums1, m, nums2, n);
console.log(nums1); // [1,2,2,3,5,6]
nums1 = [1], m = 1;
nums2 = [], n = 0;
merge(nums1, m, nums2, n);
console.log(nums1); // [1]
nums1 = [0], m = 0;
nums2 = [1], n = 1;
merge(nums1, m, nums2, n);
console.log(nums1); // [1]
✅ 特别注意:当
nums1先耗尽时,必须处理nums2剩余元素;但若nums2先耗尽,则nums1剩余元素已在正确位置,无需移动!
⏱️ 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 朴素双指针(新数组) | O(m+n) | O(m+n) |
| 三指针原地合并 | O(m+n) | O(1) |
- 时间:每个元素最多被访问一次;
- 空间:仅使用常数个指针变量,真正原地操作。
🧠 思维升华:从“合并”到“空间复用”的范式转变
| 维度 | 普通合并(如归并排序) | 本题(原地合并) |
|---|---|---|
| 目标 | 生成新有序数组 | 原地修改已有数组 |
| 空间假设 | 可申请新空间 | 不能申请新数组 |
| 指针方向 | 从前向后 | 从后向前 |
| 核心技巧 | 双指针顺序写入 | 三指针逆序写入 |
🌟 启示:
- 当目标数组尾部有空闲空间时,逆序处理是避免覆盖的关键;
- 这一思想可扩展至:合并 K 个有序链表(数组变体) 、原地归并排序优化等场景;
- “预留空间”不是摆设,而是解题的突破口!
✅ 总结
- 不要被“0”迷惑:
nums1后n个 0 是预留位,不是有效数据; - 从前向后会覆盖:必须从后往前,利用空闲空间;
- 三指针是标配:
i、j读,k写,分工明确; - 边界处理要小心:只需处理
nums2剩余,nums1剩余天然就位。
掌握这道题,你就掌握了原地操作与空间复用的精髓。它虽是 Easy 难度,却是面试官最爱的“细节题”!
📌 延伸练习:
- LeetCode 912. 排序数组(思考如何用类似思想优化归并)
- LeetCode 21. 合并两个有序链表(链表版,无空间限制)
- LeetCode 23. 合并 K 个升序链表(进阶)
刷题不在多,而在精。一道题吃透,胜过十道囫囵吞枣。