目录
1. 简介unordered_set与unordered_map
- 在C++库中,除开map与set这两个关联式容器外,还存在着另外两个此类容器,unordered_set,unordered_map。
- unordered中文释义为无序的,这也正是这一对容器使用时的表征特点,这一对容器分别对应set与map,即K模型与KV模型的存储数据结点。
- 那么,除开使用迭代器遍历时,其内存储数据无序外,这一对容器与map与set容器有何不同,为什么要在已有map与set的情况下,再向库中加入这一对乍看功能冗余且劣于原本map与set的容器呢?我们来看下面的一组对照试验。
- unordered_set与unordered_map其的关键性常用接口与使用和map,set相同,不同的是其只支持正向迭代器且多了一些桶,负载因子相关的接口。
#include <iostream>
using namespace std;
#include <unordered_set>
#include <set>
#include <stdlib.h>
#include <time.h>
#include <vector>
int main()
{
const int N = 1000000;
vector<int> data(N);
set<int> s;
unordered_set<int> us;
srand((unsigned int)time(NULL));
for (int i = 0; i < N; i++)
{
//data.push_back(rand());//重复数据多
//data.push_back(rand() + i);//重复数据少
data.push_back(i);//有序数据
}
//插入
int begin1 = clock();
for (auto e : data)
{
s.insert(e);
}
int end1 = clock();
int begin2 = clock();
for (auto e : data)
{
us.insert(e);
}
int end2 = clock();
cout << "insert number:" << s.size() << endl << endl;
//查找
int begin3 = clock();
for (auto e : data)
{
s.find(e);
}
int end3 = clock();
int begin4 = clock();
for (auto e : data)
{
us.find(e);
}
int end4 = clock();
int begin5 = clock();
for (auto e : data)
{
s.erase(e);
}
int end5 = clock();
int begin6 = clock();
for (auto e : data)
{
us.erase(e);
}
int end6 = clock();
cout << "set Insert:" << end1 - begin1 << endl;
cout << "unordered Insert:" << end2 - begin2 << endl << endl;
cout << "set Find:" << end3 - begin3 << endl;
cout << "unordered Find:" << end4 - begin4 << endl << endl;
cout << "set Erase:" << end5 - begin5 << endl;
cout << "unordered Erase:" << end6 - begin6 << endl << endl;
return 0;
}
运行结果:
- 由运行结果可得
<1> 当数据无序时在重复数据较多的情况下,unordered系列的容器,插入删除效率更高
<2> 当数据无序但重复数据较少的情况下,两种类型的容器,两者插入数据效率仿佛
<3> 当数据有序时,红黑树系列的容器插入效率更高- 在日常的应用场景中,很少出现有序数据情况,虽然map,set有着遍历自得有序这一优势,但关联式容器的主要功能为映射关系与快速查询,其他优点仅仅是附带优势。所以,unordered系列容器的加入与学习是必要的。
2. 哈希表(散列)
2.1 哈希表的引入
- 在初阶数据结构的学习中,我们学习了一种排序方式,叫做基数排序,其使用数组下标表示需排序数据,下标对应的元素代表相应数据的出现次数。以此映射数据并排序,时间复杂度只有O(n)。
- 基数排序创建的数据存储数组,除可以用于数据排序外,我们不难发现其用来查询一个数据在或不再,可以通过访问下标对应数据直接得到,查询效率及其高。
- 这一为排序所创建的存储数组,就是一个简单的哈希表,我们也称之为散列,即数据并非连续而是散列在一段连续的空间中。
- 哈希表中的哈希,是指一种数据结构的设计思想,为通过某种映射关系为存储数据创建一个key值,其的映射关系不一,但都可以通过key值找到唯一对应的一个数据,且使得数据散列在存储空间中。
- 上述的存储结构为常见哈希表结构的一种,我们称之为直接定址法哈希表,但此种哈希表其使用上存在诸多限制,当存储数据的范围跨度较大时,就会使得空间浪费十分严重。接下来,我们来学习几种在其基础上进行优化具有实用价值的常用哈希表结构。
2.2 闭散列的除留余数法
2.2.1 前置知识补充与描述
- 除留余数法:为了解决存储数据大小范围跨度过大的问题,我们不再直接使用存储数据左key值映射,而是通过存储数据除以哈希表大小得到的余数做key值。这样就能将所有数据集中映射至一块指定大小的空间中。
- 哈希冲突/哈希碰撞:取余数做key值的方式虽然能够使得数据映射到一块指定空间中,并大幅度减少空间的浪费,可是这也会产生一个无法避免的问题,那就是不同数据的经过取余得到的余数可能相同,如此就会导致同一key值映射多个数据,使得无法进行需要的存储与查询。这一问题就被称为哈希碰撞。
- 线性探测与二次探测:哈希碰撞的解决存在多种方法策略,这里我们简单了解两种常用方式
<1> 线性探测:当前key值对应数据位被占用时,向后遍历(hashi + i),直至找到为空的数据位再将数据存入,而探测查询时,也以线性逻辑搜索直至遍历至空,则代表查询数据不存在,越界回绕。
<2> 二次探测:当前key指对数据位被占用时,当前key值依次加正整数的平方(hashi + i 2 i^2 i2)直至遍历至空存入,探测查询时,依次逻辑或至空结束,越界回绕。- 补充:
<1> 负载因子:哈希表中存储数据个数与哈希表长度的比值,一般控制在0.7左右,若负载因子超过,进行扩容
<2> 线性探测容易导致数据的拥堵与踩踏,二次探测的方式为对此的优化
<3> 处理非数字类型数据,将其转换为size_t类型后,再进行key值映射,采用仿函数的方式
2.2.2 闭散列哈希表实现
- 哈希表结构
//结点状态
enum State
{
EMPTY,//可插入,查询结束
EXIST,//不可插入,向后查询
DELETE//可插入,向后查询
};
//数据结点
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
State _state;
HashNode(const pair<K, V>& kv = make_pair(K(), V(