编程挑战:一步步逼近最优三数和

105 阅读3分钟

问题:

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

返回这三个数的和。

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

示例 1:

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

示例 2:

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

提示:

  • 3 <= nums.length <= 1000
  • -1000 <= nums[i] <= 1000
  • -104 <= target <= 104

思路和力扣算法15. 三数之和 类似,排序后,枚举 nums[i] 作为第一个数,那么问题变成找到另外两个数,使得这三个数的和与 target 最接近,这同样可以用双指针解决。 想详细了解三数之和解法可以看我之前的文章:

数字的魔力:探索三数之和,引爆你的逻辑思维

设 s=nums[i]+nums[j]+nums[k],为了判断 s 是不是与 target 最近的数,我们还需要用一个变量 minDiff 维护 s−target 的最小值。分类讨论:
  • 如果 s=target,那么答案就是 s,直接返回 s。
  • 如果 s>target,那么如果 s−target<minDiff,说明找到了一个与 target更近的数,更新 minDiff 为 s−target,更新答案为 s。然后和三数之和一样,把 k 减一。
  • 否则 s<target,那么如果 target−s<minDiff,说明找到了一个与 target更近的数,更新 minDiff为 target−s,更新答案为 s。然后和三数之和一样,把 j 加一。
  • 代码:
function threeSumClosest(nums, target) {
  nums.sort((a, b) => a - b);
  let closestSum = Infinity;
  let minDiff = Infinity;

  for (let i = 0; i < nums.length - 2; i++) {
    let left = i + 1;
    let right = nums.length - 1;

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];
      const diff = Math.abs(sum - target);

      if (diff < minDiff) {
        minDiff = diff;
        closestSum = sum;
      }

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

  return closestSum;
}

除此以外,还有以下几个优化可以用于减少计算提高算法效率: 1.提前终止: 设 s=nums[i]+nums[i+1]+nums[i+2]。如果 s>target,由于数组已经排序,后面无论怎么选,选出的三个数的和不会比 s 还小,所以不会找到比 s 更优的答案了。所以只要 s>target ,就可以直接 break 外层循环了。在 break 前判断 s 是否离 target更近,如果更近,那么更新答案为 s。

2.提前跳过: 设 s=nums[i]+nums[n−2]+nums[n−1]。如果 s<target,由于数组已经排序,nums[i] 加上后面任意两个数都不超过 s,所以下面的双指针就不需要跑了,无法找到比 s 更优的答案。但是后面还有更大的 nums[i],可能找到一个离 target更近的三数之和,所以还需要继续枚举,continue 外层循环。在 continue 前判断 s 是否离 target 更近,如果更近,那么更新答案为 s,更新 minDiff 为 starget−s。

3.去重:如果 i>0 且 nums[i]=nums[i−1],那么 nums[i]和后面数字相加的结果,必然在之前算出过,所以无需跑下面的双指针,直接 continue 外层循环。(可以放在循环开头判断。)

  • 代码:
var threeSumClosest = function(nums, target) {
  nums.sort((a, b) => a - b);
  const n = nums.length;
  let ans = 0;
  let minDiff = Number.MAX_SAFE_INTEGER;
  for (let i = 0; i < n - 2; i++) {
    const x = nums[i];
    
    // 优化一:提前终止
    let s = x + nums[i + 1] + nums[i + 2];
    if (s > target) {
      if (s - target < minDiff) {
        ans = s;
        break;
      }
    }
    // 优化二:提前跳过
    s = x + nums[n - 2] + nums[n - 1];
    if (s < target) {
      if (target - s < minDiff) {
        minDiff = target - s;
        ans = s;
      }
      continue;
    }
    // 双指针
    let j = i + 1,
      k = n - 1;
    while (j < k) {
      s = x + nums[j] + nums[k];
      
      // 优化三:去重
      
      if (s === target) {
        return target;
      }
      if (s > target) {
        if (s - target < minDiff) {
          minDiff = s - target;
          ans = s;
        }
        k--;
      } else {
        if (target - s < minDiff) {
          minDiff = target - s;
          ans = s;
        }
        j++;
      }
    }
  }
  return ans;
};

总结:

无论是之前的三数之和还是现在的最接近三数之和的问题我们都可以采用双指针的用法来解决它。本篇文章中还提到了不少优化方案用于提升代码计算效率,希望这篇文章能对你的算法有所帮助。