leetcode 454.四数相加
分组+哈希映射
代码实现
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int, int> map;
int count = 0;
for(int a: nums1){
for(int b: nums2){
map[a + b]++;
}
}
for(int c: nums3){
for(int d: nums4){
if(map.find(0 - (c + d)) != map.end()){
count += map[0 - (c + d)];
}
}
}
return count;
}
};
时间复杂度O(n^2)
空间复杂度O(n^2)
细节处理
本题重点在于数组元素的存和查,由于题目要求统计满足nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0的所有(i, j, k, l)元组的数量,所以在取出nums1中任意一个数时,要找满足 nums2[j] + nums3[k] + nums4[l] = -nums1[i]的其他的数,于是此问题就转化为查询某个元素是否在集合里,故想到使用哈希表。
我们可以将四个数组分为两部分:nums1和nums2、nums3和nums4。这样就可以将nums1[i]+nums2[j]的所有情况存入哈希表、再统计nums3[k]+nums4[l]的所有情况,从哈希表中找出是否存在-1*(nums3[k]+nums4[l])的key值。这样就可以只使用一个哈希表来解决问题。
哈希表使用unordered_map,原因是题目要求返回满足要求的所有元组数量,所以需要key value对,其中key储存nums1[i]+nums2[j]的值,value放nums1[i]+nums2[j]出现的次数。
时间复杂度分析:使用了两次二重循环,时间复杂度均为O(n^2),而在循环中对哈希映射进行的查询和修改操作的期望时间复杂度为O(1),因此总时间复杂度为O(n^2)。
空间复杂度分析:即为哈希映射需要使用的空间。在最坏的情况下,nums1[i] + nums2[j]的值均不相同,因此值的个数为 n^2,也就是需要O(n^2)的空间。
leetcode 383.赎金信
哈希数组
代码实现
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;
}
};
时间复杂度O(m+n)
空间复杂度O(|S|)
细节处理
题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。
在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,很费时。数据量大的话就能体现出来差别了。 所以数组更加简单直接有效。
本题与leetcode 242.有效的字母异位词很像。
将大数组magazine先放入哈希数组中,因为我们是要在magazine中找randomNote的元素,接着用for循环遍历randomNote中的元素,如果存在哈希数组的值为负(即不存在这个元素),那么就返回false。
时间复杂度分析:其中m为字符串randomNote的长度,n为字符串magazine的长度,只需两次遍历字符串即可。当n>>m时,它也可以写为O(n)。
空间复杂度分析:S是字符集,这道题中S为全部小写英语字母,因此|S| = 26。也可以写为O(1)。
暴力解法
代码实现
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for(int i = 0; i < magazine.length(); i++){
for(int j = 0; j < ransomNote.length(); j++){
if(ransomNote[j] == magazine[i]){
ransomNote.erase(j, 1);
break;
}
}
}
if(ransomNote.length() == 0)
return true;
else
return false;
}
};
时间复杂度O(n^2)
空间复杂度O(1)
细节处理
暴力解法无需多言。
leetcode 15.三数之和
排序+双指针法
代码实现
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(); 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(left < right){
// ×去重× 不可
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(left < right && nums[left] == nums[left + 1])
left++;
while(left < right && nums[right] == nums[right - 1])
right--;
right--;
left++;
}
}
}
return result;
}
};
时间复杂度O(n^2)
空间复杂度O(logn)
细节处理
本题使用双指针法比哈希法更高效,哈希法在去重中需要注意到的细节过多。
使用双指针法前必须要排序,因为只有在一个顺序排序的数组中,才能将三数之和的结果与要求值比较进而依次左移或右移指针来增大或减少三数之和的值。
使用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相遇为止。
去重逻辑:由于本题要求返回不重复的三元组,所以去重是很重要的一部分。
对a去重:a对应的是nums[i],如果a重复了就应该直接跳过。这里需要注意的是去重操作,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。由于i是从i = 0开始遍历整个数组,那么可以看作它不断右移,如果是判断nums[i] 与 nums[i + 1]的关系的话,就等同于判断索引i的值和索引i右边的值是否相等,这里很明显能发现问题:因为i+1的值还未遍历到,要知道三元组内的值是可以重复的,我们要做到的只是不出现重复的三元组,举个例子来说{-1, -1, 2}这个三元组明显是符合要求的,但我们的判断条件将会丢弃这个三元组。所以是判断 nums[i] 与 nums[i-1] 是否相同,这么写就是当前使用 nums[i],我们判断前一位是不是一样的元素,这就能够包含{-1, -1, 2}这个三元组了,同样当nums为{-1, -1, -1, 0, 1, 2, 3}时,i取到多个-1时,就可以对i进行去重操作。
对b、c去重:b、c去重的逻辑比较简单,因为b只能右移、c只能左移,所以当nums[left] == nums[left + 1]和nums[right] == nums[right - 1],即b、c的下一位置与当前位置的值相等,此时就要相应的进行left++和right--的操作。需要注意去重操作在代码中的位置,如果nums为{0, 0, 0, 0, 0}时,如果去重操作放在while(left < right)下时(代码块13行下),就会发生因为元素相等而直接退出while循环,从而没有任何三元组的记录。同样,如果去重操作放在if移位判断后的位置,
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--来举例,这其实也是多余的操作,因为在经过if的判断后,三数之和>0,需要左移right。此时再判断nums[right]与nums[right + 1]是否相等,如果新的right仍与老right相等,那么nums[i] + nums[left] + nums[right]肯定还是>0,此时就没有必要再做一次if判断进行right--了,而是直接在下面的去重操作中进行right--。乍一看似乎没什么问题,但是这种去重其实对提升程序运行效率是没有帮助的:即使不加这个去重逻辑,仍然会根据if中的内容去完成right--的操作,多加的一行while代码其实只是将需要执行的逻辑提前了,而并没有减少判断的逻辑。用一句直白的话来概括:反正都是要减的,在哪里减都是一样的。
哈希解法
代码实现
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;
}
};
时间复杂度O(n^2)
空间复杂度O(n)
细节处理
哈希法的思路不难,难点在于去重的处理,本方法针对元素a的去重与上述排序+双指针法相同,区别在于元素b和c的去重。元素b的去重条件:j > i + 2 && nums[j] == nums[j-1] && nums[j-1] == nums[j-2],可以发现它相对元素a的要求多了:与前两项均相等才去重。举个例子,当数组为 [-2, 1, 1, 2, 3]时,如果仍采用元素a的去重准则,那么就取不到[-2, 1, 1],所以需要与前两项都相等。我理解的造成元素a和b去重准则有差别的本质原因是,在排序后的数组中a在第一位,它后面还有两位可选择的空间;而b在第二位,它后面只有一位可选择的空间。所以当a去重时只需考虑前一位是否相同,也可以取到1、2位置两个相同的数,b去重时需要考虑前两位是否相同,才能取到2、3位置两个相同的数。
对元素c的去重操作是set.erase(c),这是因为c是最后一个位置,算法中它的确定完全是根据a和b的值来的。如果没有在set中找到符合要求的值,就set.insert(nums[j]),这里可以理解成将nums中的值“遍历”了一遍并且放到了set中,当然这个“遍历”可能中途就结束了。它这样做的目的是反过头找值等于0 - (nums[i] + nums[j])的元素,也就是说最后我们输出的vector{a, b, c}中a < c < b。由于在for循环中每次都会重新遍历i和j,相当于会重新维护一个set集合,所以发现一个满足条件的三数之和时,直接将set中保存下来的满足c的值的元素删除即可。
leetcode 18.四数之和
排序+双指针
代码实现
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++){
if(nums[i] > target && nums[i] >= 0)
// or return result
break;
if(i > 0 && nums[i] == nums[i - 1])
continue;
for(int j = i + 1; j < nums.size(); 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.size() - 1;
while(left < right){
if((long) nums[i] + nums[j] + nums[left] + nums[right] > target){
right--;
}
else if((long) nums[i] + nums[j] + nums[left] + nums[right] < target){
left++;
}
else{
result.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
right--;
left++;
}
}
}
}
return result;
}
};
时间复杂度O(n^3)
空间复杂度O(logn)
细节处理
使用四重循环枚举所有的四元组,然后使用哈希表进行去重操作,得到不包含重复四元组的最终答案。假设数组的长度是n,则该方法中,枚举的时间复杂度为 O(n^4)去重操作的时间复杂度和空间复杂度也很高,所以考虑使用上一题leetcode 15.三数之和的排序+双指针法。
整体思路与上一题类似,只需多使用一层for循环,找到四个数中的两个之和的所有情况,对于剩下的两个数再使用双指针遍历即可。
去重思路与上一题类似。在开始时的判断条件处需要注意:由于此时四数之和的目标值不再是零而是target,所以考虑到target为负数的情况,不能只凭nums[i] > target就跳出循环,而是还要加上nums[i] >= 0 的条件。第二层for循环中的if条件判断也同理。另外,在第一层for循环的if判断跳出语句可以用break或return result,而第二层只能用break,因为break只跳出单层循环,第二层跳出后还可以返回第一层继续进行判断。
if((long) nums[i] + nums[j] + nums[left] + nums[right] > target),如果这里不加long的话会造成溢出,如下图所示:
时间复杂度分析:n 是数组的长度,排序的时间复杂度是O(nlogn)。由于双指针法将两次遍历减少为了一次遍历,枚举四元组的时间复杂度从O(n^4)变为了O(n^3),因此总时间复杂度为O(n^3+nlogn)=O(n^3)。
空间复杂度分析:空间复杂度主要取决于排序额外使用的空间,排序需要O(logn) 的空间复杂度。此外排序修改了输入数组nums,实际情况中不一定允许,因此也可以看成使用了一个额外的数组存储了数组nums 的副本并排序,空间复杂度为O(n)。