关联式容器unordered系列

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新元素的时候,我们要尽量避免一个一个创建节点,再一个一个释放就节点,也就是说,尽量利用好原有创建好的节点,

发布了90 篇原创文章 · 获赞 13 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_44030580/article/details/104774677
今日推荐