哈希
一.Unordered容器
在C++98中STL增加了一系列底层为红黑树的容器例如我们之前学习的map和set。他的查找速度达到了 l o g 2 N log_2 N log2N而在节点较多时他的查找速度也不尽如人意。所以C++11时STL又增添了4个以哈希思想为基础的底层结构的关联式容器即unordered_map和unordered_set。(unordered_multimap和unordered_multiset我们就不多做介绍)
1.1Unordered_map
- unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
- 在unordered_map中,键值通常用于唯一的标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
- 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
- unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
- unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
- 它的迭代器至少是前向迭代器。
unordered_map的具体接口可以查询其文档unordered_map
1.2Unordered_set
自行查询其文档unordered_set
二.底层结构(重点)
2.1哈希概念
在我们使用顺序结构或平衡树时,插入的数据与其存放的位置之间没有特定的关系。即使我们优化了其底层的结构使得我们在查找过程中进行数据比较的次数变少了例如顺序结构查找数据的时间复杂度是O(N),平衡树查找数据时间复杂度为O(
l
o
g
2
n
log_2n
log2n)。
但是理想的搜索方法是:不经过任何比较直接在表中获得想要的数据。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
而向这种结构进行插入时就是通过元素的关键码和函数来计算出存储位置,查找时也是通过关键码来进行查找。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
就例如上图:将关键值与容器容量联系起来从而产生这个哈希算法。
但是问题也随之而来:如果我现在想要插入44又会发生什么?
2.2哈希冲突
哈希冲突也即哈希碰撞是指:有着不同的关键值但是通过哈希算法得出的相同的哈希地址。
哈希冲突是使用哈希思想无法避免的一个结果,我们只能让他减少没法避免。所以我们想要减少哈希冲突就可以优化哈希函数或者优化容器结构。
2.3哈希函数
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
域必须在0到m-1之间 - 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常见哈希函数
- 直接定址法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况 - 除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址 - 平方取中法–(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况 - 折叠法–(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况 - 随机数法–(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
通常应用于关键字长度不等时采用此法
3.3闭散列和开散列
我们优化哈希函数只能稍微减少哈希冲突的产生,想要解决哈希冲突就只能利用闭散列和开散列。
3.3.1闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
- 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
即在插入后如果发生哈希冲突就往后使用线性探测寻找到下一个空位置。
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影
响。因此线性探测采用标记的伪删除法来删除一个元素即利用枚举来产生一个状态。
enum Staus
{
EXIST,//存在
EMPTY,//空
DELETE,//删除
};
//线性探测的实现
#pragma once
#include <vector>
#include <iostream>
using namespace std;
namespace ly
{
enum Staus
{
EXIST,
EMPTY,
DELETE,
};
template<class K,class V>
struct HashDate
{
pair<K, V> _kv;
Staus _s = EMPTY;
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hashi = 0;
for (auto ch : s)
{
hashi *= 31;
hashi += ch;
}
return hashi;
}
};
template<class K,class V,class HashFuncOft = HashFunc<K>>
class HashTable
{
public:
HashTable()
{
_tables.resize(10);
}
bool Insert(const pair<K,V>& kv)
{
HashFuncOft hf;
if (Find(kv.first))
{
return false;
}
//扩容
if (_n * 10 / _tables.size() == 7)
{
size_t NewSize = _tables.size() * 2;
HashTable<K,V,HashFuncOft> NewHash;
NewHash._tables.resize(NewSize);
for (int i = 0; i < _tables.size();i++)
{
if (_tables[i]._s == EXIST)
{
NewHash.Insert(_tables[i]._kv);
}
}
_tables.swap(NewHash._tables);
}
size_t hash = hf(kv.first) % _tables.size();
while (_tables[hash]._s == EXIST)
{
hash++;
hash %= _tables.size();
}
_tables[hash]._kv = kv;
_tables[hash]._s = EXIST;
++_n;
return true;
}
HashDate<K,V>* Find(const K& key)
{
HashFuncOft hf;
size_t hash = hf(key) % _tables.size();
while (_tables[hash]._s != EMPTY)
{
if (_tables[hash]._s == EXIST && _tables[hash]._kv.first == key)
{
return &_tables[hash];
}
hash++;
hash %= _tables.size();
}
return nullptr;
}
bool Erase(const K& key)
{
HashDate<K, V>* ret = Find(key);
if (ret)
{
ret->_s = DELETE;
--_n;
return true;
}
else
{
return false;
}
}
void Print()
{
for (int i = 0; i < _tables.size(); i++)
{
if (_tables[i]._s == EXIST)
{
cout << "[" << i << "]->" << _tables[i]._kv.first << ":"<< _tables[i]._kv.second << endl;
}
else if (_tables[i]._s == EMPTY)
{
cout << "[" << i << "]->" << endl;
}
else
{
cout << "[" << i << "]->D" << endl;
}
}
}
private:
vector<HashDate<K,V>> _tables;
size_t _n;//负载因子
};
}
哈希表的扩容也是需要我们特殊处理的
而哈希表什么时候进行扩容也是我们定义一个新的成员变量:负载因子
因为我们在进行扩容后通过哈希函数算出的哈希地址会发生变化
所以我们可以新建一个哈希表再将数据插入到这个新表中
最后再将旧表与新表进行交换
类似与我们之前学习的拷贝构造的现代写法
if (_n * 10 / _tables.size() == 7)
{
size_t NewSize = _tables.size() * 2;
HashTable<K,V,HashFuncOft> NewHash;
NewHash._tables.resize(NewSize);
for (int i = 0; i < _tables.size();i++)
{
if (_tables[i]._s == EXIST)
{
NewHash.Insert(_tables[i]._kv);
}
}
_tables.swap(NewHash._tables);
}
- 二次探测
线性探测是发生哈希冲突后一个一个的向后寻找空位置,而二次探测则是进行跳跃式的寻找空位置。
具体实现就不多说了,大家了解即可。
3.3.2开散列(哈希桶)(重点)
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace hash
{
template<class K,class V>
struct HashNode
{
HashNode* _next;
pair<K, V> _kv;
HashNode(const pair<K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K, class V>
class Hash_Bucket
{
public:
typedef HashNode<K,V> Node;
Hash_Bucket()
{
_tables.resize(10);
}
~Hash_Bucket()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
}
}
bool Insert(const pair<K,V>& kv)
{
if (Find(kv))
{
return false;
}
//扩容
if (_n == _tables.size())
{
size_t newsize = _tables.size() * 2;
vector<Node*> NewHash(newsize, nullptr);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = kv.first % NewHash.size();
cur->_next = _tables[i];
_tables[i] = cur->_next;
cur = next;
}
}
_tables.swap(NewHash);
}
//头插
size_t hashi = kv.first % _tables.size();
Node* cur = new Node(date);
cur->_next = _tables[hashi];
_tables[hashi] = cur;
++_n;
return true;
}
Node* Find(const K& key)
{
size_t hashi = key % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return NULL;
}
bool Erase(const K& key)
{
size_t hashi = key % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
void Print()
{
for (int i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
cout << cur->_kv.first << ":" << cur->_kv.second << endl;
cur = cur->_next;
}
}
}
private:
vector<Node*> _tables;
size_t _n;//负载因子
};
}
开散列的扩容和闭散列的扩容不同
如果我们还像闭散列一样开一个新表然后将数据一个一个的插入到新表中我们会发现对于空间的浪费实在过于过分。所以我们需要思考一个新方法
我们同样开一个新表但是我们不需要重新再开节点我们只需要计算节点在新表的位置再将旧节点插入到新表中最后再将新表和旧表进行交换即可。
//扩容
if (_n == _tables.size())
{
size_t newsize = _tables.size() * 2;
//产生新表
vector<Node*> NewHash(newsize, nullptr);
//遍历旧表中的数据
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
//遍历第i个桶
while (cur)
{
//提前记录下一个节点
Node* next = cur->_next;
//计算节点在新表的第几个桶里
size_t hashi = kv.first % NewHash.size();
//将旧节点插入到新表中
cur->_next = _tables[i];
_tables[i] = cur->_next;
cur = next;
}
}
//将旧表与新表进行交换
_tables.swap(NewHash);
}
开散列的拓展
在我们现在写的哈希桶中,我们存储key为整形的数据,想要存储key为其他类型的数据我们就需要利用仿函数将其他类型的key转为整形。
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化
//当key为string时会调用这个仿函数
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hashi = 0;
for (auto ch : s)
{
hashi *= 31;
hashi += ch;
}
return hashi;
}
};
template<class K, class V,class Hash = HashFunc<K>>
class Hash_Bucket
{
public:
typedef HashNode<K,V> Node;
Hash_Bucket()
{
_tables.resize(10);
}
~Hash_Bucket()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
}
}
bool Insert(const pair<K,V>& kv)
{
Hash hf;
if (Find(kv))
{
return false;
}
//扩容
if (_n == _tables.size())
{
size_t newsize = _tables.size() * 2;
//产生新表
vector<Node*> NewHash(newsize, nullptr);
//遍历旧表中的数据
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
//遍历第i个桶
while (cur)
{
//提前记录下一个节点
Node* next = cur->_next;
//计算节点在新表的第几个桶里
size_t hashi = hf(kv.first) % NewHash.size();
//将旧节点插入到新表中
cur->_next = _tables[i];
_tables[i] = cur->_next;
cur = next;
}
}
//将旧表与新表进行交换
_tables.swap(NewHash);
}
//头插
size_t hashi = hf(kv.first) % _tables.size();
Node* cur = new Node(kv);
cur->_next = _tables[hashi];
_tables[hashi] = cur;
++_n;
return true;
}
Node* Find(const K& key)
{
Hash hf;
size_t hashi = hf(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return NULL;
}
bool Erase(const K& key)
{
Hash hf;
size_t hashi = hf(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
void Print()
{
for (int i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
cout << cur->_kv.first << ":" << cur->_kv.second << endl;
cur = cur->_next;
}
}
}
private:
vector<Node*> _tables;
size_t _n;//负载因子
};
}
开散列和闭散列的比较
开散列处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于闭散列必须保持大量的空闲空间以确保搜索效率,如二次探查法要求负载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用开散列反而比闭散列节省存储空间。
三.模拟实现
3.1模板参数的改造
这也是在之前模拟实现map和set的常规操作了。
//Unordered_set.h
#pragma once
#include "Buctet.h"
namespace Hash_Buctet
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:
struct SetKeyOft
{
const K& operator()(const K& k)
{
return k;
}
};
bool insert(const K& kv)
{
return _ht.Insert(kv);
}
bool find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
Hash_Buctet::Bucket<K, K, SetKeyOft, Hash> _ht;
};
}
//Unordered_map
#pragma once
#include "Buctet.h"
namespace Hash_Buctet
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
struct MapKeyOft
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
bool insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
bool find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
Hash_Buctet::Bucket<K, pair<K, V>, MapKeyOft, Hash> _ht;
};
}
//Bucket.h
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace Hash_Buctet
{
template<class T>
struct HashNode
{
HashNode* _next;
T _date;
HashNode(const T& date)
:_date(date)
,_next(nullptr)
{}
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化
//当key为string时会调用这个仿函数
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hashi = 0;
for (auto ch : s)
{
hashi *= 31;
hashi += ch;
}
return hashi;
}
};
template<class K, class T,class KeyOft,class Hash>
class Bucket
{
public:
typedef HashNode<T> Node;
Bucket()
{
_tables.resize(10);
}
~Bucket()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
}
}
bool Insert(const T& date)
{
Hash hf;
KeyOft kt;
if (Find(kt(date)))
{
return false;
}
//扩容
if (_n == _tables.size())
{
size_t newsize = _tables.size() * 2;
vector<Node*> NewHash(newsize, nullptr);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hf(kt(date)) % NewHash.size();
cur->_next = _tables[i];
_tables[i] = cur->_next;
cur = next;
}
}
_tables.swap(NewHash);
}
//头插
size_t hashi = hf(kt(date)) % _tables.size();
Node* cur = new Node(date);
cur->_next = _tables[hashi];
_tables[hashi] = cur;
++_n;
return true;
}
Node* Find(const K& key)
{
Hash hf;
KeyOft kt;
size_t hashi = hf(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kt(cur->_date) == key)
{
return cur;
}
cur = cur->_next;
}
return NULL;
}
bool Erase(const K& key)
{
Hash hf;
KeyOft kt;
size_t hashi = hf(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (kt(cur->_date) == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n;//负载因子
};
}
3.2迭代器
//unordered_set.h
#pragma once
#include "Buctet.h"
namespace Hash_Bucket
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:
struct SetKeyOft
{
const K& operator()(const K& k)
{
return k;
}
};
typedef typename Hash_Bucket::Bucket<K, K, SetKeyOft, Hash>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
bool insert(const K& kv)
{
return _ht.Insert(kv);
}
bool find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
Hash_Bucket::Bucket<K, K, SetKeyOft, Hash> _ht;
};
}
//Unordered_map.h
#pragma once
#include "Buctet.h"
namespace Hash_Bucket
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
struct MapKeyOft
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
typedef typename Hash_Bucket::Bucket<K, pair<K, V>, MapKeyOft, Hash>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
bool insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
bool find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
Hash_Bucket::Bucket<K, pair<K, V>, MapKeyOft, Hash> _ht;
};
}
//Bucket.h
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace Hash_Bucket
{
template<class T>
struct HashNode
{
......
};
template<class K>
struct HashFunc
{
......
};
template<>
struct HashFunc<string>
{
......
};
//哈希桶与迭代器类相互依赖
//迭代器构造需要哈希桶但是又无法找到它
//哈希桶中又需要迭代器但是迭代器又无法实例化
//所以需要将哈希桶进行前置声明
template<class K, class T, class KeyOft, class Hash>
class Bucket;
template<class K, class T, class KeyOft, class Hash>
struct __Iterator
{
typedef HashNode<T> Node;
typedef __Iterator<K, T, KeyOft, Hash> Self;
__Iterator(Node* node,Bucket<K, T, KeyOft, Hash>* pht, size_t hashi)
:_node(node)
,_pht(pht)
,_hashi(hashi)
{}
T& operator*()
{
return _node->_date;
}
T* operator->()
{
return &_node->_date;
}
Self& operator++()
{
//当节点的下一个节点不为空时
if (_node->_next)
{
_node = _node->_next;
}
//当节点的下一个节点为空时
else
{
//我们需要寻找到下一个桶
//可以通过哈希函数计算当前节点的关键值从而得到是第几个桶
//也可以将第几个桶作为迭代器的成员变量在构造迭代器时传进来
//在得到是第几个桶后我们需要得到下一个桶的情况
//所以我们可以将哈希桶传进来或者将哈希桶中的_tables传进来
++_hashi;
//寻找不为空的桶
while (_hashi < _pht->_tables.size())
{
if (_pht->_tables[_hashi])
{
_node = _pht->_tables[_hashi];
break;
}
else
{
++_hashi;
}
}
//当没有不为空的桶时
if (_hashi == _pht->_tables.size())
{
//将节点设为空
_node = nullptr;
}
}
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
Node* _node;
Bucket<K, T, KeyOft, Hash>* _pht;//哈希桶
size_t _hashi;//第几个桶
};
template<class K, class T,class KeyOft,class Hash>
class Bucket
{
public:
//迭代器使用中会使用哈希桶中的私有成员所以要将迭代器类变为哈希桶的友元类
template<class K, class T, class KeyOft, class Hash>
friend struct __Iterator;
typedef HashNode<T> Node;
typedef __Iterator<K, T,KeyOft, Hash> iterator;
Bucket()
{
......
}
~Bucket()
{
......
}
iterator begin()
{
//遍历哈希桶,返回遇到的第一个不为空的节点
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return iterator(_tables[i], this, i);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this, -1);
}
bool Insert(const T& date)
{
......
}
Node* Find(const K& key)
{
......
}
bool Erase(const K& key)
{
......
}
private:
vector<Node*> _tables;
size_t _n;//负载因子
};
}
//test.cpp
#include "Buctet.h"
#include "Unordered.set.h"
#include "Unordered_map.h"
int main()
{
Hash_Bucket::unordered_map<string, string> um;
Hash_Bucket::unordered_set<int> us;
vector<int> v{ 1,5,6,7,9,13,15,17,23,25 };
for (auto ch : v)
{
us.insert(ch);
}
vector<pair<string, string>> vv{ {"string","字符串"},{"vector","数组"},{"hash","哈希"},{"list","链表"} };
for (auto ch1 : vv)
{
um.insert(ch1);
}
Hash_Bucket::unordered_map<string, string>::iterator it = um.begin();
while (it != um.end())
{
it->first = "x";
it->second = "x";
cout << it._node->_date.first << ":" << it._node->_date.second << endl;
++it;
}
cout << endl;
Hash_Bucket::unordered_set<int>::iterator it1 = us.begin();
while (it1 != us.end())
{
*it1 += 1;
cout << *it1 << " ";
++it1;
}
return 0;
}
3.3const迭代器
在解决了迭代器后,我们还需要解决set的key不能修改的问题以及map的key不能修改的问题。即设计const迭代器
//unordered_set.h
#pragma once
#include "Buctet.h"
namespace Hash_Bucket
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:
struct SetKeyOft
{
const K& operator()(const K& k)
{
return k;
}
};
typedef typename Hash_Bucket::Bucket<K, K, SetKeyOft, Hash>::const_iterator iterator;
typedef typename Hash_Bucket::Bucket<K, K, SetKeyOft, 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<const_iterator, bool> insert(const K& kv)
{
auto ret = _ht.Insert(kv);
return pair< const_iterator, bool>(const_iterator(ret.first._node, ret.first._pht, ret.first._hashi), ret.second);
}
bool find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
Hash_Bucket::Bucket<K, K, SetKeyOft, Hash> _ht;
};
}
//unordered_map.h
#pragma once
#include "Buctet.h"
namespace Hash_Bucket
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:
struct MapKeyOft
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
typedef typename Hash_Bucket::Bucket<K, pair<const K, V>, MapKeyOft, Hash>::iterator iterator;
typedef typename Hash_Bucket::Bucket<K, pair<const K, V>, MapKeyOft, 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<iterator, bool> insert(const pair<K,V>& kv)
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
bool find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
Hash_Bucket::Bucket<K, pair<const K, V>, MapKeyOft, Hash> _ht;
};
}
//Bucket.h
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace Hash_Bucket
{
template<class T>
struct HashNode
{
HashNode* _next;
T _date;
HashNode(const T& date)
:_date(date)
,_next(nullptr)
{}
};
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hashi = 0;
for (auto ch : s)
{
hashi *= 31;
hashi += ch;
}
return hashi;
}
};
template<class K, class T, class KeyOft, class Hash>
class Bucket;
template<class K, class T,class Ref,class Ptr, class KeyOft, class Hash>
struct __Iterator
{
typedef HashNode<T> Node;
typedef __Iterator<K, T,Ref,Ptr, KeyOft, Hash> Self;
__Iterator(Node* node,Bucket<K, T, KeyOft, Hash>* pht, size_t hashi)
:_node(node)
,_pht(pht)
,_hashi(hashi)
{}
//详细看下图
__Iterator(Node* node, const Bucket<K, T, KeyOft, Hash>* pht, size_t hashi)
:_node(node)
, _pht(pht)
, _hashi(hashi)
{}
Ref operator*()
{
return _node->_date;
}
Ptr operator->()
{
return &_node->_date;
}
Self& operator++()
{
......
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
Node* _node;
const Bucket<K, T, KeyOft, Hash>* _pht;
size_t _hashi;
};
template<class K, class T,class KeyOft,class Hash>
class Bucket
{
public:
template<class K, class T,class Ref,class Ptr, class KeyOft, class Hash>
friend struct __Iterator;
typedef HashNode<T> Node;
typedef __Iterator<K, T,T&,T*,KeyOft, Hash> iterator;
typedef __Iterator<K, T,const T&,const T*,KeyOft, Hash> const_iterator;
Bucket()
{
_tables.resize(10);
}
~Bucket()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
}
}
iterator begin()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return iterator(_tables[i], this, i);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this, -1);
}
const_iterator begin()const
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return const_iterator(_tables[i], this, i);
}
}
return end();
}
const_iterator end()const
{
return const_iterator(nullptr, this, -1);
}
pair<iterator,bool> Insert(const T& date)
{
Hash hf;
KeyOft kt;
iterator it = Find(kt(date));
if(it != end())
{
return make_pair(it,false);
}
//扩容
if (_n == _tables.size())
{
size_t newsize = _tables.size() * 2;
vector<Node*> NewHash(newsize, nullptr);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hf(kt(date)) % NewHash.size();
cur->_next = _tables[i];
_tables[i] = cur->_next;
cur = next;
}
}
_tables.swap(NewHash);
}
//头插
size_t hashi = hf(kt(date)) % _tables.size();
Node* cur = new Node(date);
cur->_next = _tables[hashi];
_tables[hashi] = cur;
++_n;
return make_pair(iterator(cur,this,hashi),true);
}
iterator Find(const K& key)
{
Hash hf;
KeyOft kt;
size_t hashi = hf(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kt(cur->_date) == key)
{
return iterator(cur,this,hashi);
}
cur = cur->_next;
}
return end();
}
bool Erase(const K& key)
{
......
}
private:
vector<Node*> _tables;
size_t _n;//负载因子
};
}
四.位图和布隆过滤器
在学习了哈希思想和手撕哈希桶之后我想我们对哈希都有了一定了解,哈希思想让我们查找数据的时间复杂度从O(N)转变成了O(1)。可是哈希桶仍有一定的弊端:空间消耗过大。万一我们只是要查找某个数在不在,我们又需要将数据插入后才能判断,有一点大炮打蚊子的感觉。
例如这道面试题:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在 这40亿个数中。【腾讯】
那我们学习了哈希之后有没有一种结构可以让每个数分别对应一个位置并且空间消耗很少时间复杂度又很低呢?
位图:所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
4.1位图的实现
在面对这种海量数据时我们想要一一对应某位就只能利用bit,因为bit只有0和非0两种状态刚刚好对应了不存在和存在两个状态。
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace ly
{
//N:开辟多少个bit位
template<size_t N>
class bit_set
{
public:
bit_set()
{
//一个整形有四个字节
//一个字节有八个bit位
//因为除法会省略后面的余数所以需要多开辟一个整形
//例如:39/32 = 1 实际需要两个整形
_bs.resize(N / 32 + 1);
}
//将x对应的bit位设为1
void set(size_t x)
{
//第几个整形
int i = x / 32;
//第几个比特位
int j = x % 32;
//有大小端机的区别 但是左移就是往高位移动 这里用大端机为例
//00000000 00000000 00000000 00000001
//在向高位移动j位后变为 假如j=4
//00000000 00000000 00000000 00010000
//再与存储的第i个整形进行或 无论这个整形的第j个bit位为0还是1
//与1或都会变为1
_bs[i] |= (1 << j);
}
//将x对应的bit位设为0
//与0与
void reset(size_t x)
{
int i = x / 32;
int j = x % 32;
_bs[i] &= ~(1 << j);
}
//判断x对应的bit位是0还是1
//与1与
bool test(size_t x)
{
int i = x / 32;
int j = x % 32;
if (_bs[i] & (1 << j))
{
cout << "1" << endl;
return true;
}
else
{
cout << "0" << endl;
return false;
}
}
private:
vector<int> _bs;
};
}
这里的位图只能判断存在不存在,但是我们可以通过增加几个位图来判断不存在,出现1一次,出现两次及以上等的存在拓展问题即:不存在对应00,存在一次对应01,存在两次对应10。
4.2布隆过滤器
在学习了哈希表和位图后我们发现他们各有各的缺点各有各的好处
- 用哈希表存储数据,缺点:浪费空间
- 用位图存储数据,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理
了。 - 将哈希与位图结合,即布隆过滤器
4.2.1布隆过滤器的概念
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
4.2.2布隆过滤器的实现
#pragma once
#include "bit_set.h"
#include <string>
namespace ly
{
struct BKDR_Hash
{
size_t operator()(const string& s)
{
size_t hashi = 0;
for (auto ch : s)
{
hashi *= 31;
hashi += ch;
}
return hashi;
}
};
struct AP_Hash
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (long i = 0; i < s.size(); i++)
{
char ch = s[i];
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJB_Hash
{
size_t operator()(const string& s)
{
register size_t hash = 5381;
for(auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N,class K = string,class HashFunc1 = BKDR_Hash,class HashFunc2 = AP_Hash,class HashFunc3 = DJB_Hash>
class BloomFilter
{
public:
void set(const K& key)
{
size_t hash1 = HashFunc1()(key) % N;
size_t hash2 = HashFunc2()(key) % N;
size_t hash3 = HashFunc3()(key) % N;
_bs.set(hash1);
_bs.set(hash2);
_bs.set(hash3);
}
bool test(const K& key)
{
//只要三个对应位置有一个为false就返回false
size_t hash1 = HashFunc1()(key) % N;
if (_bs.test(hash1) == false)
{
return false;
}
size_t hash2 = HashFunc2()(key) % N;
if (_bs.test(hash2) == false)
{
return false;
}
size_t hash3 = HashFunc3()(key) % N;
if (_bs.test(hash3) == false)
{
return false;
}
return true;
}
private:
ly::bit_set<N> _bs;
};
}
4.2.3布隆过滤器的查找
因为哈希函数的缺陷,所以不同的数据通过哈希函数计算出的哈希地址可能会重合。这导致了布隆过滤器查找的缺陷。
布隆过滤器的查找只有不存在是准确的,存在是不准确的。原因如下图。
ps:布隆过滤器正常是不能有删除操作的也是因为怕影响其他的数据,但是我们可以利用之前学习的引用计数来使其存在。但是使用引用计数又会产生计数回绕的问题
4.2.4布隆过滤器的优缺点
优点:
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无
关 - 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点:
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再
建立一个白名单,存储可能会误判的数据) - 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
五.总结
哈希思想是一项非常著名的思想,通过这个思想产生的结构又有着非常多类今天我们只是大致学习了其中一些比较关键的地方。而在今天最开头的地方我们发现C++还是非常与时俱进的,所以下一期我们会学习C++11时开拓的全新的特性!