C++之unordered_map和unordered_set以及哈希详解
Posted 小赵小赵福星高照~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++之unordered_map和unordered_set以及哈希详解相关的知识,希望对你有一定的参考价值。
unordered_map和unordered_set的使用
文章目录
map和set底层是红黑树实现的,map是KV模型,set是K模型,而unordered_map和unordered_set底层是哈希表实现的,unordered_set是K模型,unordered_map是KV模型
unordered_map和unordered_set的命名体现特点,在功能和map/set是一样的,区别在于,它遍历出来是无序的,另外,它们的迭代器是单向迭代器
unordered_map的文档
- unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的
value。- 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键
和映射值的类型可能不同。- 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所
对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。- unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率
较低。- unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
- 它的迭代器至少是前向迭代器。
下面我们来看一下unordered_map和unordered_set的使用:
#include<iostream>
#include<unordered_set>
#include<unordered_map>
using namespace std;
void test_unordered_set()
unordered_set<int> s;
s.insert(3);
s.insert(4);
s.insert(5);
s.insert(3);
s.insert(1);
s.insert(2);
s.insert(6);
unordered_set<int>::iterator it = s.begin();
while (it != s.end())
cout << *it << " ";
++it;
cout << endl;
int main()
test_unordered_set();
return 0;
可以看到它遍历出来是无序的,并且相同的数只会插入一次
#include<iostream>
#include<unordered_map>
using namespace std;
void test_unordered_map()
unordered_map<string, string> dict;
dict.insert(make_pair("string", "字符串"));
dict.insert(make_pair("sort", "排序"));
dict.insert(make_pair("string", "字符串"));
dict.insert(make_pair("string", "字符串"));
auto it = dict.begin();
while (it != dict.end())
cout << it->first << ":" << it->second << endl;
it++;
int main()
test_unordered_map();
return 0;
它遍历出来也是无序的,并且相同的数只会插入一次
unordered_set和unordered_map的两个OJ题
两个数组的交集
题目思路:
用unordered_set对nums1中的元素去重,然后用unordered_set对nums2中的元素去重,最后遍历s1,如果s1中某个元素在s2中出现过,即为交集
class Solution
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2)
// 用unordered_set对nums1中的元素去重
unordered_set<int> s1;
for (auto e : nums1)
s1.insert(e);
// 用unordered_set对nums2中的元素去重
unordered_set<int> s2;
for (auto e : nums2)
s2.insert(e);
// 遍历s1,如果s1中某个元素在s2中出现过,即为交集
vector<int> vRet;
for (auto e : s1)
if (s2.find(e) != s2.end())
vRet.push_back(e);
return vRet;
;
两个数组的交集二
题目思路:
1、两个位置的值相等,则是交集,同时++
2、两个位置的值不相等,则不是交集值,小的++
set和undered_set的性能比较
void test_op()
set<int> s;
unorder_set<int> us;
const int n = 100000;
vector<int> v;
srand(time(0));
for(size_t i = 0;i<n;++i)
v.push_back(rand());
//插入性能比较
size_t begin1 = clock();
for(auto e:v)
us.insert(e);
size_t end1 = clock();
cout<<end1-begin1<<endl;
size_t begin2 = clock();
for(auto e:v)
s.insert(e);
size_t end2 = clock();
cout<<end2-begin2<<endl;
//查找性能比较
size_t begin3 = clock();
for(auto e:v)
us.find(e);
size_t end3 = clock();
cout<<end1-begin1<<endl;
size_t begin4 = clock();
for(auto e:v)
s.find(e);
size_t end4 = clock();
cout<<end2-begin2<<endl;
//删除效率比较
size_t begin5 = clock();
for(auto e:v)
us.erase(e);
size_t end5 = clock();
cout<<end5-begin5<<endl;
size_t begin6 = clock();
for(auto e:v)
s.erase(e);
size_t end6 = clock();
cout<<end6-begin6<<endl;
发现不管运行多少次,都是哈希表比较优,unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
哈希
哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经
过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log2N),搜索的效率取决
于搜索过程中元素的比较次数。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过
某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函
数可以很快找到该元素。当向该哈希结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比
较,若关键码相等,则搜索成功该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表
(Hash Table)(或者称散列表)
例如:我们有数据集合1,7,6,4,5,9;
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中
插入元素44,会出现什么问题?发现4这个位置已经被占用了
哈希冲突
对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过
相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?
常见哈希函数
- 直接定址法
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
- 除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函
数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
哈希冲突的解决
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那
么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
- 线性探测:index+i (i = 1,2,3,4…)
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
- 二次探测:index+i^2(i = 1,2,3,4…)
线性探测的缺陷是产生冲突的数据堆积在一块,相比线性探测的好处,如果一个位置有很多值映射,冲突剧烈,那么它们存储时相对会比较分散,不会引发一片一片的冲突
开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码
归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结
点存储在哈希表中。
闭散列的实现
我们实现哈希表,那么数据怎么存储呢?数据的结构又是什么呢?数据的存储很好想到,可以用一个vector存就好了,那么数据的结构出来存储数据还需要什么呢?比如我们有以下数据:
我们查找一个值时,按照哈希函数求得位置,如果当前位置不是,则可能遇到了冲突,所以一直往后找,当遇到空位置时说明这个值肯定不存在了,因为如果存在,肯定存储在了第一个空位置,那么当我们删除一个值,直接删除的话这个位置变成了空,但是这样有问题:使得查找一个值提早遇到空,那么就找不到这个值
比如删除333,直接删除后变成了空位置,我们再查找14会怎么样?我们先求得14的位置是4,发现4这个位置是空,所以直接就返回不存在,导致14存在,但是查找不到14
解决方案:每个位置存储值得同时再存储一个状态标记:空、满、删除
如果一个值删除了,将该位置标记为删除,查找一个值如果遇到了删除,那么不停止继续查找,插入一个值时遇到了空和删除都可以插入
所以还需要该存储位置的当前状态:
- EMPTY(空数据的位置)
- EXIST(已存储数据)
- DELETE(以前有数据,但是删除了)
enum Status
EMPTY,//空
EXIST,//存在
DELETE//删除
;
数据的存储结构
我们实现KV模型,所以数据弄成pair,另外还需要状态:
template<class K,class V>
struct HashData
pair<K,V> _kv;
Status _status = EMPTY; //状态
哈希表的结构
vector来存储HashData,_n用来存储有效数据的个数
template<class K,class V>
class HashTable
public:
private:
vector<HashData<K,V>> _tables;//vector来存储HashData
size_t _n = 0;//存储有效数据的个数
;
哈希表的插入
我们为了避免哈希冲突变多,我们引入一个负载因子,当负载因子过大时需要对哈希表进行增容
- 如果哈希表中已经存在该键值对,则插入失败返回false
- 判断该哈希表的大小以及负载因子,确定是否需要增容
- 将键值对插入哈希表
- ++_n
首先我们解决增容的问题,首先我们确定的是_table.size() == 0时需要增容,其次我们设置负载因子=有效数据/表的大小,如果负载因子大于0.7时就增容。
那么怎么增容呢?
有一种方法是创建一个新vector,resize新空间大小,然后将旧表中的数据复制到新表当中
//方法一
size_t newSize = _table.size()==0? 10:_table.size()*2;
vector<HashData<K,V>> newTable;
newTable.resize(newSize);
for(size_t i =0;i<_tables.size();++i)
if(_table[i]._status == EXIST)
//将存在的数据复制到新表中
size_t index = _table[i]._kv.first % newTable.size();
//...
比较好的方法是建立一个临时新表,给新表开扩容后的空间,然后遍历旧表将旧表的数据插入新表,最后将新表和旧表交换:
//方法二:
size_t newSize = _table.size()==0? 10 : _table.size()*2;
HashTable<K,V> newHT;//建立一个临时新表
newHT._tables.resize(newSize);//给新表开扩容后的空间
for(auto& e:_tables)
if(e._status == EXIST)
newHT.Insert(e._kv);//将旧表的数据插入新表
_table.swap(newHT._tables);//将新表和旧表交换
插入哈希表的代码(首先计算出插入的位置,如果该位置存在的话就遍历后面的位置,遍历的过程中如果等于的表的末尾,则返回起始位置继续遍历,直到遇到空或者删除):
size_t start = kv.first % _tables.size();
size_t i = 0;
size_t index = start + i;
while(_table[index]._status == EXIST)
++i;
//index = start+i;//线性探测
index = start+i*i;//二次探测
if(index == _tables.size())
//当index到达最后的时候,让它回到起始
index = 0;
//走到这里要么是空要么是删除
_tables[index]._kv = kv;
_tables[index]._status = EXIST;
++_n;
插入的完整代码:
//插入
bool Insert(const pair<K,V>& kv)
if(Find(kv.first))
return false;
if(_table.size() == 0 || (double)(_n / _table.size()) > 0.7)
//扩容
size_t newSize = _table.size()==0? 10 : _table.size()*2;
HashTable<K,V> newHT;//建立一个临时新表
newHT._tables.resize(newSize);//给新表开扩容后的空间
for(auto& e:_tables)
if(e._status == EXIST)
newHT.Insert(e._kv);//将旧表的数据插入新表
_table.swap(newHT._tables);//将新表和旧表交换
size_t start = kv.first % _tables.size();
size_t i = 0;
size_t index = start + i;
while(_table[index]._status == EXIST)
++i;
//index = start+i;//线性探测
index = start+i*i;//二次探测
if(index == _tables.size())
//当index到达最后的时候,让它回到起始
index = 0;
//走到这里要么是空要么是删除
_tables[index]._kv = kv;
_tables[index]._status = EXIST;
++_n;
return true;
哈希表的查找
哈希表的查找逻辑很简单:先计算出查找的key的位置,当这个位置不等于空时,判断key值是不是相等并且状态位存在,如果是的话返回,不是的话进行线性探测或者二次探测,直到遇到位置为空状态
HashData<K,V>* Find(const K& key)
if(_table.size() == 0)
//防止除0错误
return nullptr;
size_t start = key % _table.size();
size_t i = 0;
size_t index = start + i;
while(_tables[index]._status != EMPTY)
if(_tabled[index]._kv.first == key && _table[index]._status == EXIST)
return &_tabled[index];
else
++i;
//index = start +i;
index = start + i*i;//二次探测
index %= _tables.size();
return nullptr;
哈希表的删除
首先找到这个节点,进行伪删除,将该节点的状态设置成删除然后–_n即可
bool Erase(const K& key)
HashData<K,V>* ret = Find(key);
if(ret == nullptr)
//没有这个值
return false;
else
//伪删除
ret->_status = DELETE;
_n--;
return true;
哈希表闭散列的代码
#pargma once
namespace close_hash
enum Status
EMPTY,//空
EXIST,//存在
DELETE//删除
;
template<class K,class V>
struct HashData
pair<K,V> _kv;
Status _status = EMPTY; //状态
template<class K,class V>
class HashTable
public:
bool Erase(const K& key)
HashData<K,V>* ret = Find(key);
if(ret == nullptr)
//没有这个值
c++ unordered_map 自定义key
C++ unordered_map 的 rehash() 和 reserve() 方法有啥区别?
C++中map/set和unordered_map/unordered_set的区别及其适用情况