目录
一. unordered_map unordered_set 和 map set的区别
二. 哈希
1. 哈希,哈希表,哈希函数。
2. 哈希冲突。
3. 哈希函数补充
3. 解决哈希冲突的两大方法:闭散列,开散列
闭散列
闭散列实现代码:
闭散列的删除问题:
负载因子:
开散列:
开散列哈希表代码实现:
开散列的负载因子和扩容问题:
开散列和闭散列的比较:
一. unordered_map unordered_set 和 map set的区别
1. map set底层采取的红黑树的结构,unordered_xxx 底层数据结构是哈希表。unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
2. Java中对应的容器名为 HashMap HashSet TreeMap TreeSet,命名方面比C++好了很多。主要是早期C++并没有实现哈希结构的容器(C++11之前),也就是unordered系列,在C++11中新增了unordered_map,unordered_set,unordered_multimap,unordered_multiset,故因为历史命名问题,取了这样的名字。
3. 它们的使用大体上几乎一致。显著的差别是:
a、map和set为双向迭代器,unordered_xxx和是单向迭代器。
b、map和set存储为有序存储(红黑树结构,中序遍历有序),unordered_xxx为无序存储(哈希表的结构致使)
4. 性能差异:采取哈希表的unordered系列容器在大量数据的增删查改效率更优,尤其是查(搜索)
二. 哈希
1. 哈希,哈希表,哈希函数。
哈希的思想是:将元素的关键码与元素在哈希表中的存储位置构建某种函数关系,使得查找时,可以通过这个函数,直接获取元素的存储位置。对比顺序表和搜索树,顺序表需要将关键码一一对比,时间复杂度为O(N),而搜索树这样的结构,时间复杂度也是(log_2 N),取决于元素个数,树的高度,同样需要若干次比较。而哈希可以使得查找元素的存储位置不需要比较,直接通过函数获取。
将这个转换函数成为哈希函数(散列函数),得到的元素的存储位置成为哈希地址(也就是在哈希表中的下标,哈希表一般为顺序表结构),将构造出的数据结构成为哈希表(散列表)。
2. 哈希冲突。
难免有某些元素的不同关键码通过同一个哈希函数转换后得到的哈希地址相同,这种现象称为哈希冲突或哈希碰撞。
故,要想构造出理想的哈希存储结构,必须解决哈希冲突,合理安排那些关键码不同,而哈希地址相同的元素。
3. 哈希函数补充
哈希函数有很多种,也就是将元素的关键码,转换为元素存储位置的函数。哈希函数的设计越精妙,产生哈希冲突的概率越低,但是无法完全避免哈希冲突。
下方哈希表的实现代码中,采取的哈希函数为除留余数法,即将关键码取模哈希表的长度,获取哈希地址。
3. 解决哈希冲突的两大方法:闭散列,开散列
闭散列
闭散列:也叫开放定址法。核心思想比较简单:若某两个元素发生哈希冲突,第二个得到该哈希地址的元素插入时,将该元素存放到哈希表中发生哈希冲突的“下一个”空位置中去。其实也就是再找一个空位置,然后插入。
如何寻找下一个“空位置”的方法有两种,1. 线性探测,即从哈希地址处起,逐个位置进行判断,直到找到空位置。2. 二次探测:第一次看hashAdd+0^2下标处有没有被占用(设哈希地址为hashAdd。其实也就是看获取的哈希地址处有没有没被占用),第二次看hashAdd+1^2,第三次看hashAdd+2^2,直到找到没有被占用的位置。
相比于线性探测,二次探测只是将发生哈希冲突的元素的存储位置分离一些,不像线性探测一样连续存储,能一定程度减少哈希冲突带来的性能损耗,但是不能根本解决问题。
闭散列实现代码:
// 开散列(链地址法)解决哈希表中的哈希冲突
namespace OpenHashing
{
// 这里仅仅是实现哈希表的最简单逻辑,Find等函数的实现没有考虑unordered_map等容器
template <class K, class V>
struct HashNode
{
HashNode<K,V>(const pair<K,V>& kv)
:_kv(kv)
{ }
pair<K, V> _kv;
HashNode* _next = nullptr;
};
template <class K>
struct HashAddressConvert
{
size_t operator()(const K& key) {
return key;
}
};
// 模板特化
template <>
struct HashAddressConvert<string>
{
size_t operator()(const string& str) {
size_t sum = 0;
for(auto&ch:str)
{
sum*=131;
sum+=ch;
}
return sum;
}
};
template <class K, class V, class HDC = HashAddressConvert<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
~HashTable() {
for(auto& ptr : _table) {
Node* cur = ptr;
while(cur) {
Node* next = cur->_next;
delete cur;
cur = next;
}
ptr = nullptr;
}
}
bool Insert(const pair<K, V>& kv) {
if(Find(kv.first)) {
return false;
}
HDC convert;
if(_table.size() == 0 || 10 * _size / _table.size() >= 10) {
// 开散列法哈希表扩容
vector<Node*> newTable;
size_t newSize = _table.size() == 0 ? 10 : _table.size()*2;
newTable.resize(newSize);
for(size_t i = 0; i < _table.size(); ++i) {
Node* cur = _table[i];
while(cur) {
Node* next = cur->_next;
size_t hashAddress = convert(cur->_kv.first) % newTable.size();
cur->_next = newTable[hashAddress];
newTable[hashAddress] = cur;
cur = next;
}
_table[i] = nullptr;
// if(_table[i]) {
// Node* cur = _table[i];
// Node* next = cur->_next;
// while(cur) {
// size_t hashAddress = convert(cur->_kv.first) % newTable.size();
// cur->_next = newTable[hashAddress];
// newTable[hashAddress] = cur;
// cur = next;
// if(cur)
// next = cur->_next;
// }
// // 也没必要其实
// _table[i] = nullptr;
// }
}
_table.swap(newTable);
}
// 通过哈希函数求哈希地址
size_t hashAddress = convert(kv.first) % _table.size();
Node* ptr = _table[hashAddress];
Node* newNode = new Node(kv);
// 每个哈希桶中进行头插
newNode->_next = ptr;
_table[hashAddress] = newNode;
++_size;
return true;
}
Node* Find(const K& key) {
if(_table.size() == 0) {
return nullptr;
}
HDC convert;
size_t hashAddress = convert(key) % _table.size();
Node* cur = _table[hashAddress];
while(cur) {
if(cur->_kv.first == key) {
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key) {
if(_table.size() == 0)
return false;
HDC convert;
size_t hashAddress = convert(key) % _table.size();
Node* cur = _table[hashAddress];
Node* prev = nullptr;
while(cur) {
if(cur->_kv.first == key) {
if(prev)
prev->_next = cur->_next;
else
_table[hashAddress] = cur->_next;
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
// 不存在该节点
return false;
}
// 哈希表的长度
size_t TableSize() {
return _table.size();
}
// 非空哈希桶的个数
size_t BucketNum() {
size_t num = 0;
for(auto&ptr:_table) {
if(ptr)
num++;
}
return num;
}
// 哈希表中数据的个数
size_t Size() {
return _size;
}
// 最大的桶的长度
size_t MaxBucketLength() {
size_t max = 0;
for(size_t i = 0; i < _table.size(); ++i) {
size_t len = 0;
Node* ptr = _table[i];
while(ptr)
{
len++;
ptr = ptr->_next;
}
if(len > max)
max = len;
// if (len > 0)
// printf("[%d]号桶长度:%d\n", i, len);
}
return max;
}
private:
vector<HashNode<K, V>*> _table;
size_t _size = 0;
};
}
闭散列的删除问题:
如上哈希表中,关键值4和44采取除留余数法哈希函数获取的哈希地址相同,都是4,最后插入44时,向后找空位置,无论是线性探测还是二次探测,都会插入到哈希表中哈希地址(下标)为8处。
此时,若删除5(或者4,6,7),再查找44,就会影响44的查找,故在闭散列的哈希表实现中,哈希表中每个存储位置的存储状态,包括:存在EXIST,删除DELETE,空EMPTY。查找44时,不能因为5位置为空就停止线性探测查找,也就是遇到DELETE不停。(看代码)。线性探测采用标记的伪删除法来删除一个元素。
负载因子:
负载因子为哈希表中的元素个数/哈希表长度。
负载因子越大,同样长度的哈希表存储的元素越多,发生哈希冲突概率越大,查找时效率越低,但是空间使用率越高。
负载因子越小,同样长度的哈希表存储的元素越少,发生哈希冲突概率越小,查找时效率越高,但是空间使用率越低。
理论上,采取闭散列的哈希表的最大负载因子为1,负载因子为1时必须扩容。但是,闭散列哈希表当负载因子超过0.8时,查表时CPU缓存不命中按照指数曲线上升。故,负载因子需要取一个合适的值,对于闭散列(开放定址法)的哈希表,负载因子非常重要,应控制在0.7 ~ 0.8以下,若超出,即需要扩容哈希表。
开散列:
开散列是另一种解决哈希冲突的方法。核心思想是:首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码(发生哈希冲突)归于同一子集合,每一个子集合称为一个哈希桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
哈希表代码实现中,哈希表数组存储的是结点指针(结点封装数据和单链表next指针)。
开散列的哈希表中每个哈希桶存放的都是发生哈希冲突的元素。
开散列哈希表代码实现:
// 开散列(链地址法)解决哈希表中的哈希冲突
namespace OpenHashing
{
// 这里仅仅是实现哈希表的最简单逻辑,Find等函数的实现没有考虑unordered_map等容器
template <class K, class V>
struct HashNode
{
HashNode<K,V>(const pair<K,V>& kv)
:_kv(kv)
{ }
pair<K, V> _kv;
HashNode* _next = nullptr;
};
template <class K>
struct HashAddressConvert
{
size_t operator()(const K& key) {
return key;
}
};
// 模板特化
template <>
struct HashAddressConvert<string>
{
size_t operator()(const string& str) {
size_t sum = 0;
for(auto&ch:str)
{
sum*=131;
sum+=ch;
}
return sum;
}
};
template <class K, class V, class HDC = HashAddressConvert<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
~HashTable() {
for(auto& ptr : _table) {
Node* cur = ptr;
while(cur) {
Node* next = cur->_next;
delete cur;
cur = next;
}
ptr = nullptr;
}
}
bool Insert(const pair<K, V>& kv) {
if(Find(kv.first)) {
return false;
}
HDC convert;
if(_table.size() == 0 || 10 * _size / _table.size() >= 10) {
// 开散列法哈希表扩容
vector<Node*> newTable;
size_t newSize = _table.size() == 0 ? 10 : _table.size()*2;
newTable.resize(newSize);
for(size_t i = 0; i < _table.size(); ++i) {
Node* cur = _table[i];
while(cur) {
Node* next = cur->_next;
size_t hashAddress = convert(cur->_kv.first) % newTable.size();
cur->_next = newTable[hashAddress];
newTable[hashAddress] = cur;
cur = next;
}
_table[i] = nullptr;
// if(_table[i]) {
// Node* cur = _table[i];
// Node* next = cur->_next;
// while(cur) {
// size_t hashAddress = convert(cur->_kv.first) % newTable.size();
// cur->_next = newTable[hashAddress];
// newTable[hashAddress] = cur;
// cur = next;
// if(cur)
// next = cur->_next;
// }
// // 也没必要其实
// _table[i] = nullptr;
// }
}
_table.swap(newTable);
}
// 通过哈希函数求哈希地址
size_t hashAddress = convert(kv.first) % _table.size();
Node* ptr = _table[hashAddress];
Node* newNode = new Node(kv);
// 每个哈希桶中进行头插
newNode->_next = ptr;
_table[hashAddress] = newNode;
++_size;
return true;
}
Node* Find(const K& key) {
if(_table.size() == 0) {
return nullptr;
}
HDC convert;
size_t hashAddress = convert(key) % _table.size();
Node* cur = _table[hashAddress];
while(cur) {
if(cur->_kv.first == key) {
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key) {
if(_table.size() == 0)
return false;
HDC convert;
size_t hashAddress = convert(key) % _table.size();
Node* cur = _table[hashAddress];
Node* prev = nullptr;
while(cur) {
if(cur->_kv.first == key) {
if(prev)
prev->_next = cur->_next;
else
_table[hashAddress] = cur->_next;
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
// 不存在该节点
return false;
}
// 哈希表的长度
size_t TableSize() {
return _table.size();
}
// 非空哈希桶的个数
size_t BucketNum() {
size_t num = 0;
for(auto&ptr:_table) {
if(ptr)
num++;
}
return num;
}
// 哈希表中数据的个数
size_t Size() {
return _size;
}
// 最大的桶的长度
size_t MaxBucketLength() {
size_t max = 0;
for(size_t i = 0; i < _table.size(); ++i) {
size_t len = 0;
Node* ptr = _table[i];
while(ptr)
{
len++;
ptr = ptr->_next;
}
if(len > max)
max = len;
// if (len > 0)
// printf("[%d]号桶长度:%d\n", i, len);
}
return max;
}
private:
vector<HashNode<K, V>*> _table;
size_t _size = 0;
};
}
开散列的负载因子和扩容问题:
注意:哈希表是需要扩容的,而是否扩容的衡量指标就是负载因子。
开散列中,当负载因子为1时,也就是理想情况下每个哈希桶中只挂一个元素时,进行扩容。因为再继续插入元素时,每一次都会发生哈希冲突,而开散列哈希表中若一个桶中的元素个数比较多时,会影响哈希表的性能。理想情况就是每个哈希桶中只挂一个元素,此时搜索时间复杂度为O(1)。
开散列和闭散列的比较:
应用链地址法处理哈希冲突,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。