Skip to content

实战:合并两个有序数组

这道题也是双指针的应用,但有一个关键技巧:从后往前遍历。

这个技巧在很多原地修改的题目中都会用到。

题目描述

LeetCode 88. 合并两个有序数组

给你两个按非递减顺序排列的整数数组 nums1nums2,另有两个整数 mn,分别表示 nums1nums2 中的元素数目。

请你合并 nums2nums1 中,使合并后的数组同样按非递减顺序排列。

nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0,应忽略。

示例

输入:nums1 = [1, 2, 3, 0, 0, 0], m = 3, nums2 = [2, 5, 6], n = 3
输出:[1, 2, 2, 3, 5, 6]

题目分析

关键点:

  1. 两个数组都已排序:可以利用有序性
  2. 原地合并到 nums1:nums1 末尾有足够的空间
  3. 结果也要有序

思考一下:如果允许使用额外空间,怎么做?

归并排序的合并过程:用两个指针分别指向两个数组的开头,每次选较小的放入新数组。

但题目要求原地合并到 nums1。从前往后填充会有问题——会覆盖 nums1 中还没处理的元素。

怎么办?从后往前填充。

解法一:从前往后(需要额外空间)

先来看需要额外空间的解法,理解基本思路。

javascript
function merge(nums1, m, nums2, n) {
    // 复制 nums1 的有效部分
    const temp = nums1.slice(0, m);
    
    let i = 0, j = 0, k = 0;
    
    while (i < m && j < n) {
        if (temp[i] <= nums2[j]) {
            nums1[k++] = temp[i++];
        } else {
            nums1[k++] = nums2[j++];
        }
    }
    
    // 处理剩余元素
    while (i < m) nums1[k++] = temp[i++];
    while (j < n) nums1[k++] = nums2[j++];
}

这是归并排序的经典合并过程。时间 O(m+n),空间 O(m)。

但能不能不用额外空间?

解法二:从后往前(最优解)

关键观察:nums1 的末尾有 n 个空位,正好用来存放合并结果。

如果从后往前填充:

  • 我们填充的位置在末尾
  • nums1 的有效元素在开头
  • 两者不会重叠!
javascript
function merge(nums1, m, nums2, n) {
    let i = m - 1;      // nums1 有效元素的末尾
    let j = n - 1;      // nums2 的末尾
    let k = m + n - 1;  // 填充位置,从末尾开始
    
    // 从后往前比较并填充
    while (i >= 0 && j >= 0) {
        if (nums1[i] > nums2[j]) {
            nums1[k--] = nums1[i--];
        } else {
            nums1[k--] = nums2[j--];
        }
    }
    
    // 如果 nums2 还有剩余
    while (j >= 0) {
        nums1[k--] = nums2[j--];
    }
    
    // nums1 剩余元素已经在正确位置,不用处理
}

执行过程

nums1 = [1, 2, 3, 0, 0, 0], m=3, nums2 = [2, 5, 6], n=3 为例:

初始状态:
nums1: [1, 2, 3, 0, 0, 0]
                ↑        ↑
               i=2      k=5
nums2: [2, 5, 6]

            j=2

第 1 步:比较 nums1[2]=3 和 nums2[2]=6
        6 > 3,放 6 到 nums1[5]
        [1, 2, 3, 0, 0, 6], j=1, k=4

第 2 步:比较 nums1[2]=3 和 nums2[1]=5
        5 > 3,放 5 到 nums1[4]
        [1, 2, 3, 0, 5, 6], j=0, k=3

第 3 步:比较 nums1[2]=3 和 nums2[0]=2
        3 > 2,放 3 到 nums1[3]
        [1, 2, 3, 3, 5, 6], i=1, k=2

第 4 步:比较 nums1[1]=2 和 nums2[0]=2
        2 <= 2,放 nums2[0]=2 到 nums1[2]
        [1, 2, 2, 3, 5, 6], j=-1, k=1

j < 0,nums2 处理完毕,结束。
nums1 剩余的 [1, 2] 已经在正确位置。

结果:[1, 2, 2, 3, 5, 6] ✓

为什么不用处理 nums1 剩余元素?

当 nums2 处理完毕(j < 0)时,nums1 剩余的元素已经在它们应该在的位置上了。

因为我们是从后往前填充,nums1 的剩余元素在前面,填充的位置在后面,它们根本没有被动过。

但如果 nums1 先处理完(i < 0),nums2 还有剩余,那就需要继续把 nums2 的元素填进去。

复杂度分析

  • 时间复杂度:O(m + n)——每个元素最多被访问一次
  • 空间复杂度:O(1)——只用了三个指针变量

常见错误

  1. 从前往后导致覆盖:这是最常见的错误
javascript
// 错误示范
function merge(nums1, m, nums2, n) {
    let i = 0, j = 0;
    while (i < m && j < n) {
        if (nums1[i] <= nums2[j]) {
            i++;  // 保持原位
        } else {
            // 想在 i 位置插入 nums2[j]...
            // 但这会覆盖 nums1[i]!
        }
    }
}
  1. 忘记处理 nums2 剩余元素
javascript
// 错误:只有主循环,没有处理剩余
function merge(nums1, m, nums2, n) {
    let i = m - 1, j = n - 1, k = m + n - 1;
    while (i >= 0 && j >= 0) {
        // ...
    }
    // 如果 j >= 0,还有元素没处理!
}
  1. 边界条件错误:指针的初始值和终止条件要仔细

相关题目

  • 21. 合并两个有序链表:链表版本,思路类似
  • 977. 有序数组的平方:也用到"从两端向中间"的双指针

本章小结

这一章我们学习了从后往前遍历的技巧:

  1. 问题:从前往后填充会覆盖未处理的元素
  2. 解决:从后往前填充,利用数组末尾的空间
  3. 关键:每次选两个数组末尾较大的元素放到最后

这个"从后往前"的思想很重要。当你遇到原地修改且可能覆盖的问题时,考虑反向遍历。

下一章,我们来看「旋转数组」。

实战:合并两个有序数组 has loaded