LeetCode 88. 合并两个有序数组

题目链接

LeetCode 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) 的算法解决此问题吗?


题解

方法一:合并后排序(最简洁写法)

思路

这是最直观、代码量最少的解法。既然 nums1 已经预留了足够的空间,我们可以直接将 nums2 的所有元素复制到 nums1 的尾部(覆盖掉那些 0),然后直接调用系统自带的排序函数对整个 nums1 进行排序。


复杂度分析

  • 时间复杂度: O ( ( m + n ) log ⁡ ( m + n ) ) O((m+n)\log(m+n)) O((m+n)log(m+n))
    • 复制数组耗时 O ( n ) O(n) O(n)。
    • 排序耗时 O ( ( m + n ) log ⁡ ( m + n ) ) O((m+n)\log(m+n)) O((m+n)log(m+n))。
    • 总时间复杂度由排序主导。
  • 空间复杂度: O ( log ⁡ ( m + n ) ) O(\log(m+n)) O(log(m+n))
    • 这是排序算法(如快速排序)递归调用栈所需的额外空间。

代码

java 复制代码
import java.util.Arrays;

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        // 1. 将 nums2 的元素复制到 nums1 的尾部
        // 从 nums2 的索引 0 开始,复制 n 个元素到 nums1 的索引 m 处
        System.arraycopy(nums2, 0, nums1, m, n);
        
        // 2. 对整个 nums1 数组进行排序
        Arrays.sort(nums1);
    }
}

方法二:正向双指针(额外空间 O(m+n))

思路

由于两个数组已经是非递减顺序排列,我们可以使用双指针分别遍历两个数组的有效部分。每次比较两个指针指向的元素,将较小的元素放入临时数组中,直到其中一个数组遍历完毕,再将另一个数组的剩余元素追加到临时数组末尾。最后将临时数组的元素复制回 nums1

这种方法类似于归并排序中的合并操作。


复杂度分析

  • 时间复杂度: O(m + n),每个元素最多被访问一次。
  • 空间复杂度: O(m + n),需要额外的 ArrayList 存储合并结果。

代码

java 复制代码
class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        // 合并后的总长度
        int len = m + n;
        // 用于存储合并结果的临时列表
        ArrayList<Integer> nums = new ArrayList<>();
        // s1 指向 nums1 有效部分的起始位置
        // s2 指向 nums2 的起始位置
        int s1 = 0, s2 = 0;

        // 当两个数组都还有元素未比较时,比较并放入较小的元素
        while (s1 < m && s2 < n) {
            if (nums1[s1] <= nums2[s2]) {
                // nums1 当前元素较小或相等,将其加入结果
                nums.add(nums1[s1]);
                s1++;
            } else {
                // nums2 当前元素较小,将其加入结果
                nums.add(nums2[s2]);
                s2++;
            }
        }

        // 如果 nums1 还有剩余元素,全部追加到结果末尾
        while (s1 < m) {
            nums.add(nums1[s1]);
            s1++;
        }

        // 如果 nums2 还有剩余元素,全部追加到结果末尾
        while (s2 < n) {
            nums.add(nums2[s2]);
            s2++;
        }

        // 将合并结果复制回 nums1
        for (int i = 0; i < len; i++) {
            int number = nums.get(i);
            nums1[i] = number;
        }
    }
}

图解示例

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

复制代码
初始状态:s1=0, s2=0
nums1(有效部分): [1, 2, 3]
                  ↑
                 s1

nums2:           [2, 5, 6]
                  ↑
                 s2

临时数组 nums:   []

第1步:比较 nums1[s1]=1 和 nums2[s2]=2,1 更小,加入临时数组,s1++
nums1(有效部分): [1, 2, 3]
                     ↑
                    s1

nums2:           [2, 5, 6]
                  ↑
                 s2

临时数组 nums:   [1]

第2步:比较 nums1[s1]=2 和 nums2[s2]=2,相等,取 nums1,s1++
nums1(有效部分): [1, 2, 3]
                        ↑
                       s1

nums2:           [2, 5, 6]
                  ↑
                 s2

临时数组 nums:   [1, 2]

第3步:比较 nums1[s1]=3 和 nums2[s2]=2,2 更小,加入临时数组,s2++
nums1(有效部分): [1, 2, 3]
                        ↑
                       s1

nums2:           [2, 5, 6]
                     ↑
                    s2

临时数组 nums:   [1, 2, 2]

第4步:比较 nums1[s1]=3 和 nums2[s2]=5,3 更小,加入临时数组,s1++
nums1(有效部分): [1, 2, 3]
                           ↑
                          s1 (s1=m,nums1 遍历完)

nums2:           [2, 5, 6]
                     ↑
                    s2

临时数组 nums:   [1, 2, 2, 3]

第5步:nums1 已遍历完,将 nums2 剩余元素全部追加
nums2 剩余:[5, 6]

临时数组 nums:   [1, 2, 2, 3, 5, 6]

第6步:将临时数组复制回 nums1
nums1:           [1, 2, 2, 3, 5, 6] 

方法二:逆向双指针(最优解,空间 O(1))

思路

方法一需要从前往后合并,因此需要额外的空间来存储结果。但题目中 nums1 的末尾已经预留了 n 个空位,我们可以利用这一点,从后往前进行合并。

设置三个指针:

  • s1 指向 nums1 有效元素的最后一个位置(索引 m-1
  • s2 指向 nums2 的最后一个位置(索引 n-1
  • tail 指向 nums1 的最后一个位置(索引 m+n-1

每次比较 nums1[s1]nums2[s2],将较大的元素放到 nums1[tail],然后相应指针前移。由于是从后往前填充,不会覆盖 nums1 中还未比较的元素。

nums2 的元素全部放置完毕后,合并完成;若 nums1 先遍历完,则只需将 nums2 剩余元素依次放入即可。


复杂度分析

  • 时间复杂度: O(m + n),每个元素最多被访问一次。
  • 空间复杂度: O(1),原地修改,无需额外空间。

代码

java 复制代码
class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        // s1 指向 nums1 有效元素的末尾
        int s1 = m - 1;
        // s2 指向 nums2 的末尾
        int s2 = n - 1;
        // tail 指向 nums1 的最终末尾位置
        int tail = n + m - 1;

        // 从后往前比较,将较大值放到 tail 位置
        while (s1 >= 0 && s2 >= 0) {
            if (nums1[s1] >= nums2[s2]) {
                // nums1 当前元素较大,放到 tail 位置
                nums1[tail] = nums1[s1];
                s1--;
            } else {
                // nums2 当前元素较大,放到 tail 位置
                nums1[tail] = nums2[s2];
                s2--;
            }
            // tail 指针前移
            tail--;
        }

        // 如果 nums2 还有剩余元素,继续放入 nums1
        // 注意:如果 nums1 有剩余,说明它们已经在正确位置,无需处理
        while (s2 >= 0) {
            nums1[tail] = nums2[s2];
            s2--;
            tail--;
        }
    }
}

图解示例

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

复制代码
初始状态:s1=2, s2=2, tail=5
nums1: [1, 2, 3, 0, 0, 0]
              ↑        ↑
             s1      tail

nums2:       [2, 5, 6]
                    ↑
                   s2

第1步:比较 nums1[s1]=3 和 nums2[s2]=6,6 更大,放 6 到 tail,s2--, tail--
nums1: [1, 2, 3, 0, 0, 6]
              ↑     ↑
             s1    tail

nums2:       [2, 5, 6]
                 ↑
                s2

第2步:比较 nums1[s1]=3 和 nums2[s2]=5,5 更大,放 5 到 tail,s2--, tail--
nums1: [1, 2, 3, 0, 5, 6]
              ↑  ↑
             s1 tail

nums2:       [2, 5, 6]
              ↑
             s2

第3步:比较 nums1[s1]=3 和 nums2[s2]=2,3 更大,放 3 到 tail,s1--, tail--
nums1: [1, 2, 3, 3, 5, 6]
           ↑ ↑
          s1 tail

nums2:       [2, 5, 6]
              ↑
             s2

第4步:比较 nums1[s1]=2 和 nums2[s2]=2,相等,取 nums1,s1--, tail--
nums1: [1, 2, 2, 3, 5, 6]
        ↑↑
       s1 tail

nums2:       [2, 5, 6]
              ↑
             s2

第5步:s1=-1 < 0,nums1 遍历完,将 nums2 剩余元素全部放入
nums2 剩余:[2]

nums1: [1, 2, 2, 3, 5, 6]
       ↑
     tail

nums2:       [2, 5, 6]
              ↑
             s2

放入 nums2[0]=2:
nums1: [1, 2, 2, 3, 5, 6] 

最终结果:[1, 2, 2, 3, 5, 6]

方法对比总结

方法 核心思想 时间复杂度 空间复杂度 评价
方法一:合并后排序 暴力拼接后调用库函数 O ( ( m + n ) log ⁡ ( m + n ) ) O((m+n)\log(m+n)) O((m+n)log(m+n)) O ( log ⁡ ( m + n ) ) O(\log(m+n)) O(log(m+n)) 代码最短,适合快速实现,但效率最低。
方法二:正向双指针 归并排序思想,使用额外数组 O ( m + n ) O(m+n) O(m+n) O ( m + n ) O(m+n) O(m+n) 逻辑清晰,但未做到原地修改。
方法二:逆向双指针 利用尾部空间,从后往前填 O ( m + n ) O(m+n) O(m+n) O ( 1 ) O(1) O(1) 最优解,面试标准答案。
相关推荐
水蓝烟雨2 小时前
LeetCode刷题笔记:合并两个有序链表(0021)
笔记·leetcode·链表
生信研究猿2 小时前
leetcode 101.对称二叉树(不会做)
算法·leetcode·职场和发展
样例过了就是过了2 小时前
LeetCode热题100 跳跃游戏 II
c++·算法·leetcode·贪心算法·动态规划
香蕉鼠片2 小时前
第三大的数
数据结构·算法·leetcode
小辉同志3 小时前
79. 单词搜索
开发语言·c++·leetcode·回溯
人道领域3 小时前
【LeetCode刷题日记】哈希表:从0基础到实战全解析
算法·leetcode·哈希算法
py有趣3 小时前
力扣热门100题之矩阵置零
算法·leetcode·矩阵
Boop_wu11 小时前
[Java 算法] 字符串
linux·运维·服务器·数据结构·算法·leetcode
6Hzlia16 小时前
【Hot 100 刷题计划】 LeetCode 42. 接雨水 | C++ 动态规划与双指针题解
c++·算法·leetcode