目录
一、unordered系列关联式容器
1.1 unordered_map
1.1.1 unordered_map
1.1.2 unordered_map接口说明
1. unordered_map的容量
2. unordered_map的迭代器
3.unordered_map的元素访问
4. unordered_map的查询
5. unordered_map的修改操作
6. unordered_map的桶操作unordere_set在线文档说明
1.2 unordered_set
1.3 unordered_map与map查找性能比较
二、底层结构
2.1 哈希概念
2.2 哈希冲突
2.3 哈希函数
2.4 哈希冲突解决
2.4.1 闭散列
线性探测
线性探测的实现
线性探测优缺点
2.4.2 开散列
1.概念
2.开散列实现
3.开散列增容
4.非整形key的开散列
三、模拟实现unordered系列
3.1 哈希表的改造
1.模板参数列表的改造
2.增加迭代器操作
3、增加通过key获取value操作
3.2 模拟实现unordered_map
3.3 模拟实现unordered_set
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到$log_2N$,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同。
unordere_map在线文档说明
1. unordered_map是存储
键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部,unordered_map没有对按照任何特定的顺序排序, 为了能在常数范围内
找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问
value。
6. 它的迭代器至少是前向迭代器。
函数声明 | 功能介绍 |
bool empty() const | 检测unordered_map是否为空 |
size_t size() const | 获取unordered_map的有效元素个数 |
函数声明 | 功能介绍 |
begin | 返回unordered_map第一个元素的迭代器 |
end | 返回unordered_map最后一个元素下一个位置的迭代器 |
cbegin | 返回unordered_map第一个元素的const迭代器 |
cend | 返回unordered_map最后一个元素下一个位置的const迭代器 |
函数声明 | 功能介绍 |
operator[] | 返回与key对应的value,没有一个默认值 |
注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶
中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,
将key对应的value返回。
函数声明 | 功能介绍 |
iterator find(const K& key) | 返回key在哈希桶中的位置 |
size_t count(const K& key) | 返回哈希桶中关键码为key的键值对的个数 |
注意:unordered_map中key是不能重复的,因此count函数的返回值最大为1
函数声明 | 功能介绍 |
insert | 向容器中插入键值对 |
erase | 删除容器中的键值对 |
void clear() | 清空容器中有效元素个数 |
void swap(unordered_map&) | 交换两个容器中的 |
size_t bucket_count()const | 返回哈希桶中桶的总个数 |
size_t bucket_size(size_t n)const | 返回n号桶中有效元素的总个数 |
size_t bucket(const K& key) | 返回元素key所在的桶号 |
unordere_set在线文档说明
以下是使用C++的unordered_map和map进行性能比较的示例代码:
cpp
#include
#include
unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一映射的关系,那么在查找时通过该函数可以很快找到该元素!
当插入元素:根据关键码key,用哈希函数计算出元素的存储位置进行存放
当搜索元素:同样通过关键码映射存储位置,在比较判断关键码是否一致
哈希冲突:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞。我们可以预想的是哈希冲突无法完全杜绝,只能用更高效的哈希函数算法来减少冲突。下面来谈谈常见的哈希函数!
1. 直接定址法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
面试题:字符串中第一个只出现一次字符
2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
3. 平方取中法--(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4. 折叠法--(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5. 随机数法--(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。
通常应用于关键字长度不等时采用此法
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定
相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
列地址。例如:
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同
的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还
可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移
位、前两数与后两数叠加(如1234改成12+34=46)等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突的常见方法:闭散列和开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
呢?
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
1.通过哈希函数将关键字映射到哈希表中位置
2.判断该位置没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影
响。因此线性探测采用标记的伪删除法来删除一个元素。
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
// 注意:假如实现的哈希表中元素唯一,即key相同的元素不再进行插入
// 为了实现简单,此哈希表中我们将比较直接与元素绑定在一起
template
class HashTable
{
struct Elem
{
pair _val;
State _state;
};
public:
HashTable(size_t capacity = 3)
: _ht(capacity), _size(0)
{
for (size_t i = 0; i < capacity; ++i)
_ht[i]._state = EMPTY;
}
bool Insert(const pair& val)
{
// 检测哈希表底层空间是否充足
// _CheckCapacity();
size_t hashAddr = HashFunc(key);
// size_t startAddr = hashAddr;
while (_ht[hashAddr]._state != EMPTY)
{
if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first
== key)
return false;
hashAddr++;
if (hashAddr == _ht.capacity())
hashAddr = 0;
/*
// 转一圈也没有找到,注意:动态哈希表,该种情况可以不用考虑,哈希表中元
素个数到达一定的数量,哈希冲突概率会增大,需要扩容来降低哈希冲突,因此哈希表中元素是
不会存满的
if(hashAddr == startAddr)
return false;
*/
}
// 插入元素
_ht[hashAddr]._state = EXIST;
_ht[hashAddr]._val = val;
_size++;
return true;
}
int Find(const K& key)
{
size_t hashAddr = HashFunc(key);
while (_ht[hashAddr]._state != EMPTY)
{
if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first
== key)
return hashAddr;
hashAddr++;
}
return hashAddr;
}
bool Erase(const K& key)
{
int index = Find(key);
if (-1 != index)
{
_ht[index]._state = DELETE;
_size++;
return true;
}
return false;
}
size_t Size()const;
bool Empty() const;
void Swap(HashTable& ht);
private:
size_t HashFunc(const K& key)
{
return key % _ht.capacity();
}
private:
vector _ht;
size_t _size;
};
思考:哈希表什么情况下进行扩容?如何扩容?
扩容代码(思路联想现代拷贝构造):
void CheckCapacity()
{
if (_size * 10 / _ht.capacity() >= 7)
{
HashTable newHt(GetNextPrime(ht.capacity));
for (size_t i = 0; i < _ht.capacity(); ++i)
{
if (_ht[i]._state == EXIST)
newHt.Insert(_ht[i]._val);
}
Swap(newHt);
}
}
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素
template
struct HashBucketNode
{
HashBucketNode(const V& data)
: _pNext(nullptr), _data(data)
{}
HashBucketNode* _pNext;
V _data;
};
// 本文所实现的哈希桶中key是唯一的
template
class HashBucket
{
typedef HashBucketNode Node;
typedef Node* PNode;
public:
HashBucket(size_t capacity = 3) : _size(0)
{
_ht.resize(GetNextPrime(capacity), nullptr);
}
// 哈希桶中的元素不能重复
PNode* Insert(const V& data)
{
// 确认是否需要扩容。。。
// _CheckCapacity();
// 1. 计算元素所在的桶号
size_t bucketNo = HashFunc(data);
// 2. 检测该元素是否在桶中
PNode pCur = _ht[bucketNo];
while (pCur)
{
if (pCur->_data == data)
return pCur;
pCur = pCur->_pNext;
}
// 3. 插入新元素
pCur = new Node(data);
pCur->_pNext = _ht[bucketNo];
_ht[bucketNo] = pCur;
_size++;
return pCur;
}
// 删除哈希桶中为data的元素(data不会重复),返回删除元素的下一个节点
PNode* Erase(const V& data)
{
size_t bucketNo = HashFunc(data);
PNode pCur = _ht[bucketNo];
PNode pPrev = nullptr, pRet = nullptr;
while (pCur)
{
if (pCur->_data == data)
{
if (pCur == _ht[bucketNo])
_ht[bucketNo] = pCur->_pNext;
else
pPrev->_pNext = pCur->_pNext;
pRet = pCur->_pNext;
delete pCur;
_size--;
return pRet;
}
}
return nullptr;
}
PNode* Find(const V& data);
size_t Size()const;
bool Empty()const;
void Clear();
bool BucketCount()const;
void Swap(HashBucket& ht;
~HashBucket();
private:
size_t HashFunc(const V& data)
{
return data % _ht.capacity();
}
private:
vector _ht;
size_t _size; // 哈希表中有效元素的个数
};
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,
再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可
以给哈希表增容。
void _CheckCapacity()
{
size_t bucketCount = BucketCount();
if (_size == bucketCount)
{
vector newTable;
newTable.resize(_table.size() * 2, nullptr);
//移动结点
for (auto cur : _table)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = cur->_data % newTable.size();
//头插入新链表
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
}
_table.clear();
//交换数组,自动析构原数组
swap(newTable, _table);
}
}
1.如何将类型变为size_t类型
字符串哈希算法!!!
//如果是整形的相似类型,使用强转
template
class Hash{
public:
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//如果是字符串,模板特化,我们需要自己实现仿函数!
template<>
class Hash
{
public:
size_t operator()(const string& key)
{
size_t keyi = 0;
for (int i = 0;i < key.size();i++)
{
keyi += key[i];
}
return keyi;
}
};
2.开辟空间开素数个效果好
//素数表
inline unsigned long __stl_next_prime(unsigned long n)
{
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
for (int i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return __stl_prime_list[__stl_num_primes - 1];
}
// K:关键码类型
// V: 不同容器V的类型不同,如果是unordered_map,V代表一个键值对,如果是
unordered_set,V 为 K
// KeyOfValue: 因为V的类型不同,通过value取key的方式就不同,详细见
unordered_map/set的实现
// HF: 哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能
取模
template >
class HashBucket;
//哈希桶中与迭代器相互引用双方,迭代器用到桶需要首先申明一下!
//前置申明模板类,模板也一定要带上!!!
template
class HashTable;
template
struct Hash_iterator
{
typedef HashNode Node;
typedef Hash_iterator Self;
typedef HashTable Ht;
typedef Hash_iterator iterator;//申明一下,与std区别
Node* pnode;
Ht* _ht;//传哈希表是因为++中需要查找桶!
Hash_iterator(Node* p, Ht* ht)
:pnode(p)
, _ht(ht)
{}
Hash_iterator(const iterator& it)
:pnode(it.pnode)
,_ht(it._ht)
{}
//-->
Self& operator++()
{
//判断是否为该桶链表的尾 1.是 -> 判断下一个桶是否为空 2.否 直接等于next
if (pnode->_next)
pnode = pnode->_next;
else {
HashFunc Hash;
KeyofT getK;
size_t hashi = Hash(getK(pnode->_data)) % _ht->_table.size()+1;
while (hashi < _ht->_table.size())
{
//判断桶是否为空桶
if (_ht->_table[hashi])
{
pnode = _ht->_table[hashi];
break;
}
else ++hashi;
}
//后面没有桶了
if (hashi == _ht->_table.size())
pnode = nullptr;
}
return *this;
}
ref operator*()
{
return pnode->_data;
}
ptr operator->()
{
return &pnode->_data;
}
bool operator==(const Self& p)const
{
return pnode == p.pnode;
}
bool operator!=(const Self& p)const
{
return pnode != p.pnode;
}
};
template
class HashTable
{
public:
typedef HashNode Node; //结点
template
friend struct Hash_iterator;
template
friend struct Hash_const_iterator;
// Key Val& Val* 类型->整数映射 Val中取Key
typedef Hash_iterator iterator;
typedef Hash_const_iterator const_iterator;
HashTable()
{
//__stl_next_prime(0)
_table.resize(5, nullptr);
_n = 0;
}
pair insert(const T& data)
{
HashFunc Hash;
KeyofT getK;
iterator it = Find(getK(data));
if (it != end())
return make_pair(it, false);
//判断负载因子-->扩容?
if (_n / _table.size() == 1)
{
vector newTable;
//__stl_next_prime(_table.size() * 2
newTable.resize(_table.size()*2,nullptr);
//移动结点
for (auto cur : _table)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = Hash(getK(cur->_data)) % newTable.size();
//头插入新链表
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
}
_table.clear();
swap(newTable, _table);
}
size_t hashi = Hash(getK(data)) % _table.size();
Node* newnode = new Node(data);
if (_table[hashi])
{
newnode->_next = _table[hashi];
_table[hashi] = newnode;
}
else {
_table[hashi] = newnode;
}
++_n;
return make_pair(iterator(newnode,this),true);
}
iterator Find(const K& key)
{
HashFunc Hash;
KeyofT getK;
size_t hashi = Hash(key) % _table.size();
Node* cur = _table[hashi];
Node* next=nullptr;
while (cur)
{
next = cur->_next;
if (getK(cur->_data) == key)
return iterator(cur,this);
cur = next;
}
return end();
}
bool Erase(const K& key)
{
HashFunc Hash;
KeyofT getK;
size_t hashi = Hash(key) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (getK(cur->_data) == key)
{
if (cur == _table[hashi])
_table[hashi] = cur->_next;
else prev->_next = cur->_next;
delete cur;
--_n;
return true;
}
else {
prev = cur;
cur = cur->_next;
}
}
return false;
}
iterator begin()
{
for (int i = 0;i < _table.size();i++)
{
if (_table[i])
return iterator(_table[i], this);//!!! this 太妙了
}
return iterator(nullptr,this);
}
const_iterator begin()const
{
for (int i = 0;i < _table.size();i++)
{
if (_table[i])
return const_iterator(_table[i], this);//!!! this 太妙了
}
return const_iterator(nullptr, this);
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator end()const
{
return const_iterator(nullptr, this);
}
~HashTable()
{
Node* cur = nullptr;
Node* next = nullptr;
for (int i = 0;i < _table.size();i++)
{
cur = _table[i];
while (cur)
{
next = cur->_next;
delete cur;
cur = next;
}
}
}
private:
vector _table;
size_t _n;
};
namespace wyz
{
template
class unorder_map
{
public:
struct GetKey
{
const K& operator()(const pair& kv)
{
return kv.first;
}
};
typedef HashTable, GetKey, Hash> hash;
typedef typename hash::iterator iterator;
typedef typename hash::const_iterator const_iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator begin()const
{
return _ht.begin();
}
const_iterator end()const
{
return _ht.end();
}
pair insert(const pair& val)
{
return _ht.insert(val);
}
iterator Find(const K& val)
{
return _ht.Find(val);
}
bool Erase(const K& val)
{
return _ht.Erase();
}
V& operator[](const K& key)
{
pair ret = insert(make_pair(key, V()));
return ret.first->second;
}
const V& operator[](const K& key)const
{
pair ret = insert(make_pair(key, V()));
return ret.first->second;
}
private:
hash _ht;//底层是哈希表
};
}
测试代码:
void Test_unorder_map()
{
string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
wyz::unorder_map countMap;
for (auto e : arr)
{
countMap[e]++;
}
wyz::unorder_map::iterator it = countMap.begin();
while (it != countMap.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
}
测试map const 迭代器:
void Test_unordermap()
{
int arr[] = { 9,2,1,10,3,56,78,28,30,29 };
wyz::unorder_map countMap;
for (auto e : arr)
{
countMap[e]++;
}
wyz::unorder_map::const_iterator it = countMap.begin();
while (it != countMap.end())
{
++(it->second);
cout << it->first << ":" << it->second << endl;
++it;
}
}
template
class unorder_set
{
public:
struct GetKey
{
const K& operator()(const K& val)
{
return val;
}
};
typedef HashTable> hash;
typedef typename hash::const_iterator iterator;
typedef typename hash::const_iterator const_iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator begin()const
{
return _ht.begin();
}
const_iterator end()const
{
return _ht.end();
}
pair insert(const K& k)
{
//注意ret类型中的迭代器是普通迭代器
pair ret = _ht.insert(k);
//我们这里需要用到普通迭代器拷贝构造const迭代器!!!
return make_pair(iterator(ret.first), ret.second);
}
bool Find(const K& val)
{
return _ht.Find(val);
}
bool Erase(const K& val)
{
return _ht.Erase();
}
private:
hash _ht;
};
测试代码:
void Test_unorder_set()
{
wyz::unorder_set myset;
int arr[10] = { 9,8,5,3,2,1,6,7,4,11 };
for (auto e : arr)
{
myset.insert(e);
}
wyz::unorder_set::iterator it = myset.begin();
while (it != myset.end())
{
//++(*it);
cout << *it << ' ';
++it;
}
}