【 前端三剑客-38 /Lesson64(2025-12-12)】三数之和问题详解:从暴力到双指针优化🧮

5 阅读5分钟

🧮 在算法与数据结构的学习中,「三数之和」(3Sum)是一个经典且极具教学意义的问题。它不仅考察对数组操作的熟练度,还涉及排序、去重、双指针等核心技巧。本文将深入剖析该问题,并结合代码实现、时间复杂度分析以及相关知识扩展,帮助读者全面掌握这一题型。


🔍 问题描述

给定一个整数数组 nums,判断是否存在三个不同的元素 abc,使得 a + b + c = 0。要求返回所有满足条件的不重复三元组。

注意:答案中不可以包含重复的三元组。例如 [−1, 0, 1][0, −1, 1] 被视为相同,只应出现一次。

LeetCode 题目链接:leetcode.cn/problems/3s…


⚡ 暴力解法(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(1)(忽略结果存储)

但此方法在 n 较大时(如 n = 1000)会超时,且去重逻辑复杂——需要对每个三元组排序后用 Set 去重,效率低下。


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

✅ 核心思想

  1. 先对数组排序(升序)

    • 排序后便于跳过重复元素。
    • 为双指针提供有序性基础。
  2. 固定第一个数 nums[i]

    • 遍历 i0n - 3
    • nums[i] > 0,由于数组已排序,后续所有数都 ≥ nums[i],三数之和不可能为 0,可提前终止。
  3. 使用双指针在剩余区间 [i+1, n-1] 中寻找两数之和为 -nums[i]

    • 左指针 left = i + 1

    • 右指针 right = n - 1

    • 计算 sum = nums[i] + nums[left] + nums[right]

      • sum === 0 → 找到一组解,加入结果,并移动双指针继续搜索。
      • sum < 0 → 左指针右移(增大和)
      • sum > 0 → 右指针左移(减小和)
  4. 关键:跳过重复元素

    • 固定 i 时,若 nums[i] === nums[i-1],跳过(避免重复三元组)。
    • 找到解后,leftright 移动时也要跳过重复值。

💻 完整代码实现(JavaScript)

function threeSum(nums) {
  // 先排序(快排 O(n log n))
  // JavaScript 的 sort 默认使用 Timsort(混合排序),但比较函数决定顺序
  nums.sort((a, b) => a - b);

  const res = [];

  // 固定第一个数字 i
  for (let i = 0; i < nums.length - 2; i++) {
    // 跳过重复的起点
    // i === 0 时是第一个数字,不需要跳过
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    // 若当前最小值 > 0,三数之和必 > 0,提前结束
    if (nums[i] > 0) break;

    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;
}

📊 时间与空间复杂度分析

  • 排序:O(n log n)
  • 外层循环:O(n)
  • 内层双指针:O(n)
  • 总时间复杂度:O(n log n) + O(n²) = O(n²)
  • 空间复杂度:O(1)(不计结果数组)

远优于暴力 O(n³),适用于 n ≤ 3000 的典型 LeetCode 输入规模。


🧠 关于排序的细节补充

在 JavaScript 中,Array.prototype.sort() 默认按字符串 Unicode 码排序,因此必须传入比较函数:

nums.sort((a, b) => a - b); // 升序
  • a - b < 0 时,a 排在 b 前面 → 升序。
  • 内部实现并非冒泡排序(尽管教学时常以冒泡类比),现代引擎多采用 Timsort(Python 启发的混合稳定排序),兼具快排与归并的优点。

示例(来自 2.js):

const nums = [2, 1, 6, 3, 4, 5];
nums.sort((a, b) => {
  console.log(a, b); // 展示比较过程(实际调用顺序依赖内部算法)
  return a - b;
});
console.log(nums); // 输出 [1, 2, 3, 4, 5, 6]

🔁 与「两数之和」的对比

特性两数之和(Two Sum)三数之和(Three Sum)
目标找两个数和为 target找三个数和为 0
是否可哈希✅ 可用 HashMap O(n)❌ 哈希难以处理三元组去重
是否需排序否(若只需返回索引)✅ 必须排序以支持双指针和去重
去重要求通常无(索引唯一)✅ 必须去重三元组
典型解法HashMap排序 + 双指针

💡 三数之和无法直接用 HashMap 高效解决,因为需要确保三元组不重复,而哈希表难以维护组合的唯一性。


🛑 常见陷阱与注意事项

  1. 越界问题

    • 外层循环 i < nums.length - 2,确保 leftright 有空间。
  2. 重复跳过时机

    • i > 0 && nums[i] === nums[i-1] → 在进入内层前跳过。
    • 找到解后,先移动指针再跳过重复,避免漏解。
  3. 提前终止条件

    • if (nums[i] > 0) break; 是重要优化,但仅在目标为 0 时成立。
  4. 空输入处理

    • nums.length < 3,直接返回空数组。

🌟 扩展思考

  • 四数之和(4Sum)
    可在三数之和基础上再套一层循环,时间复杂度 O(n³),同样使用排序+双指针+去重。
  • 通用 kSum 问题
    可用递归将 kSum 降维至 2Sum,时间复杂度 O(n^{k-1})。
  • 若目标不是 0 而是任意 target
    只需将判断条件改为 sum === target,其余逻辑不变。

✅ 总结

「三数之和」是一道集排序、双指针、去重、边界处理于一体的综合题。通过将暴力 O(n³) 优化至 O(n²),不仅提升了效率,更展示了算法设计中“利用有序性降低复杂度”的核心思想。掌握此题,对理解后续的 kSum 系列问题、滑动窗口、双指针技巧都有极大帮助。

🎯 记住口诀
一排序,二固定,三双指,四去重,五剪枝

现在,你已经准备好征服所有类似的求和问题了!🚀