每日算法-两数之和
双指针法高效解决有序数组两数之和问题
引言
在算法面试中,数组操作类问题一直是热门考点。今天我们来探讨一个经典问题:如何在两个有序数组中找到所有和为特定值的元素组合,要求算法复杂度尽可能优化。这个问题不仅考察对数组特性的理解,还能很好地体现算法设计的精妙之处。
问题描述
给定两个非递减排序的整数数组 nums1
和 nums2
,以及一个目标和 m
。请找出所有满足 a + b = m
的元素组合,其中 a
来自 nums1
,b
来自 nums2
。要求算法的时间复杂度尽可能低,最好能做到线性时间复杂度。
问题分析
暴力解法思路
最直观的思路是采用双层循环:遍历 nums1
中的每个元素 a
,然后遍历 nums2
查找是否存在 b = m - a
。这种方法的时间复杂度为 O(n×m),其中 n 和 m 分别是两个数组的长度。当数组规模较大时,这种解法效率极低,显然不符合优化要求。
优化思路
由于数组是有序的,我们可以利用这个特性来优化查找过程。有序数组最常用的优化手段包括二分查找和双指针法。对于这个问题,双指针法是更优的选择。
双指针法解题思路
双指针法的核心思想是设置两个指针,分别从两个数组的特定位置开始遍历,根据当前元素和与目标值的比较结果来移动指针,从而减少不必要的比较。
具体到这个问题,我们可以:
- 设置指针
i
从nums1
的起始位置(索引 0)开始 - 设置指针
j
从nums2
的末尾位置(索引nums2.length - 1
)开始 - 计算当前指针指向的元素和
sum = nums1[i] + nums2[j]
- 根据 sum 与目标值 m 的关系移动指针:
- 如果
sum == m
:找到一组解,记录下来,同时移动两个指针(i++,j–) - 如果
sum < m
:当前和太小,需要增大,移动 nums1 的指针(i++) - 如果
sum > m
:当前和太大,需要减小,移动 nums2 的指针(j–)
- 如果
- 继续上述过程,直到任一指针超出数组范围
算法步骤详解
让我们通过一个流程图来理解算法的执行步骤:
初始化 i = 0, j = nums2.length - 1
┌─────────────┐
│ i=0, j=n-1 │
└──────┬──────┘│▼
┌─────────────────────┐
│ 计算 sum = nums1[i] + nums2[j] │
└───────────┬─────────┘│┌───────┼───────┐▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ sum < m │ │ sum = m │ │ sum > m │
└───┬────┘ └───┬────┘ └───┬────┘│ │ │▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ i++ │ │记录结果│ │ j-- │
│ │ │ i++,j--│ │ │
└───┬────┘ └───┬────┘ └───┬────┘│ │ │└──────────┼──────────┘│┌──────────┴──────────┐│ │
┌───▼────┐ ┌───▼────┐
│i越界或j越界? ──────►│继续循环 │
└───┬────┘ └────────┘│▼
┌─────────────┐
│ 返回结果集 │
└─────────────┘
Java代码实现
下面是使用双指针法解决这个问题的Java实现:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;public class TwoSumPairs {/*** 查找两个有序数组中和为目标值的所有元素对* @param nums1 第一个有序数组* @param nums2 第二个有序数组* @param target 目标和* @return 包含所有符合条件的元素对的列表*/public List<List<Integer>> findPairs(int[] nums1, int[] nums2, int target) {List<List<Integer>> result = new ArrayList<>();// 边界条件处理if (nums1 == null || nums2 == null || nums1.length == 0 || nums2.length == 0) {return result;}int i = 0; // nums1的起始指针int j = nums2.length - 1; // nums2的末尾指针while (i < nums1.length && j >= 0) {int sum = nums1[i] + nums2[j];if (sum == target) {// 找到一对解,记录下来List<Integer> pair = new ArrayList<>();pair.add(nums1[i]);pair.add(nums2[j]);result.add(pair);// 移动两个指针,继续寻找其他可能的组合i++;j--;} else if (sum < target) {// 当前和太小,需要增大,移动nums1的指针i++;} else {// 当前和太大,需要减小,移动nums2的指针j--;}}return result;}/*** 查找两个有序数组中和为目标值的所有元素对(去重版本)* @param nums1 第一个有序数组* @param nums2 第二个有序数组* @param target 目标和* @return 包含所有符合条件且不重复的元素对的列表*/public List<List<Integer>> findPairsWithDeduplication(int[] nums1, int[] nums2, int target) {List<List<Integer>> result = new ArrayList<>();if (nums1 == null || nums2 == null || nums1.length == 0 || nums2.length == 0) {return result;}int i = 0, j = nums2.length - 1;while (i < nums1.length && j >= 0) {int sum = nums1[i] + nums2[j];int currentNum1 = nums1[i];int currentNum2 = nums2[j];if (sum == target) {// 记录结果result.add(Arrays.asList(currentNum1, currentNum2));// 跳过nums1中的重复元素while (i < nums1.length && nums1[i] == currentNum1) {i++;}// 跳过nums2中的重复元素while (j >= 0 && nums2[j] == currentNum2) {j--;}} else if (sum < target) {// 跳过nums1中的重复元素while (i < nums1.length && nums1[i] == currentNum1) {i++;}} else {// 跳过nums2中的重复元素while (j >= 0 && nums2[j] == currentNum2) {j--;}}}return result;}public static void main(String[] args) {TwoSumPairs solution = new TwoSumPairs();// 测试用例1:基本情况int[] nums1 = {1, 3, 5, 7, 9};int[] nums2 = {2, 4, 6, 8, 10};int target = 10;List<List<Integer>> result1 = solution.findPairs(nums1, nums2, target);System.out.println("测试用例1结果: " + result1);// 预期输出: [[1, 9], [3, 7], [5, 5]]// 测试用例2:无符合条件的组合int[] nums3 = {1, 2, 3};int[] nums4 = {4, 5, 6};int target2 = 10;List<List<Integer>> result2 = solution.findPairs(nums3, nums4, target2);System.out.println("测试用例2结果: " + result2);// 预期输出: [] (因为3+7不存在,2+8不存在,1+9不存在)// 测试用例3:包含重复元素int[] nums5 = {1, 2, 2, 3};int[] nums6 = {3, 4, 4, 5};int target3 = 6;List<List<Integer>> result3 = solution.findPairs(nums5, nums6, target3);System.out.println("测试用例3结果(不去重): " + result3);// 预期输出: [[1,5], [2,4], [2,4], [3,3]]// 测试用例4:包含重复元素(去重版本)List<List<Integer>> result4 = solution.findPairsWithDeduplication(nums5, nums6, target3);System.out.println("测试用例4结果(去重): " + result4);// 预期输出: [[1,5], [2,4], [3,3]]}
}
代码解析
上面的代码实现了两个版本的解决方案:
-
基本版本(findPairs):
- 双指针遍历两个数组
- 找到符合条件的元素对并记录
- 不处理重复元素,适用于无重复元素的数组
-
去重版本(findPairsWithDeduplication):
- 在基本版本的基础上增加了去重逻辑
- 当遇到重复元素时,跳过相同的元素
- 适用于可能包含重复元素的数组
两个版本都遵循双指针法的核心思想,但去重版本在处理重复元素时更加健壮。
复杂度分析
- 时间复杂度:O(n + m),其中n和m分别是两个数组的长度。每个数组最多被遍历一次。
- 空间复杂度:O(1)(不考虑存储结果所需的空间)。算法本身只使用了常数级别的额外空间。
示例演示
让我们通过一个具体的例子来演示算法的执行过程:
示例:nums1 = [1, 3, 5, 7], nums2 = [2, 4, 6, 8], target = 9
执行步骤:
-
初始状态:i=0, j=3 (nums1[0]=1, nums2[3]=8)
sum = 1+8=9,等于target,记录(1,8),i=1, j=2 -
当前状态:i=1, j=2 (nums1[1]=3, nums2[2]=6)
sum = 3+6=9,等于target,记录(3,6),i=2, j=1 -
当前状态:i=2, j=1 (nums1[2]=5, nums2[1]=4)
sum = 5+4=9,等于target,记录(5,4),i=3, j=0 -
当前状态:i=3, j=0 (nums1[3]=7, nums2[0]=2)
sum = 7+2=9,等于target,记录(7,2),i=4, j=-1 -
此时i越界,循环结束
结果:[(1,8), (3,6), (5,4), (7,2)]
结果顺序说明:由于我们从nums1的开头和nums2的末尾开始遍历,结果中的元素对顺序会呈现nums1递增、nums2递减的特点。这是算法的自然结果,算法保证找到所有组合,但不保证特定的顺序。如果需要按特定顺序输出,可以在收集完所有结果后进行排序。
边界情况处理
在实际应用中,我们需要考虑各种边界情况:
- 数组为空:如果任一数组为空,直接返回空结果
- 无符合条件的组合:算法会自然结束,返回空列表
- 数组中有重复元素:可以使用去重版本的算法(findPairsWithDeduplication)
- 目标值超出可能范围:如果目标值小于两个数组的最小元素之和,或大于两个数组的最大元素之和,直接返回空结果
- 数组长度为1:算法同样适用,不会出现问题
算法优化与扩展
优化方向
- 提前终止:如果nums1[i]已经大于target(假设数组元素为正),可以提前终止循环
- 二分查找结合:对于某些特殊情况,可以结合二分查找进一步优化
- 并行处理:在大规模数据处理时,可以考虑并行化处理
扩展问题
- 三数之和:在一个数组中找到所有和为目标值的三元组
- 四数之和:类似三数之和,但需要找到四个元素的组合
- 两数之和II:设计一个函数,找出数组中和为目标值的两个元素的索引
- 最接近的三数之和:找出与目标值最接近的三个元素的和
总结
双指针法是解决有序数组问题的高效技术,通过合理设置和移动指针,可以将时间复杂度从O(n×m)优化到O(n+m)。本文详细介绍了双指针法在两个有序数组两数之和问题中的应用,包括基本思路、Java实现、复杂度分析和边界情况处理。
我们提供了两个版本的实现:基本版本和去重版本,以适应不同的应用场景。通过多个测试用例验证了算法的正确性,并对结果顺序和可能的优化方向进行了讨论。
掌握双指针技术不仅能帮助我们解决这类特定问题,更重要的是培养我们利用数据结构特性进行算法优化的思维方式。在实际开发中,这种思维方式对于提高程序性能至关重要。
希望本文能帮助你深入理解双指针法的应用,如果有任何问题或建议,欢迎在评论区留言讨论!