LeetCode 88. 合并两个有序数组(双指针详解 + Java/Python 实现)
题目链接
题目描述
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中 ,使合并后的数组同样按 非递减顺序 排列。
注意 :最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0,应忽略。nums2 的长度为 n。
示例
示例 1
text
输入: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]。
示例 2
text
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 []。
合并结果是 [1]。
示例 3
text
输入: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。但这种方法需要额外的 O(m+n) 空间。
更优的解法是利用 nums1 后面已经预留了 n 个位置的特点,从后往前 进行合并,这样可以原地完成,不需要额外空间。
方法一:新建临时数组(双路归并)
思路
这是最直观的解法,类似于归并排序中的合并步骤:
- 创建一个长度为
m + n的临时数组temp。 - 使用两个指针
i和j分别指向nums1和nums2的开头。 - 比较两个指针指向的元素,将较小的放入
temp中,并移动对应指针。 - 当某个数组遍历完后,将另一个数组剩余的元素全部复制到
temp中。 - 最后将
temp的内容复制回nums1。
Java 代码
java
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int[] temp = new int[m + n];
int i = 0, j = 0, idx = 0;
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) {
temp[idx++] = nums1[i++];
} else {
temp[idx++] = nums2[j++];
}
}
while (i < m) temp[idx++] = nums1[i++];
while (j < n) temp[idx++] = nums2[j++];
// 把临时数组全部复制回 nums1
System.arraycopy(temp, 0, nums1, 0, m + n);
}
}
Python 代码
python
class Solution(object):
def merge(self, nums1, m, nums2, n):
temp = []
p1 = p2 = 0
while p1 < m and p2 < n:
if nums1[p1] <= nums2[p2]:
temp.append(nums1[p1])
p1 += 1
else:
temp.append(nums2[p2])
p2 += 1
while p1 < m:
temp.append(nums1[p1])
p1 += 1
while p2 < n:
temp.append(nums2[p2])
p2 += 1
# nums1[:] 原地覆盖原列表(关键!不能写 nums1 = temp)
nums1[:] = temp
复杂度分析
- 时间复杂度 :
O(m + n),需要遍历两个数组的所有元素。 - 空间复杂度 :
O(m + n),需要额外的临时数组。
优缺点
- 优点:思路简单直观,容易理解。
- 缺点 :需要额外的
O(m+n)空间,不符合题目"原地合并"的最佳要求。
方法二:原地从后往前双指针(最优解法|推荐背诵)
思路
这种方法的核心是 从后往前 合并,利用 nums1 后面预留的空间:
- 使用三个指针:
p1指向nums1有效元素的末尾(m - 1)。p2指向nums2的末尾(n - 1)。tail指向nums1的最终末尾(m + n - 1)。
- 比较
nums1[p1]和nums2[p2],将较大的元素放到nums1[tail]位置。 - 移动对应的指针和
tail指针。 - 当某个数组遍历完后,如果
nums2还有剩余元素,将其全部复制到nums1前面。 - 如果
nums1还有剩余元素,不需要处理,因为它们已经在正确的位置上。
为什么从后往前不会覆盖有效数据?
因为 nums1 后面有 n 个空位(值为 0),从后往前填充时,即使覆盖了某些位置,那些位置也是已经处理过的或者原本就是空位,不会影响还未处理的有效数据。
Java 代码
java
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m - 1; // nums1有效元素末尾
int p2 = n - 1; // nums2末尾
int tail = m + n - 1; // 写入位置(nums1最终末尾)
while (p1 >= 0 && p2 >= 0) {
if (nums1[p1] > nums2[p2]) {
nums1[tail--] = nums1[p1--];
} else {
nums1[tail--] = nums2[p2--];
}
}
// 如果nums2还有剩余元素,全部向前填充
while (p2 >= 0) {
nums1[tail--] = nums2[p2--];
}
}
}
Python 代码
python
class Solution(object):
def merge(self, nums1, m, nums2, n):
p1 = m - 1
p2 = n - 1
tail = m + n - 1
while p1 >= 0 and p2 >= 0:
if nums1[p1] > nums2[p2]:
nums1[tail] = nums1[p1]
p1 -= 1
else:
nums1[tail] = nums2[p2]
p2 -= 1
tail -= 1
# 处理nums2剩下的数据
while p2 >= 0:
nums1[tail] = nums2[p2]
p2 -= 1
tail -= 1
代码说明
- 初始化三个指针 :
p1 = m - 1:指向nums1最后一个有效元素。p2 = n - 1:指向nums2最后一个元素。tail = m + n - 1:指向nums1最终末尾位置。
- 主循环 :当
p1 >= 0 && p2 >= 0时,比较两个指针指向的��素,将较大的放到tail位置。 - 处理剩余元素 :
- 如果
nums2还有剩余(p2 >= 0),说明nums2的元素都比较小,需要填充到nums1前面。 - 如果
nums1还有剩余(p1 >= 0),不需要处理,因为它们已经在正确的位置上。
- 如果
复杂度分析
- 时间复杂度 :
O(m + n),需要遍历两个数组的所有元素。 - 空间复杂度 :
O(1),只使用了常数级别的额外空间。
优点
- 原地合并:不需要额外的数组空间。
- 效率高:每个元素只被处理一次。
- 代码简洁:逻辑清晰,容易记忆。
两种方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|
| 新建临时数组 | O(m + n) | O(m + n) | 思路简单,但需要额外空间 |
| 从后往前双指针 | O(m + n) | O(1) | 原地合并,最优解法 |
常见问题解答
Q1:为什么从后往前合并不会覆盖有效数据?
因为 nums1 的总长度是 m + n,后面有 n 个空位。从后往前填充时,tail 指针始终在 p1 指针的后面(或相等),所以即使覆盖,也是覆盖已经处理过的位置或者空位,不会影响还未处理的有效数据。
Q2:为什么只需要处理 nums2 剩余的情况,不需要处理 nums1 剩余的情况?
因为 nums1 的有效元素本来就在数组的前面部分,如果 p1 还有剩余,说明这些元素已经比 nums2 的所有元素都小,它们已经在正确的位置上,不需要移动。
Q3:Python 中为什么用 nums1: = temp 而不是 nums1 = temp?
因为 nums1 = temp 只是将局部变量 nums1 指向了新的列表,不会修改原列表的内容。而 nums1[:] = temp 是切片赋值,会原地修改 nums1 的内容,这样才能正确地将结果写回原数组。
总结
这道题是双指针技巧的经典应用,重点掌握 从后往前合并 的技巧:
- 利用预留空间 :
nums1后面有n个空位,可以从后往前填充。 - 三个指针 :
p1指向nums1有效元素末尾,p2指向nums2末尾,tail指向最终写入位置。 - 比较并移动 :每次将较大的元素放到
tail位置。 - 处理剩余 :只需处理
nums2剩余的情况。
关键点回顾:
- 从后往前合并,避免覆盖有效数据
- 使用三个指针:
p1、p2、tail - 只需处理
nums2剩余的情况 - 时间复杂度
O(m+n),空间复杂度O(1)
这个技巧在很多数组合并题目中都会用到,建议熟练掌握。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注,我会持续更新更多 LeetCode 题解和算法干货!