LeetCode 刷题日记:合并两个有序数组 —— 双指针与逆向三指针的深度剖析
📌 题目链接:leetcode.cn/problems/me…
💡 难度:简单 | 标签:数组、双指针、原地算法
🧑💻 语言:JavaScript
🎯 一、题目再理解:不只是“合并”,而是“原地合并”
给定两个非递减排序的整数数组:
nums1:长度为m + n,其中前m个元素是有效数据,后n个位置是预留的(值任意,通常为 0)。nums2:长度为n,全部为有效数据。
要求:将 nums2 合并到 nums1 中,使得 nums1 成为一个长度为 m + n 的非递减有序数组,且必须在 nums1 上原地完成修改,不能返回新数组。
✅ 输入输出示例
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出:
nums1 = [1,2,2,3,5,6]
⚠️ 注意:你不能创建新数组来存储结果,必须直接修改
nums1。
🔍 二、为什么不能用“正向双指针”直接写入 nums1?
很多初学者会想:“既然两个数组都已排序,那我用两个指针从前往后比较,小的放前面不就行了?”
但问题在于:nums1 的后半部分虽然是空的,但前半部分的有效数据会被覆盖!
举个例子:
nums1 = [4,5,6,0,0,0] (m=3)
nums2 = [1,2,3] (n=3)
如果 i=0, j=0,发现 nums2[0]=1 < nums1[0]=4,
于是把 1 写入 nums1[0] → nums1 变成 [1,5,6,0,0,0]
但原来的 4 被覆盖了!后续无法恢复。
所以,正向写入会破坏未处理的数据,不可行。
那怎么办?—— 换个方向:从后往前!
🧩 三、解法一:正向双指针 + 辅助数组(逻辑清晰,但非严格原地)
虽然题目强调“原地”,但在学习阶段,先掌握标准的“双指针合并”模式很有必要。这是解决所有有序序列合并问题的基础模板。
📐 算法思想
- 创建一个临时数组
temp - 使用两个指针
i(指向nums1有效部分)、j(指向nums2) - 比较
nums1[i]与nums2[j],将较小者放入temp - 最后将
temp全部拷贝回nums1
⏱️ 复杂度分析
- 时间复杂度:O(m + n) —— 每个元素访问一次
- 空间复杂度:O(m + n) —— 需要额外数组存储结果
✅ 优点:逻辑直观,不易出错
❌ 缺点:不符合“严格原地”要求,面试中可能被追问优化
💻 JavaScript 实现
var merge = function(nums1, m, nums2, n) {
let i = 0, j = 0;
let temp = [];
// 双指针合并
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) {
temp.push(nums1[i]);
i++;
} else {
temp.push(nums2[j]);
j++;
}
}
// 处理剩余元素
while (i < m) {
temp.push(nums1[i]);
i++;
}
while (j < n) {
temp.push(nums2[j]);
j++;
}
// 拷贝回 nums1
for (let k = 0; k < temp.length; k++) {
nums1[k] = temp[k];
}
};
🧪 调试小技巧
你可以打印 temp 来验证合并过程是否正确:
console.log("Merged temp:", temp); // 查看中间结果
🚀 四、解法二:逆向三指针(真正原地,面试高分答案)
这才是本题的灵魂解法。它巧妙利用了 nums1 末尾的“空闲空间”,通过从大到小反向填充,避免了数据覆盖。
🧠 核心洞察
nums1的后n个位置是空的 → 我们可以从最后一个位置开始写入最大值,逐步向前推进,永远不会覆盖未处理的有效数据。
📌 三指针定义
| 指针 | 含义 | 初始值 |
|---|---|---|
i | nums1 有效部分的最后一个元素索引 | m - 1 |
j | nums2 的最后一个元素索引 | n - 1 |
k | nums1 的物理末尾(待写入位置) | m + n - 1 |
🔁 合并逻辑(关键!)
- 比较
nums1[i]和nums2[j] - 将较大的放入
nums1[k] - 对应指针左移(
i--或j--),k-- - 重复直到其中一个数组处理完
❓ 为什么放“较大的”?
因为我们是从最大值开始填,保证最终数组从前往后是非递减的。
🧹 收尾处理
- 如果
nums2还有剩余(j >= 0):说明nums1已空,把nums2剩余全填入 - 如果
nums1还有剩余:它们本来就在nums1前部,且位置正确,无需移动
⏱️ 复杂度分析
- 时间复杂度:O(m + n) —— 每个元素最多被访问一次
- 空间复杂度:O(1) —— 仅用三个指针,完全原地
💻 JavaScript 实现(含详细注释)
var merge = function(nums1, m, nums2, n) {
// 三指针初始化
let i = m - 1; // nums1 有效数据末尾
let j = n - 1; // nums2 末尾
let k = m + n - 1; // nums1 总末尾(写入位)
// 从后往前合并,每次选较大的填入
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k] = nums1[i];
i--;
} else {
nums1[k] = nums2[j];
j--;
}
k--; // 写入后,k 左移
}
// 如果 nums2 还有剩余元素(nums1 已处理完)
// 把它们全部复制到 nums1 前部
while (j >= 0) {
nums1[k] = nums2[j];
j--;
k--;
}
// 注意:若 i >= 0(nums1 有剩余),它们已在正确位置,无需操作
};
🖼️ 图解示例(以题目样例为例)
初始状态:
nums1 = [1, 2, 3, 0, 0, 0]
↑ ↑
i=2 k=5
nums2 = [2, 5, 6]
↑
j=2
第1步:nums1[2]=3 < nums2[2]=6 → nums1[5] = 6,j--, k--
第2步:3 < 5 → nums1[4] = 5
第3步:3 > 2 → nums1[3] = 3
第4步:2 == 2 → nums1[2] = 2(取 nums2 的)
第5步:nums1[0]=1 < nums2[0]=2 → nums1[1] = 2
最后:nums1[0] = 1
结果:[1,2,2,3,5,6] ✅
🆚 五、两种解法对比总结
| 维度 | 正向双指针 + 辅助数组 | 逆向三指针(原地) |
|---|---|---|
| 是否原地 | ❌ 需 O(m+n) 额外空间 | ✅ O(1) 空间 |
| 时间复杂度 | O(m + n) | O(m + n) |
| 代码难度 | 简单,易理解 | 中等,需理解方向 |
| 面试推荐度 | 作为过渡思路 | 强烈推荐,标准答案 |
| 适用场景 | 允许额外空间、教学演示 | 严格原地、性能敏感 |
💬 面试建议:
先说出“可以用辅助数组的双指针”,然后主动说:“但考虑到空间效率,我们可以优化为原地的逆向三指针……”
这样既展示基础能力,又体现优化意识!