哈希表理论基础
哈希表是根据关键码的值快速直接访问元素的数据结构。
- 解决的问题:快速判断一个元素是否存在于某个集合中,如果逐个枚举的话时间复杂度为 O(n),但如果事先讲元素存在哈希表中,可以实现 O(1) 复杂度的查找。
- 哈希函数:获取哈希表就是建立数据与哈希索引之间的对应关系。一般地,我们可以将其他数据格式通过某种编码方式转换成数字,后续可选地将这些数字转换到一个有限的值域区间来减少空间的浪费。
- 哈希碰撞:考虑一种具体情况,如果
hashtable_size
小于原始数据的取值可能,即使哈希值分布得再均匀,也会有不同的元素映射到同一索引,也就是出现的不一一对应的映射,就发生了哈希冲突。
两种方法解决哈希碰撞:一种是拉链法,一种是线性探测法。
拉链法:将溢出元素用链表的结构存储起来,注意选取哈希表大小,既不要产生过多的空值浪费空间,也不要链表过程影响查找性能。
线性探测法:依靠哈希表中的空位来存储溢出,需要保证哈希表的大小大于原始数据的分布范围。
常见的哈希表结构
- 数组
- set
- map
在 C++ 中,以上三种是常用的哈希表结构,对于set
,有std::set
、std::multiset
、std::unordered_set
;对于map
,有std::map
、std::multimap
、std::unordered_map
。
注:对与这两种结构的三种不同实现,有以下几点需要注意:
- 前两种实现的底层都是基于红黑树,第3种是哈希表,红黑树增删查为 O(logn),哈希表为 O(1),因此使用时优先
unordered
- 顾名思义,
unordered
的实现对于 key 没有顺序要求,但前两种都有;multi
允许 key 重复值,其他两种则不允许。因此如果集合有序,就用set
,如果要允许重复 key,就用multi
map
只对 key 有要求,对 value 没要求,所有结构都不能修改 key。那些红黑树做底层的实现我们仍然是 key-value 这样的用法,所以也叫哈希法,C++ 中还有一些民间模板 hash_map 等,功能相同,优先用官方的吧
有效的字母异位词
思路简单,就是将字母映射到0~25作为哈希索引,数组即可解决。
class Solution{
public:
bool isAnagram(string s, string t){
int res[26] = {0};
for(int i = 0; i < s.size(); i++){
res[s[i] - 'a']++;
}
for(int i = 0; i < t.size(); i++){
res[t[i] - 'a']--;
}
for(int i = 0; i < 26; i++){
if(res[i]) return false;
}
return true;
}
};
两个数组的交集
这道题仍然是一道数组题,可以利用双指针的方法,但前提是需要将数组进行排序,同时要注意去重复。
对于哈希方法,本题不同于上一题,上一题的哈希值分布是较小的,只需初始化一个固定大小的数组就可以实现。这道题数据范围大,哈希值可能是比较分散的,使用数组会造成比较大的空间浪费。同时题目要求输出不能重复,不需要对输出进行排序,因此选择std::unordered_set
实现。
class Solution{
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2){
unordered_set<int> result_set;
unordered_set<int> num_set(nums1.begin(), nums1.end()); // 将num1转换为set,避免重复,同时进行哈希查找提升效率
for(int num : nums2){
if(num_set.find(num) != num_set.end()){
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
注:其实如果能用数组,尽量还是用数组,因为相比之下 set 空间占用更多,将数据映射到 key 时还需要进行 hash 运算,数据规模较大时与数组相比会更慢。
快乐数
注意这里的无限循环很重要,说明不是快乐数时最终的 sum 会循环出现,问题判断一个数是否出现过,出现过便不是快乐数,也要注意先判断 sum == 1,不然快乐数也会在1处进入循环。
class Solution{
public:
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> num_set;
while(1){
int sum = getSum(n);
if(sum == 1) return true;
if(num_set.find(sum) != num_set.end()) return false;
num_set.insert(sum);
n = sum;
}
}
};
两数之和
分析:这道题需要对数值进行比对,却返回的是下标,同时数据的范围相当大。
由于 set 只是一个集合,只能存储一个个的 key,这里需要引入 key-value 这样的结构,比较的是数值,返回的是相应下标。map 正好合适,元素数值当做 key,下标做 value。由于这道题没有对顺序做要求,因此选择std::unordered_map
。
class Solution{
public:
vector<int> twoSum(vector<int>& nums, int target){
unordered_map<int, int> map;
for(int i = 0; i < nums.size(); i++){
auto it = map.find(target - nums[i]);
if(it != map.end()) return {it->second, i};
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};