【哈希表】leetcode15. 三数之和(C/C++/Java/Python/Js)--梦破碎的地方
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
leetcode15. 三数之和--梦破碎的地方
用哈希表解决了两数之和 那么三数之和呢
1 题目
给你一个整数数组 nums 判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != 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
2 思路
可以结合Carl老师的视频讲解
梦破碎的地方| LeetCode15.三数之和
思路整理于Carl老师的《代码随想录》
2.1 哈希解法–含代码
两层for循环就可以确定 a 和b 的数值了可以使用哈希法来确定 0-(a+b) 是否在 数组里出现过。
其实这个思路是正确的但是我们有一个非常棘手的问题就是题目中说的不可以包含重复的三元组。
把符合条件的三元组放进vector中然后再去重这样是非常费时的很容易超时也是这道题目通过率如此之低的根源所在。
去重的过程不好处理有很多小细节如果在面试中很难想到位。
时间复杂度可以做到O(n^2)但还是比较费时的因为不好做剪枝操作。
- 哈希法C++代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
2.2 双指针
其实这道题目使用哈希法并不十分合适因为在去重的操作中有很多细节需要注意在面试中很难直接写出没有bug的代码。
而且使用哈希法 在使用两层for循环的时候能做的剪枝操作很有限虽然时间复杂度是O(n^2)也是可以在leetcode上通过但是程序的执行时间依然比较长 。
接下来我来介绍另一个解法双指针法这道题目使用双指针法 要比哈希法高效一些那么来讲解一下具体实现的思路。
动画效果如下
动画展示
拿这个nums数组来举例首先将数组排序然后有一层for循环i从下标0的地方开始同时定一个下标left 定义在i+1的位置上定义下标right 在数组结尾的位置上。
依然还是在数组中找到 abc 使得a + b +c =0我们这里相当于 a = nums[i]b = nums[left]c = nums[right]。
接下来如何移动left 和right呢
如果nums[i] + nums[left] + nums[right] > 0
就说明 此时三数之和大了因为数组是排序后了所以 right下标就应该向左移动这样才能让三数之和小一些
如果 nums[i] + nums[left] + nums[right] < 0
说明 此时 三数之和小了left 就向右移动才能让三数之和大一些直到left与right相遇为止。
- 时间复杂度O(n^2)。
2.3 去重逻辑的思考
说道去重其实主要考虑三个数的去重。 a, b ,c, 对应的就是 nums[i]nums[left]nums[right]
2.3.1 a的去重
a 如果重复了怎么办a是nums里遍历的元素那么应该直接跳过去。
但这里有一个问题是判断 nums[i] 与 nums[i + 1]是否相同
还是判断 nums[i] 与 nums[i-1] 是否相同
。
- 有什么区别吗
- 还真有
都是和 nums[i]进行比较是比较它的前一个还是比较他的后一个。
如果我们的写法是 这样
if (nums[i] == nums[i + 1]) { // 去重操作
continue;
}
那就我们就把 三元组中出现重复元素的情况直接pass掉了。 例如 {-1, -1 ,2}
这组数据当遍历到第一个-1 的时候判断 下一个也是-1那这组数据就pass了。
我们要做的是 不能有重复的三元组但三元组内的元素是可以重复的
那么应该这么写
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
这么写就是当前使用 nums[i]我们判断前一位是不是一样的元素在看 {-1, -1 ,2} 这组数据当遍历到 第一个 -1 的时候只要前一位没有-1那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。
这是一个非常细节的思考过程。
2.3.2 b与c的去重
很多同学写本题的时候去重的逻辑多加了 对right 和left 的去重
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
// 去重 right
while (left < right && nums[right] == nums[right + 1]) right--;
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
// 去重 left
while (left < right && nums[left] == nums[left - 1]) left++;
} else {
}
}
但细想一下这种去重其实对提升程序运行效率是没有帮助的。
拿right去重为例即使不加这个去重逻辑依然根据 while (right > left) 和 if (nums[i] + nums[left] + nums[right] > 0) 去完成right-- 的操作。
多加了 while (left < right && nums[right] == nums[right + 1]) right–; 这一行代码其实就是把 需要执行的逻辑提前执行了但并没有减少 判断的逻辑。
最直白的思考过程就是right还是一个数一个数的减下去的所以在哪里减的都是一样的。
所以这种去重 是可以不加的。 仅仅是 把去重的逻辑提前了而已。
3 代码–双指针法
3.1 C++版本
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// a = nums[i], b = nums[left], c = nums[right]
int a, b, c, left, right;
for (int i = 0; i < nums.size(); i++) {
a = nums[i];
if (a > 0) //排序之后的第一个元素大于零那么之后的三元组相加都不可能为0
return result;
if (i > 0 && nums[i] == nums[i-1]) //对a的去重
continue;
left = i + 1;
right = nums.size() - 1;
while (left < right) {
b = nums[left];
c = nums[right];
if (a + b + c > 0) //说明大了right收缩
right--;
else if (a + b + c < 0) //说明小了left右移
left++;
else { //找到a+b+c=0的三元组之后对b和c去重
result.push_back(vector<int>{a, b, c});
while (left < right && nums[right] == nums[right - 1])
right--;
while (left < right && nums[left] == nums[left + 1])
left++;
// 找到答案时双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
3.2 C版本
//qsort辅助cmp函数
int cmp(const void* ptr1, const void* ptr2) {
return *((int*)ptr1) > *((int*)ptr2);
}
int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
//开辟ans数组空间
int **ans = (int**)malloc(sizeof(int*) * 18000);
int ansTop = 0;
//若传入nums数组大小小于3则需要返回数组大小为0
if(numsSize < 3) {
*returnSize = 0;
return ans;
}
//对nums数组进行排序
qsort(nums, numsSize, sizeof(int), cmp);
int i;
//用for循环遍历数组结束条件为i < numsSize - 2(因为要预留左右指针的位置)
for(i = 0; i < numsSize - 2; i++) {
//若当前i指向元素>0则代表left和right以及i的和大于0。直接break
if(nums[i] > 0)
break;
//去重i > 0 && nums[i] == nums[i-1]
if(i > 0 && nums[i] == nums[i-1])
continue;
//定义左指针和右指针
int left = i + 1;
int right = numsSize - 1;
//当右指针比左指针大时进行循环
while(right > left) {
//求出三数之和
int sum = nums[right] + nums[left] + nums[i];
//若和小于0则左指针+1因为左指针右边的数比当前所指元素大
if(sum < 0)
left++;
//若和大于0则将右指针-1
else if(sum > 0)
right--;
//若和等于0
else {
//开辟一个大小为3的数组空间存入nums[i], nums[left]和nums[right]
int* arr = (int*)malloc(sizeof(int) * 3);
arr[0] = nums[i];
arr[1] = nums[left];
arr[2] = nums[right];
//将开辟数组存入ans中
ans[ansTop++] = arr;
//去重
while(right > left && nums[right] == nums[right - 1])
right--;
while(left < right && nums[left] == nums[left + 1])
left++;
//更新左右指针
left++;
right--;
}
}
}
//设定返回的数组大小
*returnSize = ansTop;
*returnColumnSizes = (int*)malloc(sizeof(int) * ansTop);
int z;
for(z = 0; z < ansTop; z++) {
(*returnColumnSizes)[z] = 3;
}
return ans;
}
3.3 Java版本
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0)
return result;
if (i > 0 && nums[i - 1] == nums[i])
continue;
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0)
right--;
else if (sum < 0)
left++;
else {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[right] == nums[right - 1]) right--;
while (left < right && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
}
}
3.4 Python3版本
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
if len(nums) < 3:
return []
nums, result = sorted(nums), []
for i in range(len(nums) - 2):
a, l, r = nums[i], i + 1, len(nums) - 1
if result != [] and result[-1][0] == a:
continue
while l < r:
sum = a + nums[l] + nums[r]
if sum > 0:
r -= 1
elif sum < 0:
l += 1
else:
result.append([a, nums[l], nums[r]])
while l < r and nums[l] == nums[l + 1]:
l += 1
while l < r and nums[r] == nums[r - 1]:
r -= 1
l += 1
r -= 1
return result
3.5 JavaScript版本
var threeSum = function(nums) {
const res = [], len = nums.length
// 将数组排序
nums.sort((a, b) => a - b)
for (let i = 0; i < len; i++) {
let l = i + 1, r = len - 1, iNum = nums[i]
// 数组排过序如果第一个数大于0直接返回res
if (iNum > 0) return res
// 去重
if (iNum == nums[i - 1]) continue
while(l < r) {
let lNum = nums[l], rNum = nums[r], threeSum = iNum + lNum + rNum
// 三数之和小于0则左指针向右移动
if (threeSum < 0) l++
else if (threeSum > 0) r--
else {
res.push([iNum, lNum, rNum])
// 去重
while(l < r && nums[l] == nums[l + 1]){
l++
}
while(l < r && nums[r] == nums[r - 1]) {
r--
}
l++
r--
}
}
}
return res
};
4 总结
双指针法妙哉
可以结合Carl老师的视频讲解
梦破碎的地方| LeetCode15.三数之和
思路整理于Carl老师的《代码随想录》
这道题细节也很多对于去重逻辑的思考十分重要。
By – Suki 2023/2/1