LeetCode 15. 三数之和:排序+双指针解法全解析

在LeetCode数组类题目中,"三数之和"是经典的中等难度题,核心考点在于去重逻辑双指针优化。本题不仅要求找到所有和为0的三元组,还需保证结果无重复,直接暴力枚举会因时间复杂度过高和重复结果问题无法通过测试。本文将详细拆解排序+双指针的最优解法,逐行解析代码逻辑,帮你彻底搞懂这道题的解题关键。

一、题目回顾

给定一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足:

  • 下标互不相等(i != j、i != k、j != k

  • 三数之和为0(nums[i] + nums[j] + nums[k] == 0

返回所有满足条件且不重复的三元组。注意:答案中不可包含重复三元组,顺序无关紧要。

二、解题思路:为什么选择"排序+双指针"?

首先分析暴力解法的问题:直接三重循环枚举所有三元组,时间复杂度为 O(n³),对于 n=3000 的测试用例(本题常见规模),会严重超时;同时需要额外处理大量重复三元组,代码复杂度极高。

而"排序+双指针"解法可将时间复杂度优化至 O(n²),且能天然适配去重逻辑,核心思路如下:

  1. 排序预处理:先对数组排序,一方面可通过有序性优化双指针移动逻辑,另一方面能快速跳过重复元素实现去重;

  2. 固定单指针 :遍历数组,将当前元素作为三元组的第一个元素 nums[i],转化为"在剩余元素中找两个数,使其和为 -nums[i]"的两数之和问题;

  3. 双指针找目标 :用左指针 left = i+1(剩余元素起始)、右指针 right = n-1(剩余元素末尾),根据三数之和调整指针位置,找到符合条件的三元组;

  4. 多层去重:对固定的第一个元素、左指针、右指针分别做去重处理,避免生成重复三元组。

三、完整代码与逐行解析

以下是TypeScript实现的最优解法,代码结构清晰,包含关键注释,我们逐段拆解核心逻辑:

typescript 复制代码
function threeSum(nums: number[]): number[][] {
  const res: number[][] = [];
  const n = nums.length;
  
  // 边界条件:数组长度小于3,不可能组成三元组,直接返回空数组
  if (n < 3) return res;
  
  // 1. 排序(核心前提:为双指针移动和去重提供基础)
  nums.sort((a, b) => a - b);
  
  // 2. 固定第一个数,遍历数组作为三元组的第一个元素nums[i]
  for (let i = 0; i < n; i++) {
    // 去重1:当前数与前一个数相同,跳过(避免重复起始元素导致重复三元组)
    if (i > 0 && nums[i] === nums[i - 1]) continue;
    
    // 优化:排序后若当前数>0,后续所有数都>=它,三数之和必>0,直接退出循环
    if (nums[i] > 0) break;
    
    // 双指针初始化:左指针从i+1开始(避免重复使用nums[i]),右指针从数组末尾开始
    let left = i + 1;
    let right = n - 1;
    
    // 双指针遍历剩余元素
    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];
      
      if (sum === 0) {
        // 找到符合条件的三元组,加入结果集
        res.push([nums[i], nums[left], nums[right]]);
        
        // 去重2:左指针跳过重复值(避免同一nums[i]下,左指针重复导致重复三元组)
        while (left < right && nums[left] === nums[left + 1]) left++;
        // 去重3:右指针跳过重复值(同理,避免右指针重复)
        while (left < right && nums[right] === nums[right - 1]) right--;
        
        // 移动指针,寻找下一组可能的三元组
        left++;
        right--;
      } else if (sum < 0) {
        // 和偏小:左指针右移,增大数值(排序后左指针右移数值递增)
        left++;
      } else {
        // 和偏大:右指针左移,减小数值(排序后右指针左移数值递减)
        right--;
      }
    }
  }
  
  return res;
}

核心逻辑拆解

1. 边界条件处理

当数组长度 n < 3 时,无法组成三元组,直接返回空数组,这是最基础的剪枝操作,避免无效遍历。

2. 排序的关键作用

排序不仅能让双指针根据和的大小调整位置(和小移左、和大移右),更重要的是为去重提供了便利------重复元素会相邻排列,只需跳过相邻重复值即可避免重复三元组。注意排序函数需写全 (a, b) => a - b,否则会按字符串字典序排序,导致结果错误。

3. 固定元素的去重与优化
  • 去重逻辑if (i > 0 && nums[i] === nums[i - 1]) continue。若当前元素与前一个元素相同,说明已以该元素为起始生成过三元组,跳过可避免重复(例如数组 [-1,-1,2],i=1时与i=0元素相同,跳过i=1)。

  • 剪枝优化if (nums[i] > 0) break。排序后数组递增,若当前元素已大于0,后续元素均≥它,三数之和必大于0,无需继续遍历,直接退出循环,大幅提升效率。

4. 双指针的移动与去重

双指针遍历剩余元素时,根据三数之和 sum 的大小调整位置:

  • sum === 0:找到目标三元组,加入结果集。之后需跳过左右指针的相邻重复值(例如左指针下一个元素与当前相同,右移左指针),再移动双指针寻找下一组;

  • sum < 0:和偏小,左指针右移(增大数值),使和向0靠近;

  • sum > 0:和偏大,右指针左移(减小数值),使和向0靠近。

注意:双指针的去重需在找到目标三元组后执行,若提前去重可能跳过有效组合。

四、常见问题与避坑指南

1. 去重逻辑遗漏导致重复三元组

这是本题最容易出错的点,需保证三层去重:固定元素去重、左指针去重、右指针去重。例如未处理左指针重复,会导致 [-1,0,1,-1,2] 生成多个 [-1,0,1]

2. 双指针初始化错误

左指针必须从 i+1 开始,而非0,否则会重复使用固定元素(导致下标重复或三元组重复)。

3. 排序后直接暴力枚举

排序后若仍用三重循环,时间复杂度仍为 O(n³),无法通过大测试用例,双指针是降低时间复杂度的核心。

4. 忽略"和为0但元素重复"的特殊场景

例如数组 [0,0,0],需正确处理固定元素和双指针的重复,最终返回 [[0,0,0]],而非空数组或多个重复三元组。

五、测试用例验证

我们用几个典型测试用例验证代码正确性:

typescript 复制代码
// 测试用例1:常规场景
console.log(threeSum([-1,0,1,2,-1,-4])); 
// 输出:[[-1,-1,2],[-1,0,1]]

// 测试用例2:边界场景(空数组/长度不足3)
console.log(threeSum([])); // 输出:[]
console.log(threeSum([0])); // 输出:[]

// 测试用例3:全零场景
console.log(threeSum([0,0,0])); // 输出:[[0,0,0]]

// 测试用例4:含重复元素场景
console.log(threeSum([-2,0,0,2,2])); // 输出:[[-2,0,2]]

六、复杂度分析

  • 时间复杂度O(n²)。排序时间为 O(n log n),外层循环遍历固定元素为 O(n),内层双指针遍历为 O(n),整体主导项为 O(n²)

  • 空间复杂度O(1)(不计算结果存储)。排序使用的空间取决于语言实现(JavaScript/TypeScript的sort为混合排序,空间复杂度可视为O(1)),双指针仅使用常数额外空间。

七、总结

LeetCode 15. 三数之和的核心是"排序+双指针"的组合策略,通过排序解决去重和指针移动逻辑问题,通过双指针将三重循环优化为二重循环,实现时间复杂度的降级。解题的关键在于精准把控三层去重逻辑双指针的移动规则,同时通过剪枝操作进一步提升效率。

这类"多数之和"问题有通用解题思路:两数之和用哈希表,三数之和/四数之和用"排序+双指针",本质是通过预处理和指针优化降低时间复杂度。掌握本题后,可类比解决 LeetCode 18. 四数之和等同类题目。

相关推荐
踩坑记录2 小时前
leetcode hot100 环形链表 easy 快慢指针
leetcode·链表
美狐美颜SDK开放平台2 小时前
直播场景下抖动特效的实现方案:美颜sdk开发经验分享
前端·人工智能·美颜sdk·直播美颜sdk·视频美颜sdk
挽天java2 小时前
数据结构习题--寻找旋转排序数组中的最小值
数据结构·算法·排序算法
你怎么知道我是队长2 小时前
C语言---排序算法4---希尔排序法
c语言·算法·排序算法
草青工作室2 小时前
java-FreeMarker3.4自定义异常处理
java·前端·python
iAkuya2 小时前
(leetcode)力扣100 54实现Trie树
算法·leetcode·c#
美狐美颜sdk2 小时前
抖动特效在直播美颜sdk中的实现方式与优化思路
前端·图像处理·人工智能·深度学习·美颜sdk·直播美颜sdk·美颜api
Mr Xu_2 小时前
Vue3 + Element Plus 实战:App 版本管理后台——动态生成下载二维码与封装文件上传
前端·javascript·vue.js
闻哥2 小时前
从 AJAX 到浏览器渲染:前端底层原理与性能指标全解析
java·前端·spring boot·ajax·okhttp·面试