目录
1. unordered系列关联式容器
1.1 unordered_map
1.1.1 unordered_map的介绍
1.1.2 unordered_map的接口说明
1.2 unordered_set
2. 底层结构
2.1 哈希概念
2.2 哈希冲突
2.3 哈希函数
2.4 哈希冲突解决
2.4.1 闭散列
2.4.2 开散列
3. 封装unorder_map和unorder_set
3.1 unorder_map 的封装
3.2 unorder_set 的封装
3.4 unordered_map 加迭代器的封装
3.5 unordered_set 加迭代器的封装
1. unordered_map是存储
2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部,unordered_map没有对
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭
代方面效率较低。
5. unordered_map实现了直接访问操作符(operator[ ]),它允许使用key作为参数直接访问
value。
6. 它的迭代器是正向迭代器
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,没有一个默认值 |
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所在的桶号 |
与unordered_map类似,详情见文档unordered_set
unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素:
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素:
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
为哈希表(Hash Table)(或者称散列表)。
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity ; capacity为存储元素底层空间总的大小。
问题:如果继续向上述哈希表中插入44,会出现什么问题?
上述问题中,不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
域必须在0到m-1之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单
常见哈希函数:
1. 直接定址法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
class Solution {
public:
int firstUniqChar(string s) {
int hash[256] = {0};
for(int i = 0; i < s.size(); i++){
hash[s[i]]++;
}
for(int i = 0; i < s.size(); i++){
if(hash[s[i]] == 1){
return i;
}
}
return -1;
}
};
2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
那如何寻找下一个空位置呢?
1. 线性探测
比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入:
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。
删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State
{EMPTY, EXIST, DELETE};
载荷因子:
为了解决 “哈希表什么情况下进行扩容?如何扩容?” 的问题,我们引入载荷因子的概念。
载荷因子 = 表中元素个数 / 散列表容量;
载荷因子越大,表中元素越多,产生冲突可能性越大;
载荷因子越小,表中元素越少,产生冲突可能性越小;
当达到载荷因子我们就扩容,如果载荷因子设置的太小,空间资源浪费就多,载荷因子设置越大,产生冲突的可能性就越大。因此,一般设置载荷因子为 0.7——0.8。
线性探测简要实现哈希表代码如下:
enum State//标志位
{
EMPTY,
EXIST,
DELETE
};
template
struct HashData//节点元素包含键值对和标志位
{
pair _kv;
State _state = EMPTY;
};
template
struct HashFunc//用于方便比较的仿函数
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化
template<>
struct HashFunc
{
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
//防止出现abc = cba 这种情况,有大佬提出加每个字符前*131
val *= 131;
val += ch;
}
return val;
}
};
//因为如果K为string类型,无法用 % 来实现映射关系,因此我们写个Hash仿函数
template>
class HashTable
{
public:
bool Insert(const pair& kv)
{
if (Find(kv.first))
return false;
//表为空或载荷因子超70%就扩容
if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable newHT;
newHT._tables.resize(newSize);
//旧表数据映射到新表
for (auto e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
_tables.swap(newHT._tables);
}
Hash hash;
size_t hashi = hash(kv.first) % _tables.size();
//线性探测
while (_tables[hashi]._state == EXIST)
{
hashi++;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;
return true;
}
HashData* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
hashi++;
hashi %= _tables.size();
}
return nullptr;
}
bool Erase(const K& key)
{
HashData* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_size;
return true;
}
else
{
return false;
}
}
private:
vector> _tables;
size_t _size = 0;//存储多少有效数据
};
总结:
线性探测优点:实现非常简单
线性探测缺点:某个位置冲突很多的情况下,互相占用,冲突一片,如下图:
2. 二次探测
二次探测只需要把原来线性探测略微改动一下即可:
bool Insert(const pair& kv)
{
if (Find(kv.first))
return false;
//表为空或载荷因子超70%就扩容
if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable newHT;
newHT._tables.resize(newSize);
//旧表数据映射到新表
for (auto e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
_tables.swap(newHT._tables);
}
Hash hash;
size_t start = hash(kv.first) % _tables.size();
size_t i = 0;
size_t hashi = start;
//二次探测
while (_tables[hashi]._state == EXIST)
{
++i;
hashi = start + i * i;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;
return true;
}
开散列概念
开散列法又叫链地址法(拉链法/哈希桶),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
哈希桶简单实现:
template
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化
template<>
struct HashFunc
{
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
//防止出现abc = cba 这种情况,有大佬提出加每个字符前*131
val *= 131;
val += ch;
}
return val;
}
};
namespace HashBucket
{
template
struct HashNode
{
pair _kv;
HashNode* _next;
HashNode(const pair& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template>
class HashTable
{
typedef HashNode Node;
public:
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
bool Insert(const pair& kv)
{
//去重
if (Find(kv.first))
{
return false;
}
Hash hash;
//负载因子到1就扩容
if (_size == _tables.size())
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector newTables;
newTables.resize(newSize, nullptr);
//旧表中节点移动映射到新表
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hash(kv.first) % _tables.size();
//头插
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
// 1、头删
// 2、中间删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
size_t Size()
{
return _size;
}
// 表的长度
size_t TablesSize()
{
return _tables.size();
}
// 桶的个数
size_t BucketNum()
{
size_t num = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
++num;
}
}
return num;
}
size_t MaxBucketLenth()
{
size_t maxLen = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
size_t len = 0;
Node* cur = _tables[i];
while (cur)
{
++len;
cur = cur->_next;
}
//if (len > 0)
//printf("[%d]号桶长度:%d\n", i, len);
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
private:
vector _tables;
size_t _size = 0;//存储有效数据个数
};
}
简单修改上述哈希桶用于封装实现unorder_map和unorder_set.
首先,我们将原来节点中用来存储节点值的 pair键值对 改为 模板参数 T.
// 因为关联式容器中存储的是的键值对,因此
// k为key的类型,
// T: 如果是map,则为pair; 如果是set,则为k
// KeyOfT: 通过T来获取key的一个仿函数类
namespace Bucket
{
template
struct HashNode
{
T _data;
HashNode* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
template
class HashTable
{
typedef HashNode Node;
public:
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
bool Insert(const T& data)
{
Hash hash;
KeyOfT kot;
//去重
if (Find(kot(data)))
{
return false;
}
//负载因子到1就扩容
if (_size == _tables.size())
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector newTables;
newTables.resize(newSize, nullptr);
//旧表中节点移动映射到新表
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(kot(cur->_data)) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hash(kot(data)) % _tables.size();
//头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
// 1、头删
// 2、中间删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
size_t Size()
{
return _size;
}
// 表的长度
size_t TablesSize()
{
return _tables.size();
}
// 桶的个数
size_t BucketNum()
{
size_t num = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
++num;
}
}
return num;
}
size_t MaxBucketLenth()
{
size_t maxLen = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
size_t len = 0;
Node* cur = _tables[i];
while (cur)
{
++len;
cur = cur->_next;
}
//if (len > 0)
//printf("[%d]号桶长度:%d\n", i, len);
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
private:
vector _tables;
size_t _size = 0;//存储有效数据个数
};
}
#include"HashTable.h"//HashTable.h即为上面哈希桶的实现
namespace zj
{
template>
class unorder_map
{
struct MapKeyOfT
{
const K& operator()(const pair& kv)
{
return kv.first;
}
};
public:
bool Insert(const pair& kv)
{
return _ht.Insert(kv);
}
private:
Bucket::HashTable, Hash, MapKeyOfT> _ht;
};
}
#include"HashTable.h"//HashTable.h即为上面哈希桶的实现
namespace zj
{
template>
class unorder_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
bool Insert(const K& key)
{
return _ht.Insert(key);
}
private:
Bucket::HashTable _ht;
};
}
3.3 哈希桶迭代器模板
namespace Bucket
{
template
struct HashNode
{
T _data;
HashNode* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
//前置声明,不然__HashIterator里定义不了_pht
template
class HashTable;
template
struct __HashIterator
{
typedef HashNode Node;
typedef HashTable HT;
typedef __HashIterator Self;
Node* _node;
HT* _pht;
__HashIterator(Node* node, HT* pht)
:_node(node)
,_pht(pht)
{}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
Self& operator++()
{
if (_node->_next)
{
// 当前桶中迭代
_node = _node->_next;
}
else
{
// 找下一个桶
Hash hash;
KeyOfT kot;
size_t i = hash(kot(_node->_data)) % _pht->_tables.size();
++i;
for (; i < _pht->_tables.size(); ++i)
{
if (_pht->_tables[i])
{
_node = _pht->_tables[i];
break;
}
}
// 说明后面没有有数据的桶了
if (i == _pht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
};
template
class HashTable
{
typedef HashNode Node;
template//模板参数友元要带声明
friend struct __HashIterator;
public:
typedef __HashIterator iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
return iterator(_tables[i], this);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
pair Insert(const T& data)
{
Hash hash;
KeyOfT kot;
// 去重
iterator ret = Find(kot(data));
if (ret != end())
{
return make_pair(ret, false);
}
//负载因子到1就扩容
if (_size == _tables.size())
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector newTables;
newTables.resize(newSize, nullptr);
//旧表中节点移动映射到新表
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(kot(cur->_data)) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hash(kot(data)) % _tables.size();
//头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return make_pair(iterator(newnode, this), true);
}
iterator Find(const K& key)
{
if (_tables.size() == 0)
{
return end();
}
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
return end();
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
// 1、头删
// 2、中间删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
....
private:
vector _tables;
size_t _size = 0;//存储有效数据个数
};
}
#include"HashTable.h"
namespace zj
{
template>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair& kv)
{
return kv.first;
}
};
public:
typedef typename Bucket::HashTable, Hash, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair Insert(const pair& kv)
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
private:
Bucket::HashTable, Hash, MapKeyOfT> _ht;
};
void test_map()
{
unordered_map dict;
dict.Insert(make_pair("sort", ""));
dict.Insert(make_pair("string", ""));
dict.Insert(make_pair("left", ""));
unordered_map::iterator it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
}
#include"HashTable.h"
namespace zj
{
template>
class unorder_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename Bucket::HashTable::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair Insert(const K& key)
{
return _ht.Insert(key);
}
private:
Bucket::HashTable _ht;
};
void test_set()
{
unordered_set s;
s.insert(2);
s.insert(3);
s.insert(1);
s.insert(2);
s.insert(5);
unordered_set::iterator it = s.begin();
//auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
}