454、四数相加
假设:目标数组分别为A\B\C\D
分析:本题要求返回一个四元组,该组所有元素求和的结果是0,那么直接的想法就是暴力求解了,时间复杂度会达到O(n^4)这样一个数量级,但是如果能够先对数组AB进行遍历,并用一个容器存储求和的结果以及相同的元素出现的次数(这样做的目的是要满足题目的条件,即重复的结果也算),之后还要在遍历CD的时候通过计算查询该容器是否有出现过想要的值(0-(c+d)),自然想到使用unordered_map,求和的结果作为查询的索引KEY,出现的次数作为value
KEY:
使用迭代器来查询unordered_map中是否有所需元素,若返回的结果不为尾后迭代器,那么查询成功。
让计数器的值加上map中该KEY的value。
总结:本题使用了先对AB进行遍历再对CD进行遍历的操作,这是因为题目要求返回的是四数和为0的结果数量,而不是具体结果,所以可以用这种巧妙的方式来做。
代码:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
//这道题目选用先遍历A,B,再遍历C,D的方式来求解
//由于要计算元组的个数(包含重复的),因此既需要统计AB中元素的和的值,还需要统计它们出现的次数
//为了便于后面查询数值,选用unordered_map
unordered_map<int,int>map;
for(int a:nums1){
for(int b:nums2){
map[a+b]++;//若a+b的结果未出现过,则插入容器,否则容器的值加加
}
}
int count=0;
for(int c:nums3){
for(int d:nums4){
auto iter=map.find(0-(c+d));
if(iter!=map.end()){
count+=iter->second;
}
}
}
return count;
}
};
383、赎金信
思路:之前做过一道字母异位词,要求两个字符串能否相互组成,现在这道题目的实质是要求一个字符串a是否能由另一个字符串b的元素构成,但是字符串b不需要由字符串a构成。因此,同样可以通过统计字符出现的频率来计算结果。
先统计字符串a中各字符出现的频率并将其结果保存到数组中,再统计字符串b的字符出现的频率,只不过做的是减减的操作,如果数组中有某个元素的值在减减后小于0了,那么说明字符串a无法表示字符串b,return false,否则,如果遍历完字符串b后,没有出现该情况,证明字符串a可以表示字符串b,return true。
总结:对比字母异位词,可以发现,当容量较小,且存储的信息只有一个数值时,使用数组来解决问题是简单直接有效的。
代码:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26]={0};
for(int i=0;i<magazine.size();i++){
record[magazine[i]-'a']++;
}
for(int j=0;j<ransomNote.size();j++){
record[ransomNote[j]-'a']--;
if(record[ransomNote[j]-'a']<0){
return false;
}
}
return true;
}
};
15、 三数之和
思路:乍一看,这道题目会不会跟四数相加一样,也能用哈希法来解决呢,实际上的确可以,但是这里不合适,因为哈希法适合用于查询一个元素之前是否出现过的情况,而这里要使得三数之和为0,并返回这三个元素的值,并且结果还不能重复,四数相加那里可以通过巧妙的使用map来统计元素出现的次数,但这里要求返回的是三个元素的值,即便用map,实现也比较困难。
因此能不能试试别的思路呢,实际上我们分析下就可以发现,这道题目和两数之和之间是存在着密切的联系的,当时要返回的是两个元素的下标,这里要返回的是三个元素的值,如果能够先固定住其中一个值,那么接下来只需要要让另两个数的值之和为target-固定住的值就好了,要操纵另外两个数,可以使用双指针法。
因此本题可以先用变量固定住一个值,这个变量负责遍历整个数组,让后设置一个左指针和一个右指针,如果这两个指针指向的结果满足题意,为target-固定住的值,那么就返回这两个指针指向的结果,和固定住的值,结果容器用一个二维数组来保存即可。
那么问题来了,我如何移动这两个指针呢,很显然,如果我们能够知道两个指针指向的结果加上固定住的值是大了还是小了,就可以在排好序的数组中有选择性的移动指针了。因此再操纵双指针之前,我们应该先对数组进行一个排序。
时间复杂度分析:对数组排序的时间复杂度为nlogn,遍历套上双指针的遍历时间复杂度为n2,因此最终的时间复杂度为n2这样一个量级。
KEY:本题的关键之一是对双指针的使用
但是刚刚的分析还有点问题,虽然我用了一个二维数组来保存结果,但题目要求结果必须是不重复的,因此我还需要进行一个去重的操作,对于固定值的去重,如果当前指向的值与前一个值相同,那么继续下一轮循环,这里不能够把判断条件改成与后一个值相同,否则可能错过原本的解集,例如:-1,-1,2.
其次是对双指针所指元素的去重,首先对于双指针而言,左指针当然应该是小于右指针的下标的,如果等于的话就不满足三数之和的题意了。这是去重时的第一个条件,第二个条件就比较自然了,left=left+1的元素时,左指针++,right=right-1的元素时,右指针–,当找到结果时,双指针在去重后还要做一次收缩,让左指针++,右指针–。
总结:双指针不仅在链表和数组中有着强大的作用,在本题中进一步展示了它的威力,当哈希法去重困难,难以实现时,双指针或许能够提供思路。
补充:当nums[i]大于所求0时,直接return即可,因为数组已经排过序了,后面的元素都不小于nums[i],相加的结果更不能是0了。
代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(),nums.end());
for(int i=0;i<nums.size()-1;i++){
if(nums[i]>0){
return result;
}
if(i>0&&nums[i]==nums[i-1]){
continue;
}
int left=i+1;
int right=nums.size()-1;
while(right>left){
if(nums[i]+nums[left]+nums[right]>0){
right--;
} else if(nums[i]+nums[left]+nums[right]<0){
left++;
}else{
result.push_back(vector<int>{nums[i],nums[left],nums[right]});
while(right>left&&nums[right]==nums[right-1]){right--;}
while(right>left&&nums[left]==nums[left+1]){left++;}
//当收割完结果并去重后,双指针同时收缩
left++;
right--;
}
}
}
return result;
}
};
18、四数之和(基于三数之和的思路)
思路:四数之和的整体思路和三数之和几乎一样,都是利用双指针来解决问题,不过是在其的基础上多套一层循环。
KEY:
需要注意的是,在剪枝的时候这里要减两次,剪枝的条件要注意发生了变化,三数之和那里在排序完之后如果当前元素的值已经大于0,那么可以剪枝,但是这里不同,由于这里的目标值没确定,可能是负数,如果是负数的话,那么下标指向的元素大于目标值就不能剪枝,可能漏解,如-4,-3,-2,-1,目标值是-10,不能因为第一个元素大于目标值就跳过第一个元素。
在去重的时候,同理,也是和前一个元素进行比较,并且要注意判断去重元素下标的合法性。
在计算四数之和时,为了保证结果不发生溢出,可以用一个long型来保存结果。
总结:双指针在链表中可以用于反转链表,删除元素,判断有环,查找环的入口等等,在数组中可以用于删除元素,对有序数组的平方进行排序,在求和问题中,合理的使用双指针同样可以降低时间复杂度。
代码:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
//仍然使用二维数组来保存结果
//本题的难点在于剪枝和去重
vector<vector<int>>result;
sort(nums.begin(),nums.end());
for(int k=0;k<nums.size();k++){
if(nums[k]>target&&nums[k]>=0){
break;
}
if(k>0&&nums[k]==nums[k-1]){
continue;
}
for(int i=k+1;i<nums.size();i++){
if(nums[k]+nums[i]>target&&nums[i]>=0){
break;
}
if(i>k+1&&nums[i]==nums[i-1]){
continue;
}
int left=i+1;
int right=nums.size()-1;
while(right>left){
//注意四数之和是和target比较
if((long)nums[k]+nums[i]+nums[left]+nums[right]-target>0){
right--;
}else if ((long)nums[k]+nums[i]+nums[left]+nums[right]-target<0){
left++;
}else{
result.push_back(vector<int>{nums[k],nums[i],nums[left],nums[right]});
while(right>left&&nums[right]==nums[right-1]){right--;}
while(right>left&&nums[left]==nums[left+1]){left++;}
//当找到满足条件的四元组时,双指针同时收缩
left++;
right--;
}
}
}
}
return result;
}
};