Python·算法·每日一题(3月6日)最接近的三数之和

题目

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

示例

示例一

输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

示例二

输入:nums = [0,0,0], target = 1
输出:0

提示

  • 3 <= nums.length <= 1000
  • -1000 <= nums[i] <= 1000
  • - 1 0 4 10^4 104 <= target <= 1 0 4 10^4 104

思路及算法代码

方法:排序加双指针

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

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

  • 假设数组的长度为 n,我们先枚举 a,它在数组中的位置为 i;
  • 为了防止重复枚举,我们在位置 [i+1,n) 的范围内枚举 b 和 c。
    当我们知道了 b 和 c 可以枚举的下标范围,并且知道这一范围对应的数组元素是有序(升序)的,那么我们是否可以对枚举的过程进行优化呢?

答案是可以的。借助双指针,我们就可以对枚举的过程进行优化。我们用 p b p_b pb p c p_c pc
​分别表示指向 b 和 c 的指针,初始时, p b p_b pb 指向位置 i+1,即左边界; p c p_c pc指向位置 n−1,即右边界。在每一步枚举的过程中,我们用 a+b+c 来更新答案,并且:

  • 如果 a+b+c≥target,那么就将 p c p_c pc向左移动一个位置;
  • 如果 a+b+c p b p_b pb向左移动一个位置;

这是为什么呢?我们对 a+b+c≥target 的情况进行一个详细的分析:

  • 如果 a+b+c≥target,并且我们知道 p b p_b pb p c p_c pc 这个范围内的所有数是按照升序排序的,那么如果 p c p_c pc 不变而 p b p_b pb 向右移动,那么 a+b+c 的值就会不断地增加,显然就不会成为最接近 target 的值了。因此,我们可以知道在固定了 p c p_c pc 的情况下,此时的 p b p_b pb 就可以得到一个最接近 target 的值,那么我们以后就不用再考虑 p c p_c pc 了,就可以将 p c p_c pc 向左移动一个位置。

同样地,在 a+b+c

  • 如果 a+b+c p b p_b pb p c p_c pc 这个范围内的所有数是按照升序排序的,那么如果 p b p_b pb 不变而 p c p_c pc 向左移动,那么 a+b+c 的值就会不断地减小,显然就不会成为最接近 target 的值了。因此,我们可以知道在固定了 p b p_b pb 的情况下,此时的 p c p_c pc 就可以得到一个最接近 target 的值,那么我们以后就不用再考虑 p b p_b pb 了,就可以将 p b p_b pb 向左移动一个位置。
    实际上, p b p_b pb p c p_c pc 就表示了我们当前可以选择的数的范围,而每一次枚举的过程中,我们尝试边界上的两个元素,根据它们与 target 的值的关系,选择「抛弃」左边界的元素还是右边界的元素,从而减少了枚举的范围。

小优化

  • 当我们枚举到恰好等于target 的 a+b+c 时,可以直接返回target 作为答案,因为不会有再比这个更接近的值了。

  • 当我们枚举 a,b,c 中任意元素并移动指针时,可以直接将其移动到下一个与这次枚举到的不相同的元素,减少枚举的次数。

代码

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        # 对输入的列表进行排序,以便后续使用双指针技术
        nums.sort()
        n = len(nums)
        # 初始化最佳结果为一个很远的数,确保第一个找到的三元组会替换它
        best = 10**7
        
        # 定义一个内部函数用于更新当前找到的最接近结果
        def update(cur):
            nonlocal best  # 声明外部变量'best'可以在内部函数中被修改
            if abs(cur - target) < abs(best - target):  # 比较当前差值与之前记录的最佳差值
                best = cur  # 如果当前差值更小,则更新最佳结果
        
        # 遍历每个可能的起始点a
        for i in range(n):
            # 跳过重复的起始点以优化性能
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            # 设置双指针j和k分别指向起始点之后的第一个元素和列表的最后一个元素
            j, k = i + 1, n - 1
            # 当双指针没有交叉时循环
            while j < k:
                s = nums[i] + nums[j] + nums[k]  # 计算当前三元组的和
                # 如果和正好等于目标值,直接返回这个和
                if s == target:
                    return target
                update(s)  # 更新当前找到的最接近的结果
                # 如果和大于目标值,移动c指针向左寻找更小的和
                if s > target:
                    k0 = k - 1
                    # 跳过所有重复的c值以提高效率
                    while j < k0 and nums[k0] == nums[k]:
                        k0 -= 1
                    k = k0
                # 如果和小于目标值,移动b指针向右寻找更大的和
                else:
                    j0 = j + 1
                    # 跳过所有重复的b值以提高效率
                    while j0 < k and nums[j0] == nums[j]:
                        j0 += 1
                    j = j0

        return best  # 返回最终找到的最接近目标值的和

复杂度分析

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

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


知识点

  • list.sort(reverse=False, key=None)可以对列表进行排序,但会直接修改原列表。其中,reverse参数用于指定排序的顺序,默认为False,即升序排列;若设置为True,则列表将按降序排列。key参数则允许你提供一个函数,这个函数将作用于列表的每个元素上,并根据函数返回的结果进行排序。

  • 在Python中,nonlocal关键字用于在嵌套函数中声明一个变量为非局部变量(即不是局部变量也不是全局变量)。这通常用在闭包或者装饰器中,当你需要在内部函数中修改外部函数的变量时。

  • abs()是Python的内置函数,用于获取一个数的绝对值。

你可能感兴趣的:(python·每日一题,算法,python,leetcode)