从暴力到最优——力扣88.合并两个有序数组
力扣88.合并两个有序数组
合并两个有序数组题解(详解三种方法:直接合并、双指针正序、双指针逆序)
一、题目回顾
给定两个按 非递减顺序 排列的整数数组 nums1
和 nums2
,其中:
nums1
的前m
个元素为有效数据,后n
个元素为 0(预留空间);nums2
的长度为n
;- 要求将
nums2
合并到nums1
中,使得nums1
成为一个新的有序数组; - 函数不需要返回值,直接在原地修改
nums1
。
示例
输入:nums1 = [1,2,3,0,0,0], m = 3nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
二、题目分析
题目要求:
- 数组非递减(即升序,可有重复);
- 在原数组
nums1
中合并结果; - 空间有限,尽可能在 O(1) 额外空间内完成;
- 时间复杂度要求为 O(m + n)。
核心挑战在于:
如何避免提前覆盖掉
nums1
的有效元素。
如果我们从前往后直接合并,当 nums1
较小时可能会被覆盖。因此,需要考虑从后往前合并的策略。
三、方法一:直接合并 + 排序(简单但效率不高)
思路
- 先将
nums2
中的元素直接放入nums1
的尾部空位; - 调用排序函数(如
Arrays.sort
)对整个nums1
排序。
代码实现
import java.util.Arrays;class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {for (int i = 0; i < n; i++) {nums1[m + i] = nums2[i];}Arrays.sort(nums1);}
}
复杂度分析
- 时间复杂度:O((m + n) log(m + n))
- 空间复杂度:O(1)
优缺点
- 优点:实现简单,几行代码即可;
- 缺点:没有利用数组原有的有序性,不符合题目的“进阶要求”。
四、方法二:双指针正序合并(需要额外数组)
思路
- 创建一个新数组
sorted
; - 使用两个指针
p1
、p2
分别指向nums1
和nums2
; - 比较两个指针所指的元素,将较小的放入
sorted
; - 当某一方到达末尾,直接拷贝另一方剩余元素;
- 最后将
sorted
的内容拷回nums1
。
代码实现
class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {int[] sorted = new int[m + n];int p1 = 0, p2 = 0, p = 0;while (p1 < m && p2 < n) {if (nums1[p1] <= nums2[p2]) {sorted[p++] = nums1[p1++];} else {sorted[p++] = nums2[p2++];}}while (p1 < m) sorted[p++] = nums1[p1++];while (p2 < n) sorted[p++] = nums2[p2++];System.arraycopy(sorted, 0, nums1, 0, m + n);}
}
复杂度分析
- 时间复杂度:O(m + n)
- 空间复杂度:O(m + n)
优缺点
- 优点:清晰直观;
- 缺点:使用了额外数组,空间复杂度较高。
五、方法三:双指针逆序合并(最优解)
核心思想
从后往前合并可以避免覆盖问题:
-
设三个指针:
p1 = m - 1
指向nums1
的最后一个有效元素;p2 = n - 1
指向nums2
的最后一个元素;p = m + n - 1
指向nums1
的最后一个位置(总长度)。
-
比较
nums1[p1]
与nums2[p2]
:- 较大的放到
nums1[p]
; - 指针左移;
- 较大的放到
-
重复直到
p2
< 0; -
若
nums1
剩余部分无需处理;
若nums2
还有剩余,拷贝剩下的部分。
图示(示例)
nums1 = [1,2,3,0,0,0]
nums2 = [2,5,6]
初始:
p1=2, p2=2, p=5比较 3 vs 6 → 6 放到 nums1[5]
nums1 = [1,2,3,0,0,6]继续比较 3 vs 5 → 5 放到 nums1[4]
nums1 = [1,2,3,0,5,6]继续比较 3 vs 2 → 3 放到 nums1[3]
nums1 = [1,2,3,3,5,6]最后将 nums2 剩下的 [2] 放入。
代码实现
class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {int p1 = m - 1, p2 = n - 1, p = m + n - 1;while (p1 >= 0 && p2 >= 0) {if (nums1[p1] > nums2[p2]) {nums1[p--] = nums1[p1--];} else {nums1[p--] = nums2[p2--];}}// 如果 nums2 还有剩余,拷贝到前面while (p2 >= 0) {nums1[p--] = nums2[p2--];}}
}
复杂度分析
- 时间复杂度:O(m + n)
- 空间复杂度:O(1)
- 不需要额外空间,完全原地合并。
六、三种方法对比
方法 | 思路 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|---|
1. 直接合并排序 | 简单粗暴 | O((m+n)log(m+n)) | O(1) | 简洁 | 效率不高 |
2. 双指针正序 | 从前往后合并 | O(m+n) | O(m+n) | 逻辑直观 | 需要额外数组 |
3. 双指针逆序 | 从后往前合并 | O(m+n) | O(1) | 原地合并,最优 | 稍复杂但经典 |
七、总结
- 核心思想: 充分利用已排序数组的特性;
- 关键技巧: 从尾部开始合并,避免元素覆盖;
- 最优实现: 双指针逆序法(O(m + n) 时间,O(1) 空间)。