前言
我们以前或多或少的都听说过哈希表,到底什么是哈希?我们没有具体的介绍过,本期我们就来介绍一下赫赫有名的哈希表以及哈希思想!
本期内容介绍
• 哈希的概念
• 哈希冲突
• 哈希函数
• 解决哈希冲突并实现哈希表
unordered系列的关联式容器之所以效率比较高,是因为他们的底层使用了哈希结构!什么是哈希?我们下面来一一揭晓!
• 哈希的概念
在顺序结构以及平衡树结构中,元素的关键码(key)与其存储位置之间没有对应的关系,因此在查找时,必须得遍历与每个元素的关键码(key)比较,其时间复杂度为O(N),平衡二叉搜索树的时间复杂度是其树的高度次即O(logN);这些其实都不是最理想的方式!理想的方式是:不经过任何比较,一次直接从表中得到想要的搜索元素。
如果构造出来一种存储结构,通过某种函数(hashFunc)使存储元素的位置与关键码(key)之间能够建立一一的映射关系,那么在搜索时通过该函数就可以很快的找到!像这种存储位置与关键码之间建立一一的映射关系的思想就是哈希(散列)!根据哈希思想设计出的这种数据结构叫做哈希表(散列表)!根据存储元素的关键码计算与该元素一一对应的存储位置的函数叫做哈希函数!
OK,举个例子:
这就是哈希,搜索时不需要遍历查找!只需要通过哈希函数就可以得到当前key对应元素的存储位置,因此很快!例如此时需要查询6,将6给hash函数哈希函数会处理的得到key为6对应的存储位置就是6,所以就直接得到了!但是此时我们发现,如果向上述的哈希表中存入44时该位置已经被占了!这其实就是我们下面要介绍的哈希冲突;
• 哈希冲突
对于两个数据的关键码(key)i和j,又i != j,但是hash(i) == hash(j),即:不同的关键字通过哈希函数计算出了相同的哈希地址,这种现象就被称为哈希冲突或哈希碰撞!把具有不同关键码而具有相同哈希地址的数据元素成为 "同义词"!
发生了哈希冲突该如何解决呢?
• 哈希函数
引起哈希冲突的一个原因是:哈希函数设计的不够合理!
下面我们就来介绍一下常见的哈希函数:
• 直接定值法(常用)
取关键码的某个线性函数为散列的地址:Hash (key) = A*key + B;
优点:简单、均匀
缺点:需要事先知道关键码的分布情况
使用场景:适合查找比较小且连续的情况
• 除留余数法(常用)
设散列表中允许的地址数为m,取一个不大于m,但是最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = Key % p (p <= m),将关键码转换成为地址,上面我们一开始演示的就是除留余数法设计的哈希函数!
• 平方取中法(了解)
假设关键字为1234,其平方就是1522756,抽取中间的三位就是227作为哈希地址;
再比如关键字为4321,其平方就是18671041,抽取中间的3位671(或710)作为哈希地址;
平方取中法比较适合,不知道关键字的分布,而位数又不是很大的情况!
• 折叠法(了解)
折叠发是将大部分关键字从左往右分割成相等的几部分(最后的一部分肯恩那个有些短)然后将这几部分叠加求和,并按散列表的表长,取后即位作为散列地址!
折叠法适合,事前不需要知道关键字的分布,适合关键字位数较多的情况
• 随机数法(了解)
选择一个随机函数,取关键字随机函数的值作为哈希地址,即Hash(key) = random(key);其中random为随机函数!
通常关键字的成都不等时选择此法!
• 数学分析法(了解)
• 注意:哈希函数设计的越精妙,产生哈希冲突的可能性越低,但是哈希冲突无法避免!
我们后面按照除留余数法实现!
• 解决哈希冲突并实现哈希表
解决哈希冲突的两种常见的方式是:闭散列和开散列!下面我们就分别来用这两种方式解决哈希冲突并实现哈希表!
• 闭散列
闭散列:也叫开放定值法,当发生冲突时,如果哈希表未被装满,说明在哈希表中必然有空位置,那么可以把key存放到冲突位置的"下一个"位置中去!本质就是抢占!例如你的车位被占了,你就得再找一个空位置占个空位置!如何寻找下一个空的位置呢?
• 线性探测法
当发生冲突时,从冲突的位置开始,依次向后探测,知道寻找到下一个空的位置为止!
例如我们一开始的那个例子:
本来应该利用哈希函数得到的地址是4但是4位置已经被4占了,所以就得向后线性探测找下一个空的位置!找到就是8的位置,放进去即可!
OK ,我们基于线性探测实现以下哈希表吧:
我们得有一个状态的枚举以及数据的类和一个哈希表的类,我们还是先搭一个框架出来:
enum Status
{
EMPTY,
EXIST,
DELECT
};
template<class K,class V>
struct HashData
{
pair<K, V> _kv;
Status _status = EMPTY;//这里可以只给一个缺省值就不用写构造了
};
template<class K,class V>
class HashTable
{
typedef HashData<K, V> HashData;
public:
private:
vector<HashData> _tables;
size_t _size = 0;//记录哈希表中的元素个数
};
• 查找
这里查找返回指针的原因是方便后面的插入和删除的复用!
思路:根据key通过哈希函数获取哈希地址,然后从该哈希地址开始找"空"位置插入!防止越界以及后面找了前面没找的情况,所以每次++完后,对表长取一次模!
HashData* Find(const K& key)
{
//表中没有数据
if (_size == 0)
return nullptr;
//获取key对应的哈希地址
size_t hashi = key % _tables.size();
size_t start = hashi;//标记一开始查找的位置
//找到空就停止
while (_tables[hashi]._status != EMPTY)
{
//当前的状态为存在,且当前位置的kv.first 和 key相等, 说明找到了
if (_tables[hashi]._status == EXIST && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
++hashi;
hashi = hashi % _tables.size();//后前找完了到前面找一找
//找了一圈没找到
if (hashi == start)
break;
}
return nullptr;
}
• 插入
插入前可以检查一下插入的元素是否已经存在, 然后再检查是否扩容, 正式的插入!
扩容
这里扩容的情况有两种(这里没写构造):如果一开始_size == 0 或者哈希表的负载因子超过了0.7!(负载因子 = 表中元素的个数 / 表的长度);一般的扩容都是2倍扩容,这里也是!扩容的实现也有两种即传统写法和现代写法!
这里两个整数相除有可能是得不到小数的,所以我们可以将_size扩大十倍,然后当大于等于7时就扩容!
传统写法
思路:开一个新的哈希表,大小是原来的2倍,然后遍历旧表,如果旧表中的状态是存在,将旧表中的数据重新映射到新表即可!最后交换两个表!
//扩容
if (_size == _tables.size() || _size * 10 / _tables.size() >= 7)
{
//传统写法
size_t newSize = _size == 0 ? 4 : _tables.size() * 2;
vector<HashData> newTable(newSize);
for (auto& e : _tables)
{
if (e._status == EXIST)
{
size_t hashi = e._kv.first % newTable.size();
//寻找插入的位置
while (newTable[hashi]._status == EXIST)
{
hashi++;
hashi = hashi %_tables.size();
}
//找到了插入的位置,插入
newTable[hashi]._kv = e._kv;
newTable[hashi]._status = EXIST;
}
}
//将新表和旧表交换
_tables.swap(newTable);
}
现代写法
思路:创建一个哈希表对象,新的哈希表对象的表长设置为原来旧表的2倍,然后遍历旧表,如果旧表中当前位置的状态是存在,新的哈希白表对象直接调用Insert即可!最后将哈希表对象的表和原来的表一交换即可!这其实是一种复用~!
//扩容
if (_size == _tables.size() || _size * 10 / _tables.size() >= 7)
{
//现代写法
size_t newSize = _size == 0 ? 4 : _tables.size() * 2;
HashTable<K, V> newTable;
newTable._tables.resize(newSize);
//遍历旧表将数据重新映射到新表
for (auto& e : _tables)
{
if (e._status == EXIST)
{
newTable.Insert(e._kv);
}
}
//将新表和旧表交换
_tables.swap(newTable._tables);
}
完整的插入代码
bool Insert(const pair<K, V>& kv)
{
//要插入的元素已经存在
if (Find(kv.first))
return false;
//扩容
if (_size == _tables.size() || _size * 10 / _tables.size() >= 7)
{
//现代写法
size_t newSize = _size == 0 ? 4 : _tables.size() * 2;
HashTable<K, V> newTable;
newTable._tables.resize(newSize);
//遍历旧表将数据重新映射到新表
for (auto& e : _tables)
{
if (e._status == EXIST)
{
newTable.Insert(e._kv);
}
}
//将新表和旧表交换
_tables.swap(newTable._tables);
传统写法
//size_t newSize = _size == 0 ? 4 :_tables.size() * 2;
//vector<HashData> newTable(newSize);
//for (auto& e : _tables)
//{
// if (e._status == EXIST)
// {
// size_t hashi = e._kv.first % newTable.size();
// //寻找插入的位置
// while (newTable[hashi]._status == EXIST)
// {
// hashi++;
// hashi = hashi % _tables.size();
// }
// //找到了插入的位置,插入
// newTable[hashi]._kv = e._kv;
// newTable[hashi]._status = EXIST;
// }
//}
将新表和旧表交换
//_tables.swap(newTable);
}
//通过哈希函数获取哈希地址
size_t hashi = kv.first % _tables.size();
//寻找插入的位置
while (_tables[hashi]._status == EXIST)
{
hashi++;
hashi = hashi % _tables.size();
}
//找到了插入的位置,插入
_tables[hashi]._kv = kv;
_tables[hashi]._status = EXIST;
_size++;//有效元素个数++
return true;
}
• 删除
思路:先用Find查找要删除的元素,如果不存在直接返回;如果存在将该位置的状态设置为删除,有效的元素个数--
这可能是我们写数据结构以来最简单的删除了~
bool Erase(const K& key)
{
HashData* ret = Find(key);
//没找到
if (ret == nullptr)
return false;
ret->_status = DELECT;//将状态设置为删除
--_size;//有效元素个数--
return true;
}
• Size
直接返回_size即可
size_t Size() const
{
return _size;
}
• Empty
直接返回_size == 0即可
bool Empty() const
{
return _size == 0;
}
• Clear
遍历哈希表将每个位置的状态设置为EMPTY,然后将_size置0
void Clear()
{
for (auto& e : _tables)
{
e._status = EMPTY;
}
_size = 0;
}
OK,测试一下:
void Test_Open_add1()
{
open_address::HashTable<int, int> hash;
hash.Insert({ 1,1 });
hash.Insert({ 2,2 });
hash.Insert({ 3,3 });
hash.Insert({ 4,4 });
hash.Insert({ 5,5 });
hash.Print();
cout << "size : " << hash.Size() << endl;
bool ret = hash.Find(1);
if (ret) cout << ret << endl;
cout << "----------------------------------------" << endl;
hash.Erase(1);
ret = hash.Find(1);
if (ret) cout << ret << endl;
else cout << "没找到" << endl;
cout << "size : " << hash.Size() << endl;
hash.Print();
cout << "----------------------------------------" << endl;
hash.Clear();
cout << "size : " << hash.Size() << endl;
hash.Print();
}
OK,这来的Print是我为了可视化,自己写的一个类成员函数;
线性探测的优点:实现简单
线性探测的缺点:一但发生冲突,所有的冲突都连在一块,容易出现堆积的现象!即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低;
• 二次探测法
线性探测法是当自己的位置被占了后,去逐个位置找空位置插入,一旦冲突堆积,查找的次数就会大大增加,为了避免这种冲突堆积的情况:我们可以采用二次探测法:下一个空的位置是当前位置+i^2作为哈希地址!即:hash(i) = (adds + i^2 ) % m; adds是原来冲突的哈希地址,m是哈希表的长度!i的取值范围值i >= 1
//通过哈希函数获取哈希地址
size_t hashi = kv.first % _tables.size();
int i = 1;
//寻找插入的位置
while (_tables[hashi]._status == EXIST)
{
hashi += (i * i);//二次探测
i++;
hashi = hashi % _tables.size();
}
//找到了插入的位置,插入
_tables[hashi]._kv = kv;
_tables[hashi]._status = EXIST;
_size++;//有效元素个数++
研究表明: 当表的长度为质数且表装载因子 a 不超过 0.5 时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子 a 不超过 0.5 , 如果超出必须考虑增容。
所以,二次探测的最大的缺点就是空间利用率较低,当让这也是哈希的缺陷,所以我们还是采用线性探测!
OK,目前我们的哈希表还存在一些问题就是执行存储正整数,负数、浮点数、字符串就不行了!如何解决呢?其实我们可以增加一个获取key值对应正整数的类,实现对应的仿函数在获取哈希地址时调用方函数即可!
• 解决不能存储浮点数和负数
将浮点数或者负数全部强转为size_t即可
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
void Test_Open_add2()
{
open_address::HashTable<int, int> hash;
hash.Insert({ -1,1 });
hash.Insert({ -2,2 });
hash.Insert({ -3,3 });
hash.Insert({ -4,4 });
hash.Insert({ -5,5 });
hash.Print();
}
• 解决不能存储字符串
其实这是一种算法叫做字符串哈希:字符串哈希算法
既然BKDR一骑绝尘,那我们就选择它实现:
template<>//模板的特化
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t sum = 0;
for (auto& c : key)
{
sum *= 131;//乘以131是为了更好的避免哈希冲突
sum += c;
}
return sum;
}
};
void Test_Open_add3()
{
open_address::HashTable<string, int> hash;
hash.Insert({ "aaa", 1 });
hash.Insert({ "bbb", 2 });
hash.Insert({ "ccc", 3 });
hash.Insert({ "ddd", 4 });
hash.Insert({ "cpdd", 5 });
hash.Insert({ "fgsd", 66 });
hash.Print();
}
全部源码
#pragma once
#include <vector>
namespace open_address
{
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>//模板的特化
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t sum = 0;
for (auto& c : key)
{
sum *= 131;
sum += c;
}
return sum;
}
};
enum Status
{
EMPTY,
EXIST,
DELETE
};
template<class K,class V>
struct HashData
{
pair<K, V> _kv;
Status _status = EMPTY;//这里可以只给一个缺省值就不用写构造了
};
template<class K,class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashData<K, V> HashData;
public:
HashData* Find(const K& key)
{
//表中没有数据
if (_size == 0)
return nullptr;
//获取key对应的哈希地址
Hash hs;//获取key对应整数的对象
size_t hashi = hs(key) % _tables.size();
size_t start = hashi;//标记一开始查找的位置
//找到空就停止
while (_tables[hashi]._status != EMPTY)
{
//当前的状态为存在,且当前位置的kv.first 和 key相等, 说明找到了
if (_tables[hashi]._status == EXIST && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
++hashi;
hashi = hashi % _tables.size();//后前找完了到前面找一找
//找了一圈没找到
if (hashi == start)
break;
}
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
//要插入的元素已经存在
if (Find(kv.first))
return false;
Hash hs;//获取key对应整数的对象
//扩容
if (_size == _tables.size() || _size * 10 / _tables.size() >= 7)
{
//现代写法
size_t newSize = _size == 0 ? 4 : _tables.size() * 2;
HashTable<K, V, Hash> newTable;
newTable._tables.resize(newSize);
//遍历旧表将数据重新映射到新表
for (auto& e : _tables)
{
if (e._status == EXIST)
{
newTable.Insert(e._kv);
}
}
//将新表和旧表交换
_tables.swap(newTable._tables);
传统写法
//size_t newSize = _size == 0 ? 4 :_tables.size() * 2;
//vector<HashData> newTable(newSize);
//for (auto& e : _tables)
//{
// if (e._status == EXIST)
// {
// size_t hashi = hs(e._kv.first) % newTable.size();
// //寻找插入的位置
// while (newTable[hashi]._status == EXIST)
// {
// hashi++;
// hashi = hashi % _tables.size();
// }
// //找到了插入的位置,插入
// newTable[hashi]._kv = e._kv;
// newTable[hashi]._status = EXIST;
// }
//}
将新表和旧表交换
//_tables.swap(newTable);
}
//通过哈希函数获取哈希地址
size_t hashi = hs(kv.first) % _tables.size();
//寻找插入的位置
while (_tables[hashi]._status == EXIST)
{
hashi++;
hashi = hashi % _tables.size();
}
通过哈希函数获取哈希地址
//size_t hashi = hs(kv.first) % _tables.size();
//int i = 1;
寻找插入的位置
//while (_tables[hashi]._status == EXIST)
//{
// hashi += (i * i);//二次探测
// i++;
// hashi = hashi % _tables.size();
//}
//找到了插入的位置,插入
_tables[hashi]._kv = kv;
_tables[hashi]._status = EXIST;
_size++;//有效元素个数++
return true;
}
bool Erase(const K& key)
{
HashData* ret = Find(key);
//没找到
if (ret == nullptr)
return false;
ret->_status = DELETE;//将状态设置为删除
--_size;//有效元素个数--
return true;
}
size_t Size() const
{
return _size;
}
bool Empty() const
{
return _size == 0;
}
void Clear()
{
for (auto& e : _tables)
{
e._status = EMPTY;
}
_size = 0;
}
void Print()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i]._status == EXIST)
{
cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
}
else if (_tables[i]._status == EMPTY)
{
printf("[%d]->\n", i);
}
else
{
printf("[%d]->DELETE\n", i);
}
}
cout << endl;
}
private:
vector<HashData> _tables;
size_t _size = 0;//记录哈希表中的元素个数
};
}
• 开散列
开散列法又称拉链法,首先对关键码集合用哈希函数计算出哈希地址,具有相同哈希地址的关键码看做一个集合,该集合称为桶,每个集合(桶)通过单链表连接起来,各个链表的头结点存储在哈希表中!
什么意思呢?画个图解释一下(我这里是没有到头的单链表,且用的是头插):
从图中可以看出,每个同种放的是发生哈希冲突的元素
OK,下面我们来实现一下吧!
由于这里冲突的元素都挂在了一个桶里面(单链表)所以我们不再需要状态标记了,此时我们的哈希数据类中只需要一个pair和一个节点类的后继指针!
OK,先搭一个框架出来:
template<class K, class V>
struct HashNode
{
pair<K, V>& _kv;//存储的元素
HashNode<K, V>* _next;//后继指针
//构造函数初始化
HashNode(const pair<K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K, class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
//开放地址法,没有写构造,扩容有两种情况,这里写一个构造,扩容就只是负载因子引起的
HashTable()
:_tables(4, nullptr)//一开始的大小给为4
,_size(0)
{}
private:
vector<Node*> _tables;//哈希表
size_t _size;
};
• 析构函数
由于我们是在哈希表中挂的链表,所以我们得手动的写析构清理链表!
思路:直接调Clear,将每个桶中的单链表给释放掉,然后vector毁掉自己的析构回收空间;
//这里必须得写析构函数,否则会出现内存泄漏
~HashTable()
{
Clear();
}
• Clear
思路:遍历哈希表,如果该位置的桶中有数据,将该桶中的链表销毁即可,最后将有效元素的个数置0
void Clear()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
Node* cur = _tables[i];//当前节点的下一个位置
while (cur)
{
Node* next = cur->_next;//保存下一个节点的指针
delete cur;
cur = next;
}
_tables[i] = nullptr;//将每个桶的链表清理完了后置为空
}
}
_size = 0;//将有效元素的个数置0
}
• Find
思路:根据key计算出哈希地址,然后从该位置的桶中遍历查找!
这里返回值设置成节点的指针也是为了方便查找复用!
Node* Find(const K& key)
{
//表中没有元素
if (_size == 0)
return nullptr;
size_t hashi = key % _tables.size();//获取哈希地址
Node* cur = _tables[hashi];//获取头结点的地址
while (cur) //遍历桶
{
if (cur->_kv.first == key)
return cur;//找到了
cur = cur->_next;
}
return nullptr;//没找到
}
• Insert
思路:先根据插入元素的key查找该元素是否存在,如果已经存在则不再插入!不存在就判断是否扩容,然后将数据插入!
扩容的写法也是有两种:现代写法和传统写法!这里的扩容是当负载因子等于1也就是_size == 表长!原因是:我们并没有将冲突的数据放到哈希表中,而是挂在了桶里!
现代写法
思路:创建一个哈希表对象,然后将哈希表对象的表设置为原来的2倍,然后遍历旧表,将旧表中的每个节点都相当于拷贝了一遍,然后将新表和旧表交换;
这种做法其实不是很好的,有些麻烦,而且效率不高!
//扩容 -》现代写法(效率不好,相当于将旧表中的数据节点拷贝到新表,然后再将旧表释放)
if (_size == _tables.size())
{
size_t newSize = _tables.size() * 2;
HashTable<K, V> newTable;
newTable._tables.resize(newSize);
//遍历旧表
for (auto& cur : _tables)
{
while (cur)
{
newTable.Insert(cur->_kv);
cur = cur->_next;
}
}
_tables.swap(newTable._tables);
}
传统写法
思路:创建一个新的哈希表,大小是是原来旧表的2倍,遍历旧表将旧表中的节点直接挂到新表中的映射位置!最后交换新表和旧表!
这种写法效率较高,且没有拷贝!
//扩容 -> 传统写法(直接将旧表中的节点拿下来,映射到新表)
if (_size == _tables.size())
{
size_t newSize = _tables.size() * 2;
//创建一个新的哈希表,大小是旧表的2倍
vector<Node*> newTable(newSize, nullptr);
//遍历旧表
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = cur->_kv.first % newTable.size();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
cur = nullptr;
}
_tables.swap(newTable);
}
插入的完成代码
bool Insert(const pair<K, V>& kv)
{
//要插入的元素在哈希表中已经存在
if (Find(kv.first))
return false;
扩容 -》现代写法(效率不好,相当于将旧表中的数据节点拷贝到新表,然后再将旧表释放)
//if (_size == _tables.size())
//{
// size_t newSize = _tables.size() * 2;
// HashTable<K, V> newTable;
// newTable._tables.resize(newSize);
// //遍历旧表
// for (auto& cur : _tables)
// {
// while (cur)
// {
// newTable.Insert(cur->_kv);
// cur = cur->_next;
// }
// }
// _tables.swap(newTable._tables);
//}
//扩容 -> 传统写法(直接将旧表中的节点拿下来,映射到新表)
if (_size == _tables.size())
{
size_t newSize = _tables.size() * 2;
//创建一个新的哈希表,大小是旧表的2倍
vector<Node*> newTable(newSize, nullptr);
//遍历旧表
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = cur->_kv.first % newTable.size();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
cur = nullptr;
}
_tables.swap(newTable);
}
//插入
size_t hashi = kv.first % _tables.size();
Node* newnode = new Node(kv);
//头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
_size++;
return true;
}
• Erase
思路:根据key通过哈希函数找到哈希地址,然后在该哈希地址的桶中,进行删除!这里的删除本质上是单链表的删除,所以的记录前驱和后继!
bool Erase(const K& key)
{
size_t hashi = key % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
Node* next = cur->_next;
//找到关键码和key一样的桶了
if (cur->_kv.first == key)
{
if (prev == nullptr)//头删
_tables[hashi] = next;
else
prev->_next = next;//正常删除
delete cur;
cur = nullptr;
--_size;
return true;
}
else//没找到继续找
{
prev = cur;
cur = next;
}
}
return false;//没找到删除的
}
• Size
思路:直接返回成员_size的值
size_t Size() const
{
return _size;
}
• Empty
思路:判断_size是否等于0
bool Empty() const
{
return _size == 0;
}
• 解决不能存储负数和浮点数
这里还是和上面的闭散列的处理方式一样的,单独提供一个将key值转换为无符号整数的类,内部提供仿函数,将那个内传给哈希表类的模板,在哈希表内部创建对象调用其仿函数即可获得!
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
• 解决不能存储字符串
这里依旧采用BKDR算法解决字符串哈希的问题!
template<>//模板的特化
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t sum = 0;
for (auto& c : key)
{
sum *= 131;
sum += c;
}
return sum;
}
};
全部源码
#pragma once
#include <vector>
namespace hash_bucket
{
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>//模板的特化
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t sum = 0;
for (auto& c : key)
{
sum *= 131;
sum += c;
}
return sum;
}
};
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;//存储的元素
HashNode<K, V>* _next;//后继指针
//构造函数初始化
HashNode(const pair<K,V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
//开放地址法,没有写构造,扩容有两种情况,这里写一个构造,扩容就只是负载因子引起的
HashTable()
:_tables(4, nullptr)//一开始的大小给为4
,_size(0)
{}
//这里必须得写析构函数,否则会出现内存泄漏
~HashTable()
{
Clear();
}
Node* Find(const K& key)
{
//表中没有元素
if (_size == 0)
return nullptr;
Hash hs;
size_t hashi = hs(key) % _tables.size();//获取哈希地址
Node* cur = _tables[hashi];//获取头结点的地址
while (cur) //遍历桶
{
if (cur->_kv.first == key)
return cur;//找到了
cur = cur->_next;
}
return nullptr;//没找到
}
bool Insert(const pair<K, V>& kv)
{
//要插入的元素在哈希表中已经存在
if (Find(kv.first))
return false;
Hash hs;
扩容 -》现代写法(效率不好,相当于将旧表中的数据节点拷贝到新表,然后再将旧表释放)
//if (_size == _tables.size())
//{
// size_t newSize = _tables.size() * 2;
// HashTable<K, V> newTable;
// newTable._tables.resize(newSize);
// //遍历旧表
// for (auto& cur : _tables)
// {
// while (cur)
// {
// newTable.Insert(cur->_kv);
// cur = cur->_next;
// }
// }
// _tables.swap(newTable._tables);
//}
//扩容 -> 传统写法(直接将旧表中的节点拿下来,映射到新表)
if (_size == _tables.size())
{
size_t newSize = _tables.size() * 2;
//创建一个新的哈希表,大小是旧表的2倍
vector<Node*> newTable(newSize, nullptr);
//遍历旧表
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = hs(cur->_kv.first) % newTable.size();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
cur = nullptr;
}
_tables.swap(newTable);
}
//插入
size_t hashi = hs(kv.first) % _tables.size();
Node* newnode = new Node(kv);
//头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
_size++;
return true;
}
bool Erase(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
Node* next = cur->_next;
//找到关键码和key一样的桶了
if (cur->_kv.first == key)
{
if (prev == nullptr)//头删
_tables[hashi] = next;
else
prev->_next = next;//正常删除
delete cur;
cur = nullptr;
--_size;
return true;
}
else//没找到继续找
{
prev = cur;
cur = next;
}
}
return false;//没找到删除的
}
size_t Size() const
{
return _size;
}
bool Empty() const
{
return _size == 0;
}
void Clear()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
Node* cur = _tables[i];//当前节点的下一个位置
while (cur)
{
Node* next = cur->_next;//保存下一个节点的指针
delete cur;
cur = next;
}
_tables[i] = nullptr;//将每个桶的链表清理完了后置为空
}
}
_size = 0;//将有效元素的个数置0
}
void Print()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
printf("[%d]", i);
while (cur)
{
cout << "->" << "{" << cur->_kv.first << "," << cur->_kv.second << "}";
cur = cur->_next;
}
cout << endl;
}
else
{
printf("[%d]->\n", i);
}
}
cout << endl;
}
private:
vector<Node*> _tables;//哈希表
size_t _size;
};
}
正常测试
void Test_Hash_Bucket1()
{
hash_bucket::HashTable<int, int> hash;
hash.Insert({ 1,1 });
hash.Insert({ 11,2 });
hash.Insert({ 21,3 });
hash.Insert({ 31,4 });
hash.Insert({ 5,5 });
hash.Print();
cout << "------------------" << endl;
auto ret = hash.Find(1);
if (ret) cout << ret->_kv.first << endl;
else cout << "不存在" << endl;
hash.Erase(1);
hash.Erase(11);
hash.Erase(21);
hash.Erase(31);
hash.Erase(5);
hash.Print();
}
负数/浮点数测试
void Test_Hash_Bucket3()
{
hash_bucket::HashTable<double, int> hash;
hash.Insert({ 1.1, 1 });
hash.Insert({ 2.9, 1 });
hash.Insert({ 12.1, 1 });
hash.Insert({ 9.9, 1 });
hash.Print();
hash.Clear();
hash.Print();
}
字符串测试
void Test_Hash_Bucket2()
{
hash_bucket::HashTable<string, int> hash;
hash.Insert({ "aaa", 1 });
hash.Insert({ "vvv", 1 });
hash.Insert({ "eee", 1 });
hash.Insert({ "cpdd", 1 });
hash.Print();
cout << "empty: " << hash.Empty() << endl;
cout << "size: " << hash.Size() << endl;
hash.Erase("aaa");
hash.Erase("vvv");
hash.Erase("eee");
hash.Erase("cpdd");
hash.Print();
cout << "empty: " << hash.Empty() << endl;
cout << "size: " << hash.Size() << endl;
}
OK,本期内容就介绍到这里,好兄弟我们下期再见~!
结束语:满怀希望就会所向披靡!