向Carl老师学习,用最简单的话,讲述最复杂的知识。( •̀ ω •́ )✧
什么是哈希表?
灵魂一问,这是正常人看到后,都会产生的疑问,就好比你是谁。
初学者可以将其看成一个数组!
大家都知道数组是怎么存数据的!
通过下标(0~N,是一串连续的数字),将对应数据为其赋值。
哈希表大致就是这样存储的。
既然如此,那为啥它不叫数组,而叫哈希表(⊙_⊙)?
因为哈希表计算计算下标的时候,是通过一个名为 哈希函数 的工具,将key值(也就是输入内容,字符串也好,数字也好) 通通转化为一个特定的下标。而这个被计算出来的数据,功能就相当于数组中的下标!
(哈希表就是数组的拓展。也就是说,若没有数组,自然也就没有哈希表!)
为什么需要哈希表?
看到这,那肯定有人要问,哈希表 取个下标还要计算,多么麻烦呐?为啥不用数组?
假如我们有一组数据,某位工程师每年某个月的收入情况
2017年1月 -- 10000
2018年2月 -- 13000
2019年4月 -- 14000
2020年12月 -- 20000
你拿数组怎么存?
怎么存?
在上方说过,你可以通过哈希函数(hashFunction),将他们转化为特定的数字。
2017年1月 --> 11
2018年2月 --> 13
2019年4月 --> 14
2020年12月 --> 20
这样就能直接实现存入。
反过来操作,就能实现查找!完美的实现了时间复杂度为O(1)的操作。
存在了一起怎么办?
思维严密的人,在看到这种通过计算,而得出的下标时,肯定会有疑惑!
万一计算出了相同的下标怎么办?
这里就涉及到了哈希碰撞,就是计算结果相同。
其实现在有多种解决方案,而在本篇博客中,主要详细讲解两种方法。
拉链法
想必链表大家都学过了,当你遇到这种问题时,如图
这样通过索引,就能链接下标相同,但是数值不同的数据。
线性探测法
这个就是当两个下标相同时,自动往后移动,直到遇到空位。
常见的3种哈希结构
- 数组
- set (集合)
- map (映射)
1、数组
这里就不多说了,想必大家都懂。
2、set(集合)
官方主要有3种,set,multiset,unordered_set。
在这里,先说unordered_set,unordered_set的底层,就是咱们上述所说的哈希表。查询速率为O(1),基础用法。
#include <iostream>
#include <unordered_set>
using namespace std;
int main(){
// 构建
unordered_set<int> uset;
// 增
uset.insert(3);
// 删
uset.erase(3);
uset.clear();
// 查
int size = uset.size();
bool isEmpty = uset.empty();
for(auto it = uset.begin(); it!=uset.end(); ++it){
cout<<*it<<endl;
}
for(int i:uset) cout<<i<<endl;
return 0;
}
其中,set与multiset的底层实现是红黑树,可能大家现在对“红黑树”还不够了解,只需知道,这俩底层不是用哈希表实现的就行,自然查询速率也就不是O(1),而是O(log N),这里就不做详细的解释啦。
3、map(映射)
同样大致也只有这3种,跟set其实相差不大。
map(映射),也主要分为map、multimap、unordered_map。
咱们这里也先说unordered_map,他的底层同unordered_set一样是哈希表。
#include <iostream>
#include <unordered_map>
using namespace std;
int main(){
// 默认构造
unordered_map<int,int> umap;
// 构造并初始化
unordered_map<int,string> myMap1 = {{1,"one"},{2,"two"}};
// 构造并指定初始容量
unordered_map<int,string> myMap2(10);
// 构造,并直接复制
unordered_map<int,string> myMap3 = myMap2;
// 插入
myMap2.insert({2,"2"});
// 访问
cout<<myMap2[2];
// 删除
myMap2.erase(2);
// 查找元素
auto it = myMap2.find(2);
if(it!=myMap2.end()){
cout<<"存在改元素"<<endl;
}
return 0;
}
今天就学到这里喽!( •̀ ω •́ )✧