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
进阶
你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?
题解
方法一:合并后排序(最简洁写法)
思路
这是最直观、代码量最少的解法。既然 nums1 已经预留了足够的空间,我们可以直接将 nums2 的所有元素复制到 nums1 的尾部(覆盖掉那些 0),然后直接调用系统自带的排序函数对整个 nums1 进行排序。
复杂度分析
- 时间复杂度: O ( ( m + n ) log ( m + n ) ) O((m+n)\log(m+n)) O((m+n)log(m+n))
- 复制数组耗时 O ( n ) O(n) O(n)。
- 排序耗时 O ( ( m + n ) log ( m + n ) ) O((m+n)\log(m+n)) O((m+n)log(m+n))。
- 总时间复杂度由排序主导。
- 空间复杂度: O ( log ( m + n ) ) O(\log(m+n)) O(log(m+n))
- 这是排序算法(如快速排序)递归调用栈所需的额外空间。
代码
java
import java.util.Arrays;
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// 1. 将 nums2 的元素复制到 nums1 的尾部
// 从 nums2 的索引 0 开始,复制 n 个元素到 nums1 的索引 m 处
System.arraycopy(nums2, 0, nums1, m, n);
// 2. 对整个 nums1 数组进行排序
Arrays.sort(nums1);
}
}
方法二:正向双指针(额外空间 O(m+n))
思路
由于两个数组已经是非递减顺序排列,我们可以使用双指针分别遍历两个数组的有效部分。每次比较两个指针指向的元素,将较小的元素放入临时数组中,直到其中一个数组遍历完毕,再将另一个数组的剩余元素追加到临时数组末尾。最后将临时数组的元素复制回 nums1。
这种方法类似于归并排序中的合并操作。
复杂度分析
- 时间复杂度: O(m + n),每个元素最多被访问一次。
- 空间复杂度: O(m + n),需要额外的 ArrayList 存储合并结果。
代码
java
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// 合并后的总长度
int len = m + n;
// 用于存储合并结果的临时列表
ArrayList<Integer> nums = new ArrayList<>();
// s1 指向 nums1 有效部分的起始位置
// s2 指向 nums2 的起始位置
int s1 = 0, s2 = 0;
// 当两个数组都还有元素未比较时,比较并放入较小的元素
while (s1 < m && s2 < n) {
if (nums1[s1] <= nums2[s2]) {
// nums1 当前元素较小或相等,将其加入结果
nums.add(nums1[s1]);
s1++;
} else {
// nums2 当前元素较小,将其加入结果
nums.add(nums2[s2]);
s2++;
}
}
// 如果 nums1 还有剩余元素,全部追加到结果末尾
while (s1 < m) {
nums.add(nums1[s1]);
s1++;
}
// 如果 nums2 还有剩余元素,全部追加到结果末尾
while (s2 < n) {
nums.add(nums2[s2]);
s2++;
}
// 将合并结果复制回 nums1
for (int i = 0; i < len; i++) {
int number = nums.get(i);
nums1[i] = number;
}
}
}
图解示例
以 nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 为例:
初始状态:s1=0, s2=0
nums1(有效部分): [1, 2, 3]
↑
s1
nums2: [2, 5, 6]
↑
s2
临时数组 nums: []
第1步:比较 nums1[s1]=1 和 nums2[s2]=2,1 更小,加入临时数组,s1++
nums1(有效部分): [1, 2, 3]
↑
s1
nums2: [2, 5, 6]
↑
s2
临时数组 nums: [1]
第2步:比较 nums1[s1]=2 和 nums2[s2]=2,相等,取 nums1,s1++
nums1(有效部分): [1, 2, 3]
↑
s1
nums2: [2, 5, 6]
↑
s2
临时数组 nums: [1, 2]
第3步:比较 nums1[s1]=3 和 nums2[s2]=2,2 更小,加入临时数组,s2++
nums1(有效部分): [1, 2, 3]
↑
s1
nums2: [2, 5, 6]
↑
s2
临时数组 nums: [1, 2, 2]
第4步:比较 nums1[s1]=3 和 nums2[s2]=5,3 更小,加入临时数组,s1++
nums1(有效部分): [1, 2, 3]
↑
s1 (s1=m,nums1 遍历完)
nums2: [2, 5, 6]
↑
s2
临时数组 nums: [1, 2, 2, 3]
第5步:nums1 已遍历完,将 nums2 剩余元素全部追加
nums2 剩余:[5, 6]
临时数组 nums: [1, 2, 2, 3, 5, 6]
第6步:将临时数组复制回 nums1
nums1: [1, 2, 2, 3, 5, 6]
方法二:逆向双指针(最优解,空间 O(1))
思路
方法一需要从前往后合并,因此需要额外的空间来存储结果。但题目中 nums1 的末尾已经预留了 n 个空位,我们可以利用这一点,从后往前进行合并。
设置三个指针:
s1指向nums1有效元素的最后一个位置(索引m-1)s2指向nums2的最后一个位置(索引n-1)tail指向nums1的最后一个位置(索引m+n-1)
每次比较 nums1[s1] 和 nums2[s2],将较大的元素放到 nums1[tail],然后相应指针前移。由于是从后往前填充,不会覆盖 nums1 中还未比较的元素。
当 nums2 的元素全部放置完毕后,合并完成;若 nums1 先遍历完,则只需将 nums2 剩余元素依次放入即可。
复杂度分析
- 时间复杂度: O(m + n),每个元素最多被访问一次。
- 空间复杂度: O(1),原地修改,无需额外空间。
代码
java
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// s1 指向 nums1 有效元素的末尾
int s1 = m - 1;
// s2 指向 nums2 的末尾
int s2 = n - 1;
// tail 指向 nums1 的最终末尾位置
int tail = n + m - 1;
// 从后往前比较,将较大值放到 tail 位置
while (s1 >= 0 && s2 >= 0) {
if (nums1[s1] >= nums2[s2]) {
// nums1 当前元素较大,放到 tail 位置
nums1[tail] = nums1[s1];
s1--;
} else {
// nums2 当前元素较大,放到 tail 位置
nums1[tail] = nums2[s2];
s2--;
}
// tail 指针前移
tail--;
}
// 如果 nums2 还有剩余元素,继续放入 nums1
// 注意:如果 nums1 有剩余,说明它们已经在正确位置,无需处理
while (s2 >= 0) {
nums1[tail] = nums2[s2];
s2--;
tail--;
}
}
}
图解示例
以 nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 为例:
初始状态:s1=2, s2=2, tail=5
nums1: [1, 2, 3, 0, 0, 0]
↑ ↑
s1 tail
nums2: [2, 5, 6]
↑
s2
第1步:比较 nums1[s1]=3 和 nums2[s2]=6,6 更大,放 6 到 tail,s2--, tail--
nums1: [1, 2, 3, 0, 0, 6]
↑ ↑
s1 tail
nums2: [2, 5, 6]
↑
s2
第2步:比较 nums1[s1]=3 和 nums2[s2]=5,5 更大,放 5 到 tail,s2--, tail--
nums1: [1, 2, 3, 0, 5, 6]
↑ ↑
s1 tail
nums2: [2, 5, 6]
↑
s2
第3步:比较 nums1[s1]=3 和 nums2[s2]=2,3 更大,放 3 到 tail,s1--, tail--
nums1: [1, 2, 3, 3, 5, 6]
↑ ↑
s1 tail
nums2: [2, 5, 6]
↑
s2
第4步:比较 nums1[s1]=2 和 nums2[s2]=2,相等,取 nums1,s1--, tail--
nums1: [1, 2, 2, 3, 5, 6]
↑↑
s1 tail
nums2: [2, 5, 6]
↑
s2
第5步:s1=-1 < 0,nums1 遍历完,将 nums2 剩余元素全部放入
nums2 剩余:[2]
nums1: [1, 2, 2, 3, 5, 6]
↑
tail
nums2: [2, 5, 6]
↑
s2
放入 nums2[0]=2:
nums1: [1, 2, 2, 3, 5, 6]
最终结果:[1, 2, 2, 3, 5, 6]
方法对比总结
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 | 评价 |
|---|---|---|---|---|
| 方法一:合并后排序 | 暴力拼接后调用库函数 | O ( ( m + n ) log ( m + n ) ) O((m+n)\log(m+n)) O((m+n)log(m+n)) | O ( log ( m + n ) ) O(\log(m+n)) O(log(m+n)) | 代码最短,适合快速实现,但效率最低。 |
| 方法二:正向双指针 | 归并排序思想,使用额外数组 | O ( m + n ) O(m+n) O(m+n) | O ( m + n ) O(m+n) O(m+n) | 逻辑清晰,但未做到原地修改。 |
| 方法二:逆向双指针 | 利用尾部空间,从后往前填 | O ( m + n ) O(m+n) O(m+n) | O ( 1 ) O(1) O(1) | 最优解,面试标准答案。 |