LeetCode16. 最接近的三数之和

16. 最接近的三数之和
给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。
返回这三个数的和。
假定每组输入只存在恰好一个解。

方法一:
看到这道题目想到的解题思路是:
三轮循环, 把第二轮循环和第三轮循环通过另外的自定义方法实现, 在threeSumClosest中的第一轮循环中调用。
首先是, 如果数组的长度<=3, 那么直接return;
然后, 进行三大轮循环, 比较差值, 得出最小差值, 并通过map返回最小差值(key)对应的三数之和(value)。
class Solution {
    //  静态属性Map; k为差值, v为三数之和
    static Map<Integer, Integer> map = new HashMap<>();

    public int threeSumClosest(int[] nums, int target) {
        int length = nums.length;

        //  给出的数组长度<=3时, 直接返回。
        if (length <= 3) {
            int sum = 0;
            for (int i = 0; i < length; i++) {
                sum += nums[i];
            }
            return sum;
        }

        int sum = 0;    // 三数之和的初始值
        int firstSum = nums[0] + nums[1] + nums[2];
        int minNum = Math.abs(target - firstSum);   // 最小差值的初始值
        for (int i = 0; i < length - 2; i++) {
            minNum = Math.min(minNum, getMinNum(nums, i, firstSum, target));
            //  如果数组中有三个数的和正好是target, 直接返回。
            if (minNum == 0) {
                break;
            }
        }
        sum = map.get(minNum);

//        map.forEach((k, v) -> System.out.println(k + ":" + v));   辅助debug的方法

        return sum;
    }

    public int getMinNum(int[] nums, int fromIndex, int firstNum, int target) {
        int sum = firstNum;     // 三数之和的初始值
        int minNum = Math.abs(firstNum - target);   // 最小差值的初始值

        for (int i = fromIndex + 1; i < nums.length - 1; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                sum = Integer.sum(nums[i] + nums[j], nums[fromIndex]);
                minNum = Math.min(minNum, Math.abs(sum - target));

//                System.out.println(nums[fromIndex] + " " + nums[i] + " " + nums[j] + " " + sum);      辅助debug的方法

                map.put(Math.abs(sum - target), sum);
                if (minNum == 0) {
                    break;
                }
            }
        }

        return minNum;
    }
}
超出时间限制
总结缺点:
暴力解题, 太暴力了! 因为无序, 循环起来毫无章法, 想到可以先给数组sort排序。   -> 方法二

方法二:
思路:
先排序,过滤掉出现超过3次的;
每一轮数字的循环都要跳过相同的数字;
确定第一个数字, 再确定第二个数字, 寻找和绝对值最近的数字。

因为暴力解决超出时间限制, 所以想到先对数组进行sort排序;
但是在解题过程中没有想到实施性强的过滤三次的方法, 所以在每一大轮循环中可以过滤掉和上一个角标数值相同的角标;
确定第一个数字, 再确定第二个数字, 得出前两个数字的和, 用target-(num1+num2), 得出idealThirdNum, 也就是理想状态下第三个数的数值;
在第三个数字的循环中, 找最邻近idealThirdNum的数字:
此时是先定下来前两个数字, 用单指针一个个查找最接近idealThirdNum的值;
因为数组是已经排好序的, 如果能指向的最小的数大于差值, 那么后面的数就更不用比了;
如果在循环中遇到了第一个等于或者大于idealThirdNum的数值, 同理, 后面的数也不用比了;
如果在循环中能指向的所有数值都小于idealThirdNum, 也就是没能进入if()体,
根据isFlag标识, 在该次循环结束后将最后一个角标的值作为第三个数, 计算差值和三数之和, 存入map。
第三个数字的循环小结: 在前两个数字确定的情况下, 只往map中存入最接近的那组数据,
即 存入的要么是根据 1.首次遇到的等于或者大于idealThirdNum 所得出的k-v的,
要么就是根据 2.排序后数组中最后一个数(数组中的最大值)作为第三个数 所得出的k-v的。
class Solution {
    static Map<Integer, Integer> map = new HashMap<>();

    public int threeSumClosest(int[] nums, int target) {
        int length = nums.length;

        if (length <= 3) {
            int sum = 0;
            for (int i = 0; i < length; i++) {
                sum += nums[i];
            }
            return sum;
        }

        int minNum = Math.abs(nums[0] + nums[1] + nums[2] - target);    // 最小差值的初始值

        Arrays.sort(nums);  // 对数组进行排序

        for (int i = 0; i < nums.length - 2; i++) {
            //  过滤掉第一轮重复的数值
            if (i != 0) {
                if (nums[i] == nums[i - 1]) {
                    continue;
                }
            }

            int num = getMinNum(nums, target, i);   // 每一轮最小差值
            minNum = Math.min(minNum, num);
        }

        return map.get(minNum);
    }

    //  返回最小差值
    public int getMinNum(int[] nums, int target, int fromIndex) {

        //每轮最小值起始以每轮的第一组开始
        int sum = nums[fromIndex] + nums[fromIndex + 1] + nums[fromIndex + 2];
        int subtract = Math.abs(sum - target);//差值
        int minNum = subtract;
        map.put(subtract, sum);

        for (int i = fromIndex + 1; i < nums.length - 1; i++) {
            //  过滤掉第二轮重复的数值
            if (i != fromIndex + 1) {
                if (nums[i] == nums[i - 1]) {
                    continue;
                }
            }

            int twoSum = nums[fromIndex] + nums[i]; //  前两个数的和
            int idealThirdNum = target - twoSum;    // 理想中的第三个数(差值为0)

            boolean isFlag = true;  //判断第三个数组有没有等于或者大于差值的

            for (int j = i + 1; j < nums.length; j++) {
                //  过滤掉第三轮重复的数值
                if (j != i + 1) {
                    if (nums[j] == nums[j - 1]) {
                        continue;
                    }
                }

                if (nums[j] >= idealThirdNum) {
                    isFlag = false;     // 检验该轮循环是否进入过if体内

                    sum = nums[fromIndex] + nums[i] + nums[j];
                    subtract = Math.abs(target - sum);
                    map.put(subtract, sum);
                    minNum = Math.min(minNum, subtract);
                    break;
                    /* 由于数组是排好序的, 一旦遇到第三个数大于idealThirdNum理想数的,
                    就可以把当前subtract和sum存入map, 跳出最内轮的循环。
                    如果遇到等于理想数的最好, 直接return。
                     */
                }

            }

            // 如果没有等于大于理想第三个数的, 取其中最大的数字
            if (isFlag) {
                sum = nums[fromIndex] + nums[i] + nums[nums.length - 1];
                subtract = Math.abs(target - sum);
                map.put(subtract, sum);
                minNum = Math.min(minNum, subtract);
            }
        }
        return minNum;
    }
}
虽然执行通过, 但是执行用时215ms, 速度太差。
总结缺点:
1.单指针, 只是第三个数字有指针从前往后查找, 对于某一轮所指向的数值都小于idealThirdNum的情况, 效率很差。
2.此时只有第三个数字在动, 想一下如果第二个数字和第三个数字都在动, 效率会提升。如何实现?  -> 方法三

方法三: 

思路: 使用双指针
1.该题中对双指针的使用方法是: 第二个数字和第三个数字分别持有一个指针, 先指到各自的极端(最左与最右);
三数之和大了就右指针(第三个数)往左挪, 使sum减小; 三数之和小了就左指针(第二个数)往右挪, 使sum增大。
2.不需要map存储, 只需要在得到新的minNum时, 将minSum也同时修改为新的三数之和即可。

热评:
双指针: 先确定第一个数字的角标i, 左指针指到i+1, 右指针指到length-1, 计算三数之和与target的关系:
1.sum>target, 说明大了, 右指针往左挪
2.sum<target, 说明小了, 左指针往右挪
3.sum==target, 直接return
class Solution {
    public int threeSumClosest(int[] nums, int target) {
        int length = nums.length;

        if (length <= 3) {
            int sum = 0;
            for (int i = 0; i < length; i++) {
                sum += nums[i];
            }
            return sum;
        }

        Arrays.sort(nums);  // 对数组排序

        int sum = nums[0] + nums[1] + nums[2];
        int minNum = Math.abs(sum - target);
        int minSum = sum;

        for (int i = 0; i < nums.length - 2; i++) {
            if (i != 0) {
                if (nums[i] == nums[i - 1]) {
                    continue;
                }
            }

            //  左右指针初始位置
            int left = i + 1;
            int right = nums.length - 1;

            while (left < right) {
                sum = nums[i] + nums[left] + nums[right];
                if (minNum > Math.abs(sum - target)) {
                    minNum = Math.abs(sum - target);
                    minSum = sum;
                }

                if (sum == target) {
                    return sum;
                } else if (sum > target) {
                    right--;
                } else {
                    left++;
                }

            }
        }

        return minSum;
    }
}
总结:
1.先对数组进行排序; 2.学会使用双指针
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值