1. unordered系列关联式容器
在
C++98
中,
STL
提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到
log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到。因此在C++11
中,
STL
又提供了
4
个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map
和unordered_set中的unordered_map进行详细介绍
2. unordered_map
1. unordered_map
是存储
<key, value>
键值对的关联式容器,其允许通过
keys
快速的索引到与其对应的value
2.
在
unordered_map
中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同
3.
在内部
,unordered_map
没有对
<kye, value>
按照任何特定的顺序排序
,
为了能在常数范围内找到key
所对应的
value
,
unordered_map
将相同哈希值的键值对放在相同的桶中
4. unordered_map
容器通过
key
访问单个元素要比
map
快,但它通常在遍历元素子集的范围迭代方面效率较低
5. unordered_maps
实现了直接访问操作符
(operator[])
,它允许使用
key
作为参数直接访问 value
6.
它的迭代器至少是前向迭代器
2.1 unordered_map的接口说明
1. unordered_map的构造
unordered_map
|
构造不同格式的
unordered_map
对象
|
2. unordered_map的容量
bool empty() const
|
检测
unordered_map
是否为空
|
size_t size() const
|
获取
unordered_map
的有效元素个数
|
3. unordered_map的迭代器
begin
| 返回unordered_map第一个元素的迭代器 |
end
| 返回unordered_map最后一个元素下一个位置的迭代器 |
cbegin
|
返回
unordered_map
第一个元素的
const
迭代器
|
cend
|
返回
unordered_map
最后一个元素下一个位置的
const
迭代器
|
4. unordered_map的元素访问
operator[]
|
返回与
key
对应的
value
,没有一个默认值
|
注意:该函数中实际调用哈希桶的插入操作,用参数
key
与
V()
构造的一个默认值往底层哈希桶中插入,如果key
不在哈希桶中,插入成功,返回
V();
插入失败,说明
key
已经在哈希桶中,将key
对应的
valu
返回
5. unordered_map的查询
iterator find(const K& key)
|
返回
key
在哈希桶中的位置
|
size_t count(const K& key)
|
返回哈希桶中关键码为
key
的键值对的个数
|
注意:unordered_map
中
key
是不能重复的,因此
count
函数的返回值最大为
1
6. unordered_map的修改操作
insert
|
向容器中插入键值对
|
erase
|
删除容器中的键值对
|
void clear()
|
清空容器中有效元素个数
|
void swap(unordered_map&)
|
交换两个容器中的元素
|
7. unordered_map的桶操作
size_t bucket_count()const
|
返回哈希桶中桶的总个数
|
size_t bucket_size(size_t n)const
|
返回
n
号桶中有效元素的总个数
|
size_t bucket(const K& key)
|
返回元素
key
所在的桶号
|
3. 底层结构
unordered
系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构
3.1 哈希
理想的搜索方法:可以
不经过任何比较,一次直接从表中得到要搜索的元素
。
如果构造一种存储结构,通过某种函数
(hashFunc)
使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素,该方式即为哈希(
散列
)
方法,哈希方法中使用的转换函数称为哈希
(
散列
)
函数,构造出来的结构称
为哈希表
(Hash Table)(
或者称散列表
)
哈希函数一般设置为:
hash(key) = key % capacity
; capacity
为存储元素底层空间总的大小
3.2 哈希冲突
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为
“
同义词”,哈希冲突很大程度上是因为哈希函数设置不够合理
3.3 哈希函数
哈希函数设计原则
:
1.哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有
m
个地址时,其值域必须在0
到
m-1
之间
2.哈希函数计算出来的地址能均匀分布在整个空间中
3.哈希函数应该比较简单
常见哈希函数
1.
直接定址法
--(
常用
)
取关键字的某个线性函数为散列地址:Hash
(
Key
)
= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2.
除留余数法
--(
常用
)
设散列表中允许的地址数为
m
,取一个不大于
m
,但最接近或者等于
m
的质数
p
作为除数,
按照哈希函数:
Hash(key) = key% p(p<=m),
将关键码转换成哈希地址
3.
平方取中法
假设关键字为1234
,对它平方就是
1522756
,抽取中间的
3
位
227
作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4.
折叠法
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5.
随机数法
通常应用于关键字长度不等时采用此法
6.
数学分析法
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
3.4 哈希冲突解决
解决哈希冲突
两种常见的方法是:
闭散列
和
开散列
3.4.1 闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把
key
存放到冲突位置中的
“
下一个
” 空位置中去,根据如何寻找下一个位置又分为线性探测和二次探测
1. 线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入:
通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素
![](https://img-blog.csdnimg.cn/direct/b2039863342f43cab0f19691b7a538d9.png)
删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索
。比如删除元素
4
,如果直接删除掉,
44
查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
线性探测优点:实现非常简单
线性探测缺点:
一旦发生哈希冲突,所有的冲突连在一起,容易产生数据
“
堆积
”
,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低
2. 二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法
为:
H_i= (H_0+i^2)%m,
或者:
H_i =(H_0- i^2)%m
。其中:
i =
1,2,3…
,
$H_0$
是通过散列函数
Hash(x)
对元素的关键码
key
进行计算得到的位置,
m
是表
的大小
![](https://img-blog.csdnimg.cn/direct/d16778ce94d740a08389d2a2eb69e754.png)
研究表明:
当表的长度为质数且表装载因子
a
不超过
0.5
时,新的表项一定能够插入,而且任
何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在
搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子
a
不超过
0.5
,如果超出,必须考虑增容。因此,比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
3.4.2 开散列
开散列法:又叫链地址法
(
开链法
)
,首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中,开散列中每个桶中放的都是发生哈希冲突的元素
4. 模拟实现
这里模拟实现的是开散列形式的哈希表
4.1 哈希表的改造
1. 对unordered_map和unordered_set不同传参的处理
// K:关键码类型
// V: 不同容器V的类型不同
// 如果是unordered_map,V代表一个键值对pair<const K,V>
// 如果是unordered_set,V 为 K
// KeyOfValue: 因为V的类型不同,通过value取key的方式就不同
// HF: 哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能取模,
//不同类型的key,方法不同
template<class K, class V, class KeyOfValue, class HF = HashFunc<K>>
class HashBucket;
2. unordered_map和unordered_set对KeyOfValue的处理
//unordered_set
struct KeyOfValue
{
const K& operator()(const K& data)
{
return data;
}
};
//unordered_map
struct KeyOfValue
{
const K& operator()(const pair<K, V>& data)
{
return data.first;
}
};
3. unordered_map和unordered_set对哈希函数HashFunc<K>的处理
//正常模板,能直接算Hashi的类型T
template<class K>
class HashFunc
{
public:
size_t operator()(const K& val)
{
//size_t防止出现负数val
return (size_t)val;
}
};
//string特化
template<>
class HashFunc<string>
{
public:
size_t operator()(const string& s)
{
const char* str = s.c_str();
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + *str;
str++;
}
return hash;
}
};
#pragma once
#include<vector>
#include<iostream>
using namespace std;
//正常模板,能直接算Hashi的类型T
template<class K>
class HashFunc
{
public:
size_t operator()(const K& val)
{
//size_t防止出现负数val
return (size_t)val;
}
};
//string特化
template<>
class HashFunc<string>
{
public:
size_t operator()(const string& s)
{
const char* str = s.c_str();
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + *str;
str++;
}
return hash;
}
};
namespace OpenHash
{
template<class T>
struct HashBucketNode
{
HashBucketNode(const T& data)
:_pNext(nullptr)
, _data(data)
{}
HashBucketNode<T>* _pNext;
T _data;
};
// 声明HashBucket
template<class K, class T, class KeyOfValue, class HF>
class HashBucket;
// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
template <class K, class T, class KeyOfValue, class HF>
struct HBIterator
{
typedef HashBucket<K, T, KeyOfValue, HF> HashBucket;
typedef HashBucketNode<T>* PNode;
typedef HBIterator<K, T, KeyOfValue, HF> Self;
HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr)
{
_pNode = pNode;
_pHt = pHt;
}
Self& operator++()
{
// 当前迭代器所指节点后还有节点时直接取其下一个节点
if (_pNode->_pNext)
_pNode = _pNode->_pNext;
else
{
// 找下一个不空的桶,返回该桶中第一个节点
size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode->_data)) + 1;
for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
{
if (_pHt->_table[bucketNo])
{
_pNode = _pHt->_table[bucketNo];
break;
}
}
// 后面没有桶了
if (bucketNo == _pHt->BucketCount())
{
_pNode = nullptr;
}
}
return *this;
}
//后置++
Self operator++(int)
{
PNode cur = _pNode;
// 当前迭代器所指节点后还有节点时直接取其下一个节点
if (_pNode->_pNext)
_pNode = _pNode->_pNext;
else
{
// 找下一个不空的桶,返回该桶中第一个节点
size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode->_data)) + 1;
for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
{
if (_pNode = _pHt->_table[bucketNo])
break;
}
}
return cur;
}
T& operator*()
{
return _pNode->_data;
}
T* operator->()
{
return &_pNode->_data;
}
bool operator==(const Self& it) const
{
return _pNode == it._pNode;
}
bool operator!=(const Self& it) const
{
return _pNode != it._pNode;
}
// HBIterator内部封装了一个指向HashBucket的指针和HashBucket里的当前结点指针
PNode _pNode; // 当前迭代器关联的节点
HashBucket* _pHt; // 哈希桶--主要是为了找下一个空桶时候方便
};
// 这里实现的哈希桶中key是唯一的
template<class K, class T, class KeyOfValue, class HF = HashFunc<K>>
class HashBucket
{
template<class K, class T, class KeyOfValue, class Hash>
friend struct HBIterator;
typedef HashBucketNode<T> Node;
typedef Node* PNode;
typedef HashBucket<K, T, KeyOfValue, HF> Self;
public:
typedef HBIterator<K, T, KeyOfValue, HF> iterator;
HashBucket(size_t capacity=10)
: _table(capacity), _size(0)
{}
~HashBucket()
{
Clear();
}
// 找到第一个桶的第一个节点
iterator begin()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i])
{
return iterator(_table[i], this);
}
}
return end();
}
// 返回以空结点构成的iterator
iterator end()
{
return iterator(nullptr, this);
}
// 哈希桶中的元素不能重复
pair<iterator, bool> Insert(const T& data)
{
KeyOfValue kot;
//Find返回非空指针说明表里已经有data了,不能插入
iterator found = Find(kot(data));
if (found!=end())
return make_pair(found, false);
CheckCapacity();
size_t Bucket = HashFunc(kot(data));
//头插到对应桶中
Node* cur = _table[Bucket];
Node* newnode = new Node(data);
newnode->_pNext = cur;
_table[Bucket] = newnode;
_size++;
return make_pair(iterator(newnode,this), true);
}
// 删除哈希桶中为data的元素(data不会重复)
// 返回指向删除位置后的iterator
iterator Erase(iterator position)
{
KeyOfValue kot;
size_t Bucket = HashFunc(kot(*position));//*position -> data
Node* cur = _table[Bucket];
Node* prev = nullptr;
while (cur)
{
if (cur == position._pNode)
{
Node* next = cur->_pNext;
//如果prev为空,说明该值是桶的第一个结点
if (prev == nullptr)
_table[Bucket] = next;
else
prev->_pNext = next;
delete cur;
_size--;
return iterator(next);
}
prev = cur;
cur = cur->_pNext;
}
return iterator(nullptr);
}
bool Erase(const K& key)
{
KeyOfValue kot;
//Find返回空,说明表里没有data,不能删除
if (Find(key) == nullptr)
return false;
size_t Bucket = HashFunc(key);
Node* cur = _table[Bucket];
Node* prev = nullptr;
while (cur)
{
if (kot(cur->_data) == key)
{
//如果prev为空,说明该值是桶的第一个结点
if (prev == nullptr)
_table[Bucket] = cur->_pNext;
else
prev->_pNext = cur->_pNext;
delete cur;
_size--;
return true;
}
prev = cur;
cur = cur->_pNext;
}
}
iterator Find(const K& key)
{
KeyOfValue kot;
//找到对应的桶
size_t Bucket = HashFunc(key);
Node* cur = _table[Bucket];
//遍历这个桶
while (cur)
{
//找到就返回该指针
if (kot(cur->_data) == key)
return iterator(cur, this);
cur = cur->_pNext;
}
//该桶为空或桶里找不到就返回空
return iterator(nullptr, this);
}
size_t Count(const K& key)
{
if (Find(key))
return 1;
else
return 0;
}
size_t Size()const
{
return _size;
}
bool Empty()const
{
return 0 == _size;
}
void Clear()
{
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
//遍历每一个非空桶,并把该桶中的每一个非空结点释放
while (cur)
{
//保存下一个结点
Node* nextcur = cur->_pNext;
delete cur;
cur = nextcur;
}
_table[i] = nullptr;
}
}
size_t BucketCount()const
{
return _table.capacity();
}
size_t BucketSize(const K& key)const
{
size_t Bucket = HashFunc(key);
Node* cur = _table[Bucket];
size_t Bucketsize = 0;
while (cur)
{
Bucketsize++;
cur = cur->_pNext;
}
return Bucketsize;
}
void Swap(Self& ht)
{
_table.swap(ht._table);
swap(_size, ht._size);
}
private:
//class HF = HashFunc<V> 套了两层
size_t HashFunc(const K& key)
{
return HF()(key) % _table.size();
}
void CheckCapacity()
{
KeyOfValue kot;
//负载因子到1就扩容
if (_size == _table.size())
{
//创建新的_table,并把之前的数据给新表
vector<Node*> new_table(_table.size() * 2, nullptr);
//遍历每一个桶,是空桶就跳过,不是空再遍历这个桶
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
//遍历每一个非空桶,并把该桶中的每一个非空结点头插到新表中
while (cur)
{
//保存下一个结点
Node* nextcur = cur->_pNext;
/*size_t hashi = cur->_data % new_table.size();*/
size_t hashi = HashFunc(kot(cur->_data));
cur->_pNext = new_table[hashi];
new_table[hashi] = cur;
cur = nextcur;
}
}
_table.swap(new_table);
}
}
private:
vector<Node*> _table;
size_t _size; // 哈希表中有效元素的个数
};
}
4.2 unordered_map的模拟实现
#pragma once
#include"HashTable.h"
namespace bit
{
// unordered_map中存储的是pair<K, V>的键值对,K为key的类型,V为value的类型,HF哈希函数类型
// unordered_map在实现时,只需将hashbucket中的接口重新封装即可
template<class K, class V, class HF= HashFunc<K>>
class unordered_map
{
// 通过key获取value的操作
struct KeyOfValue
{
const K& operator()(const pair<K, V>& data)
{
return data.first;
}
};
public:
typedef typename OpenHash::HashBucket<K, pair<const K, V>, KeyOfValue, HF> HT;
typedef typename HT::iterator iterator;
public:
unordered_map() : _ht()
{}
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
// capacity
size_t size()const
{
return _ht.Size();
}
bool empty()const
{
return _ht.Empty();
}
// Acess
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.fisrt->second;
}
// lookup
iterator find(const K& key)
{
return _ht.Find(key);
}
size_t count(const K& key)
{
return _ht.Count(key);
}
// modify
pair<iterator, bool> insert(const pair<K, V>& valye)
{
return _ht.Insert(valye);
}
iterator erase(iterator position)
{
return _ht.Erase(position);
}
// bucket
size_t bucket_count()
{
return _ht.BucketCount();
}
size_t bucket_size(const K& key)
{
return _ht.BucketSize(key);
}
private:
HT _ht;
};
}
4.3 unordered_set的模拟实现
#pragma once
#include"HashTable.h"
namespace bit
{
// unordered_set中存储的是K类型,HF哈希函数类型
// unordered_set在实现时,只需将hashbucket中的接口重新封装即可
template<class K, class HF = HashFunc<K>>
class unordered_set
{
// 通过key获取value的操作
struct KeyOfValue
{
const K& operator()(const K& data)
{
return data;
}
};
public:
//typedef HashBucket<K, K, KeyOfValue, HF> HT;
//typename typedef HT::Iterator iterator;
typedef typename OpenHash::HashBucket<K, const K, KeyOfValue, HF> HT;
typedef typename HT::iterator iterator;
public:
unordered_set() : _ht()
{}
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
// capacity
size_t size()const
{
return _ht.Size();
}
bool empty()const
{
return _ht.Empty();
}
// lookup
iterator find(const K& key)
{
return _ht.Find(key);
}
size_t count(const K& key)
{
return _ht.Count(key);
}
// modify
pair<iterator, bool> insert(const K& valye)
{
return _ht.Insert(valye);
}
// 返回指向删除位置后的iterator
iterator erase(iterator position)
{
return _ht.Erase(position);
}
bool erase(const K& data)
{
return _ht.Erase(data);
}
// bucket
size_t bucket_count()
{
return _ht.BucketCount();
}
size_t bucket_size(const K& key)
{
return _ht.BucketSize(key);
}
private:
HT _ht;
};
}