训练营第七天打卡继续,今天直面天命了一小会,并穿插干了些dirty work,然后做了后天出差准备,晚上刷题不能停!今天头两题用哈希表很好解决,后两题用哈希表的去重逻辑很复杂,不如用双指针!后两题不会做对着随想录又做了一遍后加深了印象,还得多练!
454.四数相加Ⅱ
解题过程
- 采用哈希表存数组1和数组2的和为key,相同和的次数为value。
知识点
- 时间复杂度: O(n^2)
- 空间复杂度: O(n^2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n^2
哈希表
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
int result = 0;
unordered_map<int, int>sum12;
for (int i = 0; i < nums1.size(); i++) {
for (int j = 0; j < nums2.size(); j++) {
sum12[nums1[i] + nums2[j]]++;
}
}
for (int i = 0; i < nums3.size(); i++) {
for (int j = 0; j < nums4.size(); j++) {
if (sum12.find(-nums3[i] - nums4[j]) != sum12.end()) {
result += sum12[-nums3[i] - nums4[j]];
}
}
}
return result;
}
};
383.赎金信
解题过程
- 使用一个长度为26的数组记录小写字母的个数
- magazine里的字母在数组中个数加1
- ransomNote里的字母在数组中个数减1
知识点
- 时间复杂度: O(n)
- 空间复杂度: O(1)
数组哈希表
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
vector<int>count(26);
for (char c : magazine) count[c - 'a']++;
for (char c : ransomNote) count[c - 'a']--;
for (int num : count) if (num < 0) return false;
return true;
}
};
15.三数之和
解题过程
-
见到这题首先往哈希法的思路去想了,然而用哈希的一个必要条件就是结果能够有重复数组,此题不适合用哈希。
-
没思路后,看随想录的思路才写出来。本题使用双指针的思路,用一个
i
指针遍历数组,left
和right
遍历除i
外的元素,注意要对i
left
right
三个指针都有去重操作。 -
首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
-
如果
nums[i] + nums[left] + nums[right] > 0
就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。如果
nums[i] + nums[left] + nums[right] < 0
说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
知识点
-
时间复杂度: O(n^2)
-
空间复杂度: O(1)
-
如果我们的写法是 这样:
if (nums[i] == nums[i + 1]) { // 去重操作 continue; }
那我们就把 三元组中出现重复元素的情况直接pass掉了。 例如
{ -1, -1, 2 }
这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。 -
两数之和 就不能使用双指针法,因为1.两数之和 要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。
如果1.两数之和 要求返回的是数值的话,就可以使用双指针法了。
双指针法
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
int left, right;
vector<vector<int>>result;
for (int i = 0; i < nums.size() - 2; i++) {
if (nums[i] > 0) return result;
if (i != 0 && nums[i] == nums[i - 1]) continue;
left = i + 1;
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({ nums[i], nums[left], nums[right] });
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
}
}
}
return result;
}
};
18.四数之和
解题过程
- 运用三数之和的双指针思路做,大部分测试案例都能过,但有几个案例一直debug不过。
- 四个数相加可能超过int的最大值,所以需要处理数据溢出问题
知识点
- 时间复杂度: O(n^3)
- 空间复杂度: O(1)
- 本题剪枝不能和三数之和一样,因为是求和大于target,而不是0
双指针法
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
if (nums.size() < 4) return {};
vector<vector<int>>result;
sort(nums.begin(), nums.end());
int left, right;
for (int i = 0; i < nums.size() - 3; i++) {
if (nums[i] >= 0 && nums[i] > target) break;
if (i != 0 && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < nums.size() - 2; j++) {
if (nums[i] + nums[j] >= 0 && nums[i] + nums[j] > target) break;
if (j != i + 1 && nums[j] == nums[j - 1]) continue;
left = j + 1;
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({ 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--;
left++;
right--;
}
}
}
}
return result;
}
};
哈希表总结篇
哈希表的理论基础
-
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用。
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
接下来是常见的三种哈希结构:
- 数组
set
(集合)map
(映射)
在C++语言中,
set
和map
都分别提供了三种数据结构,每种数据结构的底层实现和用途都有所不同什么时候用
std::set
,什么时候用std::multiset,什么时候用std::unordered_set
,都是很有考究的。只有对这些数据结构的底层实现很熟悉,才能灵活使用,否则很容易写出效率低下的程序。
哈希表经典题目
-
数组作为哈希表
-
- 提到了数组就是简单的哈希表,但是数组的大小是受限的!
- 这道题目包含小写字母,那么使用数组来做哈希最合适不过。
-
- 同样要求只有小写字母,给我们暗示数组的大小是受限的,用数组!
-
上面两道题目用map确实可以,但使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效!
-
-
set作为哈希表
-
用数组就不行了,需要用set。这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
-
主要因为如下两点:
- 数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
- 如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
-
关于set,C++ 给提供了如下三种可用的数据结构:
std::set
std::multiset
std::unordered_set
std::set
和std::multiset
底层实现都是红黑树,std::unordered_set
的底层实现是哈希, 使用unordered_set
读写效率是最高的,本题并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set
。 -
使用了unordered_set来判断一个数是否重复出现过。
-
map作为哈希表
-
使用数组和set来做哈希法的局限。
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
- set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
-
map是一种
<key, value>
的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。C++提供如下三种map:(详情请看关于哈希表,你该了解这些! (opens new window))
std::map
std::multimap
std::unordered_map
-
std::unordered_map
底层实现为哈希,std::map 和std::multimap
的底层实现是红黑树。同理,
std::map
和std::multimap
的key也是有序的。两数之和中并不需要key有序,选择std::unordered_map
效率更高! -
需要哈希的地方都能找到map的身影。
-
与18. 四数之和,15.三数之和关键差别是本题为四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑重复问题,而是一个数组(集合)里找到和为0的组合,可就难很多了!