理论基础
常见的三种哈希结构:
- 数组
- set(集合)
- map(映射)
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
| 集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
|---|---|---|---|---|---|---|
| std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
| std::multiset | 红黑树 | 有序 | 是 | 否 | O(log n) | O(log n) |
| std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
| 映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
|---|---|---|---|---|---|---|
| std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(log n) | O(log n) |
| std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
| std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
数组作为哈希表的题目
有效的字母异位词
- 计数排序的思想
- 力扣题目链接
- 解法:
bool isAnagram(string s, string t) {
if(s.size()!=t.size()) return false;
int alphabet[26]={0}, n=s.size();
for(int i=0; i<n; i++) alphabet[s[i]-'a']++;
for(int i=0; i<n; i++){
alphabet[t[i]-'a']--;
if(alphabet[t[i]-'a']<0) return false;
}
return true;
}
- 算法复杂度:时间复杂度为O(n),空间复杂度为O(1)
赎金信
- 力扣题目链接
- 解法:
bool canConstruct(string ransomNote, string magazine) {
int alphabet[26]={0};
for(auto ch:magazine) alphabet[ch-'a']++;
for(auto ch:ransomNote){
alphabet[ch-'a']--;
if(alphabet[ch-'a']<0) return false;
}
return true;
}
- 算法复杂度:时间复杂度为O(n),空间复杂度为O(1)
set作为哈希表的题目
两个数组的交集
- 掌握unordered_set的用法
- 力扣题目链接
- 解法:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
int a[1001]={0};
unordered_set<int> nums_set(nums1.begin(),nums1.end());
unordered_set<int> result;
for(auto num:nums2)
if(nums_set.find(num)!=nums_set.end())//说明是交集元素
result.insert(num);
return vector<int>(result.begin(),result.end());
}
- 算法复杂度:时间复杂度为O(m+n),空间复杂度为O(m)
快乐数
- 无限循环变不到1退出的逻辑:出现重复的数字
- 力扣题目链接
- 解法:
int getSum(int n){
int sum=0;
while(n){
sum+=(n%10)*(n%10);
n/=10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1){
int sum=getSum(n);
if(sum==1) return true;
if(set.find(sum)!=set.end()) return false;
set.insert(sum);
n=sum;
}
}
- 算法复杂度:时间复杂度为O(log n),空间复杂度为O(log n)(复杂度分析)
map作为哈希表的题目
两数之和
- 掌握unordered_map的基本用法
- 力扣题目链接
- 解法:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> map;
for(int i=0; i<nums.size(); i++){
auto iter=map.find(target-nums[i]);
if(iter!=map.end()) return {iter->second, i};
map.insert(pair<int,int>(nums[i],i));
}
return {};
}
- 算法复杂度:时间复杂度为O(n),空间复杂度为O(n)
四数相加II
- 本题只要求统计解的个数,将4个数两两分组,统计前两个数a和b的和的个数放入map中,然后遍历c和d在map中寻找-(c+d)的个数累加
- 力扣题目链接
- 解法:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> map;
for(int a:nums1) for(int b:nums2) map[a+b]++;
int cnt=0;
for(int c:nums3) for(int d:nums4)
if(map.find(-(c+d))!=map.end())
cnt+=map[-(c+d)];
return cnt;
}
- 算法复杂度:时间复杂度为O(),空间复杂度为O(),最坏情况下A和B的值各不相同,相加产生的数字个数为
看似用哈希表,实则用双指针更好的题目
三数之和
- 思路:对数组进行排序,辅以一定的去重和剪枝方法
- 力扣题目链接
- 解法:
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) break;
//去重方法
if(i>0 && nums[i]==nums[i-1]) continue;
//双指针精髓——两边夹
int left=i+1, right=nums.size()-1;
while(left<right){
int sum=nums[i]+nums[left]+nums[right];
if(sum>0) right--;
else if(sum<0) left++;
else{
result.push_back(vector<int>{nums[i],nums[left],nums[right]});
//内循环去重
while(left<right&&nums[right]==nums[right-1]) right--;
while(left<right&&nums[left]==nums[left+1]) left++;
//双指针收紧
right--; left++;
}
}
}
return result;
}
- 算法复杂度:时间复杂度为O(),空间复杂度为O(1)
四数之和
- 算法主要原理同三数之和,多一层循环
- 力扣题目链接
- 解法:
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++){
//1级剪枝
if(nums[k]>target && nums[k]>=0) break;
//1级去重
if(k>0 && nums[k]==nums[k-1]) continue;
for(int i=k+1; i<nums.size(); i++){
//2级剪枝
if(nums[k]+nums[i]>target && nums[i]>=0) break;
//2级去重
if(i>k+1 && nums[i]==nums[i-1]) continue;
int left=i+1, right=nums.size()-1;
while(left<right){
long sum=(long)nums[k]+nums[i]+nums[left]+nums[right]; //有溢出风险
if(sum>target) right--;
else if(sum<target) left++;
else{
result.push_back(vector<int>{nums[k],nums[i],nums[left],nums[right]});
//3级去重
while(left<right && nums[right]==nums[right-1]) right--;
while(left<right && nums[left]==nums[left+1]) left++;
right--; left++;
}
}
}
}
return result;
}
- 算法复杂度:时间复杂度为O(),空间复杂度为O(1)
参考资料
[1] 代码随想录
[2] Leetcode题解