我第一次看这题时,直觉也是“这不就是归并两个有序数组吗”。但真下手写就会发现,麻烦的地方不在比较大小,而在于题目要求直接在 nums1 上原地完成合并。
原题链接:
先把题目条件拎清楚:
nums1的前m个元素有效,后面预留了n个空位nums2有n个有效元素- 两个数组都已经有序
- 要把
nums2合并进nums1,并且结果仍然有序
一、我为什么没有顺着往前合并
如果只是普通归并,我通常会从前往后比较,把较小的那个先放进去。
但这题不一样,结果不能写到新数组里,只能写回 nums1。
这时候问题马上就出来了。
nums1 的前半部分原本就存着有效元素,如果我们一边从前往后遍历,一边把结果写回前面的位置,就很容易把还没比较过的值覆盖掉。
所以我看到这里时,脑子里冒出来的问题其实不是“怎么归并”,而是:
如何在写入答案的同时,不破坏
nums1里还没处理的元素。
二、真正有用的条件,其实是 nums1 后面那段空位
nums1 的后 n 个位置是预留出来的空位,这意味着:
- 从前往后写,会覆盖有效元素
- 从后往前写,不会覆盖任何还没处理的值
我就是在这里把方向彻底转过来的:既然前面不能乱写,那就干脆从后面开始填。
也就是说,不再想着“谁小谁先放前面”,而是改成:
- 每次比较
nums1当前末尾有效值和nums2当前末尾值 - 谁更大,就把谁放到
nums1的最后面
这样写就顺了,这题本质上就是一个逆向双指针。
三、这三个指针怎么设
做法很直接,开三个指针:
i:指向nums1当前有效部分的末尾,也就是m - 1j:指向nums2的末尾,也就是n - 1k:指向nums1整个数组的末尾,也就是m + n - 1
接下来每轮只做一件事:
- 比较
nums1[i]和nums2[j] - 把较大的那个放到
nums1[k] - 对应指针左移
因为我一直在填 nums1 尾部那段空位,所以前面的有效元素不会被提前覆盖。
图解
拿题目里最常见的例子走一遍会更直观:
nums1 = [1, 2, 3, 0, 0, 0],m = 3nums2 = [2, 5, 6],n = 3
初始状态:
nums1: [1, 2, 3, 0, 0, 0]
i k
nums2: [2, 5, 6]
j
第一步,比较 3 和 6,较大的 6 放到最后面:
nums1: [1, 2, 3, 0, 0, 6]
i k
nums2: [2, 5, 6]
j
第二步,比较 3 和 5,较大的 5 继续放到后面:
nums1: [1, 2, 3, 0, 5, 6]
i k
nums2: [2, 5, 6]
j
第三步,比较 3 和 2,这次把 3 放到 k:
nums1: [1, 2, 3, 3, 5, 6]
i k
nums2: [2, 5, 6]
j
第四步,比较 2 和 2,把 nums2[j] 放进去也可以:
nums1: [1, 2, 2, 3, 5, 6]
i k
nums2: [2, 5, 6]
j
这时候 nums2 已经用完,前面剩下的 1, 2 本来就在正确位置上,不需要再动。
这个过程里最关键的一点就是:每次都在往 nums1 的尾部空位写,所以前面的有效元素一直是安全的。
四、为什么最后通常只补 nums2
这一步我一开始也绕了一下。
主循环结束后,只会有两种情况:
第一种,nums2 先用完。
这时 nums1 前面剩下的那部分其实不用动。它们本来就有序,而且位置也没坏。
第二种,nums1 的有效部分先用完。
这时 nums2 如果还有剩余,就得继续往前补。因为这些值还没进 nums1。
所以最后真正需要补的,通常只有这一段:
while (j >= 0) {
nums1[k--] = nums2[j--];
}
想通这一点以后,代码会一下子简洁很多,因为 nums1 的剩余部分根本不用专门处理。
五、最终解法
代码如下:
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1;
int j = n - 1;
int k = m + n - 1;
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
while (j >= 0) {
nums1[k--] = nums2[j--];
}
}
}
六、这题里我最容易踩的两个坑
这题思路不难,但很容易写得拧巴。我觉得最容易踩的是两个点。
1. 还在按正向归并去想
只要脑子里还是“谁小谁先放前面”,代码就会一直和“原地写回 nums1”这个条件打架。
所以这题第一反应不该是普通归并,而应该是:
结果只能写回
nums1,那我怎样才能不覆盖它前面的有效元素?
2. 以为两边剩余部分都要处理
实际上不是。
nums2有剩余,需要继续复制nums1有剩余,不需要处理
这个地方一旦想通,很多多余分支自然就没了。
七、复杂度
- 时间复杂度:
O(m + n) - 空间复杂度:
O(1)
没有开额外数组,吃满了 nums1 末尾那段预留空间。
八、总结
这题最值得记住的,不是“逆向双指针”这个名字,而是这个转弯过程:
- 题目要求原地合并
nums1前面有有效元素,正向写会覆盖nums1后面有空位,逆向写最安全- 每次把较大值放到末尾
一句话概括:
不是普通归并,而是利用尾部空位做一次“从后往前”的原地归并。