一.unordered_set
1.unordered_set的介绍
1>unordered_set底层关键字类型为Key
2>unordered_set默认要求Key⽀持转换为整形,并支持比较大小
3>unordered_set底层存储数据的内存是从空间配置器申请的
4>unordered_set底层⽤哈希桶实现,增删查平均效率是O(1),注意此时迭代器遍历不再有序
2.unordered_set和set的异同
同:两者增删查的使用方法相同
异:
1>set要求Key⽀持⼩于⽐较,⽽unordered_set要求Key⽀持转成整形且⽀持等于⽐较
2>set的iterator是双向迭代器,unordered_set是单向迭代器,其次set底层是红⿊树,红⿊树是⼆叉搜索树,⾛中序遍历是有序的,所以set迭代器遍历是有序+去重。⽽unordered_set底层是哈希表,迭代器遍历是⽆序+去重
3>unordered_set的增删查改更快⼀些,因为红⿊树增删查改效率是O(logN),⽽哈希表增删查平均效率是O(1)
二.unordered_map和map(与unordered_set和set的内容相同)
注:unordered_multimap/unordered_multiset⽀持Key冗余。
三.哈希表
1.哈希概念
1>本质就是通过哈希函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过哈希函数计算出Key存储的位置,进⾏快速查找。
2.直接定址法
1>本质:⽤关键字计算出⼀个绝对位置或者相对位置
2>使用条件:关键字的范围⽐较集中
3>缺点:当关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤
3.哈希冲突
1>定义:两个不同的key映射到同⼀个位置,这种情况就叫哈希冲突
注:实际上哈希冲突是不可避免的,所以在设计哈希函数时要尽量减少冲突
2>负载因子:假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么负载因⼦= N/M。
负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低
4.将关键字转为整数
将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整数
5.哈希函数
注:好的哈希函数要尽量做到让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中
1>除法散列法/除留余数法
(1)方法介绍:假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key)=key%M
(2)M的选取:M尽量避免去2的幂,10的幂之类的数。建议M取不太接近2的整数次幂的⼀个素数。
例:key%2^X本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了
2>乘法散列法:1.⽤关键字K乘上常数A(0<A<1),并抽取出k*A的⼩数部分。2.后再⽤M乘以k*A的⼩数部分,再向下取整。
注:1.此法对于M没有取值限制
2.h(key)=floor(M×((A×key)%1.0)),A建议取黄金分割点
3>全域散列法:给散列函数增加随机性,hab (key)=((a×key+b)%P)%M,P需要选⼀个⾜够⼤的质数,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了⼀个P*(P-1)组全域散列函数组。
注:每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使⽤,后续增删查
改都固定使⽤这个散列函数
6.处理哈希冲突
1>开放定址法
(1)方法介绍:当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于0.7的。这⾥的规则有三种:线性探测、⼆次探测、双重探测。
(2)线性探测:
规则介绍:从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。
公式:hc(key,i)=hashi=(hash0+i) % M, i = {1,2,3,...,M−1}
缺点:可能会出现你抢占我的位置,我去抢占他人位置的堆积/群集问题
(3)二次探测:
规则介绍:从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表尾的位置
公式:hc(key,i)=hashi=(hash0±i) % M, i = {1,2,3,..., M/2 }
⼆次探测当hashi=(hash0−i*i)%M时,当hashi<0时,hashi+=M
(4)双重探测:
规则介绍:第⼀个哈希函数计算出的值发⽣冲突,使⽤第⼆个哈希函数计算出⼀个跟key相关的偏移量值,不断往后探测,直到寻找到下⼀个没有存储数据的位置为⽌。
公式:hc(key,i)=hashi=(hash0+ i∗h (key)) % M, i = {1,2,3,...,M}
要求:h (key)<M且h (key)和M互为质数,
取值⽅法:
1、当M为2整数幂时,h (key)从[0,M-1]任选⼀个奇数;
2、当M为质数时,h (key) = key % (M−1) + 1,保证h (key)与M互质是因为根据固定的偏移量所寻址的所有位置将形成⼀个群
2>链地址法(哈希桶)
(1)内容:哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯。
(2)链地址法的负载因⼦没有限制,可以⼤于1。stl中unordered_xxx的最⼤负载因⼦基本控制在1,⼤于1就扩容。
(3)极端情况:当桶的⻓度超过⼀定阀值(8)时就把链表转换成红⿊树。⼀般情况下,不断扩容
四.哈希表的模拟实现:以开放地址法中的线性探测解决哈希冲突
1.哈希表每个位置对应的状态:存在,删除,空
enum State
{
Empty,
Delete,
Exist
};
2.构造函数:为了保证扩容后哈希表的存储空间仍然是接近2的整数幂的素数,直接将所有可能情况列举出来,直接调用,以减少哈希冲突
//哈希表存储空间的大小:M
inline unsigned long __stl_next_prime(unsigned long n)
{
// Note: assumes long is at least 32 bits.
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
};
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list +__stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - 1) : *pos;
}
HashTable()
{
_table.resize(__stl_next_prime(_table.size()));
}
3.插入:
1>因为不支持冗余,所以应该先查找kv是否在哈希表中出现
2>插入要增加数据个数,为了减小哈希冲突,需要根据负载因子判断是否需要扩容
扩容时可以新开一个待扩容大小的哈希表,将原来的数据拷贝进去,在与旧的哈希表交换,更加更方便
3>寻找插入位置
4>将kv插入指定位置,修改该位置的相关属性
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first)) return false;
//检查是否需要扩容:当负载因子大于等于0.7时,哈希冲突的可能性会增加,此时通过扩容来减小哈希冲突
if (_size * 10 / _table.size() >= 7)
{
HashTable<K,V,Hash> newtable;
newtable._table.resize(__stl_next_prime(_table.size()));
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._state == Exist)
{
newtable.Insert(_table[i]._kv);
}
}
_table.swap(newtable._table);
}
//插入
Hash hs;
size_t hashi = hs(kv.first) % _table.size();//确定对应的映射位置
while (_table[hashi]._state != Empty)
{
++hashi;
hashi %= _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._state = Exist;
_size++;
return true;
}
4.查找:找到key对应的映射下标,若该处有元素,进行判断,依次向后查找
HashData<K,V>* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();//确定对应的映射位置
while (_table[hashi]._state != Empty)
{
if (_table[hashi]._state == Exist && _table[hashi]._kv.first == key)
return &_table[hashi];
++hashi;
hashi %= _table.size();
}
return nullptr;
}
5.删除:先判断key是否在哈希表内,若在将key对应位置的状态改成Delete,表明此处的值已被删除
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret == nullptr) return false;
else
{
ret->_state = Delete;
return true;
}
}
6.将key转为整型
1>为了便于找到key对应的位置,需要将key强转成size_t类型,此时可以利用仿函数来实现这一功能
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
//key要求强转成整型
return (size_t)key;
}
};
2>假设key为string类型,要单独写一个仿函数来实现key强转成size_t类型的目的,但是我们也可以对之前的仿函数特化,当key为string类型时,先调用特化类型
//特化:处理字符串存哈希表的情况
template<>
struct HashFunc<string>
{
//用字符串中所有字符的ASCII码值之和来确定所在位置
//为了避免相同ASCII码值之和的情况,可以在加上字符的ASCII码值前,对和*131
size_t operator()(const string& s)
{
size_t ret=0;
for (auto ch : s)
{
ret *= 131;
ret += ch;
}
return ret;
}
};
注:若是key为不常见类型,可以单独写一个仿函数,传给HashTable就可以了
7.完整代码
#include<iostream>
#include<vector>
using namespace std;
enum State
{
Empty,
Delete,
Exist
};
template<class K,class V>
struct HashData
{
pair<K, V> _kv;
State _state = Empty;
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
//key要求强转成整型
return (size_t)key;
}
};
//特化:处理字符串存哈希表的情况
template<>
struct HashFunc<string>
{
//用字符串中所有字符的ASCII码值之和来确定所在位置
//为了避免相同ASCII码值之和的情况,可以在加上字符的ASCII码值前,对和*131
size_t operator()(const string& s)
{
size_t ret=0;
for (auto ch : s)
{
ret *= 131;
ret += ch;
}
return ret;
}
};
template<class K,class V,class Hash = HashFunc<K>>
class HashTable
{
public:
//扩容
inline unsigned long __stl_next_prime(unsigned long n)
{
// Note: assumes long is at least 32 bits.
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
};
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list +__stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - 1) : *pos;
}
HashTable()
{
_table.resize(__stl_next_prime(_table.size()));
}
HashData<K,V>* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();//确定对应的映射位置
while (_table[hashi]._state != Empty)
{
if (_table[hashi]._state == Exist && _table[hashi]._kv.first == key)
return &_table[hashi];
++hashi;
hashi %= _table.size();
}
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first)) return false;
//检查是否需要扩容:当负载因子大于等于0.7时,哈希冲突的可能性会增加,此时通过扩容来减小哈希冲突
if (_size * 10 / _table.size() >= 7)
{
HashTable<K,V,Hash> newtable;
newtable._table.resize(__stl_next_prime(_table.size()));
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._state == Exist)
{
newtable.Insert(_table[i]._kv);
}
}
_table.swap(newtable._table);
}
//插入
Hash hs;
size_t hashi = hs(kv.first) % _table.size();//确定对应的映射位置
while (_table[hashi]._state != Empty)
{
++hashi;
hashi %= _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._state = Exist;
_size++;
return true;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret == nullptr) return false;
else
{
ret->_state = Delete;
return true;
}
}
private:
vector<HashData<K, V>> _table;
int _size = 0;//哈希表内存储数据的个数
};
五.哈希表的模拟实现:以链地址法解决哈希冲突问题
1.哈希节点
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode* _next;
HashNode(const pair<K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
2.构造函数
//扩容
inline unsigned long __stl_next_prime(unsigned long n)
{
// Note: assumes long is at least 32 bits.
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
};
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list + __stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - 1) : *pos;
}
public:
HashTable()
{
_table.resize(__stl_next_prime(_table.size()),nullptr);
}
3.析构函数:逐个桶释放
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
//逐个桶释放
Node* cur = _table[i];
while (cur)
{
Node* tmp = cur->_next;
delete cur;
cur = tmp;
}
_table[i] = nullptr;
}
}
4.插入
1>计算待插入的位置坐标
2>判断是否需要扩容:若负载因子为1,则扩容
创建一个大小为待扩容大小的指针数组,将原数组中的值一次复制过来后,交换两个数组
bool Insert(const pair<K, V>& kv)
{
Hash hs;
size_t hashi = hs(kv.first) % _table.size();
if (_n == _table.size())//负载因子为1,扩容
{
vector<Node*> newtables(__stl_next_prime(_tables.size()), nullptr);
for (size_t i = 0; i < _table.size(); i++)
{
Node * cur = _table[i];
while (cur)
{
Node* next = cur->_next;
// 旧表中节点,挪动新表重新映射的位置
size_t hashi = hs(cur->_kv.first) %newtables.size();
// 头插到新表
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_tables.swap(newtables);
}
//插入
Node* tmp = new Node(kv);
tmp->_next = _table[hashi];
_table[hashi] = tmp;
_n++;
return true;
}
5.查找
Node* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key) return cur;
cur = cur->_next;
}
return nullptr;
}
6.删除
bool Erase(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
_n--;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}