三数之和:从暴力到双指针的JS最优解拆解

44 阅读6分钟

三数之和:从暴力到双指针的JS最优解拆解

在算法面试中,三数之和(3Sum)是数组类题目中的经典高频题,也是考察双指针技巧的核心题型。本文会从暴力解法的痛点切入,结合实际代码逐行拆解排序+双指针的最优解法,带你彻底掌握这道题的解题逻辑。

一、题目背景与核心要求

题目描述

给定一个包含 n 个整数的数组 nums,找出所有和为 0 且不重复的三元组 [nums[i], nums[j], nums[k]],其中 ijk 互不相等。

核心难点

  1. 避免重复的三元组(如 [-1,0,1][0,-1,1] 视为重复);
  2. 降低暴力解法的高时间复杂度。

二、暴力解法:思路直观但效率低下

1. 暴力思路分析

三数之和最直观的思路是三层循环枚举所有可能的三元组,判断其和是否为0,最后去重。

  • 时间复杂度:O(n³),三层循环嵌套,n为数组长度,数据量稍大就会超时;
  • 空间复杂度:O(1)(不考虑结果存储);
  • 额外问题:需要额外处理重复三元组,增加代码复杂度。

2. 暴力解法的优化方向

暴力解法的核心问题是「重复计算」和「重复结果」,要优化需解决两个核心问题:

  • 如何减少循环层数?→ 降维:固定一个数,将三数之和转为两数之和;
  • 如何高效去重?→ 排序:让重复元素相邻,便于跳过。

三、最优解:排序 + 双指针

1. 核心思路拆解

最优解的思路可分为三步:

步骤操作目的
排序数组升序排序(nums.sort((a,b) => a - b)1. 重复元素相邻,便于去重;2. 可通过指针移动控制和的大小
固定一数遍历数组,固定 nums[i]将三数之和转化为「找两个数,使其和为 -nums[i]」的两数之和问题
双指针找两数左指针 left = i+1,右指针 right = nums.length-1根据三数之和与0的大小关系移动指针,降低内层循环复杂度

2. 逐行解析核心代码

以下是结合注释的完整代码,并逐行拆解关键逻辑:

function threeSum(nums) {
  // 1. 数组升序排序
  // JS sort默认是字符串排序,必须传入(a,b)=>a-b实现数值升序
  // 排序时间复杂度:O(nlogn)(JS内置sort基于快排/归并,接近快排)
  nums.sort((a,b) => a - b);
  const res = []; // 存储最终结果
  
  // 2. 固定第一个数,遍历数组(i最多到nums.length-3,保证leftright有值)
  for (let i = 0; i < nums.length-2; i++) {
    // 跳过重复的固定数:i>0时,若当前值等于前一个,直接跳过(避免重复三元组)
    // 例如:nums=[-1,-1,0,1]i=1时nums[1]=nums[0],跳过避免重复结果
    if (i > 0 && nums[i] === nums[i-1]) continue;
    
    // 3. 双指针初始化
    let left = i + 1; // 左指针从固定数的下一个开始,避免重复使用同一元素
    let right = nums.length - 1; // 右指针从数组末尾开始
    
    // 4. 双指针遍历(left < right 保证指针不重叠)
    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) {
        // 和小于0,需要增大总和 → 左指针右移(升序数组,右移值更大)
        left ++;
      } else {
        // 和大于0,需要减小总和 → 右指针左移(升序数组,左移值更小)
        right --;
      }
    }
  }
  return res;
}

3. 关键细节深度解析

(1)排序的核心作用
  • 去重基础:重复元素相邻,只需判断当前元素与前/后元素是否相等即可跳过;
  • 指针移动依据:升序数组中,左指针右移值增大,右指针左移值减小,可精准控制和的大小。
(2)去重逻辑的三层防护
去重位置代码逻辑作用
固定数去重i > 0 && nums[i] === nums[i-1]避免同一数字多次作为三元组第一个数
左指针去重nums[left] === nums[left - 1]避免同一数字多次作为三元组第二个数
右指针去重nums[right] === nums[right + 1]避免同一数字多次作为三元组第三个数
(3)双指针移动的边界条件
  • left < right:若指针重叠,说明已遍历完所有可能的两数组合,无需继续;
  • 找到和为0的组合后,先移动指针再去重:避免漏解(如 [-2,0,0,2,2],先移动指针再跳过重复的0和2)。

4. 复杂度分析

  • 时间复杂度:O(n²)。排序的O(nlogn) + 外层循环O(n) + 内层双指针遍历O(n),整体由O(n²)主导;
  • 空间复杂度:O(logn)(排序的系统栈空间),若不考虑结果存储,可视为O(1)。

四、测试用例验证

通过典型用例验证代码正确性:

// 测试用例1:常规场景
console.log(threeSum([-1,0,1,2,-1,-4])); // [[-1,-1,2],[-1,0,1]]

// 测试用例2:全重复元素
console.log(threeSum([0,0,0,0])); // [[0,0,0]]

// 测试用例3:无符合条件的组合
console.log(threeSum([1,2,3])); // []

// 测试用例4:边界场景(数组长度不足3)
console.log(threeSum([1,2])); // []

五、同类问题举一反三

掌握三数之和的思路后,可快速解决同类问题:

  1. 两数之和(有序数组) :直接复用双指针思路,时间复杂度O(n);
  2. 最接近的三数之和:将「和等于0」改为「和最接近目标值」,记录最小差值即可;
  3. 四数之和:固定前两个数,双指针找后两个数,时间复杂度O(n³)。

六、总结

三数之和的最优解本质是「排序降维 + 双指针优化」:

  1. 排序是基础,解决了去重和指针移动的核心问题;
  2. 固定一数将三数问题降为两数问题,实现维度降低;
  3. 双指针将内层循环从O(n²)优化为O(n),大幅降低时间复杂度;
  4. 去重逻辑是关键,需覆盖固定数、左指针、右指针三层。

这道题的核心思想不仅适用于三数之和,更是双指针技巧的典型应用。掌握后,面对多数数组类的求和问题,都能快速找到优化方向。