LeetCode-88-合并两个有序数组

题目信息

题目描述

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

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

注意:最终合并好的数组不应该被返回,而是存储在数组 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 中没有元素。那个 0 只是为了确保合并结果能放入 nums1 中。

解题思路

初步思考

这道题看似简单,但有一个小陷阱:如果我们直接使用两个指针从前往后遍历合并,会遇到一个经典问题------当处理到 nums1 的开头时,那些等待被覆盖的元素可能还没来得及被使用就已经被覆盖了。

举个例子,假设 nums1 = [2,4,6,0,0], m = 3, nums2 = [1,5,6], n = 3。如果我们从前往后合并,当我们把 nums1[0] 改成 1 后,原来的 2 就丢失了!

所以关键在于:合并的顺序很重要

方法一:逆向双指针(最优解)

思路 :

既然从前往后合并会丢失数据,那我们换个思路------从后往前合并!想象一下你在整理两摞已经排好序的扑克牌,你会从最上面(最大的一张)开始比较,把大的那张放到最终位置的末尾。这样既能保证不丢失数据,又能实现原地操作。

图示说明:

复制代码
初始状态:
nums1 = [1,  2,  3,  0,  0,  0]  ← 有效元素: 1,2,3
        ↑                    ↑
       p1=2                 p=5
       
nums2 = [2,  5,  6]  ← 全部是有效元素
         ↑
       p2=2

------------------------------------------------------------
第1轮比较: nums1[2]=3 < nums2[2]=6,取较大的 6
nums1 = [1,  2,  3,  0,  0,  6]  ← p=5 放入 6
         
       
nums2 = [2,  5,  6]  ← p2=2 元素已使用
             ↑   ↑
          p2=1  (used)

------------------------------------------------------------
第2轮比较: nums1[2]=3 < nums2[1]=5,取较大的 5
nums1 = [1,  2,  3,  0,  5,  6]  ← p=4 放入 5
       
       
nums2 = [2,  5,  6]  ← p2=1 元素已使用
         ↑
        p2=0

------------------------------------------------------------
第3轮比较: nums1[2]=3 > nums2[0]=2,取较大的 3
nums1 = [1,  2,  3,  3,  5,  6]  ← p=3 放入 3
        

nums2 = [2,  5,  6]  ← p2=0 元素已使用
           (used)

------------------------------------------------------------
第4轮比较: nums1[1]=2 = nums2[0]=2,取 2 (哪个都可以)
nums1 = [1,  2,  2,  3,  5,  6]  ← p=2 放入 2
        

nums2 = [2,  5,  6]  ← nums2 已全部处理完
      (used) (used) (used)

------------------------------------------------------------
nums2 已遍历完,nums1 剩余元素 [1, 2] 已经在正确位置
nums1 = [1,  2,  2,  3,  5,  6]  ✓ 完成!

算法步骤:

  1. 初始化三个指针:p1 指向 nums1 有效元素的末尾(即 m-1),p2 指向 nums2 的末尾(即 n-1),p 指向 nums1 的末尾(即 m+n-1
  2. p1 >= 0p2 >= 0 时循环:
    • 如果 nums1[p1] > nums2[p2],将 nums1[p1] 放到 p 位置,p1--p--
    • 否则,将 nums2[p2] 放到 p 位置,p2--p--
  3. 如果循环结束后 p2 >= 0,说明 nums2 还有剩余元素,将它们全部复制到 nums1 前面
  4. 此时 nums1 已经是有序的了!

复杂度分析:

  • 时间复杂度: O(m + n),每个元素最多被访问一次
  • 空间复杂度: O(1),原地操作

代码实现:

Python 实现:

python 复制代码
class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        """
        合并两个有序数组到nums1中
        
        Args:
            nums1: 目标数组
            m: nums1中有效元素个数
            nums2: 源数组
            n: nums2中元素个数
        """
        p1, p2, p = m - 1, n - 1, m + n - 1
        
        while p1 >= 0 and p2 >= 0:
            if nums1[p1] > nums2[p2]:
                nums1[p] = nums1[p1]
                p1 -= 1
            else:
                nums1[p] = nums2[p2]
                p2 -= 1
            p -= 1
        
        while p2 >= 0:
            nums1[p] = nums2[p2]
            p2 -= 1
            p -= 1

Java 实现:

java 复制代码
class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int p1 = m - 1;
        int p2 = n - 1;
        int p = m + n - 1;
        
        while (p1 >= 0 && p2 >= 0) {
            if (nums1[p1] > nums2[p2]) {
                nums1[p] = nums1[p1];
                p1--;
            } else {
                nums1[p] = nums2[p2];
                p2--;
            }
            p--;
        }
        
        while (p2 >= 0) {
            nums1[p] = nums2[p2];
            p2--;
            p--;
        }
    }
}

*Rust 实现*:

```rust
impl Solution {
    pub fn merge(nums1: &mut Vec<i32>, m: i32, nums2: &mut Vec<i32>, n: i32) {
        let mut p1 = m - 1;
        let mut p2 = n - 1;
        let mut p = m + n - 1;
        
        while p1 >= 0 && p2 >= 0 {
            if p1 >= 0 && nums1[p1 as usize] > nums2[p2 as usize] {
                nums1[p as usize] = nums1[p1 as usize];
                p1 -= 1;
            } else {
                nums1[p as usize] = nums2[p2 as usize];
                p2 -= 1;
            }
            p -= 1;
        }
        
        while p2 >= 0 {
            nums1[p as usize] = nums2[p2 as usize];
            p2 -= 1;
            p -= 1;
        }
    }
}

方法二:辅助数组法(更直观但不是最优)

思路 :

如果你觉得从后往前太抽象,可以先用一个临时数组把 nums1 的前 m 个元素复制出来,然后用双指针从前往后合并。这种方法思路清晰,但需要额外 O(m) 的空间。

复杂度分析:

  • 时间复杂度: O(m + n)
  • 空间复杂度: O(m)

代码实现:

Python 实现:

python 复制代码
class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        temp = nums1[:m]
        p1, p2, p = 0, 0, 0
        
        while p1 < m and p2 < n:
            if temp[p1] <= nums2[p2]:
                nums1[p] = temp[p1]
                p1 += 1
            else:
                nums1[p] = nums2[p2]
                p2 += 1
            p += 1
        
        while p1 < m:
            nums1[p] = temp[p1]
            p1 += 1
            p += 1
        
        while p2 < n:
            nums1[p] = nums2[p2]
            p2 += 1
            p += 1

总结与收获

知识点

  1. 原地操作的艺术:这道题教会我们,有时候换一种顺序思考问题,就能从"需要额外空间"变成"原地操作"。逆向思维在这里发挥了关键作用。
  2. 双指针技巧:双指针不仅仅是前后遍历,还可以用于从后往前的逆向遍历,这是解决很多数组问题的利器。
  3. 边界条件处理:当其中一个数组已经遍历完时,需要正确处理剩余元素的复制。

易错点

  1. 指针初始化错误p1 应该初始化为 m-1 而不是 mp 应该初始化为 m+n-1
  2. 忘记处理剩余元素 :当 nums2 还有剩余元素时,一定要复制到 nums1 前面。
  3. 循环条件错误 :第一个循环需要同时满足 p1 >= 0p2 >= 0

优化思路

这道题的最优解已经达到了 O(m+n) 的时间复杂度和 O(1) 的空间复杂度,很难在复杂度上进一步优化。唯一可以做的可能是:

  • 使用更底层的操作来减少赋值次数(但收益不大)
  • 在某些特定场景下,如果 nums2 的大部分元素都已经大于 nums1 的元素,可以提前终止(但这种情况较少)

相似题目

相关推荐
彩妙不是菜喵9 小时前
C++:类与对象
开发语言·c++
董世昌419 小时前
添加、删除、替换、插入元素的全方法指南
java·开发语言·前端
超人小子9 小时前
自动化报表系统实战:用Python让报表工作智能化
运维·python·自动化
dagouaofei9 小时前
AI 生成 2026 年工作计划 PPT,内容质量差异在哪里
人工智能·python·powerpoint
ai_top_trends9 小时前
2026 年工作计划汇报 PPT:AI 生成方案实测对比
人工智能·python·powerpoint
源代码•宸9 小时前
Leetcode—712. 两个字符串的最小ASCII删除和【中等】
开发语言·后端·算法·leetcode·职场和发展·golang·dp
小当家.1059 小时前
JVM八股详解(上部):核心原理与内存管理
java·jvm·学习·面试
heartbeat..9 小时前
Spring 声明式事务:原理、使用及失效场景详解
java·spring·面试·事务
无风听海9 小时前
C# 中对象相等性判断的全面解析
开发语言·c#