题目信息
- 题目编号: 88
- 题目名称: 合并两个有序数组
- 标签: 数组、双指针、排序
- 难度: 简单
- 题目链接: https://leetcode.cn/problems/merge-sorted-array/
题目描述
给你两个按非递减顺序排列的整数数组 nums1 和 nums2,以及两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素个数。
请你将 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] ✓ 完成!
算法步骤:
- 初始化三个指针:
p1指向nums1有效元素的末尾(即m-1),p2指向nums2的末尾(即n-1),p指向nums1的末尾(即m+n-1) - 当
p1 >= 0且p2 >= 0时循环:- 如果
nums1[p1] > nums2[p2],将nums1[p1]放到p位置,p1--,p-- - 否则,将
nums2[p2]放到p位置,p2--,p--
- 如果
- 如果循环结束后
p2 >= 0,说明nums2还有剩余元素,将它们全部复制到nums1前面 - 此时
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
总结与收获
知识点
- 原地操作的艺术:这道题教会我们,有时候换一种顺序思考问题,就能从"需要额外空间"变成"原地操作"。逆向思维在这里发挥了关键作用。
- 双指针技巧:双指针不仅仅是前后遍历,还可以用于从后往前的逆向遍历,这是解决很多数组问题的利器。
- 边界条件处理:当其中一个数组已经遍历完时,需要正确处理剩余元素的复制。
易错点
- 指针初始化错误 :
p1应该初始化为m-1而不是m,p应该初始化为m+n-1。 - 忘记处理剩余元素 :当
nums2还有剩余元素时,一定要复制到nums1前面。 - 循环条件错误 :第一个循环需要同时满足
p1 >= 0和p2 >= 0。
优化思路
这道题的最优解已经达到了 O(m+n) 的时间复杂度和 O(1) 的空间复杂度,很难在复杂度上进一步优化。唯一可以做的可能是:
- 使用更底层的操作来减少赋值次数(但收益不大)
- 在某些特定场景下,如果
nums2的大部分元素都已经大于nums1的元素,可以提前终止(但这种情况较少)