目录
1 哈希表理论基础
1. 1 哈希函数
1.2 哈希碰撞
2 哈希表力扣题参考代码
原文链接 代码随想录
首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。哈希表是根据关键码的值而直接进行访问的数据结构。这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。例如要查询一个名字是否在这所学校里。要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。此时问题又来了,哈希表我们刚刚说过,就是一个数组。如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。接下来哈希碰撞登场
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
1.2.1 拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
(数据规模是dataSize, 哈希表的大小为tableSize)其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
1.2.2 线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
1.3 常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
这里数组就没啥可说的了,我们来看一下set。在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 |
底层实现 |
是否有序 |
数值是否可以重复 |
能否更改数值 |
查询效率 |
增删效率 |
std::set |
红黑树 |
有序 |
否 |
否 |
O(log n) |
O(log n) |
std::multiset |
红黑树 |
有序 |
是 |
否 |
O(logn) |
O(logn) |
std::unordered_set |
哈希表 |
无序 |
否 |
否 |
O(1) |
O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 |
底层实现 |
是否有序 |
数值是否可以重复 |
能否更改数值 |
查询效率 |
增删效率 |
std::map |
红黑树 |
key有序 |
key不可重复 |
key不可修改 |
O(logn) |
O(logn) |
std::multimap |
红黑树 |
key有序 |
key可重复 |
key不可修改 |
O(log n) |
O(log n) |
std::unordered_map |
哈希表 |
key无序 |
key不可重复 |
key不可修改 |
O(1) |
O(1) |
1.4 总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
242. 有效的字母异位词
class Solution {
public:
bool isAnagram(string s, string t) {
if(s.size()!=t.size())
{
return false;
}
unordered_map un_map;
for(int i=0;i
383. 赎金信
// 时间复杂度: O(n)
// 空间复杂度:O(1)
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
//add
if (ransomNote.size() > magazine.size()) {
return false;
}
for (int i = 0; i < magazine.length(); i++) {
// 通过recode数据记录 magazine里各个字符出现次数
record[magazine[i]-'a'] ++;
}
for (int j = 0; j < ransomNote.length(); j++) {
// 遍历ransomNote,在record里对应的字符个数做--操作
record[ransomNote[j]-'a']--;
// 如果小于零说明ransomNote里出现的字符,magazine没有
if(record[ransomNote[j]-'a'] < 0) {
return false;
}
}
return true;
}
};
class Solution {
public:
vector findAnagrams(string s, string p) {
int s1=s.size(),p1=p.size();//两字符串大小
if(s1S(26,0);//申请两数组装s,p
vectorP(26,0);
vectorans;//结果数组
for(int i=0;i
49. 字母异位词分组
class Solution {//思路:使用哈希表,以字符串排序后作为哈希表的键,值为一维数组存排序前的字符串,
//然后将哈希表的每个值存入二维数组里.
public:
vector> groupAnagrams(vector& strs) {
unordered_map> un_map;
vector> result;
for(auto str:strs)
{
string temp=str;
sort(temp.begin(),temp.end());
un_map[temp].push_back(str);
}
for(auto str:un_map)
{
result.push_back(str.second);
}
return result;
}
};
349. 两个数组的交集
class Solution {
public:
vector intersection(vector& nums1, vector& nums2) {
unordered_map un_map1;//用两哈希表分别对两数组去从
unordered_map un_map2;
vector res;//结果集合
for(auto num:nums1)//遍历数组1放入哈希表
{
un_map1[num]++;
}
for(auto num: nums2)//遍历数组2值放入哈希表
{
un_map2[num]++;
}
unordered_map::iterator it=un_map1.begin();//哈希表1的迭代器
for(;it!=un_map1.end();it++)//遍历哈希表1
{
if(un_map2.count(it->first))//如果哈希表1中键能在哈希表2中找到
{
res.push_back(it->first);//存入该值到哈希表中
}
}
return res;
}
};
350. 两个数组的交集 II
class Solution {
public:
vector intersect(vector& nums1, vector& nums2) {
unordered_map un_map;
vector res;
for(int i=0;i0)//判断个数是否还有
{
res.push_back(nums2[i]);//返回该元素,并且在该位置哈希表值-1
un_map[nums2[i]]--;
}
}
}
return res;
}
};
1. 两数之和
class Solution {
public:
vector twoSum(vector& nums, int target) {
unordered_map un_map;//声明一个哈希表用来存遍历的数组值和下标
for(int i=0;i vec;
vec.push_back(un_map[target-nums[i]]);
vec.push_back(i);
return vec;
}
un_map[nums[i]]=i;//没有相同的就存入哈希表中
}
return {};
}
};
202. 快乐数
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 set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
454. 四数相加 II
class Solution {
public:
int fourSumCount(vector& nums1, vector& nums2, vector& nums3, vector& nums4) {
unordered_map umap; //key:a+b的数值,value:a+b数值出现的次数
// 遍历大nums1和大nums2数组,统计两个数组元素之和,和出现的次数,放到map中
for (int a : nums1) {
for (int b :nums2) {
umap[a + b]++;
}
}
int count = 0; // 统计a+b+c+d = 0 出现的次数
// 在遍历大nums3和大nums4数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
for (int c : nums3) {
for (int d : nums4) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
原文链接 代码随想录