LeetCode-088:合并两个有序数组,从后往前填就不会覆盖还没用到的元素

本题在线练习LeetCode 88. 合并两个有序数组 — 在线练习(免费 · 无需登录 · AI 辅助)

配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开刷 Hot 100。

一、题目概述

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

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

注意:最终结果不是返回一个新数组,而是把结果存储在 nums1 里。nums1 的长度为 m + n,其中前 m 个元素是有效数据,后 n 个元素是 0,用作占位符。

示例 1

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

输出:

[1, 2, 2, 3, 5, 6]

示例 2

nums1 = [1], m = 1
nums2 = [],  n = 0

输出:

[1]

nums2 为空,不需要合并。


示例 3

nums1 = [0], m = 0
nums2 = [1], n = 1

输出:

[1]

nums1 有效部分为空,直接把 nums2 填进去。


二、核心思路

暴力做法:先合并再排序

最直接的想法:把 nums2 的元素直接复制到 nums1 的后半段,然后对整个 nums1 排序。

nums1[m:] = nums2
nums1.sort()

这当然能过,但时间复杂度是 O((m+n) log(m+n)),没有利用"两个数组都已经有序"这个条件。


最优做法:逆向双指针,从后往前填

这道题的关键限制是原地合并——不能开新数组,结果必须放在 nums1 里。

如果从前往后填,问题是:往 nums1 的前面插入元素时,会覆盖掉 nums1 还没用到的数据。

但如果从后往前填,情况就完全不同了:

  • nums1 的后半段全是 0(占位符),是空位
  • 每次比较两个数组中最大的元素,把更大的那个放到 nums1 的最后面
  • 这样填入的位置要么是占位符的位置,要么是已经被移走的位置,不会覆盖任何有效数据

核心直觉:从后往前填,每次放最大的,空位刚好够用,永远不会踩到还没处理的有效元素。

用三个指针:

  • p1:指向 nums1 有效部分的末尾(索引 m - 1
  • p2:指向 nums2 的末尾(索引 n - 1
  • p:指向 nums1 最终数组的末尾(索引 m + n - 1),也就是当前要填入的位置

每次比较 nums1[p1]nums2[p2],把大的放到 nums1[p],然后对应的指针往前移一位。


三、代码实现

class Solution:
    def merge(self, nums1: list[int], m: int, nums2: list[int], n: int) -> None:
        p1 = m - 1
        p2 = n - 1
        p = 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

四、逐行拆解代码

1. 初始化三个指针

p1 = m - 1
p2 = n - 1
p = m + n - 1
  • p1:从 nums1 有效数据的最后一个元素开始,索引 m - 1
  • p2:从 nums2 的最后一个元素开始,索引 n - 1
  • p:从 nums1 整个数组的最后一个位置开始,索引 m + n - 1

2. 主循环:比较两个数组的尾部,选大的放到末尾

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

这是核心逻辑。只要两个数组都还有元素没处理完:

  • 如果 nums1[p1] >= nums2[p2],说明 nums1[p1] 是当前最大的,把它放到 nums1[p],然后 p1 往前移
  • 否则 nums2[p2] 更大,放到 nums1[p]p2 往前移
  • 无论哪种情况,p 都往前移一位,指向下一个要填的位置

>= 而不是 > 也可以,这里不影响正确性,因为相等时放哪个都行。


3. 收尾:如果 nums2 还有剩余

while p2 >= 0:
    nums1[p] = nums2[p2]
    p2 -= 1
    p -= 1

主循环结束后,有两种可能:

  • p1 先耗尽p1 < 0):nums2 还有剩余元素没放完,需要把它们填到 nums1 的前面
  • p2 先耗尽p2 < 0):nums2 已经全部放完了,而 nums1 剩下的元素本来就在 nums1 里,位置也不需要动

所以只需要处理 p2 还有剩余的情况。如果 p1 还有剩余,不需要额外操作——那些元素已经在 nums1 的正确位置上了。


五、手动模拟

用示例 nums1 = [1, 2, 3, 0, 0, 0]m = 3nums2 = [2, 5, 6]n = 3 走一遍。

初始状态:p1 = 2p2 = 2p = 5

步骤 比较 操作 nums1 p1 p2 p
1 nums1[2]=3 vs nums2[2]=6 6 更大,放到位置 5 [1,2,3,0,0,6] 2 1 4
2 nums1[2]=3 vs nums2[1]=5 5 更大,放到位置 4 [1,2,3,0,5,6] 2 0 3
3 nums1[2]=3 vs nums2[0]=2 3 更大,放到位置 3 [1,2,3,3,5,6] 1 0 2
4 nums1[1]=2 vs nums2[0]=2 相等,放 nums1 的 2 到位置 2 [1,2,2,3,5,6] 0 0 1
5 nums1[0]=1 vs nums2[0]=2 2 更大,放到位置 1 [1,2,2,3,5,6] 0 -1 0

p2 = -1,主循环结束。p2 < 0 所以收尾循环不执行。

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

注意第 3 步:nums1[2] 的值 3 被放到了位置 3,然后位置 2 就"空出来了"(虽然还存着旧值 3,但后续会被覆盖)。这就是从后往前填的妙处——不会丢数据。


六、复杂度分析

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

  • 每个元素最多被访问一次,三个指针都只往前走,总共移动 m + n

空间复杂度:O(1)

  • 只用了三个额外的指针变量,结果直接写在 nums1 里,没有开辟新数组

对比暴力做法的 O((m+n) log(m+n)),逆向双指针充分利用了"两个数组都已有序"这个条件,做到了线性时间。


七、总结

这道题的核心洞察就一句话:

从后往前填,每次选最大的放到末尾,就不会覆盖还没处理的有效数据。

整个解法只有三步:

  1. 三个指针分别指向 nums1 有效部分末尾、nums2 末尾、nums1 总末尾
  2. 每次比较两个数组当前最大的元素,选大的填到当前空位
  3. 如果 nums2 还有剩余,补到 nums1 前面;nums1 剩余不用管

为什么不需要处理 nums1 的剩余?因为 nums1 的剩余元素本来就在 nums1 里面,而且它们比已经填好的所有元素都小,位置天然就是对的。

这道题虽然是 Easy,但"逆向思维"的技巧非常实用。当正向操作会产生覆盖冲突时,试试反过来操作——这个思路在很多原地修改数组的题目中都会用到。

posted @ 2026-04-20 00:21  孤飞  阅读(99)  评论(0)    收藏  举报