前言
在C++中unordered系列的容器效率很高原因是在底层使用了哈希结构,让我们一起来了解一下哈希相关的知识,并且简单的实现以下哈希思想相关的容器。
目录
1.哈希概念
2.哈希冲突
3.哈希函数
4.哈希冲突解决
4.1闭散列
1.线性探测
2.二次探测 S
4.2开散列
4.2.1拉链法
4.2.2拉链法的实现
5.unordered_map和unordered_set的模拟实现
5.1unordered_map的模拟实现
5.2unordered_set的模拟实现
1.哈希概念
顺序结构以及平衡树中,元素的关键码和存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为数的高度,即O($log_2 N$),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素,如果构造一种存储结构,通过某种函数使得元素的存储位置与它的关键码直接能够建立--映射的关系,那么在查找时,通过该函数可以很快的找到该元素,这种映射关系就是哈希思想的体现。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算求得该元素的存储位置并按照此位置进行存放。
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按照此位置取元素进行比较,若关键码相同,则成功。
该方法即为哈希(散列)方法,哈希方法使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称为散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为hash(key) = key % capacity;capacity为存储元素的底层的空间总的大小。
用该方法进行搜索不必进行多次的 关键码字的比较,因此搜索速度比较快
但是如果按照上述的哈希方法,向集合中插入元素44,会出现什么问题?
如果按照上面的方法插入44,你会发现没有位置可以插入了,因为下标为4的位置已经存在值了,所以就会产生冲突。这就是哈希冲突。
2.哈希冲突
哈希冲突:不同的关键字通过相同的哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或者哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素叫做“同义词”。
发生哈希冲突该如何处理呢?
3.哈希函数
引起哈希冲突的一个原因可能是:哈希函数设计的不够合理。
哈希函数设计原则:
1.哈希函数的定义域必须包含所有的需要存储的关键字码,而如果散列表中允许有m个地址时,其值域必须在0到m-1之间。
2.哈希函数计算出的地址要能均匀的分布在整个空间中
3.哈希函数应该比较简单
常见的哈希函数:
1.直接定址法--(常用)
取关键字中的某个线性函数为散列地址:Hash(key) = A * Key + B。
优点:简单匀称。
缺点:需要事先知道关键字的分布情况。
使用场景:适合查找比较小且连续的情况
2.除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但是接近或者等于m的质数p作为除数,按照哈希函数:Hash(key)= key % p(p <= m),将关键码转化为哈希地址
3.平方取中法
假设关键字为1234,对它的平方就是1522756,抽取中间的三位227作为哈希地址;
再比如关键字为4321,对它的平方就是18671041,抽取中间的3为671(或710)作为哈希地址。
平方取中法适合于:不知道关键字的分布,而位数又不是很大的情况。
4.折叠法
折叠法是将关键字从左到右分隔为位数相等的几部分(最后一部分可以短一些),然后将这几部分叠加求和,并按照散列表表长取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。
注意:哈希函数设计的越精密,产生哈希冲突的可能性越低,但是无法避免哈希冲突。
4.哈希冲突解决
解决哈希冲突有常用的两种方法:开散列和闭散列。
4.1闭散列
闭散列:也叫开放定址法,当发M生哈希冲突时,如果哈希冲突未被装满,说明在哈希表中必然还有空位置,那么可以将key存放到冲突位置的“下一个空位置中去”,那么如何寻找下一个空位置呢?
1.线性探测
在上面的场景中,需要再插入元素44,先通过哈希函数计算哈希地址,hashAdd为4,因此理论上44因该插在该位置,但是该位置已经放了值为4的元素,即发生了哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入:
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置没有元素就直接插入新元素,如果该位置有元素发生哈希冲突使用线性探测找到下一个空位置,插入新元素。
删除 :
采用闭散列处理哈希冲突时,不可以进行元素的删除,若直接删除会影响其它元素的搜索。比如删除4,如果直接删除,那么查找44的时候就会面临找不到的情况。所以这里采用对每个位置的元素加入若干种状态来表示元素是否存在。因此线性探测采用的这种方法是一种伪删除法。
线性探测的实现:
#include<iostream>
#include<vector>
using namespace std;
namespace CloseHash
{
//结构上在闭散列中,应该存放一个val值并且要存放表示这个值的状态,所以我们需要一个枚举类型
enum State
{
EMPTY,//表示元素不存在
LIVE,//表示元素存在
DELETE,//表示元素以前存在但是已经被删除
};
template<class K, class V>
struct HashData//哈希表每个元素
{
pair<K, V> _data;//实际的数据
State _state = EMPTY;//每个数据的状态
};
template<class K>
struct Hash
{
size_t operator()(const K& key)//用来取出k中的数据
{
return key;
}
};
template<class K, class V, class HashFun = Hash<K>>
class HashTable
{
public:
//构造函数对哈希表初始化,刚开始先开11个空间,因为质数的哈系冲突的概率小一些
HashTable(size_t capacity = 11)
:_size(0)
{
_hashtable.resize(capacity);
}
//插入元素
bool Insert(const pair<K, V>& key)
{
if (Find(key.first) != nullptr)//先去找一下这个元素是否存在如果可以找到说明元素已经在表的里面了
{
return false;
}
//插入元素之前判断是否需要增容,如果需要增容就先增容
//插入元素,这里采用的是线性探测来解决哈希冲突
HashFun f1;
size_t data = f1(key.first);
size_t start = data % _hashtable.capacity();
while (_hashtable[start]._state != EMPTY)//将哈希元素插入到为空的位置
{
++start;
if (start == _hashtable.capacity())//走到最后的位置了
{
start = 0;
}
}
//将元素插入并且将元素的状态改为LIVE
_hashtable[start]._data = key;
_hashtable[start]._state = LIVE;
//然后增加哈希表统计元素个数的_size
++_size;
return true;
}
//查找元素
HashData<K,V>* Find(const K& key)
{
size_t data = HashFun()(key);//通过哈系函数取出key
//线性探测进行元素查找
size_t addr = data % _hashtable.capacity();
while (_hashtable[addr]._state != EMPTY)//只要不为空就要一直找下去
{
if (_hashtable[addr]._state == LIVE
&& _hashtable[addr]._data.first == key)
{
//找到这个元素返回true
return &_hashtable[addr];
}
//继续向后走
++addr;
if (addr == _hashtable.size())//说明此时到了表的末尾,冲突的很厉害还没有找到,从头开始找
{
addr = 0;//这里不会死循环,因为哈希表不可能满
}
}
//走到这里说明为空了还没有找到,返回false
return nullptr;
}
bool Erase(const K& key)
{
if (_size == 0)
{
//说明此时没有元素可以进行删除了
return false;
}
size_t data = HashFun()(key);
size_t start = data % _hashtable.capacity();
while (_hashtable[start]._state != EMPTY)
{
if (_hashtable[start]._data.first = key && _hashtable[start]._state == LIVE)
{
//找到元素并且改变它的状态
_hashtable[start]._state = DELETE;
--_size;
return true;
}
++start;
if (start == _hashtable.size())//说明此时到了表的末尾,冲突的很厉害还没有找到,从头开始找
{
start = 0;//这里不会死循环,因为哈希表不可能满
}
}
return false;
}
private:
vector<HashData<K,V>> _hashtable;
size_t _size = 0;//表示哈希表中的元素
};
}
想一想:哈希表在什么情况下进行扩容?如何扩容?
哈希表(散列表)的载荷因子定义为: a = 填入表中的数据/散列表的长度。
a是散列表装满程度的标志因子。由于表长是定值,a与“填入的元素的个数”成正比,所以a越大填入表中的元素越多,产生冲突的可能性越大。反之,a越小,表示填入表中的数据越少,产生冲突的可能性越少。实际中哈希表的平均查找长度是载荷因子a的函数,只是不同处理冲突的方法有不同的函数。
对于开放定值法,载荷因子很重要,应该严格的限制在0.7到0.8之间。超过0.8查表时CPU的缓存不命中(cache missing)按照指数曲线上升。因此,一些采用开放定值法的哈希库,如java的系统库限制载荷因子不超过0.75,超过此值散列表将增容。
完整代码:
#include<iostream>
#include<vector>
using namespace std;
namespace CloseHash
{
//结构上在闭散列中,应该存放一个val值并且要存放表示这个值的状态,所以我们需要一个枚举类型
enum State
{
EMPTY,//表示元素不存在
LIVE,//表示元素存在
DELETE,//表示元素以前存在但是已经被删除
};
template<class K, class V>
struct HashData//哈希表每个元素
{
pair<K, V> _data;//实际的数据
State _state = EMPTY;//每个数据的状态
};
template<class K>
struct Hash
{
size_t operator()(const K& key)//用来取出k中的数据
{
return key;
}
};
template<>
struct Hash<string>//关于string类型要进行特化处理,因为字符串如果直接像整数一样做映射是不行的,所以采用将字符串转化成整数的方式
{
size_t operator()(const string& key)
{
//将字符串转化为相应的整数,为了减少哈希冲突借鉴前人的一种方法
size_t hash = 0;
for (auto e : key)
{
hash = hash * 131 + e;
}
return hash;
}
};
template<class K, class V, class HashFun = Hash<K>>
class HashTable
{
public:
//构造函数对哈希表初始化,刚开始先开11个空间,因为质数的哈系冲突的概率小一些
HashTable(size_t capacity = 11)
:_size(0)
{
_hashtable.resize(capacity);
}
//插入元素
bool Insert(const pair<K, V>& key)
{
if (Find(key.first) != nullptr)//先去找一下这个元素是否存在如果可以找到说明元素已经在表的里面了
{
return false;
}
//插入元素之前判断是否需要增容,如果需要增容就先增容
//一般表的容量到0.7增容就比较合适,表太满了增容会增加哈希冲突,表太空增容又会浪费空间
if (_size * 10 / _hashtable.capacity() > 7)//整数是没有0.7这种说法的
{
//增容不单单要考虑开空间的问题,还要考虑到原来的数据的映射关系现在已经变了,
//就要取出旧表中的数据在新表中重新映射
size_t newCapacity = _hashtable.capacity() * 2;
HashTable<K, V> newHashTable;
newHashTable._hashtable.resize(newCapacity);
//取旧表的数据
for (auto& e : _hashtable)
{
if (e._state == LIVE)
{
//将旧表中的数据插入到新表中,这里可以直接调用Insert
newHashTable.Insert(e._data);
}
}
//交换新表和旧表
_hashtable.swap(newHashTable._hashtable);
}
//插入元素,这里采用的是线性探测来解决哈希冲突
HashFun f1;
size_t data = f1(key.first);
size_t start = data % _hashtable.capacity();
while (_hashtable[start]._state != EMPTY)//将哈希元素插入到为空的位置
{
++start;
if (start == _hashtable.capacity())//走到最后的位置了
{
start = 0;
}
}
//将元素插入并且将元素的状态改为LIVE
_hashtable[start]._data = key;
_hashtable[start]._state = LIVE;
//然后增加哈希表统计元素个数的_size
++_size;
return true;
}
//查找元素
HashData<K,V>* Find(const K& key)
{
size_t data = HashFun()(key);//通过哈系函数取出key
//线性探测进行元素查找
size_t addr = data % _hashtable.capacity();
while (_hashtable[addr]._state != EMPTY)//只要不为空就要一直找下去
{
if (_hashtable[addr]._state == LIVE
&& _hashtable[addr]._data.first == key)
{
//找到这个元素返回true
return &_hashtable[addr];
}
//继续向后走
++addr;
if (addr == _hashtable.size())//说明此时到了表的末尾,冲突的很厉害还没有找到,从头开始找
{
addr = 0;//这里不会死循环,因为哈希表不可能满
}
}
//走到这里说明为空了还没有找到,返回false
return nullptr;
}
bool Erase(const K& key)
{
if (_size == 0)
{
//说明此时没有元素可以进行删除了
return false;
}
size_t data = HashFun()(key);
size_t start = data % _hashtable.capacity();
while (_hashtable[start]._state != EMPTY)
{
if (_hashtable[start]._data.first = key && _hashtable[start]._state == LIVE)
{
//找到元素并且改变它的状态
_hashtable[start]._state = DELETE;
--_size;
return true;
}
++start;
if (start == _hashtable.size())//说明此时到了表的末尾,冲突的很厉害还没有找到,从头开始找
{
start = 0;//这里不会死循环,因为哈希表不可能满
}
}
return false;
}
size_t Size()const
{
return _size;
}
bool Empty()
{
return _size == 0;
}
private:
vector<HashData<K,V>> _hashtable;
size_t _size = 0;//表示哈希表中的元素
};
}
注意:如果直接将字符串插入哈希表里面是无法直接插入的因为字符串不是整数,所以我们在这里加入仿函数来对字符串进行处理,使得字符串可以像整数一样进行插入。
template<class K>
struct Hash
{
size_t operator()(const K& key)//用来取出k中的数据
{
return key;
}
};
template<>
struct Hash<string>//关于string类型要进行特化处理,因为字符串如果直接像整数一样做映射是不行的,所以采用将字符串转化成整数的方式
{
size_t operator()(const string& key)
{
//将字符串转化为相应的整数,为了减少哈希冲突借鉴前人的一种方法
size_t hash = 0;
for (auto e : key)
{
hash = hash * 131 + e;
}
return hash;
}
};
测试代码:
#include<iostream>
using namespace std;
void TestCloseHashTable1()
{
CloseHash::HashTable<int, int> hb1;
int a[] = { 1, 5, 10, 100000, 100, 18, 15, 7, 40, 2, 3, 4, 5, 6, 7, 8, 9 };
for (auto e : a)
{
hb1.Insert(make_pair(e, e));
}
cout << endl;
auto ret = hb1.Find(100);
if (ret)
{
cout << "找到了" << endl;
}
else
{
cout << "没有找到了" << endl;
}
hb1.Erase(100);
ret = hb1.Find(100);
if (ret)
{
cout << "找到了" << endl;
}
else
{
cout << "没有找到了" << endl;
}
}
void TestCloseHashTable2()
{
string a[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "橘子", "苹果" };
CloseHash::HashTable<string, int> ht;
for (auto str : a)
{
auto ret = ht.Find(str);
if (ret)
{
ret->_data.second++;
}
else
{
ht.Insert(make_pair(str, 1));
}
}
cout << endl;
}
struct Student
{
//学号
//id
//联系电话
//年龄
char* _id;
char* _telephone;
char* _studentNum;
size_t _oldyear;
};
struct StudentHashFunc
{
size_t operator()(const Student& kv)
{
// 如果是结构体
// 1、比如说结构体中有一个整形,基本是唯一值 - 学号
// 2、比如说结构体中有一个字符串,基本是唯一值 - 身份证号
// 3、如果没有一项是唯一值,可以考虑多项组合
size_t value = 0;
// ...
return value;
}
};
线性探测的优点:实现很简单。
缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据"堆积",即不同的关键码占据了可利用的空位置,使得寻找某关键码的位置要进行多次比较,导致搜索效率较低。其实这种解决哈希冲突的方式就像是在进行互相伤害一样,就比如本来我们都有各自的位置但是如果你占了我的位置,那么我就去占别人的位置,如果大家都这样做,很多人就都不在自己的位置上了,这就是洪水效应。
2.二次探测 S
二次探测就可以为了解决上面线性探测的问题,二次探测找下一个空位置的方法为:$H_i$ = ($H_0$ + $i^2$ )% m, 或者:$H_i$ = ($H_0$ - $i^2$ )% m。其中:i =1,2,3…, $H_0$是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
对于上面的问题如果要插入44,产生冲突,使用二次探测解决的情况为:
研究表明:当表的长度为质数且表的负载因子不超过0.5时,新的表项一定可以插入,而且任何一个位置都不会被探测两次因此只要表中有一半的空位置,就不会存在表满的问题,在搜索时,可以不考虑表装满的情况,但是在插入时必须确保表的负载因子a不超过0.5,如果超过必须增容。
因此闭散列最大的缺陷就是空间利用率低,这也是哈希的缺陷。
//二次探测的实现代码
///二次探测
//为零避免冲突将它封印在另一个命名空间里面
namespace CloseHash1
{
enum State
{
EMPTY,
LIVE,
DELETE,
};
template<class K,class V>
struct HashData
{
pair<K, V> _data;
State _state = EMPTY;
};
template<class K>
class Hash
{
public:
size_t operator()(const K& key)
{
return key;
}
};
//要特化一个string类型的出来,因为字符串很特殊,所以我们需要进行特殊处理,它和整数不同不能直接插入
template<>
class Hash<string>
{
public:
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash = hash * 131 + e;
}
return hash;
}
};
template<class K, class V, class HashFun = Hash<K>>
class HashTable
{
public:
HashTable(size_t capacity = 11)
:_size(0)
{
_HashTable.resize(11);
}
//插入元素
bool Insert(const pair<K, V>& data)
{
//先进行查找如果这个元素存在就不需要插入了
if (Find(data.first) != nullptr)
{
return false;
}
//判断是否需要增容
//当负载因子大于0.7的时候就要增容
if (_size * 10 / _HashTable.capacity() > 7)
{
//进行增容,先构造一个临时的哈希表然后将数据都插入进去
//需要重新插入因为表的大小变了以后原来的关系也就变了
size_t newCapacity = _HashTable.capacity() * 2;
HashTable<K, V> newHashTable;
newHashTable._HashTable.resize(newCapacity);
//遍历旧表将元素都插入到新表中
for (auto& e :_HashTable)
{
newHashTable.Insert(e._data);
}
_HashTable.swap(newHashTable._HashTable);//交换新表和旧表,交换以后得旧表出了作用域就会被销毁
}
//对元素进行插入
size_t key= HashFun()(data.first);
size_t start = key % _HashTable.capacity();
while (_HashTable[start]._state != EMPTY)
{
//处理哈希冲突,使用二次探测
int i = 1;
start = start + i * i % _HashTable.capacity();
++i;
if (start >= _HashTable.capacity())
{
start = 0;//防止探测时越界
}
}
_HashTable[start]._data = data;
_HashTable[start]._state = LIVE;
++_size;
return true;
}
//查找元素
HashData<K, V>* Find(const K& key)
{
if (_size == 0)
{
//哈希表为空返回nullptr;
return nullptr;
}
size_t data = HashFun()(key);
size_t start = data % _HashTable.capacity();//查找元素
while (_HashTable[start]._state != EMPTY)
{
if (_HashTable[start]._state == LIVE
&& _HashTable[start]._data.first == key)
{
//找到元素进行返回
return &_HashTable[start];
}
//存在哈希冲突
//进行二次探测
int i = 1;
start = start + i * i % _HashTable.capacity();
if (start >= _HashTable.capacity())
{
//防止探测时超出数组的长度
start = 0;//从头开始
}
//不会死循环的因为哈希表是不会满的
}
//走到这里说明哈希表中找不到这个元素
return nullptr;
}
//删除元素
bool Erase(const K& key)
{
//如果哈希表为空则不可以对元素进行删除
if (_size == 0)
{
return false;
}
//通过哈希映射查找元素
size_t data = HashFun()(key);
size_t start = data % _HashTable.capacity();
while (_HashTable[start]._state != EMPTY)
{
int i = 1;
if (_HashTable[start]._data.first == key
&& _HashTable[start]._state == LIVE)
{
//说明元素存在将元素进行删除
_HashTable[start]._state = DELETE;
--_size;//将表中的元素个数-1
return true;
}
//进行二次探测
start = start + i * i % _HashTable.capacity();
int i = 1;
if (start >= _HashTable.capacity())//确保start不会越界
{
start = 0;
}
}
//走到这里说明找不到返回falase
return false;
}
//求哈希表中的元素的个数
size_t Size()const
{
return _size;
}
//判断是否为空
bool Empty()const
{
return _size == 0;
}
private:
//用vector存储数据
vector<HashData<K,V>> _HashTable;
size_t _size;//用来表示表中的数据个数
};
}
4.2开散列
4.2.1拉链法
开散列法又叫链地址法(拉链法),首先对关键码集合用散列函数计算散列的地址,具有相同地址的关键码归于同一个 子集合,每个子集合称为一个哈希桶,每个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
4.2.2拉链法的实现
#pragma once
#include<string>
#include<vector>
#include<iostream>
using namespace std;
//简单的模拟实现一下哈系桶
//哈系桶是由两部分组成的,一部分是哈希表,还有哈希表下面挂着的单链表,
//哈系桶下面挂着的单链表可能很长,也可能很短,这是由哈希冲突的程度决定的
//哈希冲突的越厉害,哈希表下面挂着的单链表机会越长,所以哈系桶每个数据是一个节点
//节点由两部分组成,一部分是桶中的数据,另一部分是节点的指针
//这里用自定义的命名空间包裹起来,防止和库里面的冲突了。
namespace OpenHash
{
//哈系桶的节点
template<class V>
struct HashDataNode
{
//数据+节点的指针
V _data;
HashDataNode<V>* _next;
//需要一个构造函数不然后面调不动
HashDataNode(const V&data)
:_next(nullptr)
,_data(data)
{}
};
//仿函数用来将数据转换为无符号整形,方便进行哈希映射
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return (size_t)key;//这里只是简单的处理实际上如果数据不同那么所需要的hash转换也是不一样的
}
};
//这里对哈希函数进行特化
template<>
struct Hash<string>//这里只是对字符串这种常用的类型进行了特化处理
{
size_t operator()(const string& s)
{
//本 算法由于在Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得名,
//是一种简单快捷的hash算法
size_t hash = 0;
for (auto e : s)
{
hash = hash * 131 + 3;//这种转化是为了降低哈希冲突的可能性,主要是借鉴前人智慧
}
return hash;
}
};
//对于取模版中的参数比较进行特化
template<class K>
struct HashKorV
{
const K operator()(const K& key)
{
return key;
}
};
//这里想岔了不能这样特化
/*template<>
struct HashKorV<pair<int,int>>
{
int operator()(pair<int, int> key)
{
return key.first;
}
};
template<>
struct HashKorV<pair<string, int>>
{
const string operator()(const pair<string, int>& key)
{
return key.first;
}
};
template<>
struct HashKorV<pair<string, string>>
{
const string operator()(const pair<string, string>& key)
{
return key.first;
}
};*/
//迭代器的前置声明
template<class K, class V, class KorT = HashKorV<K>, class HashFunc = Hash<K>>
struct __HBIterator;
//哈希桶的实现
template<class K,class V,class KorV = HashKorV<K>,class HashFunc = Hash<K>>
class HashBucket
{
public:
typedef __HBIterator<K, V, KorV, HashFunc> iterator;
typedef HashDataNode<V> Node;
//声明为友元才可以使用operator++的逻辑
template<class K, class V, class KorT, class HashFunc>
friend struct __HBIterator;
//迭代器的实现,begin是哈系桶里面存储的第一个不为空的节点,所以需要进行查找
iterator begin()
{
for (size_t i = 0; i < _HashBucket.capacity();++i)
{
Node* cur = _HashBucket[i];
if (cur)
{
return iterator(cur, this);
}
}
return end();
}
//这里可以简单的认为end()返回的迭代器是空的
iterator end()
{
return iterator(nullptr, this);
}
iterator const_begin()const
{
for (size_t i = 0; i < _HashBucket.capacity();++i)
{
Node* cur = _HashBucket[i];
if (cur)
{
return iterator(cur, this);
}
}
return end();
}
iterator const_end()const
{
return iterator(nullptr, this);
}
//构造函数,初始化时先给10个空间,后续好处理
HashBucket(size_t capacity = 10)
:_size(0)
{
_HashBucket.resize(10);//初始化空间
}
//拷贝构造
HashBucket(const HashBucket& habt)
{
//开空间,拷贝数据
_HashBucket.resize(habt._HashBucket.capacity());
for (size_t i = 0; i < habt._HashBucket.capacity();++i)
{
Node* cur = habt._HashBucket[i];
while (cur)
{
Insert(cur->_data);//拷贝数据,
cur = cur->_next;//向后迭代
}
}
}
HashBucket& operator=(HashBucket habt)
{
//这里采用的是现代写法所以比较简洁
//注意这里要能调用到构造函数
if (this != &habt)
{
//防止自己给自己拷贝
_HashBucket.swap(habt._HashBucket);
::swap(_size, habt._size);
}
return this;
}
~HashBucket()
{
Clear();
}
void Clear()
{
//清除哈希表中保存的节点
for (auto e : _HashBucket)
{
Node* cur = e;
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = nullptr;
cur = next;
}
}
}
typedef __HBIterator<K, T, KeyOfT, HashFunc> iterator;
//哈系桶的插入
pair<iterator,bool> Insert(const V& key)
{
//检查桶里面是否有这个元素如果有,则不用插入
iterator it1 = Find(KorV()(key));
if (it1._node)
{
//有则不需要插入
//return false;
pair<iterator, bool> pair1(it1, false);
return pair1;
}
//检查桶里面的容量是否够
if (_size == _HashBucket.capacity())
{
//增容
size_t newCapacity = _size * 2;//因为这里两个相等所以能少调用函数就不调用,节省时间
HashBucket<K, V, KorV, HashFunc> newHashBucket;
newHashBucket._HashBucket.resize(newCapacity);
//将旧桶中的数据插入到新桶里面,实际上这里发生了重新映射
for (size_t i = 0; i < _size;++i)
{
Node* cur = _HashBucket[i];
while (cur)
{
newHashBucket.Insert(cur->_data);
cur = cur->_next;
}
}
//将交换新桶和旧桶
_HashBucket.swap(newHashBucket._HashBucket);
_size = newHashBucket._size;
}
//插入数据
size_t addr = HashFunc()( KorV()(key) );//这里进行了两次转换先是取出key中的需要比较的部分,
addr = addr % _HashBucket.capacity();//进行哈系映射
//然后再将它转化为可以比较的部分
Node* newNode = new Node(key);
//头插进所在位置的单链表
Node* cur = _HashBucket[addr];
newNode->_next = cur;
_HashBucket[addr] = newNode;
++_size;
iterator it(newNode, this);
pair<iterator, bool> pair2(it, true);
return pair2;
}
//查找
iterator Find(const K& key)
{
if (_size == 0)
{
return iterator(nullptr,this);//说明此时桶里面没有元素,不能进行查找
}
size_t addr = HashFunc()(key);
addr = addr % _HashBucket.capacity();//进行哈系映射
Node* cur = _HashBucket[addr];//取出桶里面的节点
while (cur)
{
//遍历挂在当前位置的单链表
if (KorV()(cur->_data) == key)
{
//说明找到这个元素进行返回即可
return iterator(_HashBucket[addr],this);
}
//迭代向后遍历
cur = cur->_next;
}
//走到这说明这个元素在桶里面是不存在的返回nullptr
return iterator(nullptr, this);
}
//以及删除
bool Erase(const K& key)
{
if (_size == 0)
{
//说明桶为空不能进行元素的删除
return false;
}
size_t addr = HashFunc()(key);
addr = addr % _HashBucket.capacity();//进行哈系映射
//遍历当前位置的单链表
Node* cur = _HashBucket[addr];
Node* prev = nullptr;
//用来记录上一个节点的位置,方便找到的时候进行删除
while (cur)
{
if (KorV()(cur->_data) == key)
{
//找到这个元素进行删除
if (prev == nullptr)//相当于头删
{
Node* next = cur->_next;//保存cur的下一个节点
//删除cur
delete cur;
_HashBucket[addr] = next;
--_size;
return true;
}
else
{
prev->_next = cur->_next;//重新链接节点的关系
delete cur;
--_size;
return true;
}
}
//迭代向后走
prev = cur;
cur = cur->_next;
}
//走到这里说明桶里面没有这个元素不需要进行删除
return false;
}
//判断桶是否为空
bool Empty()
{
return _size == 0;
}
size_t Size()//返回桶里面元素个数
{
return _size;
}
private:
size_t _size;//表示哈希桶里面的元素数量
vector<Node*> _HashBucket;//存放节点
};
//哈希桶的迭代器的实现
template<class K, class V, class KorV, class HashFunc>
struct __HBIterator
{
//这里的迭代器不仅仅是节点的指针进行封装,还需要将哈系桶也存起来,因为在迭代器++的时候
//会出现一个单链表走完的情况,这时候就需要重新根据当前节点的值,重新映射下一个单链表的开始节点
typedef HashDataNode<V> Node;
typedef HashBucket<K, V, KorV, HashFunc> Hb;
typedef __HBIterator<K, V, KorV, HashFunc> Self;
//构造函数
__HBIterator(Node* node,Hb* hb)
:_node(node)
,_hb(hb)
{}
//
Self& operator++()
{
if (_node->_next)//这个单链表还没有走完
{
_node = _node->_next;//如果node->_next存在直接返回
return *this;
}
else
{
//寻找下一个不为空的单链表
//计算node所在的位置,找到下一个不为空的位置,同时更新node,
size_t addr = HashFunc()(KorV()(_node->_data));
addr = addr % _hb->_HashBucket.capacity();
++addr;
while (addr < _hb->_HashBucket.capacity())
{
Node* cur = _hb->_HashBucket[addr];
if (cur)
{
_node = cur;
return *this;
}
++addr;
}
_node = nullptr;
}
return *this;
}
V & operator*()
{
return _node->_data;
}
V* operator->()
{
return &_node->_data;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
public:
Node* _node;
Hb* _hb;
};
//代码改了以后这个测试大概率是用不了的
//简单的测试了一下代码逻辑
//测试代码
/*void TestHashBucket()
{
HashBucket<int, int> hbk;
int a[] = { 1, 5, 10, 100000, 100, 18, 15, 7, 40, 2, 3, 4, 5, 6, 7, 8, 9 };
for (auto e : a)
{
hbk.Insert(e);
}
cout << endl;
auto ret = hbk.Find(100);*/
/*if (ret!=nullptr)
{
cout << "找到了" << endl;
}
else
{
cout << "没有找到了" << endl;
}
HashBucket<int, int> hb = hbk;
hbk.Erase(100);
ret = hbk.Find(100);
if (ret)
{
cout << "找到了" << endl;
}
else
{
cout << "没有找到了" << endl;
}
}*/
}
哈系桶是由两部分组成的,一部分是哈希表,还有哈希表下面挂着的单链表,哈系桶下面挂着的单链表可能很长,也可能很短,这是由哈希冲突的程度决定的,哈希冲突的越厉害,哈希表下面挂着的单链表机会越长,所以哈系桶每个数据是一个节点,节点由两部分组成,一部分是桶中的数据,另一部分是节点的指针,这里用自定义的命名空间包裹起来,防止和库里面的冲突了。
注意:这里迭代器的实现较为复杂,这里的迭代器不仅仅是节点的指针进行封装,还需要将哈系桶也存起来,因为在迭代器++的时候会出现一个单链表走完的情况,这时候就需要重新根据当前节点的值,重新映射下一个单链表的开始节点,所以这就是这个迭代器的实现里面为什么要将哈系桶也存起来,不然无法进行哈希映射。
5.unordered_map和unordered_set的模拟实现
5.1unordered_map的模拟实现
unordered_map的实现较为简单,只需要调用我们实现的哈系表(哈希桶中的接口),进行简单的调整就可以实现了,这里只是实现了几个较为常用且重要的接口其它的接口都不是很难,有兴趣的朋友可以自己完善以下。
//实现代码
#pragma once
#include"Hash.hpp"
#include"UnorderedMap.hpp"
namespace qyy
{
/*template<class K>
class UnorderedSet
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename OpenHash::HashBucket<K, K, SetKeyOfT> ::iterator iterator;
pair<iterator,bool> Insert(const K& key)
{
return _UnorderedSet.Insert(key);
}
iterator begin()
{
return _UnorderedSet.begin();
}
iterator end()
{
return _UnorderedSet.end();
}
iterator Find(const K& key)
{
return _UnorderedSet.Find(key);
}
bool Erase(const K& key)
{
return _UnorderedSet.Erase(key);
}
private:
OpenHash::HashBucket<K, K, SetKeyOfT> _UnorderedSet;
};*/
//UnorderedSet的实现实际上就是通过调用哈系桶的各种接口从而达到自己的需求,
//实际上是对哈系桶进行了一层封装而已
template<class K,class HashFunc = Hash<K>>
class UnorderedSet
{
public:
//为了取出要用的模板K中需要用到的,模板参数不同实现也是不同的。
struct SetKorV
{
const K& operator()(const K& key)
{
return key;
}
};
//给迭代器进行类型推导,并且重命名
typedef typename OpenHash::HashBucket<K, K, SetKorV, HashFunc>::iterator iterator;
pair<iterator,bool>Insert(const K& key)
{
return _UnorderedSet.Insert(key);
}
iterator Find(const K& key)
{
return _UnorderedSet.Find(key);
}
bool Erase(const K& key)
{
return _UnorderedSet.Erase(key);
}
//迭代器的实现也是调用哈希桶里面的迭代器
iterator begin()
{
return _UnorderedSet.begin();
}
iterator end()
{
return _UnorderedSet.end();
}
//const迭代器
iterator const_begin()
{
return _UnorderedSet.const_begin();
}
iterator const_end()
{
return _UnorderedSet.const_end();
}
private:
OpenHash::HashBucket<K, K, SetKorV, HashFunc> _UnorderedSet;
};
}
//测试代码
using namespace qyy;
void test_unordered_set1()
{
UnorderedSet<int> us;
//插入元素
us.Insert(200);
us.Insert(1);
us.Insert(2);
us.Insert(33);
us.Insert(50);
us.Insert(60);
us.Insert(243);
us.Insert(6);
//删除
us.Erase(1);
cout << *(us.Find(60)) << endl;//查找
//进行遍历
UnorderedSet<int>::iterator it = us.begin();
while (it != us.end())
{
cout << *it << " ";
++it;
}
}
这里只是进行了简单的测试,可能代码还存在一些没有察觉的bug。
5.2unordered_set的模拟实现
unordered_map的实现较为简单,只需要我们调用我们实现的哈系表(哈希桶中的接口),进行简单的调整就可以实现了,这里只是实现了几个较为常用且重要的接口其它的接口都不是很难,有兴趣的朋友可以自己完善以下。
//实现代码
#pragma once
#include"Hash.hpp"
namespace qyy
{
//仿函数用来将数据转换为无符号整形,方便进行哈希映射
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return (size_t)key;//这里只是简单的处理实际上如果数据不同那么所需要的hash转换也是不一样的
}
};
//这里对哈希函数进行特化
template<>
struct Hash<string>//这里只是对字符串这种常用的类型进行了特化处理
{
size_t operator()(const string& s)
{
//本 算法由于在Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得名,
//是一种简单快捷的hash算法
size_t hash = 0;
for (auto e : s)
{
hash = hash * 131 + 3;//这种转化是为了降低哈希冲突的可能性,主要是借鉴前人智慧
}
return hash;
}
};
//其实unordered_map就是在哈系桶的外面又封装了一层,通过调用哈系桶的接口来实现各种功能
template<class K,class V,class HashFunc = Hash<K>>
class UnorderedMap
{
//这里需要实现一个取出K的方法,因为K可能是pair
struct MapKorV
{
const K& operator()(const pair<K, V>& key)
{
return key.first;
}
};
public:
//给迭代器进行类型推导,并且重命名
typedef typename OpenHash::HashBucket<K, pair<K, V>, MapKorV,HashFunc>::iterator iterator;
pair<iterator, bool> Insert(const pair<K,V> &key)
{
return _HashBucket.Insert(key);
}
//根据operator[]的实现原理,进行实现
V& operator[](const K& key)
{
auto ret = _HashBucket.Insert( make_pair(key, V()) );
return ret.first->second;
}
//调用实现好的哈希桶来进行实现
iterator Find(const K& key)
{
return _HashBucket.Find(key);
}
bool Erase(const K& key)
{
return _HashBucket.Erase(key);
}
//迭代器也是调用哈希桶的迭代器
iterator begin()
{
return _HashBucket.begin();
}
iterator end()
{
return _HashBucket.end();
}
//const迭代器
iterator const_begin()
{
return _HashBucket.const_begin();
}
iterator const_end()
{
return _HashBucket.const_end();
}
private:
OpenHash::HashBucket<K, pair<K,V>,MapKorV, HashFunc> _HashBucket;
};
}
//测试代码
using namespace qyy;
//这里对接口进行简单的测试
void test_unordered_map1()
{
UnorderedMap<string, string> dict;
dict.Insert(make_pair("sort", "排序"));
dict["left"] = "左边";
dict["left"] = "剩余";
dict["map"] = "映射";
dict["string"] = "字符串";
dict["set"] = "集合";
dict.Erase("string");
auto e = dict.Find("string");
//cout << e->first << ":" << e->second << endl;
UnorderedMap<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
如果有写的不好的地方还请指正。