基础知识
-
哈希表定义:哈希表是根据关键码的值而直接进行访问的数据结构。
哈希表就是数组。哈希表中的关键码就是数组的索引下标,然后通过下标直接访问数组中的元素 -
应用场景:一般哈希表都是用来快速判断一个元素是否出现集合里。
例如查询一个名字是否在这所学校里。用哈希表时间复杂度O(1)。 -
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里。
-
哈希碰撞:两个学生映射的索引一样。
解决方法:
1.拉链法:将发生碰撞的学生存在链表
2.线性探测法:前提哈希表足够大,保证哈希表中还有空位存放发生碰撞的学生。 -
常见的哈希结构 数组、set、map、set的三种结构
unordered_set、set、multiset
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
map的三种结构unordered_map、map、multimap
map 是一个key,value 的数据结构,unordered_map、map、multimap和set的适用场景类似。
例题
分析:字符串中只有小写英文字符,设一个26大小数组,统计字符串中字母的个数,以字母的ascll码作为索引,映射到哈希表中。
遍历s字符串时,对s[i]对应的索引位置做加1;在遍历t时,对t[i]对应的索引位置做减1。最后判断record是不是全为0.
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26]{0};//初始化0
for(int i=0;i<s.length();i++){
record[s[i]-'a']++;
}
for(int i=0;i<t.length();i++){
record[t[i]-'a']--;
}
for(int i=0;i<26;i++){
if(record[i]){
return false;
}
}
return true;
}
};
分析:以小写字母作为索引,统计magazine中字符的个数。在遍历ransomNote时,如果record对应的值不为0,做减一;否则返回false。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int recore[26]{0};
for(int i=0;i<magazine.length();i++){
recore[magazine[i]-'a']++;
}
for(int i=0;i<ransomNote.length();i++){
if(recore[ransomNote[i]-'a']!=0){
recore[ransomNote[i]-'a']--;
}
else{
return false;
}
}
return true;
}
};
分析:使用unordered_map
每一组的字符串都是相同字母的不同组合
以strs的字符串按顺序排列作为key,以key的不同组合作为vlaue。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string> >groups;
vector<vector<string> >result;
for(int i=0;i<strs.size();i++){
string s=strs[i];
sort(s.begin(),s.end());
groups[s].push_back(strs[i]);
}
for (auto iter = groups.begin(); iter != groups.end(); ++iter){
result.push_back(iter->second);//只取值value
}
return result;
}
};
取交集立即想到set,本题对结果顺序没有要求,可以用unordered_set,效率比较快。
unordered_set不允许重复,也就是有去重的功能。先将nums1的内容放在unordered_set,判断nums2是否在unordered_set。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;//存放结果
unordered_set<int> nums_set(nums1.begin(),nums1.end());//将nums1放num_set中
for(int num:nums2){//遍历nums2,若和nums_set一样,就放在result_set中
if(nums_set.find(num)!=nums_set.end())
result_set.insert(num);
}
return vector<int>(result_set.begin(),result_set.end());
}
};
上一个题目统计不同数值的并集,也就是结果一定是互不相同的数值
本题的结果可能会出现重复的数值,例如
1 , 2 , 2 , 1
2, 2
本题的并集为2,2
本题考虑用unordered_map,先统计nums1各数值出现的次数,然后遍历nums2是否在umap且umap[num]不为0(若等于0,说明num在nums1出现的次数少,在nums2出现的次数多,取次数少的),若是,则num放在result中,umap[num]- -。
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int,int> umap;//默认value为0
vector<int> result;
for(int num:nums1){
umap[num]++;
}
for(int num:nums2){
if(umap[num]>0){
result.push_back(num);
umap[num]--;
}
}
return result;
}
};
分析:如果是快乐数,最终和会变为1;如果不是快乐数,会出现重复,可以想到set去重的功能。只要和在set中,直接返回false。
class Solution {
public:
int allSum(int n){
int sum=0;
while(n!=0){
sum=sum+(n%10)*(n%10);
n=n/10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> uset;
while(n!=1){
if(set.find(n)!=set.end()){//如果n不为1,并且set已经有n这个数,说明出现了循环。
return false;
}
set.insert(n);
n=allSum(n);
}
return true;
}
};
这里用map集合(num[i]为key,i为value),遍历nums,如果target-nums[i]在map中,说明找到了两个之和==target;否则,将i,nums[i]插入map。
emplace()插入键值对
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> umap;
vector<int> result;
for(int i=0;i<nums.size();i++){
auto it=umap.find(target-nums[i]);
if(it!=umap.end()){//找到了符合的键值对
result.push_back(it->second);
result.push_back(i);
return result;
}
umap.emplace(nums[i],i);
}
return result;
}
};
设一个unordered_map,以元素之和为key,元素之和出现的次数为value。
统计前两个数组元素之和出现的次数,并放在umap中。
遍历后两个数组,如果后两个数组元素之和的相反数在umap中,就将umap的value加到count。
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> umap;
int count=0;
for(int a:nums1){//统计nums1+nums2元素和出现次数
for(int b:nums2){
umap[a+b]++;
}
}
for(int c:nums3){
for(int d:nums4){
if(umap.find(0-c-d)!=umap.end()){//判断nums3+nums4是否与nums1+nums2互为相反数
count+=umap[0-c-d];
}
}
}
return count;
}
};
双指针,设两个指针left,right,num[i]表示三元组的第一个元素,num[left]表示三元组的第二个元素,num[right]表示三元组的第三个元素。
先排序
如果num[i]==0,直接结束,后面不会有和为0
如果nums[i]+nums[left]+nums[right]<0,left++;
如果nums[i]+nums[left]+nums[right]>0,right–
如果nums[i]+nums[left]+nums[right]==0,right–,left++,并且要去重
后两个元素去重写在条件内
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int> >result;
sort(nums.begin(),nums.end());
int len=nums.size();
for(int i=0;i<len;i++){
if(nums[i]>0){
return result;
}
if(i>0 && nums[i]==nums[i-1]){//三元组的第一个元素去重
continue;
}
int left=i+1;
int right=len-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(vector<int>{nums[i],nums[left],nums[right]});
while(right>left && nums[right]==nums[right-1])//三元组的第三个元素去重
right--;
while(right>left && nums[left]==nums[left+1])//三元组的第二个元素去重
left++;
right--;
left++;
}
}
}
return result;
}
};
和三数之和差不多,多了一层循环,另外此写法
if (nums[i] > target) { return result; }
不正确,当target是负数时,假设nums[i]>target,如果后面的数是负数,由于负数相加和变小,还可能出现==target的情况
由此五数之和、六数之和都可以这样写出。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int> >result;
sort(nums.begin(),nums.end());
int len=nums.size();
for(int i=0;i<len;i++){//四元组{nums[i],nums[j],nums[left],nums[right]},即为{a,b,c,d}
if(i>0 && nums[i]==nums[i-1]){//a去重
continue;
}
for(int j=i+1;j<len;j++){
if(j>i+1 && nums[j]==nums[j-1]){//b去重
continue;
}
int left=j+1;
int right=len-1;
while(right>left){
if(nums[i]+nums[j]>target-(nums[left]+nums[right])){
right--;
}
else if(nums[i]+nums[j]<target-(nums[left]+nums[right])){
left++;
}
else{
result.push_back(vector<int> {nums[i],nums[j],nums[left],nums[right]});
while(right>left && nums[left]==nums[left+1])//c去重
left++;
while(right>left && nums[right]==nums[right-1])//d去重
right--;
left++;
right--;
}
}
}
}
return result;
}
};
总结
哈希表用于快速判断某个元素是否在集合里
哈希表有三种结构:数组、set、map
- 数组应用的场景:集合的规模不是很大且已经确定,当集合规模很大时,就考虑后面两种结构。
- set应用的场景:集合的规模很大,数据要去重。当不仅要判断元素是否在集合里,还要求记录元素的相关信息,考虑map。
- map应用的场景:map是一种<key, value>的结构,当用于分组,归类时(一个key对应一类),可以考虑map,当数组、set无法解决问题时考虑map。
unordered_set、multiset、set的应用场景(map同理):
- 当题目不要求有序用unordered_set
- 当题目有重复值 用multiset
- 前两个都不合适,则用set
map是万能的哈希表,可以替代数组和set,但是map消耗的空间比较大,在执行效率上不如前两个高效。