哈希表是根据关键码的值而直接进行访问的数据结构。
要解决的问题:一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
问题1:如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
问题2:如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
引出哈希碰撞问题
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
刚刚小李和小王在索引1的位置发生了冲突,那么我们把发生冲突的元素都存储在链表中。 这样我们就可以通过索引找到小李和小王了
其实拉链法就是要选择适当的哈希表的大小(哈希表的长度),这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
没啥可说的
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?
实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
暴力做法:
先看暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。
哈希法:
数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。
定义一个数组叫做record用来上记录字符串s里字符出现的次数。
需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
再遍历字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
为了减少数组的使用量,检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for(int i = 0;i <s.size();i++){
record[s[i] - 'a']++;
}
for(int i = 0;i<t.size();i++){
record[t[i] - 'a']--;
}
for(int i = 0;i< 26;i++){
if(record[i] != 0)
return false;
}
return true;
}
};
1:.size()是容器或者字符串使用的
2:数组初始化方式
int arr[5];
int arr[5] = {1, 2, 3, 4, 5};
int arr[5] = {}; // 所有元素初始化为 0
必须指定大小
3:时间复杂度为3次O(n)->O(n)
4:本题使用数组构建哈希表是因为数据大小已知了,若未知要用其他的数据结构
这道题目,主要要学会使用一种哈希数据结构:unordered_set,这个数据结构可以解决很多类似的问题。
注意题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序
本题中数据大小未知,哈希值比较分散,跨度大,使用数组会造成浪费,因此本题使用set来解决问题
std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
(问题:三者区别是什么,用处?)
unordered_set 的使用
定义
不指定大小
#include
std::unordered_set<int> mySet; // 定义一个名为 mySet 的空的 unordered_set
指定大小
std::unordered_set<int> mySet(num);//例如:num = 100
插入删除
mySet.insert(10);
mySet.erase(10);
新语法的使用:
#include
#include
int main() {
std::vector<int> nums2 = {1, 2, 3, 4, 5};
for (int num : nums2) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
在上述示例中,我们定义了一个 std::vector(nums2),并使用范围-based for 循环遍历 nums2 中的每个元素。在每次循环迭代中,将当前元素赋值给迭代变量 num,然后输出 num 的值。
代码:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;//存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(),nums1.end());//将nums1的内容复制到nums_set中
for(int num : nums2){
/*如果元素 num 存在于 nums_set 中,
则 nums_set.find(num) 返回一个指向该元素的迭代器;
如果元素 num 不存在于 nums_set 中,
则返回一个指向集合末尾的迭代器 nums_set.end()。*/
if(nums_set.find(num) != nums_set.end()){
result_set.insert(num);
}
}
return vector<int>(result_set.begin(),result_set.end());
}
};
逻辑:
迭代后
1:无限循环
(1)不重复循环:由于数不会无限变大,所以不能每个结果都不一样
(2)有重复循环:检查sum的结果有无重复,有重复就返回false
2:不无限循环
返回true
class Solution {
public:
int getsum(int n){
int sum = 0;
while(n){
sum += (n%10)*(n%10);
n = n/10;
}
return sum;
}
/*这个求和比较巧妙,要记住*/
bool isHappy(int n) {
unordered_set<int> set;
int sum = 0;
while(1)
{
sum = getsum(n);
if(sum == 1)
return true;
if(set.find(sum) != set.end())
return false;
else
set.insert(sum);
n = sum;
}
}
};
将数组的数copy两份,在第一个中遍历数据的时候,查找第二个中有无可以和该数据相匹配(相加=target),在查询匹配时本题涉及到对一个集合(数组)中元素的查找,因此选择哈希法
首先我在强调一下什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否·在集合里的时候,就要第一时间想到哈希法。本题呢,就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
那么我们就应该想到使用哈希法了。
使用数组和set来做哈希法的局限。
因此要选择一种有key和value的存储结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。
map是STL的一个关联容器,它提供一对一的hash。
有三种map
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。
同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
这道题目中并不需要key有序,选择std::unordered_map 效率更高! 使用其他语言的录友注意了解一下自己所用语言的数据结构就行。
这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。
那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。(此处是为了方便进行)
所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
为了避免全盘复制的操作(浪费),在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
问题:
key值要求不唯一,但是如果数组元素重复了在怎么办?
分析:
find解析:不是在找value是在找key
map.find(key)
因为是在找key,所以把数组元素作为key用来查找,对应下标当作value
代码:
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 iter = map.find(target - nums[i]);
//找不到的话返回map.end(),找到的话返回std::pair
//寻找是否有匹配,有的话返回两个下标
if(iter != map.end()){
return {iter->second,i};//iter->second返回map中的value(第二个值)
}
//没有找到匹配则插入到map中
map.insert(pair<int,int>(nums[i],i));//std::map 的 insert 函数接受一个键值对(std::pair)作为参数来插入元素。
}
return {};
}
};
本题四个重点:
本题来源于三数之和还有四数之和的简化,和二数之和差别很大,本题不要求结果不重复,因此选择哈希法,如果结果重复就要用其他方法了,哈希法在这里去重很困难
步骤:
(对于为什么不考虑a+c和b+d或者a和b+c+d这样的情况是否和上述情况有所区别:
直接把选取数的过程看作从四个数组里面各自取一个数,只有最后一个数会受前面数的影响,那么可以转换为3+1问题,同理3相加可以转换为2+1问题,那么再等效一下就变成了2+2问题)
map操作
value = map[key];//key是索引,value是值
代码
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> map_ab;
for(int a : nums1)
for(int b : nums2){
map_ab[a+b]++;
}
int count = 0;
for(int c:nums3)
for(int d:nums4){
if(map_ab.find(0-c-d) != map_ab.end()){
count += map_ab[0-c-d];
}
}
return count;
}
};
赎金信
有一点要注意,这是赎金信,为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次
涉及到元素的查找,且数据大小确定,不要求给出索引值,则用数组是合适的
步骤:类似有效的字母异位词
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int records[26]= {0};//不是[0]
for(int i = 0; i <magazine.size();i++){
records[magazine[i] - 'a']++;
}
for(int i = 0;i < ransomNote.size();i++){
records[ransomNote[i] - 'a']--;
if(records[ransomNote[i] - 'a'] < 0)//可以放到同一个循环中,因为没有--处理到的位置必然不会<0,此处表示刚好减到杂志中对应位置
return false;
}
return true;
}
};
本题和之前的四数相加存在一个重要的区别:题目中说的不可以包含重复的三元组,意味着要进行去重操作,使用哈希法很困难,因此选择双指针来解决
主要思路有两部分:
1:如何取三个数:
拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
2:如何去重:
if (nums[i] == nums[i + 1]) { // 去重操作
continue;
}
但是,我们先确定a的位置,此时还没有判断b,c,在找b,c的过程中可能会用到nums[i+1],那么nums[i+1]是不能先去掉的,于是我们要等b,c确定了再对a降重,因此我们就要判断相同的nums[i-1]是否要去除,因为nums[i-1]已经判断过了,与之对应的b,c已经确定,而nums[i]和nums[i-1]相同,那么对应的b,c也确定了,所以可以判断了,我们这样写:
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
class Solution {
public:
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){
return result;
}
if(i > 0&&nums[i] == nums[i -1]){
continue;//跳出本次循环
}//a的降重
int left = i+1;
int right = nums.size() - 1;
while(right > left){
if(nums[i] + nums[left] + nums[right] > 0)
right--;
else if(nums[i] + nums[left] + nums[right] < 0)
left++;
else{//开始b,c的降重
result.push_back(vector<int>{nums[i],nums[left],nums[right]});
// while(nums[right] == nums[right-1]) right--;
// while(nums[left] == nums[left+1])left++;
while(right > left&&nums[right] == nums[right-1]) right--;
while(right > left&&nums[left] == nums[left+1])left++;
//找到一次之后要继续找
right--;
left++;
}
}
}
return result;
}
};
和三数之和很类似,我们将其看做先找三数之和,再找三数之和加一数==target
注意:在三树之和中,和为0,有裁剪部分
if(nums[i] > 0)return result;
但是我们要知道,本题中的target为任意数,那么target>=0target<0是不一样的,target>=0时,nums[i]就>0,再加上比他大的数不可能变小,还可以用原来的裁剪方案,但是对于target<0,此时nums[i]加上比它大的数(某一个负数)结果可能变小,所以不能裁剪,因此裁剪方案变为:
if(nums[i]>target &&(nums[i]>=0)) return result;//只有nums[i]>=0才裁剪
但要注意,在三数之和中最外面只有一重for循环,直接return和break都是一样跳出一重循环,但是四数之和有两层for循环,此时第二层不能用return了只能用break,即:
if(nums[j]+nums[i]>target&&(nums[j]+nums[i]>=0))break;//这里不要再用return result了,用break只跳一重循环
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(),nums.end());
for(int i = 0;i < nums.size();i++){
if(nums[i]>target &&(nums[i]>=0)) return result;
if(i>0&&nums[i]==nums[i-1])//&&第一个为false就不计算后面,&计算两边
continue;
for(int j = i+1;j<nums.size();j++){
if(nums[j]+nums[i]>target&&(nums[j]+nums[i]>=0))break;//这里不要再用return result了,用break只跳一重循环
if(j>i+1&&nums[j]==nums[j-1])continue;
int left=j+1;
int right=nums.size()-1;
while(left<right){
if((long)nums[i]+nums[j]+nums[left]+nums[right] > target) right--;
else if((long)nums[i]+nums[j]+nums[left]+nums[right] < target) left++;
else{
result.push_back(vector<int>{nums[i],nums[j],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;
}
};
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用.
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
当我们的思路中涉及到查找相关的步骤,可以考虑把数据放到数组,set或者map中用哈希法解决问题
对于数据大小(种类)确定,可以使用一个固定大小的数组来存放和查找,且能用数组就用数组,因为简单且效率高
在383.赎金信 中同样要求只有小写字母,那么就给我们浓浓的暗示,用数组!
本题和242.有效的字母异位词 很像,242.有效的字母异位词是求 字符串a 和 字符串b 是否可以相互组成,在383.赎金信中是求字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a
上面两道题目用map确实可以,但使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效!
首先要了解数组的局限:
当给出的数据大小不确定,且只需要一个值,用set
关于set,C++ 给提供了如下三种可用的数据结构:(详情请看关于哈希表,你该了解这些! (opens new window))
数组和set的局限:
如果需要存放两个值,且大小未知,则用map
C++提供如下三种map::(详情请看关于哈希表,你该了解这些! (opens new window))
std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层实现是红黑树。
同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解),1.两数之和 中并不需要key有序,选择std::unordered_map 效率更高!
把数据值存放在数组的下标中,数组元素用来对表示对下标的操作的结果
record[magazine[i]-'a'] ++;
record[s[i] - 'a']++;
将数据存放到set中,然后使用find查找
定义两个set,一个set_1用于数据存放到其中,另一个set_2用于数据挨个到set_1中查找,查找结果放到set_2中
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
定义一个set,边判断边存入,先判断(查询)是否符合条件,符合条件就return,不符合的就存入set中
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;
1:存放数据并查找
先判断是否满足条件,没找到匹配的,就把数据存入map中
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
2:先用key存放数据,value存放对这个数据操作的结果;然后使用find进行查询
unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
// 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
int count = 0; // 统计a+b+c+d = 0 出现的次数
// 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
用数组当成哈希表,数据当作下标,把nums[i]用于计数,先计第一个string,用++,再计第二个string,用- -;
涉及两组数据,定义两个set,一个为result,一个set放其中一组数据,把查找set中有无和另一组数据相同的数据,把相同的放到result中
定义一个set,先判断sum是否再result中出现过,然后再把sum放进result中
定义一个map,遍历,先判断目标元素是否在map中,在的话返回结果,不在的话就把当前nums[i]和下标存入map中
涉及到四组数据,按照之前的思路,应该用三层for循环,最后一层查找,但是这样时间复杂度,空间复杂度(要先复制三组数据)都比较高,由于本题要求的答案只是次数的统计,所以没必要按照这样的完整方案实行:
定义一个map,key存放a+b的值,value存放对应值出现的次数,然后再次遍历,将map[0-c-d]的数量加到count上, 最后输出count;
本题算是一种小技巧,和其他题有点区别
用数组,先再magazine中把每一个字母映射到一个数组record中,具体方法:下标存字母对应顺序,数值存字母出现次数,然后再ransom note中把字母对应的下标中的数值- -,并且在此轮循环中就可以判断是否出现record【i】<0(magazine中没有的字母但ransom note中出现了),出现了就return false,一直没出现就return true;
用哈希表很复杂(去重麻烦),双指针方便
1:一般为减少时间开支,在最后一个for循环里面,一边遍历,判断存放数据,一边就判断时候输出,能少用一个循环
2:待定(二刷说不定有新体会)