【面试必问】手撕 LeetCode “三数之和”:双指针+去重,这一篇图解给你讲透!

234 阅读5分钟

前言:为什么你总倒在“三数之和”?

太多面试者在 LeetCode 第1题 “两数之和”  上重拳出击,用 HashMap 秒杀全场;然而一旦题目变成 第15题 “三数之和” ,由于无法直接套用 Hash 策略,很多人就开始支支吾吾,最后写出了一个 

O(n3)

 的暴力三层循环。

兄弟们,面试写

O(n3)

,基本上就是“回家等通知”的节奏了,还能打包一份"凉面"。  

其实,这道题是考察  “排序 + 双指针”  的经典范例。它的难点不在于找数字,而在于 如何优雅地去重。今天,就带你一步步拆解这道大厂必考题,保证你下次遇到能手撕得明明白白!


一、核心思路:降维打击(排序 + 双指针)

解决“三数之和”的核心在于将 三维问题降低到二维

如果我们固定其中一个数字(假设为 nums[i]),那么问题就变成了:在剩下的数组中,找到两个数 left 和 right,使得 nums[left] + nums[right] = -nums[i]

看!这不就变回我们熟悉的“两数之和”了吗?

但是,为了让双指针能跑起来,我们需要一个前提:数组必须是有序的

1. 为什么要排序?

这就要我们深刻理解 sort 的意义:

JavaScript

// a - b < 0  => a 在前 b 在后 (升序)
nums.sort((a, b) => a - b);

排序有两个巨大的好处:

  1. 单调性:数组有序后,如果三数之和偏大,我们只能通过左移右指针来减小总和;反之亦然。这是双指针能生效的物理基础。
  2. 方便去重:重复的元素会挨在一起,我们只需要判断 nums[i] === nums[i-1] 就能轻松跳过重复项。

2. 双指针布局

  1. 一层 for 循环,索引 i 从 0 到 length-2,这是我们的固定桩
  2. left 指针指向 i + 1(桩子的下一位)。
  3. right 指针指向 length - 1(数组末尾)。

屏幕截图 2026-01-18 210516.png


二、动图级流程解析:指针怎么动?

一旦 i 固定了,left 和 right 就开始向中间靠拢。在这个过程中,我们计算 sum = nums[i] + nums[left] + nums[right]。

这里有三种情况,逻辑非常清晰:

  1. sum > 0(和太大了)

    • 原因:数组是升序的,右边的数太大。
    • 动作:right--(右指针左移,找个小点的数)。
  2. sum < 0(和太小了)

    • 原因:左边的数太小。
    • 动作:left++ (左指针右移,找个大点的数)。
  3. sum == 0(中奖了!)

    • 动作:把 [nums[i], nums[left], nums[right]] 加入结果集 res。
    • 关键点:不仅要记录,还要同时收缩 left++ 和 right--,继续寻找下一组可能的解。

屏幕截图 2026-01-18 210923.png


三、地狱级细节:如何优雅地去重?💀

这道题 80% 的挂科率都出在去重上。题目要求结果集中不能包含重复的三元组(例如不能出现两个 [-1, 0, 1])。

我们在两个维度进行去重:

1. 外层循环去重(固定桩去重)

代码:

JavaScript

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

面试官追问:  为什么是 nums[i] == nums[i-1] 而不是 nums[i] == nums[i+1]?

解析:

  • 如果是 nums[i] == nums[i+1],对于数组 [-1, -1, 2],当 i 指向第一个 -1 时,你就把它跳过了。那你就会漏掉 [-1, -1, 2] 这个有效解(因为这两个 -1 是不同位置的,可以共存)。
  • 我们用 nums[i] == nums[i-1] 的意思是: “如果当前的数字和上一个数字一样,说明上一个数字已经把所有可能的组合都找过了,我就不用再找一遍了”

2. 内层双指针去重

找到一个答案后,还没完!left 和 right 移动后的新位置可能还是和刚才一样的数字。
代码:

JavaScript

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

这步操作必须在 res.push 并且常规移动 left++/right-- 之后进行,确保彻底跳过重复段。


四、完整代码展示 (可以直接背诵版)

优化版本,加上了详细的注释:

JavaScript

function threeSum(nums) {
    const res = [];
    
    // 1. 必须先排序!这是双指针生效的前提
    // sort 是 JS 内置排序,a-b < 0 表示升序
    nums.sort((a, b) => a - b);
    
    const len = nums.length;
    
    // 2. 遍历每一个数字作为“固定桩” i
    for (let i = 0; i < len - 2; i++) {
        
        // 【核心去重1】:跳过重复的起点
        // 注意是 i > 0 且和 i-1 比,不是和 i+1 比
        if (i > 0 && nums[i] === nums[i-1]) {
            continue;
        }
        
        let left = i + 1;
        let right = len - 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--;
                
                // 【核心去重2】:跳过重复的 left
                while (left < right && nums[left] === nums[left-1]) {
                    left++;
                }
                
                // 【核心去重3】:跳过重复的 right
                while (left < right && nums[right] === nums[right+1]) {
                    right--;
                }
                
            } else if (sum < 0) {
                // 和太小,左指针右移让和变大
                left++;
            } else {
                // 和太大,右指针左移让和变小
                right--;
            }
        }
    }
    return res;
}

五、复杂度分析

  • 时间复杂度

    O(n2)
    
    • 数组排序通常是快排,复杂度 

      O(nlog⁡n)
      

    • 双指针遍历过程:外层循环 

      O(n)
      

      ,内层双指针 

      O(n)
      

      ,乘积是 

      O(n2)
      

    • 总体:

      O(n2)
      

      ,远优于暴力的 

      O(n3)
      

  • 空间复杂度

    O(1)
    

     或 

    O(log⁡n)
    
    • 如果你不计算存储结果的 res 数组,额外的空间主要是排序算法栈的空间(取决于语言底层实现),通常认为是 

      O(log⁡n)
      

       或 

      O(1)
      


六、总结

做这道题,心里要默念这句四步口诀

  1. 一排序:无序没法玩,sort 走在前。
  2. 二定桩:for 循环定 i,去重要判前(i-1)。
  3. 三双指:left、right 两头堵,大了左移小右顾。
  4. 四去重:找到答案别停步,while 循环跳重复。

学会了吗?别光看,赶紧打开编辑器 手撕 一遍吧!觉得有用的兄弟,点个赞再走呗!