454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和
454.四数相加II
1.思路
这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少!
如果本题想难度升级:就是给出一个数组(而不是四个数组),在这里找出四个元素相加等于0,答案中不可以包含重复的四元组.
本题解题步骤:
首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
最后返回统计值 count 就可以了
The provided Java code is a solution to a problem where the goal is to count the number of tuples (i, j, k, l) such that nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0. Here, nums1, nums2, nums3, and nums4 are four integer arrays.
The code implements a two-step process using a hash map to store intermediate sums and their frequency. Here’s a breakdown of the algorithm:
Create a Hash Map for Sums of Pairs from nums1 and nums2:
It iterates over all elements i from nums1 and all elements j from nums2.
For each pair (i, j), it calculates the sum sum = i + j.
The map is updated such that map[sum] is incremented by 1. If sum is not already in the map, it’s added with a default value of 0, which is then incremented to 1. This is done using the method map.getOrDefault(sum, 0) + 1, which gets the current count of sum from the map, or defaults to 0 if sum is not yet a key in the map.
Essentially, after this loop, map contains the frequency of each possible sum of elements from nums1 and nums2.
Iterate Over nums3 and nums4 to Find Complements:
Now, for every pair (i, j) from nums3 and nums4, the algorithm looks for the negation of their sum (0 - i - j) in the map.
If the complement (0 - i - j) exists in map, it means there are map.get(0 - i - j) pairs from nums1 and nums2 that can form a quadruplet with the current pair (i, j) such that their total sum is 0.
count is incremented by the frequency of the complement, which represents the number of valid quadruplets involving the current pair (i, j) from nums3 and nums4.
Return the Total Count:
After checking all pairs from nums3 and nums4, the total count of valid quadruplets is returned.
By using a hash map to store the intermediate sums, this solution avoids the need to compute the sum of four numbers for every possible quadruplet, thereby reducing the overall complexity. This algorithm has a time complexity of O(n^2), where n is the number of elements in each input array, assuming the hash map operations take constant time on average. This is significantly more efficient than the naive approach, which would have a time complexity of O(n^4) if we were to check all possible quadruplets directly.
2.代码实现
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
Map<Integer,Integer> map= new HashMap<>();//存放a+b,和a+b出现的次数
int count=0;
//a+b=A
for(int i: nums1)
for(int j: nums2){
int temp=i+j;
if(map.containsKey(temp))//如果a+b再次出现,则value+1
map.put(temp,map.get(temp)+1);
else//a+b未出现过,插入map
map.put(temp,1);
}
//0-(c+d)=a+b
for(int i:nums3)
for(int j: nums4){
int temp=0-(i+j);
if(map.containsKey(temp))//a+b+c+d=0,则count+map.value,即这个a+b出现过的次数
count+=map.get(temp);
}
return count;
}
}
383. 赎金信
1.思路
方法一:暴力算法 -两层for循环遍历magazine和ransomNote,如果有ransormNote的值和magazine相等,则从ransomNote中删除该元素,遍历magazine的下一个值。
方法二:
因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。
然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
依然是数组在哈希法中的应用。
一些同学可能想,用数组干啥,都用map完事了,其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!
2.代码实现
//暴力算法
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
for(int i=0;i< magazine.length();i++)
for(int j=0;j<ransomNote.length();j++){
if(magazine.charAt(i)==ransomNote.charAt(j))
{
if(j==0){
ransomNote=ransomNote.substring(j+1);
}
else{
ransomNote=ransomNote.substring(0,j)+ransomNote.substring(j+1);
break;
}
}
// 如果ransomNote为空,则说明magazine的字符可以组成ransomNote
if (ransomNote.length() == 0) {
return true;
}
return false;
}
}
//哈希表算法
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
int[] record= new int[26];
//统计magazine中各字母出现的次数
for(char c: magazine.toCharArray())
record[c-'a']+=1;
for(char c: ransomNote.toCharArray())
record[c-'a']-=1;
for(int i:record)
if(i<0)//ransomNote中出现magazine中不存在的元素
return false;
return true;
}
}
15. 三数之和
1.思路
1.和两数之和一样,用哈希法做。
两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。
把符合条件的三元组放进vector中,然后再去重,这样是非常费时的,很容易超时,也是这道题目通过率如此之低的根源所在。
去重的过程不好处理,有很多小细节,如果在面试中很难想到位。
时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。
2.双指针法
首先将数组排序,然后有一层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相遇为止。
关键是去重的逻辑
是
if (nums[i] == nums[i + 1]) {
continue;
}
还是
if (nums[i] == nums[i -1]) {
continue;
}
如果是第一种写法会漏掉 -1,-1,2 这组数据,去重要保证先至少有一个结果,再去重
考虑到数组下标异常,应该这样写:
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
包括左右指针去重代码部分所放的位置也是一个易错点,要保证至少有一个结果,再裁剪。
时间复杂度:O(n^2)。
Sort the Array: The array is sorted to streamline the subsequent two-pointer traversal and facilitate the skipping of duplicate triplets.
Iterate Over the Array Elements: An outer loop iterates over each element nums[i], treating it as the first element of the potential triplet. This loop effectively fixes the first number, allowing the search for the remaining two numbers using a two-pointer approach.
Early Return and Skip Duplicates:
If the current element nums[i] is greater than 0, the loop can return the result immediately since, after sorting, all subsequent numbers will also be greater than 0, making it impossible to find a triplet that sums to zero.
If the current element is the same as the previous one, the loop continues to the next iteration to avoid duplicates.
Two-Pointer Search: Two pointers, left and right, are initialized to point to the elements immediately following nums[i] and to the last element of the array, respectively. The two pointers are used to identify the remaining two numbers of the triplet.
Two-Pointer Movement: Inside a while loop, the pointers are moved based on the sum of nums[i], nums[left], and nums[right]:
If the sum is greater than 0, it means the sum is too large, so the right pointer is decremented to reduce the sum.
If the sum is less than 0, the sum is too small, and the left pointer is incremented to increase the sum.
If the sum equals 0, a valid triplet has been found, and it’s added to the result list. After that, both pointers are moved past any duplicate elements to avoid adding duplicate triplets to the result list.
Duplicate Avoidance: Once a zero-sum triplet is found, to avoid adding duplicates to the result, the code checks for and skips over any subsequent identical elements by adjusting the left and right pointers accordingly.
Return the Result: Once the outer loop is complete, the result list will contain all unique triplets, and it is then returned.
思考
既然三数之和可以使用双指针法,我们之前讲过的1.两数之和 (opens new window),可不可以使用双指针法呢?
如果不能,题意如何更改就可以使用双指针法呢? 大家留言说出自己的想法吧!
两数之和 就不能使用双指针法,因为1.两数之和 (opens new window)要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。
如果1.两数之和 (opens new window)要求返回的是数值的话,就可以使用双指针法了。
2.代码实现
实现的时候有一点错误,在修剪左右指针的时候,判断 nums[right]==nums[right-1]和nums[left]==nums[left+1]时应该用while不应该用if,然后加上循环终止条件。
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]==nums[i-1]){
continue;
}
else
{
int left=i+1;
int right=nums.length-1;
while(left<right){
if(nums[i]+nums[left]+nums[right]>0)
right--;
else if(nums[i]+nums[left]+nums[right]<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++;
//不管是否做裁剪,存在满足的值都应该移动指针
left++;
right--;
}
}
}
}
return result;
}
}
18. 四数之和
1.思路
四数之和,和15.三数之和 (opens new window)是一个思路,都是使用双指针法, 基本解法就是在15.三数之和 (opens new window)的基础上再套一层for循环。
但是有一些细节需要注意,例如: 不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了。
15.三数之和 (opens new window)的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下标作为双指针,找到nums[i] + nums[left] + nums[right] ==0。
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。
那么一样的道理,五数之和、六数之和等等都采用这种解法。
对于15.三数之和 (opens new window)双指针法就是将原本暴力O(n3)的解法,降为O(n2)的解法,四数之和的双指针解法就是将原本暴力O(n4)的解法,降为O(n3)的解法。
之前做过哈希表的经典题目:454.四数相加II (opens new window),相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。
而454.四数相加II (opens new window)是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于本题还是简单了不少!
双指针法将时间复杂度:O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级
新问题:
第一个剪枝操作用break return都可以,
第二个循环里的剪枝操作不可以用return,因为如果return 了 第一个循环也被终止了
2.代码实现
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result= new ArrayList<>();
Arrays.sort(nums);
for(int i=0; i<nums.length;i++){
//一级剪枝 nums[i]>target直接返回
if(nums[i]>target && nums[i]>0){
break;//统一在循环外返回
}
//去重
if(i>0 && nums[i]==nums[i-1])
continue;
for(int j=i+1;j<nums.length;j++){
//二级剪枝
if(nums[i]+nums[j]>target && nums[i]+nums[j]>0)
break;
//去重
if(j>i+1 && nums[j]==nums[j-1])
continue;
int left=j+1;
int right=nums.length-1;
while(left<right){
long sum=nums[i]+nums[j]+nums[left]+nums[right];
if(sum>target)
right--;
else if(sum<target)
left++;
else
{
result.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--;
left++;
right--;
}
}
}
}
return result;
}
}