LeetCode88. 合并两个有序数组

116 阅读7分钟

88. 合并两个有序数组

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

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

注意: 最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n

示例 1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3][2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1][] 。
合并结果是 [1]

示例 3:

输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [][1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

提示:

  • nums1.length == m + n
  • nums2.length == n
  • 0 <= m, n <= 200
  • 1 <= m + n <= 200
  • -10^9 <= nums1[i], nums2[j] <= 10^9

进阶: 你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?


第一阶段:最直观(但低效)的方法:合并后排序

思考过程

当我们刚看到这个问题时,最直接的想法就是“先不管顺序,再整体排序”。

  1. 第一步: 题目要求把 nums2 合并到 nums1 里。nums1 的末尾正好有 n 个空位(都是0)。那我们就先把 nums2 的所有元素直接复制到 nums1 的末尾空位上。
    • 例如:nums1 = [1,2,3,0,0,0], nums2 = [2,5,6]
    • 复制后:nums1 变为 [1,2,3,2,5,6]
  2. 第二步: 现在 nums1 包含了所有需要的元素,但是是无序的。题目要求最终的数组是有序的,所以我们调用一个排序函数,比如 Java 的 Arrays.sort(),对整个 nums1 数组进行排序。
    • 排序后:nums1 变为 [1,2,2,3,5,6]
  3. 完成: 得到了正确的结果。
评估与反思
  • 正确性: 这个方法是完全正确的。
  • 效率: 复制需要 O(n) 时间。对一个长度为 m+n 的数组进行排序,时间复杂度是 O((m+n) log(m+n))。所以总时间复杂度是 O((m+n) log(m+n))
  • 问题: 这个方法完全没有利用题目给的一个关键信息:“nums1nums2 本身已经是排好序的”。这通常意味着存在比通用排序更高效的解法。题目的“进阶”要求也暗示了 O(m+n) 的可能性。

第二阶段:进一步的思考:为什么从前往后合并行不通?

既然两个数组都有序,我们自然会想到类似“归并排序”中的合并步骤。

思考过程
  1. 想法: 我们用两个指针 p1p2,分别指向 nums1 的开头和 nums2 的开头。比较 nums1[p1]nums2[p2],把较小的那个放到 nums1 的开头,然后移动相应的指针。
  2. 遇到障碍:
    • 假设 nums1 = [4,5,6,0,0,0]nums2 = [1,2,3]
    • 比较 nums1[0] (4) 和 nums2[0] (1)。 1 更小,所以我们希望最终数组的第一位是 1
    • 我们必须把 1 放到 nums1[0] 的位置。但这样做会覆盖掉原来的 4!在覆盖 4 之前,我们需要把它保存下来,并且还要把 56 向后移动,为 1 腾出空间。
    • 这个“向后移动”的操作非常耗时。每从 nums2 插入一个数,都可能需要移动 nums1 中大量的元素。最坏情况下(nums2 的所有元素都比 nums1 的小),时间复杂度会变成 O(m * n),效率非常低。

结论: 从前往后合并,在原地(in-place)操作 nums1 会导致大量的元素移动,效率很差。


第三阶段:关键突破——从后往前合并

思考过程
  1. 重新审视问题: 从前往后合并的问题在于我们会覆盖掉后面需要用到的数据。那么,nums1 数组中哪里有“安全”的、可以随便覆盖的空闲空间呢?答案是数组的末尾
  2. 逆向思维: 既然最终排好序的数组,其最大的元素也应该在末尾,我们为什么不从后往前填充 nums1 呢?
  3. 指针设置:
    • 设置一个指针 p1 指向 nums1 有效元素部分的末尾(即索引 m-1)。
    • 设置一个指针 p2 指向 nums2 的末尾(即索引 n-1)。
    • 设置一个“写入指针” p 指向 nums1 数组的最末尾(即索引 m+n-1)。
  4. 比较与填充:
    • 比较 nums1[p1]nums2[p2] 的大小。
    • 将两者中较大的那个数,放到 nums1[p] 的位置。
    • 移动被选中的那个数的指针(如果选了 nums1[p1],则 p1--;如果选了 nums2[p2],则 p2--)。
    • 将写入指针 p 向前移动一位 (p--)。
  5. 循环: 重复这个过程,直到 p1p2 有一个越界(即处理完了其中一个数组的所有元素)。
  6. 处理剩余元素: 循环结束后,可能有一个数组还有剩余元素。
    • 如果 nums1 还有剩余(p1 >= 0),我们不需要做任何事。因为它们本来就在 nums1 的开头,并且它们比所有已经放置的元素都要小,所以它们的位置是正确的。
    • 如果 nums2 还有剩余(p2 >= 0),说明这些是最小的一批元素。我们需要将它们全部复制到 nums1 的开头空余位置(从 p 指针当前位置往前)。

这个方法完美地解决了问题:

  • 它从后往前填充,利用了末尾的空闲空间。
  • 它绝不会覆盖掉尚未比较的有效数据。
  • 每个元素只被访问一次,时间复杂度是 O(m+n)。
  • 它是在原地完成的,空间复杂度是 O(1)。

第四阶段:最终的 Java 代码实现

import java.util.Arrays; // 仅用于 main 方法的测试输出
import java.util.Scanner; // 用于 ACM 模式

public class Main { // ACM 模式类名通常为 Main

    public static void main(String[] args) {
        // ... (此处省略 ACM 输入处理部分,专注于核心算法) ...
        // 示例调用:
        int[] nums1 = {1, 2, 3, 0, 0, 0};
        int m = 3;
        int[] nums2 = {2, 5, 6};
        int n = 3;
        merge(nums1, m, nums2, n);
        System.out.println(Arrays.toString(nums1)); // 输出: [1, 2, 2, 3, 5, 6]
    }

    /**
     * 将两个有序整数数组 nums1 和 nums2 合并到一个数组 nums1 中。
     * 采用从后往前的双指针法,实现 O(m+n) 时间复杂度和 O(1) 空间复杂度。
     *
     * @param nums1 第一个数组,有足够的空间 (m+n)
     * @param m     nums1 中有效元素的数量
     * @param nums2 第二个数组
     * @param n     nums2 中有效元素的数量
     */
    public static void merge(int[] nums1, int m, int[] nums2, int n) {
        // --- 1. 初始化指针 ---
        // p1 指向 nums1 有效元素的末尾
        int p1 = m - 1;
        // p2 指向 nums2 的末尾
        int p2 = n - 1;
        // p 指向 nums1 数组的最末尾,这是写入位置
        int p = m + n - 1;

        // --- 2. 从后往前比较和填充 ---
        // 当 nums2 的所有元素都被处理完时 (p2 < 0),循环就可以结束
        // 因为如果 nums2 处理完了,nums1 剩余的元素本就在正确的位置上
        while (p2 >= 0) {
            // 比较 nums1 和 nums2 的尾部元素
            // 同时需要确保 nums1 的指针 p1 没有越界
            if (p1 >= 0 && nums1[p1] > nums2[p2]) {
                // 如果 nums1 的当前元素更大,则将其放置在 p 位置
                nums1[p] = nums1[p1];
                p1--; // 将 p1 指针左移
            } else {
                // 否则(nums2 的元素更大或相等,或者 nums1 已处理完),
                // 将 nums2 的元素放置在 p 位置
                nums1[p] = nums2[p2];
                p2--; // 将 p2 指针左移
            }
            // 无论放置哪个元素,写入指针 p 都需要左移
            p--;
        }
        // 当 p2 < 0 时,循环结束,所有 nums2 的元素都已合并到 nums1 中。
        // nums1 中可能剩余的元素(如果它们是最小的)已经在正确的位置,无需移动。
    }
}