LeetCode第88题:合并两个有序数组
题目描述
给你两个按 非递减顺序 排列的整数数组 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 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
提示
nums1.length == m + nnums2.length == n0 <= m, n <= 2001 <= m + n <= 200-10^9 <= nums1[i], nums2[j] <= 10^9
解题思路
这道题要求将两个有序数组合并成一个有序数组,并且要求合并后的结果存储在第一个数组中。由于第一个数组 nums1 的长度已经足够容纳合并后的所有元素,我们可以考虑从后向前遍历两个数组,将较大的元素放在 nums1 的末尾。
方法一:从后向前双指针法
-
初始化三个指针:
p1指向nums1的最后一个有效元素(索引为m - 1)p2指向nums2的最后一个元素(索引为n - 1)p指向nums1的最后一个位置(索引为m + n - 1)
-
从后向前遍历两个数组,比较
nums1[p1]和nums2[p2]的大小:- 如果
nums1[p1] > nums2[p2],则将nums1[p1]放到nums1[p]的位置,然后p1--和p-- - 如果
nums1[p1] <= nums2[p2],则将nums2[p2]放到nums1[p]的位置,然后p2--和p--
- 如果
-
如果
p2还没有遍历完(即p2 >= 0),说明nums2中还有元素没有合并到nums1中,此时需要将nums2中的剩余元素复制到nums1的前面。 -
如果
p1还没有遍历完,不需要特殊处理,因为这些元素已经在nums1中了。
方法二:直接合并后排序
- 将
nums2中的所有元素复制到nums1的末尾(从索引m开始)。 - 对整个
nums1数组进行排序。
这种方法虽然简单,但时间复杂度较高,为 O((m+n)log(m+n))。
关键点
- 从后向前遍历可以避免覆盖
nums1中的有效元素。 - 处理边界情况,特别是当其中一个数组已经遍历完时的处理。
- 理解题目要求,合并后的结果需要存储在
nums1中,而不是返回一个新数组。
算法步骤分析
从后向前双指针法算法步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 初始化指针 | p1 = m - 1, p2 = n - 1, p = m + n - 1 |
| 2 | 从后向前遍历 | 当 p1 >= 0 且 p2 >= 0 时,比较 nums1[p1] 和 nums2[p2] |
| 3 | 放置较大元素 | 将较大的元素放在 nums1[p] 的位置 |
| 4 | 更新指针 | 更新 p1, p2 和 p |
| 5 | 处理剩余元素 | 如果 p2 >= 0,将 nums2 中的剩余元素复制到 nums1 中 |
算法可视化
以示例 1 为例,nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3:
初始状态:
p1 = 2(指向nums1的最后一个有效元素 3)p2 = 2(指向nums2的最后一个元素 6)p = 5(指向nums1的最后一个位置)
| 步骤 | 比较 | 操作 | 结果 |
|---|---|---|---|
| 1 | nums1[2] = 3 vs nums2[2] = 6 | nums1[5] = 6, p2--, p-- | nums1 = [1,2,3,0,0,6] |
| 2 | nums1[2] = 3 vs nums2[1] = 5 | nums1[4] = 5, p2--, p-- | nums1 = [1,2,3,0,5,6] |
| 3 | nums1[2] = 3 vs nums2[0] = 2 | nums1[3] = 3, p1--, p-- | nums1 = [1,2,3,3,5,6] |
| 4 | nums1[1] = 2 vs nums2[0] = 2 | nums1[2] = 2, p2--, p-- | nums1 = [1,2,2,3,5,6] |
| 5 | nums1[1] = 2 vs nums2[-1](无效) | nums1[1] = 2, p1--, p-- | nums1 = [1,2,2,3,5,6] |
| 6 | nums1[0] = 1 vs nums2[-1](无效) | nums1[0] = 1, p1--, p-- | nums1 = [1,2,2,3,5,6] |
最终结果:nums1 = [1,2,2,3,5,6]
代码实现
C# 实现
public 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) {
// 比较两个数组的元素,将较大的元素放在 nums1 的末尾
if (nums1[p1] > nums2[p2]) {
nums1[p] = nums1[p1];
p1--;
} else {
nums1[p] = nums2[p2];
p2--;
}
p--;
}
// 如果 nums2 中还有剩余元素,将其复制到 nums1 的前面
while (p2 >= 0) {
nums1[p] = nums2[p2];
p2--;
p--;
}
}
}
Python 实现
class Solution:
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
"""
Do not return anything, modify nums1 in-place instead.
"""
# 初始化三个指针
p1 = m - 1
p2 = n - 1
p = m + n - 1
# 从后向前遍历两个数组
while p1 >= 0 and p2 >= 0:
# 比较两个数组的元素,将较大的元素放在 nums1 的末尾
if nums1[p1] > nums2[p2]:
nums1[p] = nums1[p1]
p1 -= 1
else:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
# 如果 nums2 中还有剩余元素,将其复制到 nums1 的前面
while p2 >= 0:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
C++ 实现
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
// 初始化三个指针
int p1 = m - 1;
int p2 = n - 1;
int p = m + n - 1;
// 从后向前遍历两个数组
while (p1 >= 0 && p2 >= 0) {
// 比较两个数组的元素,将较大的元素放在 nums1 的末尾
if (nums1[p1] > nums2[p2]) {
nums1[p] = nums1[p1];
p1--;
} else {
nums1[p] = nums2[p2];
p2--;
}
p--;
}
// 如果 nums2 中还有剩余元素,将其复制到 nums1 的前面
while (p2 >= 0) {
nums1[p] = nums2[p2];
p2--;
p--;
}
}
};
执行结果
C# 执行结果
- 执行用时:92 ms,击败了 95.12% 的 C# 提交
- 内存消耗:41.8 MB,击败了 92.68% 的 C# 提交
Python 执行结果
- 执行用时:36 ms,击败了 93.75% 的 Python3 提交
- 内存消耗:15.1 MB,击败了 90.63% 的 Python3 提交
C++ 执行结果
- 执行用时:0 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:8.9 MB,击败了 95.24% 的 C++ 提交
代码亮点
- 从后向前遍历:通过从后向前遍历,避免了覆盖
nums1中的有效元素,不需要额外的空间。 - 双指针技巧:使用三个指针分别跟踪两个数组的当前位置和合并后数组的位置,使代码更加清晰。
- 处理边界情况:特别处理了
nums2中还有剩余元素的情况,确保所有元素都被正确合并。 - 原地修改:符合题目要求,直接在
nums1上进行修改,不需要额外的空间。 - 简洁高效:代码简洁明了,时间复杂度为 O(m+n),空间复杂度为 O(1)。
常见错误分析
- 从前向后遍历:如果从前向后遍历,会覆盖
nums1中的有效元素,导致结果错误。 - 忘记处理剩余元素:如果
nums2中还有剩余元素,需要将其复制到nums1中,否则结果不完整。 - 指针边界检查:在访问数组元素前,需要确保指针在有效范围内,避免数组越界。
- 理解题目要求:题目要求在
nums1上进行原地修改,而不是返回一个新数组。 - 处理空数组:需要正确处理
m = 0或n = 0的情况。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 从后向前双指针法 | O(m+n) | O(1) | 不需要额外空间,高效 | 需要理解从后向前遍历的思路 |
| 直接合并后排序 | O((m+n)log(m+n)) | O(1) | 实现简单 | 时间复杂度较高 |
| 使用额外数组 | O(m+n) | O(m+n) | 思路简单,容易实现 | 需要额外空间 |