算法通关村—n数之和问题解析

很多人开始LeetCode的第一题就是求两数之和的问题,事实上除此之外,还有几个类似的问题,例如LeetCode15 三数之和,LeetCode18.四数相加和 LeetCode454.四数相加II等等。我们就集中看一下


1.两数之和

LeetCode1.给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。

示例1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例2:
输入:nums = [3,2,4], target = 6
输出:[1,2]

该题目最容易想到的就是使用双重for循环进行解决,第一层确定一个数i,然后下一层继续遍历,找到目标值target - i的元素为止。

 public static int[] twoSum(int[] nums, int target) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (nums[i] + nums[j] == target)
                    return new int[]{i,j};
            }
        }
        return new int[0];
    }

另外,我们还有比较节省时间复杂度的方法,使用哈希表Hash,对于查找target - x ,时间复杂度由O(n)降低为O(1)
我们创建一个哈希表,判断遍历取得的target - x是否存在于哈希表中,如果存在,则返回对应 target - x 的下标,不存在,则将该 x,及其下标存入哈希表中,再进行遍历。
代码如下:

public static int[] twoSum2(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(target - nums[i])){
                return new int[]{map.get(target - nums[i]),i};
            }
            map.put(nums[i],i);
        }
        return new int[0];
    }

2.三数之和

如果将两个数换成三个会怎样呢?LeetCode15给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。注意:答案中不可以包含重复的三元组。

示例1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

本题看似就是增加了一个数,但是难度增加了很多,我们最先想到的方法应该就是三层f循环,但是该方法的时间复杂度太高了,因此不采用。
然后,我们想想可不可以用 双指针+Hash 来实现,首先按照第一题两数之和的思路,我们可以固定一个数target,再利用两数之和的思想去map中存取或查找(-1)*target - num[j],但是这样的问题是无法消除重复结果,例如如果输入[-1,0,1,2,-1,-4],返回的结果是[[-1,1,0],[-1,-1,2],[0,1,-1],[0,-1,1],[1,-1,0],[2,-1,-1]],如果我们再增加一个去重方法,将直接导致执行超时。
所以,我们使用上一题目的方法无法解决我们,我们就应该采用其他方法,公认比较好用的解法是使用 “排序 + 双指针”,我们可以先将数组排序来处理重复结果,然后还是固定一位元素,由于数组是排好序的,所以我们用双指针来不断寻找即可求解,代码如下:

 public static List<List<Integer>> threeSum(int[] nums) {
        int n = nums.length;
        //先对数组进行排序(重要,否则后续操作无效)
        Arrays.sort(nums);
        //建立一个ArrayList来储存各种不同情况的List
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1]) {
                continue;
            }
            // c 对应的指针初始指向数组的最右端
            int third = n - 1;
            int target = -nums[first];
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1]) {
                    continue;
                }
                // 需要保证 b 的指针在 c 的指针的左侧
                while (second < third && nums[second] + nums[third] > target) {
                    --third;
                }
                // 如果指针重合,随着 b 后续的增加
                // 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
                if (second == third) {
                    break;
                }
                //当 c + b = -a 时,创建对应该三个元素的list,再将其存入ArrayList ans;
                if (nums[second] + nums[third] == target) {
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    ans.add(list);
                }
            }
        }
        return ans;
    }

3.四数之和

LeetCode18
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。

示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

对于该问题,我们可以参考上面 “排序 + 双指针”的方法进行思考。
虽然使用的方法相似,但是增加一个元素后,该问题在很多方面的考虑复杂了许多。
我们先使用双重for循环,对元素 a,b进行遍历,注意考虑枚举的数与上一次不要重复
然后使用双指针方法:c 取 b + 1, 而 d 取 n - 1.并且取 c < d 为循环条件。
c,d双指针的循环部分是该题的难点,我们讨论一下具体实现:

  1. 先存储a+b+c+d元素的和sum,方便后续分情况进行讨论。
  2. 当sum为0时,将a,b,c,d处对应元素生成对应一个数组添加到ans(总数组)中,考虑到a,b元素一定时其他c,d元素组合的情况同时也要使枚举的数与上次不同,分别对c取++,d取–操作。
  3. 对于sum<0,以及sum>0的情况,分别取c++,d–.(当sum比0小,则说明c元素的值偏小,当sum比0大,则说明d元素的值偏大)

代码如下:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author 流连
 * @version 1.0
 */
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class FourSum {
    public List<List<Integer>> fourSum(int[] nums) {
        // 对数组进行排序,方便后续双指针查找
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<>();

        int n = nums.length;
        // 四数之和,需要至少有4个元素,所以最外层循环只需到 n - 3
        for (int a = 0; a < n - 3; a++) {
            // 跳过重复的元素,确保四元组不重复
            if (a > 0 && nums[a] == nums[a - 1]) {
                continue;
            }
            // 第二层循环,选择第二个元素,类似于三数之和的思路
            for (int b = a + 1; b < n - 2; b++) {
                // 跳过重复的元素,确保四元组不重复
                if (b > a + 1 && nums[b] == nums[b - 1]) {
                    continue;
                }
                // 双指针查找剩下的两个数,即第三个和第四个元素
                int c = b + 1;
                int d = n - 1;
                while (c < d) {
                    // 计算当前四个数的和
                    int sum = nums[a] + nums[b] + nums[c] + nums[d];
                    if (sum == 0) {
                        // 如果和等于目标值,将四个数加入结果列表
                        ans.add(Arrays.asList(nums[a], nums[b], nums[c], nums[d]));

                        // 跳过重复的元素,确保四元组不重复
                        while (c < d && nums[c] == nums[c + 1]) {
                            c++;
                        }
                        while (c < d && nums[d] == nums[d - 1]) {
                            d--;
                        }

                        // 移动左指针和右指针,跳过相同的元素,继续查找下一个四元组
                        c++;
                        d--;
                    } else if (sum < 0) {
                        // 如果和小于目标值,说明需要增加和,移动左指针向右移动
                        c++;
                    } else {
                        // 如果和大于目标值,说明需要减少和,移动右指针向左移动
                        d--;
                    }
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        int[] nums = {-6, -3, -1, 0, 2, 1, 10, 5};
        List<List<Integer>> result = fourSum(nums);
        System.out.println(result);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值