深入解析Java中的四数之和问题:从基础到优化

深入解析Java中的四数之和问题:从基础到优化

在算法面试中,四数之和(4Sum)问题是非常经典的题目之一。它是“三数之和”(3Sum)问题的延伸,要求找到四个数字,使它们的和等于给定的目标值。本篇文章将带你详细探讨四数之和问题,从最基础的解法到优化后的高级解法,并提供具体的代码实现和优化策略。

问题描述

题目: 给定一个包含 n 个整数的数组 nums 和一个目标值 target,找出数组中使得 a + b + c + d = target 的所有唯一四元组(a, b, c, d)。注意:解集不能包含重复的四元组。

示例:

输入:nums = [1, 0, -1, 0, -2, 2], target = 0
输出:
[
  [-1,  0, 0, 1],
  [-2, -1, 1, 2],
  [-2,  0, 0, 2]
]
基础解法:双指针法

最基本的解法是基于“双指针”的思想。具体步骤如下:

  1. 排序: 首先对数组进行排序,以便于后续跳过重复元素和使用双指针技巧。
  2. 固定两个数: 使用两层循环分别固定两个数,然后用双指针寻找剩下的两个数。
  3. 双指针查找: 通过两个指针分别从左右两端向中间移动,查找剩余两个数,使它们的和与目标值匹配。

代码实现:

import java.util.*;

public class Sum4 {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums); // 排序
        List<List<Integer>> res = new ArrayList<>();
        for (int i = 0; i < nums.length - 3; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue; // 跳过重复元素
            for (int j = i + 1; j < nums.length - 2; j++) {
                if (j > i + 1 && nums[j] == nums[j - 1]) continue; // 跳过重复元素
                int left = j + 1, right = nums.length - 1;
                while (left < right) {
                    long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
                    if (sum == target) {
                        res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        while (left < right && nums[left] == nums[left + 1]) left++; // 跳过重复元素
                        while (left < right && nums[right] == nums[right - 1]) right--; // 跳过重复元素
                        left++;
                        right--;
                    } else if (sum < target) {
                        left++; // 左指针右移
                    } else {
                        right--; // 右指针左移
                    }
                }
            }
        }
        return res;
    }
}
解法分析

这个解法的时间复杂度为 O(n^3),其中 n 是数组的长度。虽然这种方法在一些情况下表现良好,但它并没有利用一些可能的优化技术来减少计算时间。

优化一:剪枝策略

在基础的双指针法中,我们可以利用“剪枝”技术来减少不必要的计算量,进一步提升效率。

剪枝策略:
  1. 提前终止循环: 如果当前元素和后面的最小三个元素之和已经大于目标值,那么后续的循环没有继续的必要,可以直接跳出循环。
  2. 提前跳过循环: 如果当前元素和数组中最大的三个元素之和仍然小于目标值,说明即使选择这些元素也无法达到目标值,因此可以跳过这些元素。

优化后的代码实现:

public List<List<Integer>> fourSum1(int[] nums, int target) {
    Arrays.sort(nums);
    List<List<Integer>> res = new ArrayList<>();
    int n = nums.length;

    for (int i = 0; i < n - 3; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;

        // 剪枝:最小的四个数之和已经大于目标值
        if ((long)nums[i] + nums[i + 1] + nums[i + 2] + nums[ i + 3] > target) break;

        // 剪枝:当前数与最大的三个数之和仍小于目标值
        if ((long)nums[i] + nums[n - 1] + nums[n - 2] + nums[n - 3] < target) continue;

        for (int j = i + 1; j < n -2; j++) {
            if (j > i + 1 && nums[j] == nums[j - 1]) continue;

            // 剪枝:最小的三个数与当前数之和已经大于目标值
            if ((long)nums[i] + nums[j] + nums[j+1] + nums[j+2] > target) break;

            // 剪枝:当前数与最大的两个数之和仍小于目标值
            if ((long)nums[i] + nums[j] + nums[n-1] + nums[n-2] < target) continue;

            int left = j+1, right = n-1;

            while (left < right) {
                long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];

                if (sum == target) {
                    res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                    while (left < right && nums[left] == nums[left+1]) left++;
                    while (left < right && nums[right] == nums[right-1]) right--;
                    left++;
                    right--;
                } else if (sum < target){
                    left++;
                } else {
                    right--;
                }
            }
        }
    }

    return res;
}
剪枝策略分析:

通过剪枝,许多不必要的循环被跳过或提前终止,这大大减少了算法的执行时间。特别是在数组非常大时,剪枝策略显得尤为重要。

优化二:利用哈希表加速查找

除了使用双指针法,我们还可以利用哈希表来减少查找的时间复杂度。具体来说,我们可以预先计算出数组中所有可能的两个元素的和,并将它们存储在哈希表中。这样,在查找另外两个元素时,我们只需要在哈希表中查找是否存在和为目标值的两个数。

实现步骤:
  1. 构建哈希表: 遍历数组中的每一对元素,并将它们的和存储在哈希表中,键为元素和,值为元素对的索引。
  2. 查找匹配: 再次遍历数组,查找是否存在另外两个元素的和与目标值匹配,若匹配则添加到结果集中。

代码实现:

public List<List<Integer>> fourSum2(int[] nums, int target) {
    Arrays.sort(nums);
    List<List<Integer>> res = new ArrayList<>();
    Set<List<Integer>> resSet = new HashSet<>();
    Map<Long, List<int[]>> twoSumMap = new HashMap<>();
    int n = nums.length;

    // 构建哈希表,存储所有两个元素的和及其索引
    for (int i = 0; i < n - 1; i++) {
        for (int j = i + 1; j < n; j++) {
            long sum = (long)nums[i] + nums[j];
            if (!twoSumMap.containsKey(sum)) {
                twoSumMap.put(sum, new ArrayList<>());
            }
            twoSumMap.get(sum).add(new int[]{i, j});
        }
    }

    // 查找匹配的四元组
    for (int i = 0;  i < n-3; i++) {
        if (i > 0 && nums[i] == nums[i-1]) continue;
        for (int j = i+1; j < n-2; j++) {
            if (j > i+1 && nums[j] == nums[j-1]) continue;

            long remain = (long) target - nums[i] - nums[j];

            if (twoSumMap.containsKey(remain)) {
                for (int[] pair :  twoSumMap.get(remain)) {
                    int k = pair[0], l = pair[1];

                    if (k > j) {
                        List<Integer> resList = Arrays.asList(nums[i], nums[j], nums[k], nums[l]);

                        if (resSet.add(resList)) {
                            res.add(resList);
                        }
                    }
                }
            }

            while (j < n - 2 && nums[j] == nums[j+1]) j++;
        }

        while (

i < n -3 && nums[i] == nums[i+1]) i++;
    }

    return res;
}
哈希表优化分析:

利用哈希表的解法在查找配对元素时效率更高,因为我们通过预先计算出的和直接查找匹配项,大大减少了搜索的时间。但这种方法在构建哈希表时需要额外的空间,因此在空间复杂度方面可能有所增加。

总结与比较

  1. 双指针法: 时间复杂度为 O(n^3),适用于小规模数组。在引入剪枝策略后,效率得到了一定的提升。
  2. 哈希表法: 虽然增加了空间复杂度,但在时间复杂度上有显著优化,适合需要快速查找的场景。

在实际应用中,可以根据问题规模和具体需求选择适合的方法。对于较小规模的数组,双指针法可能已经足够高效。而在需要处理大规模数据或追求更高效的查找时,哈希表法则是更好的选择。

希望通过这篇文章,你能更深入地理解四数之和问题的不同解法及其优化策略。如果你有任何疑问或更好的建议,欢迎在评论区留言,我们一起讨论和学习!

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

heromps

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值