哈希表理论基础
杂货店商品价格问题,就可以引出哈希表
哈希表
通过哈希函数将商品名映射到哈希表上,得到一个整数,这个整数就是哈希表上对应商品的索引,我们可以把商品价格存放到这个索引里面.
我们只需要将商品名给哈希函数,哈希函数得出商品索引,再在哈希表上搜索这个索引,就可以得到这个商品的价格
哈希函数
BV1qR4y1V7g6
-
功能:输入数据,生成整数
-
特点:
-
不同的输入,输出不同的整数
-
相同的输入,输出相同的整数
-
不过呢,事实上,任何算法都不能保证,不同的输入一定是输出不同的整数
如果出现不同输入,输出同样的整数,这样称为冲突(collision)
SHA-256 MD5成熟的哈希算法
常见的三种哈希结构
-
数组
-
set (集合)
-
map(映射)
set
(集合)
特点
✅ 不存储重复元素(自动去重) ✅ 默认按小到大排序(std::set
,基于红黑树) ✅ 无序哈希表实现(std::unordered_set
) ✅ 常见操作:插入、查找、删除
map
(映射/字典)
特点
✅ 存储 key-value
(键值对) ✅ 键(key)唯一,不能重复 ✅ 默认按 key 升序排序(std::map
,基于红黑树) ✅ 无序哈希表实现(std::unordered_map
) ✅ 常见操作:插入、查找、删除、更新
上面是通俗的讲了set和map,下面讲讲具体的数组-set-map构成的哈希表
和一般数组、set、map不一样噢
数组构成的哈希表
-
value 本身就是我要的值。
-
value 输入哈希函数,得到一个整数(索引),然后将 value 存入该索引对应的数组位置。
-
查找时,同样用 value 计算哈希值,找到索引,取出 value。
-
✅ 适用于 key 取值范围小的情况,查找和插入 O(1)。
-
❌ 如果 key 过大,数组会很浪费空间。
unordered_set
(基于哈希表的集合)
-
value 送入哈希函数,得到一个索引,然后将 value 存入这个索引对应的位置。
-
查找时,value 再次计算哈希值,找到索引,即可判断是否存在。
-
unordered_set
不保证元素的顺序,它的排列由哈希函数决定,并不会按照从小到大排序。 -
unordered_set
中也不允许有重复的元素。插入重复的元素时,容器会忽略这一操作。 -
✅ 查找/插入的平均时间复杂度 O(1)。
-
❌ 如果哈希冲突严重,最坏情况下查找/插入变成 O(n)。
🚨 修正点:unordered_set
存储顺序是 由哈希函数决定的,不是按从小到大排列的。 如果你要有序集合,应该用 set
(基于红黑树)。
unordered_map
(基于哈希表的键值对存储)
-
key 送入哈希函数,得到一个索引,然后将
key -> value
对存入该索引对应的位置。 -
查找时,key 再次计算哈希值,找到索引,取出 value。
-
unordered_map
不保证 key 的存储顺序,顺序由哈希函数决定。 -
✅ 查找/插入的平均时间复杂度 O(1)。
-
❌ 如果哈希冲突严重,最坏情况下查找/插入变成 O(n)。
242.有效的字母异位词
class Solution { public: bool isAnagram(string s, string t) { int result[26]={0}; for(int i=0;i<s.size();i++) { result[s[i]-'a']++; } for(int j=0;j<t.size();j++) { result[t[j]-'a']--; } for(int a=0;a<26;a++) { if(result[a]!=0) { return false; } } return true; } };
错误
第二个表写错字母了
核心思想
将 result[26]
理解为一种非常简单的哈希表,用于记录每个字母出现的次数。这个数组模拟了哈希表的功能,但它没有使用哈希函数,而是通过字符的 ASCII 值直接映射到数组索引。
或者说这里的哈希函数是s[i] - 'a'
将字母映射进哈希表 result[26]
249.两个数组的交集
输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。->用set哈希
错误
-
为什么
int it = cross.find(nums2[j]);
是错的
cross.find(nums2[j])
返回的是一个 迭代器,而不是一个整数。
应该使用一个 迭代器类型 来接收 find
返回的结果。auto it = cross.find(nums2[j]);
-
result.insert(nums2[j]);
是错的
unordered_set
使用 insert
添加元素,但 vector
并没有 insert
方法来添加单个元素(它的 insert
方法是用于在指定位置插入多个元素)。你应该使用 push_back
来将元素加入 vector
。
class Solution { public: vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { std::unordered_set<int> cross; vector<int> result; for(int i=0;i<nums1.size();i++) { cross.insert(nums1[i]); } for(int j=0;j<nums2.size();j++) { auto it = cross.find(nums2[j]); if(it!=cross.end()) { result.push_back(nums2[j]); cross.erase(it); } } return result; } };
erase的使用
cross.erase(it);
删除it
指向的数据,因为我们已经将数据送入result
了,如果不删除,那么下一次遇到还会往result
中送入一次,就会错误.
使用哈希表的优势
利用 unordered_set
存储一个数组的元素,并通过哈希表快速查找另一个数组的元素是否存在于第一个数组中。 unordered_set
它在查找、插入和删除操作上具有平均常数时间复杂度 O(1),而在传统的数组或列表中,查找某个元素需要线性扫描,时间复杂度是 O(n)。
202.快乐数
思路
如果不是快乐数,求和的过程中,sum会重复出现
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
还有一个难点就是求和的过程,如果对取数值各个位上的单数操作不熟悉的话,做这道题也会比较艰难。
// 取数值各个位上的单数之和 int getSum(int n) { int sum = 0; while (n) { sum += (n % 10) * (n % 10); n /= 10; } return sum; }
代码
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) { std::unordered_set<int> sumlist; while(1) { n=getsum(n); auto it = sumlist.find(n); if(it==sumlist.end()) { sumlist.insert(n); } else if(n==1) return true; else return false; } } };
错误
对于unordered_set
的正确使用:
erase
使用不当,会导致错误。
在 unordered_set
中,cross.erase(it)
删除的是 it
迭代器所指向的元素。
具体来说,it
是通过 find
查找得到的一个迭代器,指向 unordered_set
中一个已经存在的元素。erase(it)
调用后,会从 unordered_set
容器中删除这个元素。
1.两数之和
暴力解法
class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { int v=0; int size1 = nums.size(); vector<int> result; for(int i=0;i<size1;i++){ v=target-nums[i]; for(int j=i+1;j<size1;j++) { if(nums[j]==v){ result.push_back(i); result.push_back(j); return result; } } } return result; } };
使用哈希表(什么时候用哈希法)
什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。
本题呢,我就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
因为本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
class Solution { public: vector<int> twoSum(vector<int>& nums, int target) { std::unordered_map<int,int> mymap; for(int i=0;i<nums.size();i++) { auto it = mymap.find(target-nums[i]); { if(it!=mymap.end()) { return {it->second, i}; } else mymap.insert(pair<int,int>(nums[i],i)); } } return {}; } };
学到的
-
map因为有键值对,所以建立的时候是
unordered_map<int,int> mymap;
-
同样是因为有键值对,所以插入是
mymap.insert(pair<int,int>(nums[i],i));
-
键值对中,谁作为键谁作为值是根据要解决的问题来的,并不是索引一定要作为键,而数据作为值
-
具体而言,本文我们是查找值,返回索引
-
值是用来被查找的内容,所以键应该是数据,而值反而应该是索引
-
这里我们知道,索引可以是数组的下标
但是索引广义的意思是用来寻找的标志,当我们需要寻找值来找索引时,索引和值的定位就反过来了