LeetCode-088:合并两个有序数组,从后往前填就不会覆盖还没用到的元素
本题在线练习:LeetCode 88. 合并两个有序数组 — 在线练习(免费 · 无需登录 · AI 辅助)
配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开刷 Hot 100。
一、题目概述
给你两个按非递减顺序排列的整数数组 nums1 和 nums2,以及两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素数目。
请你将 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 - 1p2:从nums2的最后一个元素开始,索引n - 1p:从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 = 3,nums2 = [2, 5, 6],n = 3 走一遍。
初始状态:p1 = 2,p2 = 2,p = 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)),逆向双指针充分利用了"两个数组都已有序"这个条件,做到了线性时间。
七、总结
这道题的核心洞察就一句话:
从后往前填,每次选最大的放到末尾,就不会覆盖还没处理的有效数据。
整个解法只有三步:
- 三个指针分别指向
nums1有效部分末尾、nums2末尾、nums1总末尾 - 每次比较两个数组当前最大的元素,选大的填到当前空位
- 如果
nums2还有剩余,补到nums1前面;nums1剩余不用管
为什么不需要处理 nums1 的剩余?因为 nums1 的剩余元素本来就在 nums1 里面,而且它们比已经填好的所有元素都小,位置天然就是对的。
这道题虽然是 Easy,但"逆向思维"的技巧非常实用。当正向操作会产生覆盖冲突时,试试反过来操作——这个思路在很多原地修改数组的题目中都会用到。

浙公网安备 33010602011771号