手搓哈希
- 哈希概念
- 哈希冲突
- 为什么会有哈希冲突的产生?
- 常见的哈希函数
- 如何解决哈希冲突?
- 闭散列
- 闭散列的简单模拟实现
- 插入
- 查找
- 删除
- 总代码
- 开散列
- 开散列的模拟实现
- 插入
- 删除
- 查找
- 存在的问题
- 迭代器
- 开散列总的代码
哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素;这种存储结构我们叫做哈希,所使用到的用于存储位置与关键码建立映射关系的函数,我们叫做哈希函数;
当向该存储结构中插入元素时:
根据元素的Key值,通过哈希函数映射出一个存储位置,然后将该元素插入到映射位置即可;
查找元素时:
同理,根据待查找的元素的Key值通过哈希函数计算出哈希地址,然后将该位置的值与待查找元素进行比较,相等则说明找到了;不相等,则说明没找到;
删除元素时:
根据要删除的元素的Key,通过Hash函数计算出哈希地址,然后将该位置的值与到查找元素进行比较,相等则删除;不相等则删除失败;
举个简单的哈希例子:
按照上面的方法,不管我们是插入还是删除操作,效率都很快,因为时间复杂度是O(1)啊!
可是这不免又引来一个新问题:
那就是,假设我在插入一个44,阁下又该如何应对?
我们可以发现,44经过哈希函数计算出来的哈希地址是4,也就是说我们要将44插入下标为4号的位置,可是4号位置已经有元素了,如果我们在强行插入的话,势必会造成数据的覆盖!我们把这种现象叫做哈希冲突!
我们可以发现,任何效率的提高都是有代价的!
我们该如何解决解决哈希冲突呢?
且看下文!
哈希冲突
根据上文我们看到的现象:
哈希冲突也就是两个不同的Key值经过相同的哈希函数映射的哈希地址是一样的!
我们把具有不同Key值但是通过同一个哈希函数映射具有相同哈希地址的元素称为"同义词"
为什么会有哈希冲突的产生?
引发哈希冲突的一个重要原因:哈希函数设计的不合理!
哈希函数的设计原则:
- 哈希函数的定义域要包括需要存储的所有元素的全部Key值,如果哈希表允许有m个地址时,其值域必须为[0,m-1]之间;
- 哈希函数计算出来的哈希地址分布比较均匀;
- 哈希函数应该比较简单;
常见的哈希函数
1、直接定址法
哈希地址域Key值成线性关系:
Hash(Key)=Key*A+B;
优点:
产生哈希冲突概率小,简单、哈希地址均匀;
缺点:
需要事先知道Key值的分布情况;
使用场景:
适合短小却连续的Key值出现的场景;
比如:在一个全由小写字母组成的字符数组中,统计每个字符出现的次数:
我们的哈希函数就可以设计成:Hash(Key)=Key-‘a’;
2、除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址,我们后文中模拟哈希时就是采用的这种哈希函数来计算哈希地址的;
3、平方取中法
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4、折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5、随机法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。
通常应用于关键字长度不等时采用此法。
6、数学分析法
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定
相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
列地址.
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况.
简而言之,只要哈希函数设计的越好,产生哈希冲突的可能性就越低,但 无法避免哈希冲突 ;
如何解决哈希冲突?
解决哈希冲突的两种比较常见的做法就是:闭散列和开散列。
闭散列
什么是闭散列?
闭散列: 也叫开放地址法,当产生哈希冲突时,如果哈希未被插满,说明哈希表中必然还有剩余位置,那么我们就可以从产生哈希冲突的哈希地址处开始向后搜寻着空余位置,然后将元素插入进去,简单来说就是将别人的位置“抢”过来自己用;
而这个探寻空余位置又有几种比较常见的方法:
- 线性探测
从产生哈希冲突的位置开始,一步一步一次向后探寻空余位置:
插入:
比如上图中的44,通过哈希函数所计算出来的哈希地址是4,但是4号位置已经有元素了,很明显的哈希冲突,于是我们就从5号位置开始依次探寻是否是空余位置,直至找到空余位置然后插入进去;删除: 怎么插入的就怎么删除,我们根据要删除的元素的Key值通过哈希函数计算出哈希地址,然后将待删除元素与其进行比较,相等则删除;不相等,则继续向后探测,因为有可能在插入的时候产生了哈希冲突,被放在了后面,如果找到了,则删除;如果都遇到空了,还没找到,那么说明待删除元素并没有被插入过,也就不存在该元素,删除失败;
查找:
根据Key值计算出哈希地址,然后从哈希地址处开始一个一个进行比较,如果在遇到空之前找到了,那么就说明找到了;如果在遇到空之前都没遇到相等的,那么说明查找失败;
可是我们发现上面的解决哈希冲突的方式,并不是很友好,为什么?
因为,我们解决哈希冲突的手段就是不断的去抢别人的位置,可是当正主来了的时候,发现自己的位置被别人占着,它有会去占别人的位置,如此恶性循环……我们把这种现象叫做踩踏!
这样就会导致哈希表的平均查找长度变长!效率变低!
我们每查找一个元素都需要探测几次,整体来说时间消耗就上去了!
果然,解决哈希冲突,必然会消耗一点时间!
为此,我们应该减少踩踏现象的发生!
我们可以采用二次探测的方式:
2. 二次探测
也就是从哈希冲突的位置开始,不在采用每一个位置都探测了,而是采用newHashi=Hashi+i*i的方式,来探测新的空间(Hashi:为发生哈希冲突的地方,i=1,2,3,4,5……);
这样的话一定程度上能够减少踩踏现象的发生,依次类推还是有三次探测、四次探测……
闭散列的简单模拟实现
在上面我们简单的介绍了一下闭散列的方法来解决哈希冲突,现在我们就用上面的理论来指导我们模拟实现出一个闭散列的哈希;
在此之前,我们先讨论一下哈希的设计:
删除: 上面的删除只是简单的讲解了如何找到待删除的元素,可是我们找到被删除的元素过后又该具体如何来删除呢?
比如:
删除方法:
①将被删除位置置为特殊值
比如,我现在要删除45,我们根据一些列操作成功找到45的存储位置过后,不是要删除嘛,我们就用一些特殊值来覆盖该位置的值,比如用0,这样固然是起到了删除的作用,可是我们万一我们就是插入的0呢?当我们遇到0的时候,这个0到底是表示插入的元素还是起着标记作用的呢?显然这是我们区分不了的,因此该方法被pass掉;
②将后面的元素挪动到被删除位置
比如还是删除45,我们就将45后面的元素都全部向前面挪:
如果我们真的这样做的话,效率极低不说,我们原本的元素本来就在自己合法的位置,可是经过我们这么一挪动,Key值与哈希地址的关系就全被破坏掉了,比如上面的98,当我们想要查找98时,根据哈希函数计算出来的哈希地址就是8,那么我们从该哈希地址处开始查找,可是都找到空了我们都没有找到98,这次查找就会失败,但是实际上98是存在于哈希表中的!因此这个方法也行不通!
③伪删除
也就是我们将每个位置都弄两个标识,EMPTY(空)、EXIST(存在);
我们在删除45的时候,我们既不将其置为特殊值也不挪动数据,而是将该位置标记为EMPTY,这样不就解决了前面两个问题所带来的麻烦:
可是新问题来了,在我们用这样的方法删除掉45过后,我们在去查找72时,我们会发现,我们再也找不到了,为什么?因为查找是遇到空就停止,从2号位置开始查找72,然后向后探测,当探测到5号位置的时候,发现了空,于是find就结束了,但是我们还是没有找到72,于是就被误判了;
那么如何解决这个问题呢?
通过上面的例子我们可以看到,两个状态是不够的,我们还需要增加一个状态:DELETE(删除)
当有元素被删除过后我们就将其置为DELETE状态,这样我们再查找72时就不会再被误伤了!
为此我们的哈希表中的节点就可以设计为:
代码方面就是:
#define DELETE -1//删除状态
#define EMPTY 0//空
#define EXIST 1//存在
//定义哈希表的节点
//T:表示存储的元素的类型
template<class T >
struct HashNode
{
T _data;
int _state;
//节点初始状态设置为EMPTY(空)
HashNode(const T& data = T(), const int& st = EMPTY) :_data(data), _state(st) {}
};
那么哈希表方面,我们也就封装vector来实现:
//定义哈希表
//K:用于find确定key的类型
//T:需要插入节点的类型,可能是KV模型,也可能是K模型,哈希表并不知道到底是那种类型
//为此我们需要单独传递Key的类型
template<typename K,typename T>
class HashTable
{
public:
private:
size_t _n = 0;//记录哈希表中有效元素的个数
std::vector<HashNode<T>> _ht;
};
}
插入
根据我们上面所讲的理论,我们立马就能手搓一个插入操作:
bool insert(const T& data)
{
//我们采用的哈希函数是除留余数法
//1、根据哈希函数确定哈希地址
size_t Hashi = data.first % _ht.size();
int i = 0;
int index = Hashi + i;
//探测节点存在元素,继续向后探测
while (_ht[index]._state == EXIST)
{
i++;
index = Hashi + i;
}
_ht[index]._data = data;
_ht[index]._state = EXIST;
_n++;
}
上面这么写合理吗?
显然是不合理的,刚初始化的时候我们的vector的大小是0,那么我们的哈希函数就有除0的风险,因此我们需要提前进行扩容,来避免这个风险;
同理,当我们vector里面的元素插满的时候,我们也需要进行扩容!
为此上面的代码我们可以进行如下的改进:
可是我们这样一来就改对了吗?
显然没有:
reverse扩的是capacity,而不是size:
当我们再进行元素的插入的时候,虽然我们扩容了,但是我们还是无法将元素,插入进去,因为我们的哈希函数是:Hash(Key)=Key%_ht.size();
模的是size不是capacity,size都没变,模出来的结果还是0~size-1,可是 0 ~ size-1位置都被插满了,自然插不进去,我们的程序还会陷入死循环;
为此我们的改进型方式是:
可是问题来了,我们如此改进,程序又没问题了吗?
显然不是还是存在一定的问题,既然我们的size都已经改变了,那么是不是我们的哈希函数也改变了?
可是映射的时候我们不能在原vector中进行映射,因为这会造成数据覆盖:
比如:
然后交换原数组与新数组!
为此我们改进的代码如下:
bool insert(const T& data)
{
//要插入的元素已经存在
if (find(data.first))
return false;
//提前扩容,减少哈希冲突,提高效率
//负载因子超过7,就扩容
if (_ht.size()==0||_n * 10 / _ht.size() == 7)
{
//这里是扩的size
size_t newsize = _ht.size() ? 2 * _ht.size() : 10;
//这里不是对原地扩容,而是重新创建一个容器进行扩,避免数据覆盖
HashTable<K, T> tmp;
tmp._ht.resize(newsize);
for (auto&x : _ht)
{
if (x._state==EXIST)
{
tmp._insert(x._data);
}
}
//交换两个vector
_ht.swap(tmp._ht);
}
//再将本次插入的结果插进去
_insert(data);
return true;
}
bool _insert(const T&data)
{
//根据Key值计算哈希地址
size_t Hashi = data.first % _ht.size();
size_t index = Hashi;
int i = 0;
while (_ht[index]._state == EXIST)
{
i++;
index = Hashi + i;
index %= _ht.size();
}
_ht[index]._state = EXIST;
_ht[index]._data = data;
_n++;
return true;
}
}
但是实际上,我们并不是再哈希表被插满时才进行的扩容,而是提前进行的:
这里负载因子我们取0.7,为此当负载因子大于等于0.7时我们就采取扩容,减少哈希冲突的产生,适当提高效率;
为此我们的代码再改进一下:
bool insert(const T& data)
{
if (find(data.first))
return false;
//提前扩容,减少哈希冲突,提高效率
//负载因子超过7,就扩容
//if (_ht.size()==0||(double)_n / (double)_ht.size() >= 0.7)//这种做法也行
if (_ht.size()==0||_n * 10 / _ht.size() >= 7)
{
//这里是扩的size
size_t newsize = _ht.size() ? 2 * _ht.size() : 10;
//这里不是对原地扩容,而是重新创建一个容器进行扩,避免数据覆盖
HashTable<K, T> tmp;
tmp._ht.resize(newsize);
for (auto&x : _ht)
{
if (x._state==EXIST)
{
tmp._insert(x._data);
}
}
//交换两个vector
_ht.swap(tmp._ht);
}
//再将本次插入的结果插进去
_insert(data);
return true;
}
bool _insert(const T&data)
{
//根据Key值计算哈希地址
size_t Hashi = data.first % _ht.size();
size_t index = Hashi;
int i = 0;
while (_ht[index]._state == EXIST)
{
i++;
index = Hashi + i;
index %= _ht.size();
}
_ht[index]._state = EXIST;
_ht[index]._data = data;
_n++;
return true;
}
查找
插入都解决了,查找不是手到擒来:
bool find(const K& key)
{
if(_n==0)
return false;
size_t Hashi = key % _ht.size();
size_t index = Hashi;
int i = 0;
while (_ht[index]._state != EMPTY)
{
if (_ht[index]._state == EXIST && _ht[index]._data.first == key)
break;
else
{
++i;
index = Hashi + i;
index %= _ht.size();
}
}
return true;
}
上面的代码存在一个小bug,要是我们要查找的元素不存在,并且哈希表的每个节点也不为空呢?
那么我们上面的程序就会陷入死循环:
为此针对这种特殊情况,我们最多只找一圈,如果最后回到冲突位置,在查找失败:
HashNode<T>*find(const K& key)
{
if(_n==0)
return nullptr;
size_t Hashi = key % _ht.size();
size_t index = Hashi;
int i = 0;
while (_ht[index]._state != EMPTY)
{
if (_ht[index]._state == EXIST && _ht[index]._data.first == key)
break;
else
{
++i;
index = Hashi + i;
index %= _ht.size();
}
//说明已经走了一圈都没有找到
if (index == Hashi)
return nullptr;
}
return &_ht[index];
}
删除
删除得有元素吧,先检查是否有有效元素;
再查找被删除元素:找到了_n–,状态置DELETE,删除成功;
没找到删除失败:返回false;
bool erase(const K& key)
{
//空表
if (_n == 0)
return false;
HashNode<T>* ret = find(key);
if (ret == nullptr)
return false;
else
{ //找到了
ret->_state = DELETE;
_n--;
return true;
}
}
总代码
#ifndef ___Hash___
#define ___Hash___
#include<iostream>
#include<vector>
//闭散列
namespace MySpace1
{
#define DELETE -1//删除状态
#define EMPTY 0//空
#define EXIST 1//存在
//定义哈希表的节点
template<class T >
struct HashNode
{
T _data;
int _state;
HashNode(const T& data = T(), const int& st = EMPTY) :_data(data), _state(st) {}
};
//哈希表的本质就是将Key值转换为一个哈希地址,然后根据哈希地址哈希表中进行存储
//定义哈希表
//K:用于find确定key的类型
//T:需要插入节点的类型
template<typename K,typename T>
class HashTable
{
public:
bool find(const K& key)
{
if(_n==0)
return false;
size_t Hashi = key % _ht.size();
size_t index = Hashi;
int i = 0;
while (_ht[index]._state != EMPTY)
{
if (_ht[index]._state == EXIST && _ht[index]._data.first == key)
break;
else
{
++i;
index = Hashi + i;
index %= _ht.size();
}
//说明已经走了一圈都没有找到
if (index == Hashi)
return false;
}
return true;
}
bool erase(const K& key)
{
//空表
if (_n == 0)
return false;
HashNode<T>* ret = find(key);
if (ret == nullptr)
return false;
else
{//找到了
ret->_state = DELETE;
_n--;
return true;
}
}
bool insert(const T& data)
{
if (find(data.first))
return false;
//提前扩容,减少哈希冲突,提高效率
//负载因子超过7,就扩容
if (_ht.size()==0||_n * 10 / _ht.size() >= 7)
{
//这里是扩的size
size_t newsize = _ht.size() ? 2 * _ht.size() : 10;
//这里不是对原地扩容,而是重新创建一个容器进行扩,避免数据覆盖
HashTable<K, T> tmp;
tmp._ht.resize(newsize);
for (auto&x : _ht)
{
if (x._state==EXIST)
{
tmp._insert(x._data);
}
}
//交换两个vector
_ht.swap(tmp._ht);
}
//再将本次插入的结果插进去
_insert(data);
return true;
}
void Test()
{
std::cout << typeid(_ht[0]).name() << std::endl;
}
private:
bool _insert(const T&data)
{
//根据Key值计算哈希地址
size_t Hashi = data.first % _ht.size();
size_t index = Hashi;
int i = 0;
while (_ht[index]._state == EXIST)
{
i++;
index = Hashi + i;
index %= _ht.size();
}
_ht[index]._state = EXIST;
_ht[index]._data = data;
_n++;
return true;
}
size_t _n = 0;//记录哈希表中有效元素的个数
std::vector<HashNode<T>> _ht;
};
}
#endif // !___Hash___
开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
举个例子:
从图中,我们可以看出来,每个桶中的元素都是发生哈希冲突的;
开散列的模拟实现
既然大概明白了上面的理论,我们就来设计一下开散列的数据结构;
首先,节点设计,得有一个数据域和一个指针域,也就是单链表的节点的样子:
代码方面就是:
//哈希节点
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data) :_data(data), _next(nullptr) {}
};
哈希表的设计自然也就是,元素类型为HashNode*的vector了:
//K:find所使用的Key的类型
//T:需要存储的数据的类型
template<class K,class T>
class HashTable
{
private:
vector<HashNode*> _ht;
size_t _n=0;//记录有效元素
}
插入
与闭散列的插入一样,我们需要先计算出哈希地址,这里的哈希函数我们还是采用除留余数法:
然后看一看此时的哈希地址出是否发生冲突,如果没有发生冲突我们则进行插入,如果发生冲突了,我们则链接在当前哈希地址处的尾结点后面,也就是单链表的尾插!可是我们真的需要进行尾差吗?
单链表的尾插效率是很低的,但是单链表的头插效率却是O(1),为此我们可以将我们的节点进行头插,这样的话发生哈希冲突的节点还是在同一个桶里面,只是先后顺序变了,但是这不影。
为此,代码方面我们可以这样写:
这样的代码有问题吗?
答案是有的,我们需要考虑最开始的时候vector都没有空间,size=0,无法进行插入,因此我们需要提前进行扩容,同时开散列是按照单链表的形式来存储数据,因此我们无需关心元素没有空间进行存储的问题,但是如果真的这样做的话,我们的查找效率就会变的低下,因为这会造成我们的每个哈希桶可能挂了很长的一串元素,这样的话,查找效率就会变的低下,为此为了减轻这种情况对于我们效率的影响,我们规定在负载因子等于1的时候对哈希表进行扩容,这样的话横向变长了,竖向就会变的短一点,为此,我们的改进代码就是:
为此,经过扩容过后,哈希函数发生了改变,我们需要将原来的元素进行重新映射:
为此我们的代码改进如下:
上面的代码仍是存在一点小bug,我们可以发现,insert并没有处理失败的情况,什么是失败的情况,也就是哈希表中已经存在了某一个元素,然后现在又重复插入;
为此最终的insert的代码就是:
bool Insert(const T& data)
{
//存在重复元素
if (find(data.first))
return false;
if (_ht.size() == 0 || _n == _ht.size())
{
size_t newSize = _ht.size() ? 2 * _ht.size() : 10;
_ht.resize(newSize);
//重新映射……
//扩容
size_t newsize = _ht.size() ? 2 * _ht.size() : 10;
HashTable<K, T, KeyOfT, Hash> tmp;
tmp._ht.resize(newsize, nullptr);
//将原来的数据连接到新表中
for (auto& x : _ht)
{
if (x)
{
Node* cur = x;
x = nullptr;
while (cur)
{
//1、记录一下下一个节点
Node* next = cur->_next;
//2、计算新的哈希地址
size_t newHashi = HashFunc(kot(cur->_data)) % tmp._ht.size();
//3、头插
cur->_next = tmp._ht[newHashi];
tmp._ht[newHashi] = cur;
cur = next;
tmp._n++;
}
}
}
_ht.swap(tmp._ht);
}
Node* cur = new Node(data);
size_t Hashi = HashFunc(kot(data)) % _ht.size();
//头插
cur->_next = _ht[Hashi];
_ht[Hashi] = cur;
_n++;
return true;
}
删除
就是根据Key值计算出哈希地址,然后挨个遍历呗:
bool erase(const K& key)
{
//空桶
if (_n == 0)
return false;
Node* prev = nullptr;
Node* cur = nullptr;
size_t Hashi = (key) % _ht.size();
cur = _ht[Hashi];
while (cur)
{
if ((cur->_data) == key)
{
Node* next = cur->_next;
if (prev == nullptr)//头删
_ht[Hashi] = next;
else
prev->_next = next;
delete cur;
_n--;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
查找
根据Key计算出哈希地址,然后遍历哈希桶:
HashNode<T>*find(const K& key)
{
if (_n == 0)
return nullptr;
size_t Hashi = (key) % _ht.size();
Node* cur = _ht[Hashi];
while (cur)
{
if ((cur->_data) == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
存在的问题
至此,一个简单的开散列哈希就已经封装好了,可是上面的代码还是存在一定的问题:
1、站在HashTable这一层来说,它并不知道T是什么类型,T的类型有可能是Key的类型,也有可能是KV模型的封装,对于这两种不同的情况,HashTable无法用一种统一的方法来获取Key值,因此我们需要提供给HashTable一个仿函数,让HashTable能够通过我们用户提供的仿函数来获取Key值:
2、我们上面讨论的Key值都是基于Key是一个整数,可是Key值不是一个整数呢?我们该如何办呢?
假设Key是一个string类型,那么哈希函数用除留余数法就不合适了,为此我们还需要提供一个仿函数用于将自定义类型转先换成整数,然后再根据转换出来的整数进行哈希函数的映射,比如字符串,我们可以用:每个字符的阿斯克吗值之和来表示字符串的整数,可是这样的话,不同的字符创对应的整数可能是相同的,比如:“abcd"和"dcba”、"cbad"等这三个不同的字符串对应的整数是相同的,也就意味着进行哈希函数映射时,产生的哈希地址是一样的,哈希地址一样也就是说哈希冲突会发生的比较频繁,哈希冲突发生的频繁,说明我们整体的哈希效率就不是特别高,为此我们需要设计一种能够将字符串转为整数,同时不同字符串产生整数冲突的概率也比较低的算法:
比如BKDR算法:
3、 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数;
迭代器
哈希表的迭代器是单向的没有反向迭代器,同时采用开散列实现的哈希表的迭代器,需要封装一个哈希表指针和一个标识当前节点的节点指针,为此迭代器的设计如下:
开散列总的代码
#pragma once
#include<iostream>
#include<vector>
//开散列设计哈希表
namespace MySpace
{
//整数
template<class T>
struct HashPlastic
{
//这里的返回值都设置为无符号,避免负数的取模运算还是得到负数,这样就会越界
const size_t operator()(const T& t)
{
return t;
}
};
//特化
template<>
struct HashPlastic<std::string>
{
//BKDR哈希函数
const size_t operator()(const std::string&str)
{
size_t seed = 131; // 31 131 1313 13131 131313 etc..
size_t hash = 0;
for (auto& x : str)
{
hash = hash * seed + x;
}
return (hash & 0x7FFFFFFF);
}
};
//哈希节点
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data) :_data(data), _next(nullptr) {}
};
//设计哈希表
//K:key类型
//T:K、V封装类型,也就是节点里面的数据类型
//KeyOfT:取出节点数据域中的Key值,因为我们无法保证T的类型就是pair类型,同时也无法保证T就是一个单一类型,如何取Key值交给使用者决定
//Hash:有些key值是无法直接转化为整数的,需要用户自己提供函数来帮助Key值转换为整数
template<class K,class T,class KeyOfT,class Hash=HashPlastic<K>>
class HashTable
{
template<class K, class T,class Sef,class Ptr, class KeyOfT, class Hash>
friend class HashTableItrerator;
typedef HashNode<T> Node;
public:
typedef HashTableItrerator<K,T,T&,T*,KeyOfT,Hash> iterator;
typedef HashTableItrerator<K,T,const T&,const T*,KeyOfT,Hash> const_iterator;
iterator begin()
{
//找到第一个有效通
Node* cur = nullptr;
for (auto& x : _ht)
{
if (x)
{
cur = x;
break;
}
}
return iterator(this,cur);
}
iterator end()
{
return iterator(this,nullptr);
}
const_iterator begin()const
{
//找到第一个有效通
Node* cur = nullptr;
for (auto& x : _ht)
{
if (x)
{
cur = x;
break;
}
}
return const_iterator(this, cur);
}
const_iterator end()const
{
return const_iterator(this, nullptr);
}
~HashTable()
{
for (auto& x : _ht)
{
if (x)
{
Node* cur = x;
while (cur)
{
Node* del = cur;
cur = cur->_next;
delete del;
}
}
}
}
iterator find(const K& key)
{
KeyOfT kot;
Hash HashFunc;
if (_n == 0)
return iterator(nullptr,nullptr);
size_t Hashi = HashFunc(key) % _ht.size();
Node* cur = _ht[Hashi];
while (cur)
{
if (kot(cur->_data) == key)
return iterator(nullptr, cur);
cur = cur->_next;
}
return iterator(nullptr, nullptr);
}
bool erase(const K& key)
{
KeyOfT kot;
Hash HashFunc;
//空桶
if (_n == 0)
return false;
Node* prev = nullptr;
Node* cur = nullptr;
size_t Hashi = HashFunc(key) % _ht.size();
cur = _ht[Hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
Node* next = cur->_next;
if (prev == nullptr)//头删
_ht[Hashi] = next;
else
prev->_next = next;
delete cur;
_n--;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
std::pair<iterator,bool> insert(const T& data)
{
KeyOfT kot;
Hash HashFunc;
iterator pos;
if ((pos=find(kot(data)))._cur)
return std::make_pair(iterator(pos),false);
//如果表为空或负载因子达到默认大小
if (_ht.size() == 0 || _n / _ht.size() ==1)
{
//扩容
size_t newsize = GetNextPrime(_ht.size());
HashTable<K, T,KeyOfT,Hash> tmp;
tmp._ht.resize(newsize,nullptr);
//将原来的数据连接到新表中
for (auto& x : _ht)
{
if (x)
{
Node* cur = x;
x = nullptr;
while (cur)
{
//1、记录一下下一个节点
Node* next = cur->_next;
//2、计算新的哈希地址
size_t newHashi = HashFunc(kot(cur->_data)) % tmp._ht.size();
//3、头插
cur->_next = tmp._ht[newHashi];
tmp._ht[newHashi] = cur;
cur = next;
tmp._n++;
}
}
}
_ht.swap(tmp._ht);
}
Node* cur = new Node(data);
size_t Hashi = HashFunc(kot(data)) % _ht.size();
//头插
cur->_next = _ht[Hashi];
_ht[Hashi] = cur;
_n++;
return std::make_pair(iterator(nullptr,cur),true);
}
size_t GetBucketDepth()
{
//KeyOfT kot;
size_t max= 0;
size_t count = 0;
for (auto& x : _ht)
{
if (x)
{
count = 0;
Node* cur = x;
while (cur)
{
count++;
//printf("%d ->",kot(cur->_data));
cur = cur->_next;
}
// std::cout << std::endl;
if (count > max)
max = count;
}
}
return max;
}
private:
size_t GetNextPrime(size_t prime)
{
static const size_t primeList[] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul,
25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul,
805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
size_t tmp = 0;
for(auto &x:primeList)
if (x > prime)
{
tmp = x;
break;
}
return tmp;
}
std::vector<HashNode<T>*> _ht;
size_t _n = 0;//记录有效元素个数
};
//设计哈希表的迭代器
template<class K,class T,class Sef,class Ptr,class KeyOfT,class Hash>
class HashTableItrerator
{
template<class K, class T, class KeyOfT, class Hash >
friend class HashTable;
typedef HashNode<T> Node;
typedef HashTable<K, T, KeyOfT, Hash> HT;
typedef HashTableItrerator < K, T,Sef,Ptr ,KeyOfT,Hash> Self;
typedef HashTableItrerator<K, T, T&, T*, KeyOfT, Hash> iterator;//普通迭代器
typedef HashTableItrerator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;//const迭代器
//声明一下cons迭代器是普通迭代器其器的友元
friend class const_iterator;
public:
HashTableItrerator(const iterator& it)
:_pht(it._pht), _cur(it._cur) {}
HashTableItrerator(const HT*pht=nullptr,Node* cur=nullptr)
:_pht(pht), _cur(cur) {}
Sef operator*()
{
return _cur->_data;
}
Ptr operator->()
{
return &(_cur->_data);
}
Self& operator++()
{
Node* next = _cur->_next;
if (next)
{
_cur = next;
}
else
{
KeyOfT kot;
//求出key值
K key = kot(_cur->_data);
//求一求桶号
Hash HashFunc;
size_t Hashi = HashFunc(key) % _pht->_ht.size();
size_t newHashi = Hashi + 1;
for (; newHashi < _pht->_ht.size(); newHashi++)
{
if (_pht->_ht[newHashi])
break;
}
_cur = ((newHashi >= _pht->_ht.size()) ? nullptr : _pht->_ht[newHashi]);
}
return *this;
}
bool operator!=(const Self&it)const
{
return _cur != it._cur;
}
private:
//哈希表指针
const HashTable<K, T, KeyOfT, Hash>* _pht;
//当前节点
HashNode<T>* _cur;
};
}