1 unordered_map
C++ 98 中,STL提供了底层为红黑树的一系列关联式容器,查询效率可以达到O(n),即最差情况下需要比较红黑树高度次
但是 假如红黑树比较大是,那么O(log N )的查询效率仍然显得有些慢。因此
**C++11 中提供了unordered 系列容器,**可以达到查询效率为O(1);
4 个unordered 系列容器,
unordered_map
unordered_set
unordered_multimap
unordered_multiset
unordered_map 介绍
1 unordered_map 存储<key, value>键值对结构,可以通过key值快速的索引到要查询的value,(类似数组下标)
2 unordered_map 的健值唯一标识一个元素,key——value 可以类型不同
3 unordered_map 底层并不存在特定顺序
4 unordered_map 通过key可以快速的找到value,而map因为有序,就必须进行遍历查找,所以时间效率很低。
5 unordered_map 实现了 operator[]运算符的重载,因此允许使用key作为参数直接访问value
6迭代器至少是前向迭代器。
2 unordered_map 接口说明
unordered_map <key_type,value_type> mp;
构造不同键值对类型的unordered_map 对象。
bool empty() const //const类型函数(类似const类型常量),返回值为const类型
size_t size()const//返回内部有效键值对的个数
3 迭代器(不存在反向迭代器,STL中以哈希桶+链表实现, 即 线性探测 + 开散列)
begin
end
cbegin
cend
4 operator 【】
有返回相应key的value,没有创建一个默认值
它会构造一个键值对 key --V()
成功返回V() ,失败 说明已存在返回value
5 iterator find(const T& key)//返回key所在位置的迭代器,不存在返回nullptr;
size_t count(const T& key),返回哈希桶中key为关键码的键值对的个数,即 某一桶中 链表中节点的个数
insert
erase
void clear
void swap(unordered_map<,>& mp)j交换两个 unordered_map
size_t bucket_count() const //哈希桶的个数
size_t bucket_size(size_t n)const 第n个桶中元素(节点)的个数
size_t bucket(const k& key) ;返回元素key所在的桶号
为什么unordered_map 的查询速率比map高呢 ,就是因为底层采用了哈希结构
哈希
在 红黑树 或AVL树中, key与 value 不存在映射关系,因此需要比较多次。
哈希 结构的想法: 可不可以不用比较,直接得到要查的值, 哈希想呀想
于是就想出了 哈希结构。
哈希想,通过一个函数唯一得到一个相应的值,即建立一种映射关系 -----即 哈希函数。
常见的哈希函数
1 直接定制法
2 除留余数法
哈希函数 设计原则:
1 简单
2 哈希函数计算结果应分布 均匀
3 必须包含素有的关键码
哈希函数又称哈希散列函数,哈希结构被称为哈希表或散列表。
哈希函数 hash(key) = key % capacity
哈希冲突 :
对于多个元素的关键码 ,通过哈希函数计算出来的地址,可能会出现一只的情况
例如; 1,2,3,12,13,18 ,
capacity为10时,
2 与 12 计算地址冲突
3 与 12 计算结果冲突
哈希冲突解决办法
总体上 分 闭散列 和 开散列
1 闭散列(开放地址法)
描述: 当发生哈希冲突时,若哈希表未被填充满, 则采用线性探测的方法,找下一个 “空位置“ ,并将 发生冲突的元素放入其中。
线性探测:
前提: 1发生冲突 ,2 哈希表未满
从当前地址开始 ,直到找下一个空位置为止(可以回到表头接着找)找到,插入新元素
闭散列中的删除
采用闭散列处理哈希冲突时,不能随便真的删除某一个元素, 闭散列的处理冲突相当于递推,
假如真的删除一个元素,那么可能会导致,依赖这个元素推出其他元素再也不能被找到。
因此 我们为每一个元素提供一个状态信息来描述该元素存在与否。
enum state{ EMPTY, EXIST, DELETE}
闭散列的增容机制
为何增容
在 探测过程中,我们发现哈希表中的元素数量达到一定程度时,哈希冲突将会十分明显。而更多的冲突会累积下来。因此 我们需要在一定时期,对哈希表进行增容
何时增容
散列表(哈希表)的负载因子a
a = 已有数量 / 哈希表容
研究发现,在 闭散列中(开放地址法中),当负载因子 达到0.7,时 ,线性探测的不命中率很大, 所以 当 a >= 0.7 时,赶快增容!!!
如何增容:
研究也发现,散列表的大小为素数时,线性冲突的概率比较低
已往扩容,在 linux 下容量一般为扩大2倍, VS一般为扩大1.5倍。我们统一扩大2倍吧。
利用素数表 快速获取一个2 被关系的素数作为要扩容的目标大小。
我们的散列表说白了 就是一个 vector,
因此可以 利用vector 扩容的思想。
1 申请空间
2 转移元素
3 释放旧空间
要注意的是: 扩容之后,扩容之后,散列表的capacity 就扩大了,所以 ,不能直接替换,
而需要一个一个重新利用哈希函数重新计算之后,在进行插入。
线性探测优点:
思路简单
缺点:
1 空间利用率低
2 形成的冲突时,容易堆积,导致搜索效率受到影响。
二次探测
二次探测目的就是为了解决线性哈希冲突 发生时 ,容易堆积的 问题。
思路:拉大探测间距
哈希函数计算得到一个相对地址时,
若存在冲突,那么 在此计算
H = H0 - i^2
或者 H = H0 + i^2
客观来说 ,二次探测不咋滴, 当负载因子到达一定程度,照样会导致冲突加剧。
研究表明 负载 因子 不超过0.5 时,插入元素一定不会发生冲突,所以当负载因子 超过0.5 最好进行扩容。
那么 显而易见 ,在二次探测中,只有空间利用率低于0.5 才能保证闭散列的效率。
开散列
开散列又称链地址法(开链法)
思路 :哈希桶 + 链表
看图 ,一目了然了吧
开散列的插入、查询 、删除元素,不就转化为链表来处理了吗 ,更简单了吧。‘
那么开散列要查询某一个元素存不存在,只需要遍历一下链表,一般来说,虽然需要遍历 一个哈希桶中的链表,但 因为哈希桶数量比较多,所以一般认为遍历时间效率为O(1)
那么,开散列最好的效率是什么情况 ?
当然是每个桶中只有一个或不含数据的情况吧。 即不需要遍历。
那么 也就是说,当桶数bucket_count >= _size;时效率最高
元素数大于桶数是时立马扩容,永远可以保证效率最高,真真切切为 O(1)
那么 开散列如何 扩容? 能否 还用vector 的思想?
其实也可以 ,但是 这样效率不咋样。
1 vector 中存的是 链表的地址,而发生拷贝是,其实是把地址拷贝过去了,也就是说发生的类似于前拷贝,
释放旧vector后,就找不到原有数据了;
所以 我们还是得一个一个Insert,
2 当Insert新元素的时候,我们要尽量避免一个一个创建节点,再一个一个释放就节点,也就是说,尽量利用好原有创建好的节点,