15. 三数之和
经典的多数之和题
这个题之前做过一遍,花了挺久的时间才搞懂,这次重新做能够快速想到思路,但是还是存在细节问题。这题的主要难点是如何去重
基本思路:首先,想暴力解,暴力解就是将所有可能的组合均遍历一遍,那么时间复杂度为: O ( n ∗ ( n − 1 ) ∗ ( n − 2 ) ) = O ( N 3 ) O(n *(n - 1)*(n-2))=O(N^3) O(n∗(n−1)∗(n−2))=O(N3)
这样解,肯定不行,所以需要转变思路,将暴力解优化一下,由于最大的复杂度是 O ( N 3 ) O(N^3) O(N3),那么排序一下也不会影响,所以先sort,然后就能想到针对有序数组,是否可以用二分查找——确定一个数,然后针对这个数后面的子数组,看能否找到符合条件的两个值
所以,我们设定三个变量:i:用来遍历的,left是子数组的头,right是子数组的尾。每次都是确定当前的i,然后再二分查找:二分查找的移动条件是:
- 如果nums[i] + nums[left] + nums[right] > 0,说明right太大,仅移动right
- 如果 <0,说明left太小,仅移动left
- 如果=0,说明是解,那么将其取出,并且同时移动left和right(要同时移动,如果仅移动left or right,如果三数之和仍为0,那么肯定是重复解,即一旦确定两个数,另外一个数的值是确定的)
但是,还存在的是重复解问题:重复存在于2个方面
-
i:前一个值i-1,和当前值i,如果其中一个存在重复,那么他们的解是会存在重复的——需要判断i和i-1的值一样,说明上一次循环已经查找过了,那么i就跳过
-
left和right中:如果当前的left和right是满足要求的解,left++,right–之后,如果指向的值还是一样的,那么必然出现重复解
eg: -1,-1,-1,0,2,2,可以发现:i=0, left=1, right=5,-1±1+2=0,是一组解;而left++=2,right–=4,那么还是指向了-1,2,所以出现重复
——需要left、right循环移动上和上次结果不一样的地方
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums.length < 3) return res; // 去除边界情况
Arrays.sort(nums);
for(int i = 0; i < nums.length - 2; i++){ // 固定第一个数组
if(i != 0 && nums[i] == nums[i - 1]) continue; // 如果当前结点的值和前面一个的值一样,跳过这次循环(去重)
int left = i + 1, right = nums.length - 1;
while(left < right){
int temp = nums[i] + nums[left] + nums[right];
if(temp == 0){
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
left++;
right--;
while(left < right && nums[left - 1] == nums[left]) left++; // 除去可能重复的解
while(left < right && nums[right + 1] == nums[right]) right--;
}
else if(temp < 0) left++;
else right--;
}
}
return res;
}
}
时间复杂度 O ( N 2 ) O(N^2) O(N2)
一般做过一遍,碰到同样的题能够想到思路,但是细节还是要好好打磨的
18. 四数之和
本题几乎同上题,就是在3数之和外面再套一层循环,本质上还是双指针
当然,最关键的还是去重的问题,外面新增的那一层还是要去重,其余内部均一致。
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
if(nums.length < 4) return res; // 排除特殊情况
Arrays.sort(nums); // 为了用双指针,先需要排序
for(int i = 0; i < nums.length - 3; i++){
if(i != 0 && nums[i] == nums[i - 1]) continue; // 去重
for(int j = i + 1; j < nums.length - 2; j++){
if(j != i + 1 && nums[j] == nums[j - 1])continue; // 去重
int left = j + 1, right = nums.length - 1;
while(left < right){
int temp = nums[i] + nums[j] + nums[left] + nums[right];
if(temp == target){
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
left++;
right--;
while(left < right && nums[left] == nums[left - 1]) left++;
while(left < right && nums[right] == nums[right + 1]) right--;
}
else if(temp < target) left++;
else right--;
}
}
}
return res;
}
}
16. 最接近的三数之和
本质思想还是双指针,但是变化的是指针的移动条件
具体看代码,因为不需要去重所以难度并不大
class Solution {
public int threeSumClosest(int[] nums, int target) {
int res = target, delta = Integer.MAX_VALUE;
Arrays.sort(nums);
for(int i = 0; i < nums.length - 2; i++){
if(i != 0 && nums[i] == nums[i - 1]) continue; // 去个重
int left = i + 1, right = nums.length - 1;
while(left < right){
int temp = nums[i] + nums[left] + nums[right];
if(temp == target) return target; // 找到目标值了,直接返回
if(Math.abs(target - temp) < delta){ // 如果差值较小,那么更新差值
delta = Math.abs(target - temp);
res = temp;
}
if(temp < target) left++; // 如果结果太小,移动left;
if(temp > target) right--; // 结果太大,移动right
}
}
return res;
}
}
题解对双指针进行优化:进行了3点优化,我觉得比较有价值的是第2点,每次循环中,都去获取当前的最大值和最小值,最大值出现在:nums[i] + nums[right - 1] + nums[right] (right = len - 1),最小值出现在nums[i] + nums[left] + nums[left+1] (left=i+1),先看如果target比最大值还大,那么最大值一定是最接近的值;如果target比最小值还小,那么最小值一定是最接近的值,那么就能提前结束本次循环。
参考:
https://leetcode-cn.com/problems/3sum-closest/solution/dui-shuang-zhi-zhen-fa-jin-xing-yi-dian-you-hua-da/
454. 四数相加 II
这题其实和上面3题均不同,反而因为惯性思维,导致做不出来(我就是这样 😦
分析该题,同样是求4数之和,这次是将四个数分别放在4个数组中,那么即使是对每个数组排序,也没法利用双指针进行遍历。
所以,这条路行不通。
能想到的是:暴力解,遍历所有可能的组合,时间复杂度为 O ( N 4 ) O(N^4) O(N4)
根据参考的题解,想到,4个数相加,可以划分成3数之和 +1数,也可以划分成2数之和+2数之和,并且这边由于分在4个数组中,所以3数之和也无法优化,时间复杂度为 O ( N 3 ) O(N^3) O(N3),所以还是将其划分成 2数之和 + 2数之和
A和B组合,求出所有可能的解,存放在哈希表中,C和D组合,求出所有可能的解,看和AB之和哪个能够对上:
注意不能用hashSet,因为不同的index下标的解是不同的
eg:[-1, -1], [-1, 1], [-1, 1], [1, -1]
A+B存在:[-2, 0, -2, 0], C+D存在:[0, -2, 2, 0],他们之间的组合对应的index都是不同的,所以都需要记录
所以A+B在求解时,如果遇到相同的解需要记录出现的次数,而C+D在遍历中,如果出现解,需要加上的是A+B解出现的次数
class Solution {
public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
int len = A.length;
int res = 0;
HashMap<Integer, Integer> store = new HashMap<>(); // 存放A、B之和
for(int i = 0; i < len; i++){ // 找到A、B所有可能的解,放入哈希集合中
for(int j = 0; j < len; j++){
int sum = A[i] + B[j];
if(store.containsKey(sum)){
store.put(sum, store.get(sum) + 1); // 如果和一样,需要记录出现的次数
}
else{
store.put(sum, 1);
}
}
}
for(int i = 0; i < len; i++){
for(int j = 0; j < len; j++){
int temp = C[i] + D[j];
if(store.containsKey(-temp)) res += store.get(-temp);
}
}
return res;
}
}
总结:这题还是有很多技巧的,之前做过一遍还是没有记住。
参考:
- https://leetcode-cn.com/problems/4sum-ii/solution/si-shu-xiang-jia-ii-by-leetcode-solution/
- https://leetcode-cn.com/problems/4sum-ii/solution/chao-ji-rong-yi-li-jie-de-fang-fa-si-shu-xiang-jia/