【LeetCode Hot100刷题笔记】:三数之和—— 掌握双指针与排序去重的核心思想

98 阅读5分钟

【LeetCode Hot100刷题笔记】:三数之和—— 掌握双指针与排序去重的核心思想

题目来源:第 15 题(3Sum)

难度:Easy → Medium
标签:数组、哈希表、双指针、排序
Hot 100 必刷指数:⭐⭐⭐⭐⭐ image.png


在 LeetCode Hot 100 的高频题中,「两数之和」 是几乎所有初学者的第一道题,而 「三数之和」 则是进阶路上的“分水岭”。它们看似独立,实则一脉相承——前者教你用空间换时间,后者教你用排序+双指针解决更复杂的组合问题。

本文将带你:

  1. 回顾「两数之和」的经典解法;
  2. 分析为何不能直接套用哈希表解决「三数之和」;
  3. 深入讲解 JavaScript sort() 的正确用法(这是很多人的盲区!);
  4. 手把手实现高效、无重复的「三数之和」解法。

🔹 1. 两数之和(Two Sum)—— 哈希表的胜利

📌 题目简述

给定数组 nums 和目标值 target,返回两个数的下标,使它们的和等于 target

❌ 不好的方法:暴力破解法(O(n^2))

  • 暴力解法需要对每个元素 nums[i],遍历其后所有元素 nums[j]j > i),检查是否满足 nums[i] + nums[j] == target
  • 对于长度为 n 的数组,最坏情况下要进行约 n(n−1)/2 次比较。
  • 当 n 较大时(例如 n = 10⁵),操作次数将高达 50 亿次,在大多数编程环境中会超时(LeetCode 通常限制 1~2 秒运行时间)。

✅ 最优解:哈希表(O(n))

function twoSum(nums, target) {
    const map = new Map();
    for (let i = 0; i < nums.length; i++) {
        const need = target - nums[i];
        if (map.has(need)) return [map.get(need), i];
        map.set(nums[i], i);
    }
}
  • 核心思想:把“找两个数”转化为“对每个数,看它的补数是否出现过”。将求和问题变成了求差
  • 无需排序,因为题目要求返回原始下标,排序会打乱位置。

💡 小结:两数之和 → 哈希表 + 求差


🔸 2. 三数之和(3Sum)—— 为什么哈希不够用了?

❌ 能否用哈希?

理论上可以:固定 a,然后在剩余数组中用哈希找 b + c = -a
但问题在于:如何高效去重?

例如输入 [-1, 0, 1, 2, -1, -4],若不排序,你很难判断 [-1, 0, 1][0, -1, 1] 是同一组解。

✅ 结论:当问题涉及“组合去重”时,排序是天然的去重工具


🔧 3. 关键前置:JavaScript sort() 的正确打开方式

很多同学在写 3Sum 时,直接写 nums.sort(),结果在负数测试用例上翻车!

❌ 错误示范

let arr = [-1, -2, 3, 10];
arr.sort(); 
console.log(arr); // [-1, -2, 10, 3] ❌

原因:sort() 默认按字符串 Unicode 编码比较,"-1" 字符串小于 "-2"

✅ 正确写法:传入比较函数

nums.sort((a, b) => a - b); // 升序
📚 比较函数逻辑解析:
  • (a, b) => a - b

    • a < b → 返回负数 → a 排在 b 前 → 升序
    • a > b → 返回正数 → b 排在 a
  • 这不是冒泡排序!现代 JS 引擎(如 V8)使用 Timsort(混合稳定排序),效率远高于 O(n²)

💡 记住口诀: “a - b 升序,b - a 降序”


🔥 4. 三数之和:排序 + 双指针 + 去重

🧠 解题思路

  1. 排序O(n log n),为双指针和去重奠基;

  2. 固定第一个数 nums[i]

    • nums[i] > 0,直接 break(后面都是正数,和不可能为 0);
    • i > 0 && nums[i] === nums[i-1],跳过(避免重复三元组);
  3. 双指针查找

    • left = i + 1, right = n - 1
    • 根据 sum = nums[i] + nums[left] + nums[right] 调整指针;
  4. 找到解后继续去重:移动指针并跳过相同值。

💻 完整代码(带详细注释)

var threeSum = function(nums) {
    // Step 1: 正确升序排序
    nums.sort((a, b) => a - b);
    const res = [];

    // Step 2: 固定第一个数
    for (let i = 0; i < nums.length - 2; i++) {
        // 剪枝:最小值已大于0,不可能和为0
        if (nums[i] > 0) break;

        // 跳过重复的起始值
        if (i > 0 && nums[i] === nums[i - 1]) continue;

        // Step 3: 双指针
        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) {
                right--; // 和太大,右指针左移
            } 
            else {
                left++;  // 和太小,左指针右移
            }
        }
    }

    return res;
};

🧪 测试用例

console.log(threeSum([-1,0,1,2,-1,-4])); 
// [[-1,-1,2],[-1,0,1]]

console.log(threeSum([0,0,0])); 
// [[0,0,0]]

console.log(threeSum([1,2,-2,-1])); 
// []

⏱️ 复杂度分析

方法时间复杂度空间复杂度
暴力三重循环O(n³)O(1)
排序 + 双指针O(n²)O(1) 或 O(log n)(排序栈空间)

🧠 思维升华:从 2Sum 到 3Sum 的范式迁移

维度两数之和三数之和
目标找下标找值(且去重)
核心数据结构哈希表排序数组
关键技巧“求差”“双指针收缩”
是否需要排序❌ 否✅ 是
去重策略无(下标天然唯一)主动跳过重复元素

🌟 启示

  • 当问题从“找下标”变为“找组合”,且要求无重复时,排序 + 双指针 往往比哈希更优雅。
  • 这一思想可扩展至 4Sum、最接近的三数之和、三数之和小于目标值 等 Hot 100 高频题。

✅ 总结

  • 两数之和:哈希表是王者,O(n) 解决问题;
  • 三数之和:排序是基石,双指针是利剑,去重是灵魂;
  • JavaScript 开发者注意sort() 必须传入 (a, b) => a - b 才能正确排序数字!

掌握这两道题,你就掌握了 LeetCode 数组类问题的两大核心范式。继续刷下去,Hot 100 不再可怕!

📌 延伸练习

  • LeetCode 18. 四数之和(4Sum)
  • LeetCode 16. 最接近的三数之和
  • LeetCode 259. 较小的三数之和(3Sum Smaller)

刷题不在多,而在精。理解一道题,胜过盲目十道。