[LeetCode-中等-排序和双指针] 找出数组中三数之和满足特定条件的三个数
之前,我做了一道简单的LettCode题,二数之和 https://www.cnblogs.com/wphl-27/p/17861539.html, 用的是数组和Map来实现的
现在这个题目是三数之和,题目是这样的
给定一个整数数组 intArr, 判断是否存在三元组 [intArr[i],intArr[j],intArr[k]] 满足条件 i != j, i != k, j != k, 同时满足 intArr[i] + intArr[j] + intArr[k] = 0. 要求返回所有和为0并且不重复的三元组
这里,有一点注意,就是答案不能包含重复的三元组 =》 这是个什么概念呢 , 比如
intArr = [-1,0,1,3,-1,-4] 这里 intArr[0] + intArr[1] + intArr[2] = -1 + 0 + 1 = 0, 所以三元组 [-1,0,1] 但是同时 intArr[1] + intArr[2] + intArr[4] = 0 + 1 + (-1) = 0 这里三元组 是 [0,1,-1]
特别注意: 这里 [-1,0,1] 和 [0,1,-1] 两个三元组里面元素值相同,只是在数组中位置不同,我们认为是重复的三元组,所以只能算一个 =》 这点要特别注意
拿到这个题目,你首先想到的是什么,肯定是最暴力的解法,就是三层循环,对吧
第1层循环: i 从第1个元素到数组最后一个元素
第2层循环: j 从第(i+1)个元素到数组最后一个元素
第3层循环: k 从第(j+1)个元素到数组最后一个元素
只要 intArr[i] + intArr[j] + intArr[k] = 0, 就是满足条件的。 但是这种情况,还是会存在上面说的我们重复的三元组,比如 [-1,0,1] 和 [0,1,-1]这种情况
那么,如何来避免出现这种重复的三元组呢,你能想到办法么?
办法就是: 把原始数组从小到大排序,比如我们上面的例子,排完序后为 [-1,-1,0,1,3,-4],排完序能保证什么呢 =>【如果数组中存在相同的元素,排完序能够保证相同的2个元素一定是相邻的 】。 那么我们要保证最后的结果中不能出现以下情况:
[-1,-1,0,1,3,-4] =》 i = 0, j = 2, k = 3 结果 (-1,0,1) 第1层循环枚举到第一个元素 -1
[-1,-1,0,1,3,-4] =》 i = 1, j = 2, k = 3 结果 (-1,0,1) 第1层循环枚举到第二个元素 -1 => 和上次枚举到的第一个元素相同,我们需要跳过这一步,直到寻找到下一个不同的元素0
上面这两次结果完全相同,如何避免出现这种情况呢, 办法就是: 针对排完序之后的数组[-1,-1,0,1,3,-4], 每一层循环中,如果发现这次枚举到的元素和上一次枚举到的元素相同,那就果断放弃这次枚举,跳到下一轮循环
我们再来看一个例子理解: 假如排完序之后的数组为 [0,1,1,2,2,2,3] 第一层循环走到第2个元素时,发现它和第1个元素相同,都是1。 显然,这个时候,你在第一层循环走到第2个元素1时,后面第2层第三层循环走过的路,比如(1,2,2)等,在你上次第一层循环走到第1个元素1时,都已经走过了,所以造成重复。 所以,在一个有序数组中,我们在同一层循环中,一旦发现这次枚举到的元素和上一次枚举到的元素值相同,就果断skip掉它
基于这个分析,我们来写出最暴力的解法C++
class Solution { public: vector<vector<int>> GetThreeSum(vector<int>& arrs) { int arrSize = arrs.size(); if (arrSize < 3) return {}; sort(arrs.begin(), arrs.end()); //对数组排序 vector<vector<int>> rtnArr; //返回的数组 for (int i = 0; i < arrSize; i++) //第1层循环 { if (arrs[i] > 0) return rtnArr; // 数组是从小到大排序的,如果第一个数就大于0,后面的都是递增的正数,不可能最后结果相加为0 if (i > 0 && arrs[i] == arrs[i - 1]) continue; for (int j = i + 1; j < arrSize; j++) //第2层循环 { if (j > i + 1 && arrs[j] == arrs[j - 1]) continue; for (int k = j + 1; k < arrSize; k++) //第3层循环 { if (k > j + 1 && arrs[k] == arrs[k - 1]) continue; if (arrs[i] + arrs[j] + arrs[k] == 0) rtnArr.push_back(arrs[i], arrs[j], arrs[k]); } } } return rtrArr; } };
这种方法确实可以解决这个问题,但3个循环,时间复杂度为O(N*N*N). 有没有什么方法来优化呢 => 我们需要换一个思路,在第1层循环之后,比如
比如针对数组 [-1,-1,0,1,3,-4]
for (int i = 0; i < arrSize; i++) //第1层循环 {
我们需要在数组中位置在i之后的元素中,找到两个元素,这两个元素的和等于 (0-arrSize[i]). 而数组中位置为i之后的元素是有序的 =》 问题就变成了在一个有序数组中,找到和为某个值的两个数 => 会不会想到用双指针来实现,左指针从i+1的位置从左到右移动,右指针从数组最后一个位置
往左移动,直到两个指针相遇,找出这个移动过程中和等于(0-arrSize[i])的两个元素就可以
}
C++ 代码修改如下:
public: vector<vector<int>> threeSum(vector<int>& arrs) { int arrSize = nums.size(); if (arrSize < 3) return {}; vector<vector<int>> rtnArr; // 返回的数组 std::sort(nums.begin(), nums.end());// 对数组排序(默认递增) for (int i = 0; i < arrSize; i++) // 第1轮循环 { if (arrs[i] > 0) return rtnArr; // 数组是从小到大排序的,如果第一个数就大于0,后面的都是递增的正数,不可能最后结果相加为0 // 去重:如果此数已经选取过,跳过 if (i > 0 && arrs[i] == arrs[i - 1]) continue; // 左右双指针在arrs[i]后面的区间中寻找和为0-arrs[i]的另外两个数 int left = i + 1; int right = arrSize - 1; while (left < right) { if (arrs[left] + arrs[right] > -arrs[i]) right--; // 两数之和太大,右指针左移 else if (arrs[left] + arrs[right] < -arrs[i]) left++; // 两数之和太小,左指针右移 else { // 找到一个和为零的三元组,添加到结果中,左右指针内缩,继续寻找 rtnArr.push_back(vector<int>{arrs[i], arrs[left], arrs[right]}); left++; right--; // 去重: 例如:[-4,1,1,1,2,3,3,3], i=0, left=1, right=5 第二个数和第三个数也不重复选取 while (left < right && arrs[left] == arrs[left - 1]) left++; while (left < right && arrs[right] == arrs[right + 1]) right--; } } } return rtnArr; }