Skip to content
not500
Go back

LeetCode 88 合并两个有序数组:逆向双指针为什么要从后往前

On this page

我第一次看这题时,直觉也是“这不就是归并两个有序数组吗”。但真下手写就会发现,麻烦的地方不在比较大小,而在于题目要求直接在 nums1 上原地完成合并

原题链接:

先把题目条件拎清楚:

一、我为什么没有顺着往前合并

如果只是普通归并,我通常会从前往后比较,把较小的那个先放进去。

但这题不一样,结果不能写到新数组里,只能写回 nums1

这时候问题马上就出来了。

nums1 的前半部分原本就存着有效元素,如果我们一边从前往后遍历,一边把结果写回前面的位置,就很容易把还没比较过的值覆盖掉。

所以我看到这里时,脑子里冒出来的问题其实不是“怎么归并”,而是:

如何在写入答案的同时,不破坏 nums1 里还没处理的元素。

二、真正有用的条件,其实是 nums1 后面那段空位

nums1 的后 n 个位置是预留出来的空位,这意味着:

我就是在这里把方向彻底转过来的:既然前面不能乱写,那就干脆从后面开始填。

也就是说,不再想着“谁小谁先放前面”,而是改成:

这样写就顺了,这题本质上就是一个逆向双指针

三、这三个指针怎么设

做法很直接,开三个指针:

接下来每轮只做一件事:

  1. 比较 nums1[i]nums2[j]
  2. 把较大的那个放到 nums1[k]
  3. 对应指针左移

因为我一直在填 nums1 尾部那段空位,所以前面的有效元素不会被提前覆盖。

图解

拿题目里最常见的例子走一遍会更直观:

初始状态:

nums1: [1, 2, 3, 0, 0, 0]
                 i        k
nums2: [2, 5, 6]
              j

第一步,比较 36,较大的 6 放到最后面:

nums1: [1, 2, 3, 0, 0, 6]
                 i     k
nums2: [2, 5, 6]
           j

第二步,比较 35,较大的 5 继续放到后面:

nums1: [1, 2, 3, 0, 5, 6]
                 i  k
nums2: [2, 5, 6]
        j

第三步,比较 32,这次把 3 放到 k

nums1: [1, 2, 3, 3, 5, 6]
              i  k
nums2: [2, 5, 6]
        j

第四步,比较 22,把 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. 以为两边剩余部分都要处理

实际上不是。

这个地方一旦想通,很多多余分支自然就没了。

七、复杂度

没有开额外数组,吃满了 nums1 末尾那段预留空间。

八、总结

这题最值得记住的,不是“逆向双指针”这个名字,而是这个转弯过程:

  1. 题目要求原地合并
  2. nums1 前面有有效元素,正向写会覆盖
  3. nums1 后面有空位,逆向写最安全
  4. 每次把较大值放到末尾

一句话概括:

不是普通归并,而是利用尾部空位做一次“从后往前”的原地归并。


Share this post:

Previous Post
拆解 Lyra:一个开源提示词优化器提示词
Next Post
Oracle SQL 执行计划分析:从预估计划到真实优化案例