bigywater.tech
算法与数据结构 · LeetCode 16

LeetCode 16. 最接近的三数之和

给定一个整数数组和一个目标值 target,从数组里选三个不同位置的数,使它们的和最接近 target,返回这个和。题目保证答案唯一。暴力做法是三层循环,时间 O(n³)。标准做法与三数之和一样:先排序,固定第一个数,在右侧用双指针夹逼,时间 O(n²)。这道题有两个容易写错的地方:最近和的初始值怎么取,以及什么时候可以提前返回。本文用真实数据展示这两点,实测两种解法的耗时,给出五种语言的实现,并把双指针的移动过程做成分步演示。

10,000
组随机对拍,与暴力解的结果全部一致
5 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。返回的是"和"而不是那三个数,所以答案是唯一的整数。基准解是三层循环,取与 target 距离最小的和。一共 10 组用例,覆盖:官方示例、恰好只有三个数、target 远小于或远大于所有的和、大量重复值、以及和刚好等于 target。下面是前 6 组的输入与基准答案:

真实实测 · 本机执行输出
输入 [[-1,2,1,-4],1]
输出 2
输入 [[0,0,0],1]
输出 0
输入 [[0,0,0],-100]
输出 0
输入 [[1,1,1,0],-100]
输出 2
输入 [[1,1,-1,-1,3],-1]
输出 -1
输入 [[-10,-8,-3,7,9,12],3]
输出 1

陷阱:把最近和初始化成 0

最近和需要一个初始值。有人习惯写 best = 0,看起来"无所谓,后面会被更新"。但更新的条件是"新的和比 best 更接近 target",如果 0 本身就比所有真实的和更接近 target,它永远不会被替换,返回的就是一个根本不是三数之和的 0。下面是这种写法与正确写法的真实结果:

真实实测 · 本机执行输出
# 把 best 初始化为 0 的写法
nums=[10, 20, 30] target=0: 初始化为 0 -> 0    solve -> 60    暴力 -> 60
nums=[5, 6, 7, 8] target=1: 初始化为 0 -> 0    solve -> 18    暴力 -> 18
nums=[-3, -2, -1, -4] target=0: 初始化为 0 -> 0    solve -> -6    暴力 -> -6
nums=[2, 3, 4, 5] target=10: 初始化为 0 -> 10    solve -> 10    暴力 -> 10

前三行都错了:[10, 20, 30] 的唯一的和是 60,target = 0 时 0 比 60 更近,所以返回了 0;[5, 6, 7, 8] 与 [-3, -2, -1, -4] 同理。第四行 target = 10 的例子因为真实的和 10 比 0 更近,恰好答对了,这也说明这个错误不会在所有数据上暴露。正确的做法是用前三个数之和作为初始值,它至少是一个真实存在的三数之和。

排序加双指针:五种语言的实现

先排序,对每个 i 在 [i+1, n-1] 上放 l、r:和小于 target 就右移 l,大于就左移 r,等于就直接返回,因为不可能有比距离 0 更近的和了。每一步都用 abs(s - target) 与当前最近和比较并更新。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(nums, target): nums.sort() n = len(nums) best = nums[0] + nums[1] + nums[2] for i in range(n - 2): l, r = i + 1, n - 1 while l < r: s = nums[i] + nums[l] + nums[r] if abs(s - target) < abs(best - target): best = s if s < target: l += 1 elif s > target: r -= 1 else: # 恰好等于 target,不可能更近了 return s return best
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[-1,2,1,-4],1]  →  2
[[0,0,0],1]  →  0
[[0,0,0],-100]  →  0
[[1,1,1,0],-100]  →  2
[[1,1,-1,-1,3],-1]  →  -1
[[-10,-8,-3,7,9,12],3]  →  1
Java · 解法代码class Solution { public int threeSumClosest(int[] nums, int target) { Arrays.sort(nums); int n = nums.length; int best = nums[0] + nums[1] + nums[2]; for (int i = 0; i < n - 2; i++) { int l = i + 1, r = n - 1; while (l < r) { int s = nums[i] + nums[l] + nums[r]; if (Math.abs(s - target) < Math.abs(best - target)) best = s; if (s < target) l++; else if (s > target) r--; else return s; // 恰好等于 target,不可能更近了 } } return best; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[-1,2,1,-4],1]  →  2
[[0,0,0],1]  →  0
[[0,0,0],-100]  →  0
[[1,1,1,0],-100]  →  2
[[1,1,-1,-1,3],-1]  →  -1
[[-10,-8,-3,7,9,12],3]  →  1
Go · 解法代码import "sort" func abs(x int) int { if x < 0 { return -x } return x } func threeSumClosest(nums []int, target int) int { sort.Ints(nums) n := len(nums) best := nums[0] + nums[1] + nums[2] for i := 0; i < n-2; i++ { l, r := i+1, n-1 for l < r { s := nums[i] + nums[l] + nums[r] if abs(s-target) < abs(best-target) { best = s } if s < target { l++ } else if s > target { r-- } else { // 恰好等于 target,不可能更近了 return s } } } return best }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[-1,2,1,-4],1]  →  2
[[0,0,0],1]  →  0
[[0,0,0],-100]  →  0
[[1,1,1,0],-100]  →  2
[[1,1,-1,-1,3],-1]  →  -1
[[-10,-8,-3,7,9,12],3]  →  1
JavaScript · 解法代码function threeSumClosest(nums, target) { nums.sort((a, b) => a - b); const n = nums.length; let best = nums[0] + nums[1] + nums[2]; for (let i = 0; i < n - 2; i++) { let l = i + 1, r = n - 1; while (l < r) { const s = nums[i] + nums[l] + nums[r]; if (Math.abs(s - target) < Math.abs(best - target)) best = s; if (s < target) l++; else if (s > target) r--; else return s; // 恰好等于 target,不可能更近了 } } return best; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[-1,2,1,-4],1]  →  2
[[0,0,0],1]  →  0
[[0,0,0],-100]  →  0
[[1,1,1,0],-100]  →  2
[[1,1,-1,-1,3],-1]  →  -1
[[-10,-8,-3,7,9,12],3]  →  1
C++ · 解法代码#include <algorithm> #include <cstdlib> #include <vector> using namespace std; class Solution { public: int threeSumClosest(vector<int>& nums, int target) { sort(nums.begin(), nums.end()); int n = nums.size(); int best = nums[0] + nums[1] + nums[2]; for (int i = 0; i < n - 2; i++) { int l = i + 1, r = n - 1; while (l < r) { int s = nums[i] + nums[l] + nums[r]; if (abs(s - target) < abs(best - target)) best = s; if (s < target) l++; else if (s > target) r--; else return s; // 恰好等于 target,不可能更近了 } } return best; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[-1,2,1,-4],1]  →  2
[[0,0,0],1]  →  0
[[0,0,0],-100]  →  0
[[1,1,1,0],-100]  →  2
[[1,1,-1,-1,3],-1]  →  -1
[[-10,-8,-3,7,9,12],3]  →  1
真实实测 · 本机执行输出
共 10 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 10/10
  Java        通过 10/10
  Go          通过 10/10
  JavaScript  通过 10/10
  C++         通过 10/10

复杂度实测:立方对平方

下面每个规模取三次中最快的一次,数据是伪随机的偶数,target 取奇数 1,这样任何三数之和都不会恰好等于 target,"等于就提前返回"的捷径不会触发:

真实实测 · 本机执行输出
数组长度 n | 暴力解 | 本文解法
       100 |     9.603 ms | 0.350 ms
       300 |   273.949 ms | 3.134 ms
      1000 |      (未测,太慢) | 35.389 ms
      3000 |      (未测,太慢) | 326.723 ms

暴力解从 100 到 300(3 倍)耗时增长约 29 倍,接近立方(27 倍),所以更大的规模没有再测。双指针解法从 100 到 300 增长约 9 倍,从 300 到 1000 增长约 11 倍,从 1000 到 3000 增长约 9 倍,符合平方(3 倍规模对应 9 倍,3.3 倍规模对应 11 倍)。

这里有一个我踩到的坑:我最初用的测试数据里有奇数,target 也是 3,结果双指针在很小的规模就命中了"和等于 target"而提前返回,耗时几乎不随规模增长(n 从 1000 到 3000 只从 0.08ms 到 0.23ms),看起来像线性。这不是算法变快了,而是数据太容易命中。所以基准数据要刻意排除提前返回的情形,才能测到最坏的平方复杂度。

随机对拍

Python 版本另外和暴力解做了随机对拍:

真实实测 · 本机执行输出
随机对拍 10000 组(长度 3 到 12、取值 -30 到 30 的随机数组,target 取 -100 到 100(包括离数组和很远的目标);按题目保证,丢弃最近和并列的数据),solve 与 brute 的结果全部一致。

随机数据里经常有两个和与 target 距离相同(一个偏大一个偏小),这种数据违反"答案唯一"的保证,暴力解和双指针可能各返回其中一个,所以对拍时把这类数据丢掉。另外四种语言只在上面 10 组固定用例上验证过,没有做大规模随机对拍。

分步演示:指针怎么走、最近和怎么更新

下面的演示用官方示例 [-1, 2, 1, -4],target = 1,排序后是 [-4, -1, 1, 2]。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。

最接近的三数之和:排序后 nums = [-4, -1, 1, 2],target = 1
i、l、r 三个位置

    参考与说明

    • 官方题目页:LeetCode 16. 最接近的三数之和。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:数组里三个数取自相同下标以外的更多约束变体;更大规模下的其他语言耗时对比。
    京ICP备2026067502号-1京公网安备11010502063487号