news 2026/6/20 12:49:12

hot100 15 三数之和

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
hot100 15 三数之和

题目

给你一个整数数组nums,判断是否存在三元组[nums[i], nums[j], nums[k]]满足i != ji != kj != k,同时还满足nums[i] + nums[j] + nums[k] == 0。请你返回所有和为0且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]输出:[[-1,-1,2],[-1,0,1]]解释:nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]输出:[]解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]输出:[[0,0,0]]解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

可以这样理解:三数之和 = 排序后,固定一个数,再用双指针找另外两个数。

为什么不能直接三重循环?

最容易想到的是:

for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { for (int k = j + 1; k < n; k++) { if (nums[i] + nums[j] + nums[k] == 0) { // 记录答案 } } } }

问题有两个:

第一,时间复杂度是:

O(n^3)

题目中:

nums.length <= 3000

3000^3太大,会超时。

第二,结果会重复。

例如:

nums = [-1, 0, 1, 2, -1, -4]

里面有两个-1,所以可能多次得到:

[-1, 0, 1]

但题目要求答案中不能有重复三元组。

为什么要排序?

排序后的数组有两个好处。

原数组:

[-1, 0, 1, 2, -1, -4]

排序后:

[-4, -1, -1, 0, 1, 2]

好处一:方便去重。

比如两个-1是挨在一起的:

[-4, -1, -1, 0, 1, 2] ↑ ↑

所以我们可以判断:

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

意思是:

如果当前这个数和前一个数一样,说明之前已经以这个数作为第一个数找过答案了,这次直接跳过。

好处二:可以使用双指针。

因为排序后,数组是有序的:

小 ---------------------- 大

当三数之和太小时,可以让左指针右移,让总和变大。

当三数之和太大时,可以让右指针左移,让总和变小。

核心思路:固定一个数,寻找另外两个数

假设排序后:

[-4, -1, -1, 0, 1, 2]

我们固定第一个数a

比如:

a = -1

那么问题就变成:

找两个数 b 和 c,使得: a + b + c = 0

也就是:

b + c = -a

如果:

a = -1

那么:

b + c = 1

这就变成了一个“两数之和”问题。

区别是:这里数组已经排序,所以可以用双指针,不需要哈希表。


4. 双指针怎么移动?

固定i作为第一个数:

int left = i + 1; int right = n - 1;

然后计算:

sum = nums[i] + nums[left] + nums[right];

分三种情况:

情况 1:sum == 0

说明找到一个答案:

ans.push_back({nums[i], nums[left], nums[right]});

然后:

left++; right--;

继续找下一个答案。


情况 2:sum < 0

说明当前和太小了。

因为数组已经排序,想让和变大,就要让left右移:

left++;

例如:

[-4, -1, -1, 0, 1, 2] i L R sum = -4 + (-1) + 2 = -3

太小了,所以left往右走,找更大的数。


情况 3:sum > 0

说明当前和太大了。

想让和变小,就要让right左移:

right--;

例如:

[-4, -1, -1, 0, 1, 2] i L R sum = -1 + 0 + 2 = 1

太大了,所以right往左走,找更小的数。

用示例完整走一遍

输入:

[-1, 0, 1, 2, -1, -4]

排序:

[-4, -1, -1, 0, 1, 2]

第一轮:固定 nums[i] = -4

i = 0 nums[i] = -4 left = 1 right = 5
[-4, -1, -1, 0, 1, 2] i L R

计算:

sum = -4 + (-1) + 2 = -3

太小,left++

继续:

[-4, -1, -1, 0, 1, 2] i L R
sum = -4 + (-1) + 2 = -3

还是太小,left++

继续:

[-4, -1, -1, 0, 1, 2] i L R
sum = -4 + 0 + 2 = -2

还是太小,left++

继续:

[-4, -1, -1, 0, 1, 2] i L R
sum = -4 + 1 + 2 = -1

还是太小,left++

此时left == right,结束。

这一轮没有答案。


第二轮:固定 nums[i] = -1

[-4, -1, -1, 0, 1, 2] i L R

计算:

sum = -1 + (-1) + 2 = 0

找到答案:

[-1, -1, 2]

然后

left++ right--

变成:

[-4, -1, -1, 0, 1, 2] i L R

计算:

sum = -1 + 0 + 1 = 0

找到答案:

[-1, 0, 1]

继续移动:

left++ right--

此时left >= right,结束。


第三轮:固定 nums[i] = -1

下标变成i = 2,但是

nums[2] == nums[1]

也就是当前的-1和上一个-1重复。

所以跳过。

因为上一轮已经以-1作为第一个数找过答案了。


最终答案:

[[-1, -1, 2], [-1, 0, 1]]

完整代码

std::vector<std::vector<int>> threeSum(std::vector<int>& nums) { std::vector<std::vector<int>> result = {}; // 先对数组进行排序 std::sort(nums.begin(),nums.end()); int L = 1; int R = nums.size() - 1; for (int i = 0; i < nums.size();i++){ if(nums[i] > 0){ break; } if (i > 0 && nums[i] == nums[i-1]) // 跳过重复的第一个数 { continue; } L = i + 1; R = nums.size() - 1; // 在这里赋值是为了防止 nums[i] 和 nums[i-1] 相等时,L 和 R 没有被重置 while (L<R) { int sum = 0; sum = nums[i] + nums[L] + nums[R]; if(sum == 0){ result.push_back({nums[i],nums[L],nums[R]}); // 跳过重复的第二个数 while (L < R && nums[L] == nums[L + 1]) { L++; } // 跳过重复的第三个数 while (L < R && nums[R] == nums[R - 1]) { R--; } // 或者两个while循环合并 // while (L < R && nums[L] == nums[L + 1] && nums[R] == nums[R - 1]) { // L++; R--; // } 会增加常数级别的计算,性能会稍微下降 L++; R--; } else if (sum < 0){ L++; } else{ R--; } } // L=i+2; // R=nums.size() - 1; } return result; }

这里精髓在于三次去重

第一次去重

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

这次去重还是很好理解的。如果 定 后一个数和前一个数一样,则必定是重复的。

第二次去重

这段代码:

while (L < R && nums[L] == nums[L + 1]) { L++; }

意思是:

当前nums[L]已经参与组成过一个答案了,如果右边还有相同的nums[L],继续用它一定会产生重复三元组,所以直接跳过。

例如:

[-2, 0, 0, 0, 2] L

当前L指向第一个0

如果已经用:

[-2, 0, 2]

作为答案,那么后面的0再和-2、2组合,还是:

[-2, 0, 2]

所以要跳过所有重复的0


第三次去重:跳过重复的 R

这段代码:

while (L < R && nums[R] == nums[R - 1]) { R--; }

意思是:

当前nums[R]已经参与组成过一个答案了,如果左边还有相同的nums[R],继续用它一定会产生重复三元组,所以直接跳过。

例如:

[-2, 0, 2, 2, 2] R

当前R指向最后一个2

已经得到:

[-2, 0, 2]

那么前面的那些2再参与组合,也还是:

[-2, 0, 2]

所以要跳过所有重复的2

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 6:12:36

拯救者工具箱:3步掌握联想笔记本的终极性能控制方案

拯救者工具箱&#xff1a;3步掌握联想笔记本的终极性能控制方案 【免费下载链接】LenovoLegionToolkit Lightweight Lenovo Vantage and Hotkeys replacement for Lenovo Legion laptops. 项目地址: https://gitcode.com/gh_mirrors/le/LenovoLegionToolkit 你是否厌倦了…

作者头像 李华
网站建设 2026/5/20 15:50:03

终极指南:如何用Prodigal在3分钟内完成原核生物基因预测

终极指南&#xff1a;如何用Prodigal在3分钟内完成原核生物基因预测 【免费下载链接】Prodigal Prodigal Gene Prediction Software 项目地址: https://gitcode.com/gh_mirrors/pr/Prodigal 还在为复杂的基因预测工具头疼吗&#xff1f;面对海量的微生物基因组数据&…

作者头像 李华
网站建设 2026/5/20 15:49:01

3步掌握Notepad++ Markdown插件:打造高效文档创作工作流

3步掌握Notepad Markdown插件&#xff1a;打造高效文档创作工作流 【免费下载链接】MarkdownViewerPlusPlus A Notepad Plugin to view a Markdown file rendered on-the-fly 项目地址: https://gitcode.com/gh_mirrors/ma/MarkdownViewerPlusPlus &#x1f680; 还在为…

作者头像 李华
网站建设 2026/5/20 15:45:36

用数据校准方向,让实习招聘更有章法

为什么盲目投流不如精准的搜索曝光&#xff1f; 在校招实习的日常招募中&#xff0c;HR常常面临一个困惑&#xff1a;明明岗位薪资和公司平台都不错&#xff0c;为什么搜索量和投递量却迟迟上不去&#xff1f;这往往是因为在信息密度极高的春招季&#xff0c;企业的校招信息被…

作者头像 李华