三数之和:从“暴力搜索”到“聪明双指针”的奇妙旅程

67 阅读4分钟

引言

大家好!今天我们来聊聊 LeetCode 上一道非常经典的算法题 —— 三数之和(3Sum) 。你可能听说过它,也可能被它“折磨”过。别担心,今天我会用最通俗易懂的方式,带你一步步理解这道题的核心思想,还会手把手教你如何写出高效的解法!

更重要的是,我们还会一起看看 JavaScript 中一个神奇又常用的方法:nums.sort()。别小看它,它可是我们解决三数之和问题的关键第一步!


题目长啥样?

题目大概是这样的:

原题链接:15. 三数之和 - 力扣(LeetCode)

给你一个整数数组 nums,请你找出所有 不重复 的三元组 [a, b, c],使得 a + b + c = 0

比如:

输入:nums = [-1, 0, 1, 2, -1, -4]
输出:[[-1, -1, 2], [-1, 0, 1]]

注意:不能有重复的三元组!顺序不同但数字一样的也算重复哦。


暴力解法?太慢了!

最容易想到的办法就是“暴力三重循环”:遍历所有可能的三个数,看看加起来是不是 0。

时间复杂度是 O(n³) —— 如果数组有 1000 个数,那就要算 10 亿次!电脑会累趴下 😫。

所以我们需要更聪明的办法!


聪明人的思路:排序 + 双指针

第一步:先排序!

为什么排序这么重要?因为有序的数组能让我们用“双指针”技巧快速缩小搜索范围

来看这段代码:

const nums = [2, 1, 6, 3, 4, 5];
// b在前面,a在后面 a-b<0 交换位置 升序
nums.sort((a, b) => {
  console.log(a , b); // 打印结果为:
  // 1 2
  // 6 1
  // 6 2
  // 3 2
  // 3 6
  // 4 3
  // 4 6
  // 5 3
  // 5 6
  // 5 4
  return a - b;
});
console.log(nums); // [1, 2, 3, 4, 5, 6]

这段代码干了啥?

  • 它调用了 nums.sort((a, b) => a - b)
  • 这个写法的意思是:按从小到大排序(升序)
  • a - b < 0 时,说明 ab 小,不需要交换 → 正确顺序。
  • JavaScript 的 sort 方法内部其实不是冒泡排序(实际是 Timsort 或快排变种),但我们可以用冒泡的思想来理解:它会不断比较两个数,决定谁放前面。

最终,[2, 1, 6, 3, 4, 5] 变成了 [1, 2, 3, 4, 5, 6] —— 整整齐齐!

🔍 小知识:如果不传参数,sort() 会把数字当字符串排!比如 [10, 2] 会变成 [10, 2](因为 "1" < "2")。所以一定要写 (a, b) => a - b


核心算法:固定一个数 + 双指针找另外两个

现在数组有序了,我们就可以用这个绝妙策略:

  1. 固定第一个数(比如 nums[i])。

  2. 在它右边的子数组中,用左指针(left)和右指针(right) 找另外两个数。

    • left = i + 1
    • right = nums.length - 1
  3. 计算三数之和:

    • 如果 等于 0 → 找到答案!加入结果,并移动双指针继续找。
    • 如果 小于 0 → 说明太小了,把 left 右移(拿更大的数)。
    • 如果 大于 0 → 说明太大了,把 right 左移(拿更小的数)。

来看完整代码:

function threeSum(nums) {
  // 先排序
  // sort js 内置的排序 冒泡的思想理解
  // a - b < 0 ab 大 不交换位置 升序
  // b - a < 0 ba 大 交换位置 降序
  nums.sort((a, b) => a - b);
  const res = [];
  // 固定一个数字
  // 连个指针 i +1 j = nums.length-1
  for (let i = 0; i < nums.length-2; i++) {
    // 跳过重复的起点
    // i == 0 时 是第一个数字 不需要跳过
    // i j k 三个数字不能重复
    if (i > 0 && nums[i] === nums[i-1]) continue;
    // 双指针
    let left = i + 1;
    let right = nums.length - 1;
    while(left < right) {
      const sum = nums[i] + nums[left] + nums[right];
      if (sum === 0) {
        res.push([nums[i], nums[left], nums[right]]);
        // 继续找
        left++;
        right--;
        // 重复的数字,跳过
        while(left < right && nums[left] === nums[left-1]) left++;
        while(left < right && nums[right] === nums[right+1]) right--;
      } else if (sum < 0) {
        left++;
      } else {
        right--;
      }
    }
  }
  return res;
}

关键细节:去重!

你可能会问:“怎么保证结果不重复?”

看这两行:

if (i > 0 && nums[i] === nums[i-1]) continue;

→ 如果当前固定的数和上一个一样,直接跳过!避免重复三元组。

还有找到答案后:

while(left < right && nums[left] === nums[left-1]) left++;
while(left < right && nums[right] === nums[right+1]) right--;

→ 把左右指针也跳过重复值,确保下一组是全新的组合!


时间复杂度分析

  • 排序:O(n log n)
  • 外层循环:O(n)
  • 内层双指针:O(n)
  • 总体:O(n²) —— 比暴力快太多了!

总结:三步搞定三数之和

  1. 排序:让数组变得“听话”,方便我们控制方向。
  2. 固定 + 双指针:像夹心饼干一样,从两边向中间逼近目标。
  3. 跳过重复:确保答案干净、不冗余。

这道题不仅考察你的编码能力,更考验你对“有序性”和“指针移动”的理解。一旦掌握,你会发现很多类似题目(比如四数之和、最接近的三数之和)都能迎刃而解!


希望这篇博客让你觉得算法不再可怕,反而有点酷!如果你以前看到 sort 就头疼,现在应该能自信地说:“哦,我知道它在干嘛!”

下次见,继续一起征服算法世界!🚀