精选专栏链接 🔗
欢迎订阅,点赞+关注,每日精进1%,与百万开发者共攀技术珠峰
更多内容持续更新中~
【LeetCode 热题 100】三数之和
📝题目描述
给你一个整数数组 nums ,判断是否存在三元组 nums\[i, numsj, numsk] 满足 i != j、i != k 且 j != k ,同时还满足 numsi + numsj + numsk == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
bash
输入: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:
bash
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
bash
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
💡提示信息
- 3 <= nums.length <= 3000;
- − 10 5 -10^5 −105 <= numsi <= 10 5 10^5 105;
这道题最直接的暴力解法是使用三层循环遍历所有可能的组合,但这样时间复杂度会达到
O ( N 3 ) O(N^3) O(N3) ,在数据量较大时会超时。因此我们需要一种更高效的算法。
解题:排序 + 双指针
核心思路:
-
排序。首先将数组从小到大排序。排序有两个好处:
- 方便我们后续使用双指针;
- 方便我们
去重。因为数组有序,相同的数字会挨在一起,我们可以通过比较相邻元素来跳过重复项;
-
固定一个数,并使用双指针寻找另外两个数:
- 我们遍历数组,固定第一个数
nums[i]; - 问题就转化为了在
i之后的区间内,寻找两个数nums[left]和nums[right],使得它们的和等于-nums[i](即nums[i] + nums[left] + nums[right] == 0); - 定义左指针
left = i + 1,右指针right = nums.length - 1;
- 我们遍历数组,固定第一个数
-
移动指针逻辑:
- 如果
sum == 0:找到了一组解,加入结果集。然后left右移,right左移。 - 如果
sum < 0:说明总和太小了,需要增大,因此将左指针left右移(因为数组有序,右边的数更大)。 - 如果
sum > 0:说明总和太大了,需要减小,因此将右指针right左移。
- 如果
-
关键去重(Deduplication):
- 对
i去重: 如果nums[i] == nums[i-1],说明这个数字作为第一个数已经处理过了,直接跳过,避免重复结果。 - 对
left和right去重: 当找到一组解后,在移动left和right之前,需要跳过所有与当前值相同的元素。
- 对
Java 代码实现如下 :
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
// 1. 校验:如果数组为空或长度小于3,直接返回空
if (nums == null || nums.length < 3) {
return result;
}
// 2. 排序:这是使用双指针的前提
Arrays.sort(nums);
int n = nums.length;
// 3. 遍历第一个数
for (int i = 0; i < n; i++) {
// 剪枝优化:如果第一个数都大于0,后面的数肯定也大于0,肯定找不到三数之和为0
if (nums[i] > 0) {
break;
}
// 4. 去重逻辑 :跳过相同的第一个数
// 注意:i > 0 是为了防止数组越界,nums[i] == nums[i-1] 说明之前已经处理过该数字
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 5. 双指针寻找剩下的两个数
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
// 找到解,加入结果集
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 6. 去重逻辑 :跳过相同的 left 和 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) {
// 和小于0,说明需要更大的数,左指针右移
left++;
} else {
// 和大于0,说明需要更小的数,右指针左移
right--;
}
}
}
return result;
}
}
注意:
为什么 i 的去重是
nums[i] == nums[i-1],而 left 的去重是nums[left] == nums[left+1]?
- 对于 i (外层循环),我们是在处理当前 i 之前判断是否和上一个重复。因为如果 numsi == numsi-1,说明 numsi-1 已经作为第一个数,把所有可能的组合都找过了,numsi 直接跳过即可。因此逻辑为:
if (i > 0 && nums[i] == nums[i-1]) continue; - 对于 left 和 right (内层双指针),我们是在找到一组解之后,为了跳过接下来重复的元素。比如数组 -2, 0, 0, 2,当 left 指向第一个 0 时找到了解,此时如果 numsleft == numsleft+1,我们必须把 left 移动到最后一个重复元素的下一个位置。
因此去重逻辑为:while (left < right && nums[left] == nums[left+1]) left++;
提交代码,运行结果如下:

总结
算法复杂度分析
-
时间复杂度: O ( N 2 ) O(N^2) O(N2)
- 数组排序的时间复杂度是 O ( N log N ) O(N \log N) O(NlogN);
- 双重循环(外层遍历
i,内层双指针while)的时间复杂度是 O ( N 2 ) O(N^2) O(N2); - 总体复杂度由 O ( N 2 ) O(N^2) O(N2) 主导;
-
空间复杂度: O ( log N ) O(\log N) O(logN)
- 我们忽略存储答案的空间;
- 主要消耗在于排序算法所使用的栈空间(Java 的
Arrays.sort内部对于基本类型通常使用双轴快速排序,空间复杂度为 O ( log N ) O(\log N) O(logN));
这道题是 "排序 + 双指针" 的经典应用,核心考点在于:利用排序预处理 将三数之和降维转化为两数之和问题;通过双指针相向移动 将时间复杂度从暴力的 O ( N 3 ) O(N^3) O(N3) 优化至 O ( N 2 ) O(N^2) O(N2);同时极度考验对边界去重逻辑 (固定位与双指针的双重去重)及剪枝优化的严谨处理能力。
希望这篇解析能帮你彻底掌握"三数之和"!如果有疑问,欢迎在评论区留言讨论。