当前位置: 首页 > article >正文

编程题-最接近的三数之和

题目:

给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。

返回这三个数的和。

假定每组输入只存在恰好一个解。

解法一(排序+双指针):

题目要求找到与目标值 target 最接近的三元组,这里的「最接近」即为差值的绝对值最小。我们可以考虑直接使用三重循环枚举三元组,找出与目标值最接近的作为答案,时间复杂度为 O(N^3)。然而本题的 N 最大为 1000,会超出时间限制。

我们首先枚举第一个元素 a1,对于剩下的两个元素 a2和 a3,希望它们的和最接近target−a1。对于 a2 和 a3,如果它们在原数组中枚举的范围(既包括下标的范围,也包括元素值的范围)没有任何规律可言,那么我们还是只能使用两重循环来枚举所有的可能情况。因此,我们可以考虑对整个数组进行升序排序,这样一来

  • 假设数组的长度为 n,我们先枚举 a1,它在数组中的位置为 i;

  • 为了防止重复枚举,我们在位置 [i+1,n) 的范围内枚举 a2 和 a3。

当我们知道了a2和a3可以枚举的下标范围,并且知道这一范围对应的数组元素是有序(升序)的,那么我们是否可以对枚举的过程进行优化呢?

借助双指针对枚举的过程进行优化,我们用a2和a3作为双指针,初始时,a2指向位置i+1,即左边界,a3指向位置n-1,即右边界。在每一步枚举过程中,我们采用a1+a2+a3来更新答案,并且

  • 如果 a1+a2+a3≥target,那么就将 a3 向左移动一个位置;

  • 如果 a1+a2+a3<target,那么就将 a2​ 向右移动一个位置。

这是为什么呢,我们对 a1+a2+a3≥target的情况进行详细的分析:

如果a1+a2+a3≥target,并且我们知道a2和a3这个范围是按照升序排列的,那么如果a3不变而移动a2向右,那么a1+a2+a3的值就会不断地增加,显然就不会成为最接近target的值了。因此,我们可以知道在固定a3的情况下,此时的a2就可以得到一个最接近target的值了,那么我们以后就不用再考虑a3了,就可以将a3向左移动一个位置。

同样地,a1+a2+a3<target 时,如果a2不变而a3向左移动,那么a1+a2+a3的值就会不断地减小,显然就不会成为最接近target的值了。因此,我们可以知道固定了a2的情况下,此时的a3就可以得到一个最接近target的值,那么我们以后就不用再考虑a2了,就可以将a2向右移动一个位置。

实际上,a2和a3就表示我们当前选择的数的范围,而每一次枚举的过程中,我们尝试边界上的两个元素,根据它们与target的值的关系,选择【抛弃】左边界的元素还是右边界的元素,从而减少了枚举的范围。这种思路与【盛最多水的容器】中的双指针解法也是类似的。当我们枚举,a1,a2,a3 中任意元素并移动指针时,可以直接将其移动到下一个与这次枚举得到的不相同的元素,减少枚举的次数,如下代码为:

class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        int best = 1e7;

        // 根据差值的绝对值来更新答案
        auto update = [&](int cur) {
            if (abs(cur - target) < abs(best - target)) {
                best = cur;
            }
        };

        // 枚举 a
        for (int i = 0; i < n; ++i) {
            // 保证和上一次枚举的元素不相等
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            // 使用双指针枚举 b 和 c
            int j = i + 1, k = n - 1;
            while (j < k) {
                int sum = nums[i] + nums[j] + nums[k];
                // 如果和为 target 直接返回答案
                if (sum == target) {
                    return target;
                }
                update(sum);
                if (sum > target) {
                    // 如果和大于 target,移动 c 对应的指针
                    int k0 = k - 1;
                    // 移动到下一个不相等的元素
                    while (j < k0 && nums[k0] == nums[k]) {
                        --k0;
                    }
                    k = k0;
                } else {
                    // 如果和小于 target,移动 b 对应的指针
                    int j0 = j + 1;
                    // 移动到下一个不相等的元素
                    while (j0 < k && nums[j0] == nums[j]) {
                        ++j0;
                    }
                    j = j0;
                }
            }
        }
        return best;
    }
};

时间复杂度:O(N2),其中 N 是数组 nums 的长度。我们首先需要 O(NlogN) 的时间对数组进行排序,随后在枚举的过程中,使用一重循环 O(N) 枚举 a,双指针 O(N) 枚举 b 和 c,故一共是 O(N2)。

空间复杂度:O(logN)。排序需要使用 O(logN) 的空间。然而我们修改了输入的数组 nums,在实际情况下不一定允许,因此也可以看成使用了一个额外的数组存储了 nums 的副本并进行排序,此时空间复杂度为 O(N)。

下面代码是笔者在编程时所写的,虽然时间复杂度没有超限,但是相比上面代码在时间复杂度上面仍然是消耗时间比较大的,但是空间复杂度上面比上面代码占用消耗是较小的。其中第二层循环中思路也是:如果和小于target,则移动a2向右移动,进入下一次循环;如果和大于target,则移动a3向左移动,执行while循环,实现原理通过增加条件判断语句使得双指针(左边界指针、右边界指针)两个指针朝着相遇的方向进行移动(减少时间复杂度,防止重复遍历),但是阅读理解代码起来较为复杂,同样是作为正确的解决思路与上面方法进行对比,如下为笔者代码:

class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        //定义输出结果result值
        int min_value = 1000000, length=nums.size();
        int result=0;
        //将nums数组由小到大重新进行排序
        sort(nums.begin(), nums.end());
        //循环遍历查找满足条件要求的结果
        for(int a1=0; a1<length-2; a1++){
            if(a1>1 && nums[a1]==nums[a1-1]){
                continue;
            }
            for(int a2=a1+1; a2<length-1;a2++){
                if(a2>a1+1 && nums[a2]==nums[a2-1]){
                    continue;
                }
                int a3 = length-1;
                //如果和小于target,则移动a2向右移动(进入下一层循环)
                if(nums[a1]+nums[a2]+nums[a3]<target){
                    result=min_value>abs(nums[a1]+nums[a2]+nums[a3]-target)?(nums[a1]+nums[a2]+nums[a3]):result;
                    min_value = min(abs(nums[a1]+nums[a2]+nums[a3]-target), min_value);
                    continue;
                }
                while(a2<a3){
                    if(nums[a1]+nums[a2]+nums[a3]<target){
                        result=min_value>abs(nums[a1]+nums[a2]+nums[a3]-target)?(nums[a1]+nums[a2]+nums[a3]):result;
                        min_value = min(abs(nums[a1]+nums[a2]+nums[a3]-target), min_value);
                        result=min_value>abs(nums[a1]+nums[a2]+nums[a3+1]-target)?(nums[a1]+nums[a2]+nums[a3+1]):result;
                        min_value = min(abs(nums[a1]+nums[a2]+nums[a3+1]-target), min_value);
                        break;
                    }
                    //如果和大于target,则移动a3向左移动(执行while循环)
                    else{
                        result=min_value>abs(nums[a1]+nums[a2]+nums[a3]-target)?(nums[a1]+nums[a2]+nums[a3]):result;
                        min_value = min(abs(nums[a1]+nums[a2]+nums[a3]-target), min_value);
                        a3--;
                    }
                }
            }
        }
        return result;
    }
};

笔者小记:

1、借助双指针对枚举的过程进行优化,降低多重循环导致的时间复杂度。对于本题,时间复杂度可由O(N^3)降低至O(N^2)。


http://www.kler.cn/a/528770.html

相关文章:

  • 844.比较含退格的字符串
  • leetcode——验证二叉搜索树(java)
  • 【深度分析】微软全球裁员计划不影响印度地区,将继续增加当地就业机会
  • FIDL:Flutter与原生通讯的新姿势,不局限于基础数据类型
  • Mysql的主从复制及扩展功能
  • 观察者模式和订阅发布模式的关系
  • Http协议详解以及GET和POST请求
  • 吴恩达深度学习——超参数调试
  • WSL2 Ubuntu20.04 无法联网,解决方案
  • 一个缓冲区重叠漏洞分析与利用
  • 杨波 简单逻辑学:理性思考、清晰表达并解决问题 - 读书笔记
  • Vue.js 新的生命周期钩子:`onMounted`, `onUpdated` 等
  • 5.4.2 结构化设计方法+结构化程序设计方法
  • 基于Python的药物相互作用预测模型AI构建与优化(上.文字部分)
  • 自适应细粒度通道注意力机制FCA详解及代码复现
  • 使用C#开发一款通用数据库管理工具
  • 攻防世界_simple_php
  • c++ linux recv的概念和使用案例(服务端和客户端都有)
  • 【数据结构篇】时间复杂度
  • 读书笔记-《你的灯亮着吗?》
  • 嵌入式硬件篇---CPUGPUTPU
  • taskset -c 1-60
  • 5. 【Vue实战--孢子记账--Web 版开发】-- 主页UI
  • Python字典详解:从入门到实践
  • P3199 【[HNOI2009]最小圈】
  • 【自学笔记】Web前端的重点知识点-持续更新