LeetCode 面试经典 150 题:合并两个有序数组(双指针解法详解)
在算法面试中,“合并有序数组” 是一道高频基础题,不仅考察对数组操作的熟练度,更能体现对 “双指针” 这一核心技巧的理解。本文将带大家深入剖析这道题的最优解法,从题目分析到思路推导,再到复杂度解读,帮你彻底掌握这类问题的解题逻辑。
一、题目链接与题干解读
首先,大家可以通过以下链接直接访问题目,先自行思考解题思路:
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]
解释:需要将 nums2 的 3 个元素融入 nums1,最终保持非递减顺序。
二、解题思路:双指针(从后向前遍历)
拿到这道题,很多人第一反应是 “从前往后遍历”—— 比较 nums1 和 nums2 的开头元素,把小的放进新数组。但这样有个问题:如果直接在 nums1 上操作,前面的元素可能会被覆盖;如果用额外数组存结果,又会增加空间复杂度。
而 “从后向前遍历” 的双指针法,完美解决了这两个问题:既不需要额外空间,又能避免元素覆盖。下面我们一步步拆解思路。
1. 指针初始化
我们需要三个指针,分别指向三个关键位置:
- 指针 i:指向 nums1 中 “有效元素” 的末尾(即第 m-1 个索引,因为数组从 0 开始),代表当前 nums1 中待比较的最大元素;
- 指针 j:指向 nums2 的末尾(即第 n-1 个索引),代表当前 nums2 中待比较的最大元素;
- 指针 k:指向 nums1 整体的末尾(即第 m+n-1 个索引),代表合并后数组的 “待填充位置”。
用示例 1 举例,初始时三个指针的位置如下:
- nums1 = [1,2,3,0,0,0],i = 2(指向元素 3);
- nums2 = [2,5,6],j = 2(指向元素 6);
- k = 5(指向 nums1 最后一个 0 的位置)。
2. 核心遍历逻辑
我们的目标是:每次从 nums1[i] 和 nums2[j] 中选较大的元素,放到 nums1[k] 的位置,然后对应指针向前移动,直到所有元素都填充完毕。
具体步骤分为两种情况:
情况 1:两个数组都还有未比较的元素(i >= 0 且 j >= 0)
- 比较 nums1[i] 和 nums2[j]:
-
- 如果 nums1[i] >= nums2[j]:说明 nums1[i] 是当前最大的元素,把它放到 nums1[k] 的位置;然后 i--(nums1 待比较元素向前移)、k--(待填充位置向前移);
-
- 如果 nums1[i] < nums2[j]:说明 nums2[j] 是当前最大的元素,把它放到 nums1[k] 的位置;然后 j--(nums2 待比较元素向前移)、k--。
用示例 1 走一遍这个过程:
- 第一次比较:nums1[2] = 3 vs nums2[2] = 6 → 6 更大,把 6 放到 nums1[5],此时 nums1 = [1,2,3,0,0,6],j=1,k=4;
- 第二次比较:nums1[2] = 3 vs nums2[1] = 5 → 5 更大,把 5 放到 nums1[4],此时 nums1 = [1,2,3,0,5,6],j=0,k=3;
- 第三次比较:nums1[2] = 3 vs nums2[0] = 2 → 3 更大,把 3 放到 nums1[3],此时 nums1 = [1,2,3,3,5,6],i=1,k=2;
- 第四次比较:nums1[1] = 2 vs nums2[0] = 2 → 相等,选 nums1[1] 放到 nums1[2],此时 nums1 = [1,2,2,3,5,6],i=0,k=1;
- 第五次比较:nums1[0] = 1 vs nums2[0] = 2 → 2 更大,把 2 放到 nums1[1],此时 nums1 = [1,2,2,3,5,6],j=-1,k=0。
情况 2:其中一个数组已遍历完
- 如果 j < 0(nums2 已遍历完):说明剩下的 nums1 元素都是最小的,无需再操作(因为 nums1 本身就是有序的);
- 如果 i < 0(nums1 已遍历完):说明 nums2 剩下的元素都是最小的,需要把 nums2 中 j >= 0 的元素依次放到 nums1[k] 中,直到 j < 0。
比如另一个示例:nums1 = [0],m = 0;nums2 = [1],n = 1。此时 i = -1,直接把 nums2[0] 放到 nums1[0],结果就是 [1]。
3. 循环终止条件
当 k < 0 时,说明 nums1 的所有位置都已填充完毕,循环结束。
三、复杂度分析
1. 时间复杂度:O (m + n)
- 我们只遍历了 nums1 的 m 个有效元素和 nums2 的 n 个元素,每个元素只比较和移动一次,没有嵌套循环;
- 无论哪种情况,总操作次数都是 m + n 次,因此时间复杂度是线性的。
2. 空间复杂度:O (1)
- 整个过程只用到了 i、j、k 三个指针,没有额外开辟数组或其他数据结构;
- 所有操作都在原数组 nums1 上进行,因此空间复杂度是常数级的。
四、代码实现
根据上面的思路,我们可以写出简洁的代码。核心逻辑就是围绕三个指针的移动和元素比较,以下是代码框架(你可以根据自己熟悉的编程语言填充具体实现):
1,Python3
class Solution:def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:k = m + n - 1i, j = m - 1, n - 1while j >= 0:if i >= 0 and nums1[i] > nums2[j]:nums1[k] = nums1[i]i -= 1else:nums1[k] = nums2[j]j -= 1k -= 1
2,Java
class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {for (int i = m - 1, j = n - 1, k = m + n - 1; j >= 0; --k) {nums1[k] = i >= 0 && nums1[i] > nums2[j] ? nums1[i--] : nums2[j--];}}
}
你可以将上述代码复制到 LeetCode 编辑器中,结合题目示例进行测试,验证是否正确。
五、总结与拓展
这道题的关键在于 “利用数组的有序性” 和 “从后向前遍历”—— 前者避免了无序数组的复杂排序,后者解决了元素覆盖和额外空间的问题。双指针法是数组操作中的核心技巧,类似的思路还能用于 “移除元素”“两数之和(有序数组)” 等题目。
比如思考一个拓展问题:如果两个数组是递减有序的,如何合并成一个递减有序的数组?其实逻辑完全相通,只需把 “从后向前” 改成 “从前向后”,比较时选较小的元素即可。
希望通过本文的拆解,你能不仅掌握这道题的解法,更能理解双指针法的本质 —— 通过指针移动减少重复操作,优化时间和空间复杂度。