【LeetCode Hot100 刷题日记 (6/100)】15. 三数之和——双指针、排序、数组、去重📌

59 阅读5分钟

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

🔍 难度:中等 | 🏷️ 标签:数组、双指针、排序、去重

⏱️ 目标时间复杂度:O(n²)

💾 空间复杂度:O(log n)(忽略结果存储空间,仅考虑排序所需栈空间)


💡 题目分析

给定一个整数数组 nums,需找出所有 和为 0 且不重复 的三元组 [nums[i], nums[j], nums[k]]。要求:

  • 三元组中元素下标互不相同;
  • 答案不能包含重复的三元组(例如 [-1,0,1][1,0,-1] 被视为重复)。

📌 关键难点:如何高效地避免重复解?暴力枚举 O(n³) 不仅超时,还难以处理去重逻辑。


🧠 核心算法及代码讲解

我们采用 排序 + 双指针 的经典策略,将问题从三维降维到二维,同时天然支持去重。

✅ 步骤详解

1. 排序预处理

sort(nums.begin(), nums.end());
  • 将数组升序排序,使相同元素相邻,便于后续去重;
  • 同时赋予双指针移动方向性:左指针右移 → 值增大;右指针左移 → 值减小。

2. 外层循环固定第一个数 nums[i]

for (int i = 0; i < n; i++) {
    if (nums[i] > 0) return ret;
    if (i > 0 && nums[i] == nums[i - 1]) continue;
  • 剪枝优化:若 nums[i] > 0,由于数组已排序,后续所有数 ≥ nums[i] > 0,三数之和不可能为 0,直接终止。
  • 去重第一个数:若当前 nums[i] 与前一个相同,跳过,防止生成重复三元组。

3. 双指针查找剩余两数

left = i + 1right = n - 1,计算:

int sum = nums[i] + nums[left] + nums[right];
  • sum == 0:找到有效解,加入结果,并同时移动左右指针跳过重复值
  • sum < 0:和太小,左指针右移以增大;
  • sum > 0:和太大,右指针左移以减小。

4. 关键去重逻辑(面试高频考点!)

⚠️ 很多人只在外层去重,但内层去重同样重要

// 找到解后,跳过重复的 left 和 right
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
  • 这两行确保:即使有多个相同值,也只取一组;
  • 必须先跳过重复,再移动指针,否则可能漏掉边界情况(如 [0,0,0,0])。

🧪 解题思路总结

  1. 排序 → 为双指针和去重奠定基础;
  2. 固定第一个数 → 外层遍历,剪枝 + 去重;
  3. 双指针夹逼 → 在剩余区间寻找满足条件的两个数;
  4. 双重去重 → 外层防首元素重复,内层防第二、第三元素重复;
  5. 提前终止 → 第一个数 > 0 时直接返回,极大提升效率。

💬 面试提示:当被问“为什么排序后不会漏解?”
回答:因为三元组是无序的,排序只是改变了枚举顺序,不影响组合的存在性。而通过固定第一个数 + 双指针,我们系统地覆盖了所有可能的组合,且通过去重保证唯一性。


📊 算法复杂度分析

项目复杂度说明
时间复杂度O(n²)排序 O(n log n),外层循环 O(n),内层双指针 O(n),主导项为 O(n²)
空间复杂度O(log n)忽略结果存储,仅考虑快排递归栈空间(C++ std::sort 通常为 introsort)

✅ 满足 LeetCode 对时间和空间的要求,是最优解法之一。


💻 完整可运行代码(含测试用例)

#include <bits/stdc++.h>
using namespace std;

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int n = nums.size();
        vector<vector<int>> ret;
        if (n < 3) return ret;  // 特判:元素不足三个直接返回
        sort(nums.begin(), nums.end());  // 排序预处理
        
        for (int i = 0; i < n; i++) {
            if (nums[i] > 0) return ret;  // 剪枝:第一个数大于0,无解
            if (i > 0 && nums[i] == nums[i - 1]) continue;  // 去重:跳过重复的第一个数
            
            int left = i + 1, right = n - 1;
            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];
                if (sum == 0) {
                    ret.push_back({nums[i], nums[left], nums[right]});
                    // 内层去重:跳过重复的左右指针元素
                    while (left < right && nums[left] == nums[left + 1]) left++;
                    while (left < right && nums[right] == nums[right - 1]) right--;
                    left++;
                    right--;
                } else if (sum < 0) {
                    left++;  // 和太小,左指针右移
                } else {
                    right--;  // 和太大,右指针左移
                }
            }
        }
        return ret;
    }
};

int main() {
    Solution solution;
    vector<int> nums1 = {-1, 0, 1, 2, -1, -4};
    vector<vector<int>> result1 = solution.threeSum(nums1);
    cout << "测试用例1: [-1,0,1,2,-1,-4]" << endl;
    for (auto& triplet : result1) {
        cout << "[" << triplet[0] << "," << triplet[1] << "," << triplet[2] << "]" << endl;
    }
    
    vector<int> nums2 = {0, 1, 1};
    vector<vector<int>> result2 = solution.threeSum(nums2);
    cout << "测试用例2: [0,1,1]" << endl;
    for (auto& triplet : result2) {
        cout << "[" << triplet[0] << "," << triplet[1] << "," << triplet[2] << "]" << endl;
    }
    
    vector<int> nums3 = {0, 0, 0};
    vector<vector<int>> result3 = solution.threeSum(nums3);
    cout << "测试用例3: [0,0,0]" << endl;
    for (auto& triplet : result3) {
        cout << "[" << triplet[0] << "," << triplet[1] << "," << triplet[2] << "]" << endl;
    }
    return 0;
}

输出示例:

测试用例1: [-1,0,1,2,-1,-4]
[-1,-1,2]
[-1,0,1]
测试用例2: [0,1,1]
测试用例3: [0,0,0]
[0,0,0]

🎯 面试延伸思考

  1. 如果题目改为“四数之和”?
    → 同理:排序 + 两层外层循环 + 双指针,时间复杂度 O(n³),注意多层去重。

  2. 能否用哈希表解决?
    → 理论上可以(固定两个数,查第三个),但去重极其困难,且空间开销大,不推荐。

  3. 为什么不用 set 去重?
    → 虽然可行,但会增加 O(k log k) 的插入开销(k 为结果数量),且违背“原地、高效”的设计哲学。

  4. 边界测试点有哪些?

    • 全零数组 [0,0,0,0]
    • 无解数组 [1,2,3]
    • 重复元素多的数组 [-2,-2,-1,-1,0,0,1,1,2,2]


🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第26题 —— 删除有序数组中的重复项(简单)

🔹 题目:给你一个升序排列的数组 nums,请你原地删除重复出现的元素,使得每个元素只出现一次,并返回删除后数组的新长度。

🔹 核心思路:快慢双指针——快指针遍历,慢指针记录不重复元素的位置。

🔹 考点:双指针、原地修改、边界处理。

🔹 难度:简单,但却是“数组去重”类问题的基石,高频面试题!

💡 提示:不要使用额外数组空间,必须在 O(1) 额外空间下完成!


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!