三数之和(3Sum)详解:从暴力到双指针优化

88 阅读4分钟

三数之和(3Sum)详解:从暴力到双指针优化

在算法面试与实际开发中, “三数之和” 是一个经典问题,它不仅考察对数组操作的理解,更深入检验对时间复杂度优化、去重逻辑以及双指针技巧的掌握。本文将结合你的笔记与代码,系统梳理 三数之和问题的完整解法演进路径,突出核心思想与实现细节。


🧩 问题描述

给定一个整数数组 nums,找出所有不重复的三元组 [a, b, c],使得:

a+b+c=0a + b + c = 0

注意:结果中不能包含重复的三元组。

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


⏳ 解法演进:从暴力到高效

1️⃣ 暴力解法(O(n³))

最直观的方法是三层嵌套循环,枚举所有可能的三元组:

for (let i = 0; i < n; i++)
  for (let j = i+1; j < n; j++)
    for (let k = j+1; k < n; k++)
      if (nums[i] + nums[j] + nums[k] === 0) ...
  • 时间复杂度:O(n³)
  • 缺点:效率极低,无法通过大规模测试用例。
  • 难点:去重逻辑复杂(需对每个三元组排序后比对)。

💡 类比:两数之和的暴力解法是 O(n²),但可用哈希表优化到 O(n)。三数之和能否也优化?


2️⃣ 优化思路:排序 + 双指针(O(n²))

✅ 核心思想
  • 先排序:使数组有序 → 利用单调性控制指针移动方向。
  • 固定一个数 nums[i],转化为“在子数组中找两数之和等于 -nums[i]”的问题。
  • 双指针扫描:左指针 left = i+1,右指针 right = n-1,根据当前和调整指针。

这本质上是将 三数之和 转化为多次 两数之和(有序数组版)

🔑 为什么排序能帮助去重?

排序后,相同元素会相邻。因此:

  • 固定 i 时,若 nums[i] == nums[i-1],说明该值已处理过,直接跳过。
  • 找到一组解后,移动 leftright 时,也要跳过重复值。

🧠 算法步骤详解

以你的代码为基础,拆解关键步骤:

function threeSum(nums) {
  // 1. 边界处理(可选)
  if (nums.length < 3) return [];

  // 2. 升序排序
  nums.sort((a, b) => a - b); // 注意:JS sort 默认按字符串排序!

  const res = [];

  // 3. 外层循环:固定第一个数 nums[i]
  for (let i = 0; i < nums.length - 2; i++) {
    // 跳过重复起点(关键去重!)
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    let left = i + 1;
    let right = nums.length - 1;

    // 4. 双指针查找
    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;
}

🔍 关键点解析

✅ 排序的作用

  • 启用双指针:只有有序数组才能通过比较和来决定指针移动方向。
  • 简化去重:重复元素聚集,只需比较相邻项。

📌 JS 的 Array.prototype.sort() 默认按字符串 Unicode 排序!
必须传入 (a, b) => a - b 才能得到正确的数值升序。

const nums = [2, 1, 6, 3, 4, 5];
nums.sort((a, b) => a - b); // [1, 2, 3, 4, 5, 6]

✅ 去重逻辑(最容易出错的地方!)

  1. 外层去重

    if (i > 0 && nums[i] === nums[i-1]) continue;
    
    • 避免以相同值作为第一个数产生重复三元组。
  2. 内层去重(找到解后):

    while(left < right && nums[left] === nums[left-1]) left++;
    while(left < right && nums[right] === nums[right+1]) right--;
    
    • 确保下一次的 leftright 指向的是新值。

❗ 注意:必须先 left++right--,再判断重复,否则会漏掉有效解。

✅ 指针移动逻辑

当前三数之和操作原因
sum === 0记录 + 双移找到解,继续搜索其他组合
sum < 0left++需要更大的数
sum > 0right--需要更小的数

⏱️ 复杂度分析

  • 时间复杂度

    • 排序:O(n log n)
    • 外层循环 O(n) × 内层双指针 O(n) → O(n²)
    • 总计:O(n²)
  • 空间复杂度

    • 仅使用常量额外空间(不计结果数组)→ O(1)

相比暴力 O(n³),这是质的飞跃!


✅ 总结:三数之和的“黄金模板”

  1. 排序 → 启用双指针 + 便于去重
  2. 固定一数 → 将问题降维
  3. 双指针夹逼 → 利用有序性高效搜索
  4. 双重去重 → 外层跳起点,内层跳重复值

这套思路不仅适用于三数之和,还可扩展至 四数之和(4Sum)最接近的三数之和 等变种问题。


🧪 小练习

尝试修改此代码,解决以下问题:

  • 找出所有三元组,使其和等于目标值 target
  • 返回三元组的个数(而非具体组合)
  • 允许重复三元组(仅用于理解去重的重要性)

掌握三数之和,就掌握了排序 + 双指针 + 去重这一经典算法范式。它不仅是面试高频题,更是提升代码严谨性与效率思维的绝佳训练场。