哈希表基础理论
哈希表的定义:哈希表是根据关键码的值而直接进行访问的数据结构。
可以把数组理解成一张哈希表,其中哈希表的关键码就是数组的索引下标,然后可以直接通过数组下标访问数组的值。
哈希表能解决的问题:哈希表一般都是用来快速判断一个元素是否出现在集合里。
引用自:代码随想录
哈希函数:
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashcode得到的数值大于哈希表的长度tablesize。我们可以再次对数值进行一个取模运算,这样就可以保证学生姓名一定映射到哈希表上。
如果学生的数量也就是datasize大于哈希表大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射导哈希表的同一个索引下标位置。这时候就会出现哈希碰撞。
如图小李和小王都映射到了索引下标为1的位置,这就是哈希碰撞。
一般有两种方法可以解决哈希碰撞,一种是拉链法,一种是线性探测法。
拉链法:
像刚刚小李和小王再索引1的位置发生了冲突,我们可以将发生冲突的元素都存储再链表中。这样就可以通过索引下标将小李和小王都找到了。
线性探测法:
使用线性探测法,一定要保证tablesize大于datasize。这样的话,哈希表中一定会有空位可以来解决碰撞问题。例如冲突的位置,放了小李,那么就可以向下找一个空位来放置小王的信息。
常见的三种哈希结构:当我们想使用哈希法来解决问题的时候,我们一般会选择数组、set(集合)、map(映射)这三种数据结构。
再C++中,set和map都分别提供了以下三种数据结构,其底层实现以及优劣如下表所示(引用自代码随想录)
set和multiset的底层实现为红黑树,所以它们是有序的,并且查找效率和增删效率都是O(log n),并且不能更改数值,因为改变红黑数的数值会导致整棵树错乱。unordered_set的底层实现是哈希表所以其数据是无序的,并且查找效率和增删效率都为O(1)。
同理map和multimap的底层实现为红黑树,所以它们的key是有序的,并且查找效率和增删效率为O(log n),并且不能更改数值,因为改变红黑数的数值会导致整棵树错乱。unordered_map的底层实现是哈希表所以其key是无序的,并且查找效率和增删效率都为O(1)。
当需要使用集合来解决哈希问题的时候,我们可以优先使用unordered_set,因为它的查询和增删效率最优。如果需要集合是有序的,那么就可以用set,如果要求不仅有序并且还要有重复数据的话,那么就用multiset。
map同理。
本题可以使用哈希法解题。因为其给定的两个字符串仅包含小写字母。因为字母a-z的ASCII码值是相邻的。所以可以使用一个数组作为哈希表来记录字两个字符串中每个字母出现的次数。然后进行比较就能得知,两个字符串是否为有效的字母异位词。
具体操作如下:定义一个长度为26的全0数组(因为一共26个字母)。我们可以使用两个单独的for循环遍历两个字符串,可以使用字符串中的每一个字符-’a‘,然后直接对数组进行++操作。具体操作如下图。
代码如下:
class Solution {
public:
bool isAnagram(string s, string t) {
int nums[26] = {0};
for(int i =0;i<s.size();i++)
{
nums[s[i]-'a']++;
}
for(int j =0;j<t.size();j++)
{
nums[t[j]-'a']--;
}
for(int k=0;k<26;k++)
{
if(nums[k]!=0)
{
return false;
}
}
return true;
}
};
这题可以使用暴力解法,但是会很麻烦。使用哈希法可以很好地解决问题。
我们注意到题目中不考虑输出结果的顺序,并且输出结果中的每个元素一定是唯一的,所以我们可以考虑使用数组,unordered_set,unordered_map,这三种结构构造哈希表。显然,使用数组不好解决问题,因为我们并不确定给定数组的长度。然后,这题很显然不需要用到key和value键值对,所以我们使用unordered_set作为哈希表。
具体操作如下:我们可以定义一个unordered_set用来保存数组1nums1的元素我们将定义的哈希表命名为nums,顺便对nums1进行去重处理。然后可以使用一个for循环去遍历nums2中的所有元素,将nums2中的元素作为关键码去哈希表nums中查找,如果能查找到,说明该数是nums1和nums2的交集元素。
代码如下:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
unordered_set<int> nums (nums1.begin(),nums1.end());//将数组nums1中的元素做去重处理放入集合nums中
for(int num:nums2)
{
if(nums.find(num)!=nums.end())
{
result_set.insert(num);
}
}
return vector<int>(result_set.begin(),result_set.end());
}
};
需要注意的是:
for(int num:nums2)
是c++11中的新特性,大概意思就是将nums2中的值依次赋值给num
等同于代码:
int num;
for(int i=0;i<nums2.size();i++)
{
num=nums2[i];
}
然后就是判断条件为什么要这么设置:
if(nums.find(num)!=nums.end())
这是因为find函数如果发现num再nums中的话,就会返回一个正向迭代器指向该元素,如果发现num不在nums中的话就会返回一个与nums.end()相同的迭代器。所以我们这样设置判断条件。
这题思路不复杂。使用一个unordered_set作为哈希表,存储每次计算的结果。然后每次计算之后判断,结果是否为1,为1则返回true,不为1则将其作为关键码去访问哈希表中的元素,如果能查找到,说明该数不是快乐数,返回false。具体过程图解如下:
具体代码如下:
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> hash;
while(1)
{
int sum = getsum(n);
if(sum == 1)
{
return true;
}
else if(hash.find(sum)!=hash.end())
{
return false;
}
else{
hash.insert(sum);
}
n = sum;
}
}
};
这个题我们要想清楚,为什么会想到用哈希表、哈希表为什么用map、本题map是用来存什么的、map中的key和value用来存什么的。
为什么会想到用哈希表
当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题的题目要求是给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
很明显,这个题目数组中同一个元素在答案中不能重复出现,所以,我们就可以使用一个哈希表来存放遍历过的数组元素,每次遍历的时候查找一下。
哈希表为什么用map
因为本题返回的不是数组的值,而是数组的下标。意味着我们不仅要访问数组的值,还需要知道其对应的下标。所以我们就不能用数组和set来做哈希表,因为数组和set只能用来存数组的值或者下标。我们可以使用包含key和value的map来做哈希表。
本题map是用来存什么的
我们需要遍历数组,每次遍历之后去我们定义的哈希表中查找是否有符合条件的值。所以我们可以使用map将已经遍历过的数组的下标和对应的值存起来。这样的话,数组遍历到后边也可以查找前面的值,而且不会重复。
map中的key和value用来存什么的
map中的key可以用来存放数组的值,value可以用来存放数组的下标。因为我们的哈希表关键码是target-nums[i],也就是说要查找的其实是符合条件的数组值。而map一般都是查找key,然后找出对应的value。具体例子如下图:
代码如下:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map<int,int> hash;
for(int i =0;i<nums.size();i++)
{
std::unordered_map<int,int>::iterator it = hash.find(target-nums[i]);
if(it!=hash.end())
{
return {it->second,i};
}
else
{
hash.insert(pair<int,int>(nums[i],i));
}
}
return {};
}
};
代码上有一个需要注意的点,find()函数如果查找到元素存在,则会返回指向该元素的一个正向迭代器,所以find()函数的返回值类型应该是迭代器类型。如果查找不到元素存在,则会返回与end()函数一样的迭代器。
本题的:
std::unordered_map<int,int>::iterator it = hash.find(target-nums[i]);
代码也可以使用auto代替,也就是
auto it = map.find(target - nums[i]);
auto的原理就是根据后面的值,来推测前面的类型是什么。
auto的作用就是为了简化变量的初始化。所以直接使用auto的话,可以省略写迭代器类型的复杂过程。