一.哈希初步
1.哈希的思想
哈希算法的思想是将要存储的顺序按照一定规律进行存储,查询时也依据此规律进行查询
相对于string字符串,会选择开辟一个大小为26的数组,将字母(仅小写)按照Ascall码表进行映射,统计其出现的次数
相对于没有规律的数据而言,常采用取模的方法( % 数组大小),做到一一映射的关系
哈希算法的思想是编程中极为重要的思想,可以避免长字符串的匹配,根据映射关系能够快速排除部分(布隆过滤器),要深刻理解哈希的思想和实现
二.哈希模拟实现
1.闭散列
储存大量数据时采用哈希的思想,此应用的方法是开辟一个数组,让不同的数据不断模数组的大小(nums.size()),对应的位置存储值,此方法也叫开放定值法(闭散列),此方法的弊端是会发生哈希冲突,所以我们采用线性探测和二次探测两种形式
实现方法:三种存在状态:Delet / Exist / Empty,若数据进入后查到此数组位为Empty / Delete,则直接插入数据,相反,若查到Exist,则会按照线性 / 二次向下寻找,若超过数组的size,则hashi % _nums.size()
重点:Delete的存在非常重要!!!,在查询数据时遇到Empty直接停止,遇到Delete就继续向后查询,若没有Delete这个标识位会导致删除数据后,被删除数据后面的数据全部丢失
2.状态表示 / 存储数据表示
状态:Empty / Delete / Exist
数据采用pair<K, V>,key为标识位,val为标识位对应的值,所以HashData<K, V> HTData,HTData有两个标识,一个为_kv,_s
// 枚举状态
enum Status
{
Empty,
Exist,
Delete
};
// 一个模板对应一个结构体
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
Status _s;
};
3.class类中的成员变量
private成员变量有 vector<HashData<K, V>> _tables,开辟一个数组,数组中存储的类型是HashData,用HashTable<K, V>创建一个 HT,HT可以调用_tables,_tables[hashi]中对应的值又是HTData类型的,可以调用_kv,_s
_n:负载因子,当数组中数据达到70%时就进行扩容
4.线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
程序思想提升:1.if / else双条件判断 - while(单条件卡死) / 2.复用的思想:空间不够需要扩容时,先扩容,等到负载因子 < 0.7时,复用原程序插入的逻辑,大于0.7就扩容在复用小于0.7时的程序,小于0.7就直接插入
swap的思想,若扩容就创建一个新的数组,然后原数组的数据重新插入(剩余数据也全部插入)后进行交换,原数组变为新数组,新创的数组出了作用域自动调用析构函数
bool Insert_Linear(const pair<K, V>& kv)
{
//size_t hashi = kv.first % _tables.size();
//if (_tables[hashi]._s == Empty || _tables[hashi]._s == Delete)
//{
// _tables[hashi]._kv = kv;
// _tables[hashi]._s = Exist;
// return true;
//}
//else
//{
// while (_tables[hashi]._s == Exist)
// {
// ++hashi;
// if (hashi >= _tables.size())
// {
// hash %= _tables.size();
// }
// }
// _tables[hashi]._kv = kv;
// _tables[hashi]._s = Exist;
// return true;
//}
// 优化
//两种情况:1. 此位的状态是空 / 删除
// 2. 此位状态是已被占
if (Find(kv.first))
return false;
// 考虑扩容
// 当负载因子等于0.7时就扩容
if (_n * 10 / _tables.size() == 7)
{
size_t newSize = _tables.size() * 2;
HashTable<K, V> newHT;
newHT._tables.resize(newSize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if(_tables[i]._s == Exist)
newHT.Insert_Linear(_tables[i]._kv);
}
//_tables.swap(newHT._tables);
newHT._tables.swap(_tables);
}
size_t hashi = kv.first % _tables.size();
while (_tables[hashi]._s == Exist)
{
++hashi;
//if (hashi >= _tables.size())
//{
// hashi %= _tables.size();
//}
// 简化
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._s = Exist;
++_n;
return true;
}
5.二次探测
当出现哈希冲突时,分别找寻1 / 4 / 9(pow(++i, 2))(i = 1)等位置,避免线性探测在某一个地点出现大量冲突,二次探测使冲突更加分散(思想较线性探测近乎相同)
bool Insert_Twice(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
if (_n * 10 / _tables.size() == 7)
{
size_t newSize = _tables.size() * 2;
HashTable<K, V> newHt;
newHt._tables.resize(newSize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._s == Exist)
newHt.Insert_Twice(_tables[i]._kv);
}
newHt._tables.swap(_tables);
}
size_t hashi = kv.first % _tables.size();
size_t hashtwice = 0;
while (_tables[hashi]._s == Exist)
{
hashi = kv.first % _tables.size();
//++hashtwice; // 优化
// Warning: +=: 从double转换到size_t,可能丢失数据
hashi += size_t(pow(++hashtwice, 2));
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._s = Exist;
++_n;
return true;
}
6.Find查询函数
Delete状态存在的意义:如果没有Delete状态,会导致Empty后面的数据均无法访问
易错点:查询的if判断条件中要判断状态是否存在,因为程序中的删除用的是尾删除法,仅改变了此位的状态为Delete,不判断Exist的状态会将Delete误判成存在
HashData<K, V>* Find(const K& k)
{
size_t hashi = k % _tables.size();
while (_tables[hashi]._s != Empty)
{
// 易错点:如果此值是已经删除的值,那么也会被当作是存在的值,则被删除的值也会被Find,无法重新插入
// 但插入值是只要Find不到就可以插入,所以会无法插入目标值,或把目标值覆盖
// if (_tables[hashi]._kv.first == k)
if(_tables[hashi]._s == Exist
&& _tables[hashi]._kv.first == k)
{
return &_tables[hashi];
}
++hashi;
hashi %= _tables.size();
}
return nullptr;
}
7.删除Erase(伪删除法)
伪删除法,不删除位上的值,仅改变相应位的状态
bool Erase(const K& k)
{
HashData<K, V>* ret = Find(k);
if (ret)
{
ret->_s = Delete;
--_n; // 负载因子
return true;
}
return false;
}
三.哈希模拟实现完整代码展示
闭散列 :线性探测 / 二次探测(open_adress是开放定址法(闭散列的别名))
不足:1.无法支持数据为string(需要加仿函数(结构体 + 函数重载))
2.闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷(链式结构(开散列))重要思想:1.代码优化 / 2.函数自身复用 / 3.Swap的思想
namespace open_adress
{
enum Status
{
Empty,
Exist,
Delete
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
Status _s;
};
//一个结构体 / 一个类对应一个模板
template<class K, class V>
class HashTable
{
public:
// 构造函数
HashTable()
{
_tables.resize(10);
}
// 线性探测
bool Insert_Linear(const pair<K, V>& kv)
{
//size_t hashi = kv.first % _tables.size();
//if (_tables[hashi]._s == Empty || _tables[hashi]._s == Delete)
//{
// _tables[hashi]._kv = kv;
// _tables[hashi]._s = Exist;
// return true;
//}
//else
//{
// while (_tables[hashi]._s == Exist)
// {
// ++hashi;
// if (hashi >= _tables.size())
// {
// hash %= _tables.size();
// }
// }
// _tables[hashi]._kv = kv;
// _tables[hashi]._s = Exist;
// return true;
//}
// 优化
//两种情况:1. 此位的状态是空 / 删除
// 2. 此位状态是已被占
if (Find(kv.first))
return false;
// 考虑扩容
// 当负载因子等于0.7时就扩容
if (_n * 10 / _tables.size() == 7)
{
size_t newSize = _tables.size() * 2;
HashTable<K, V> newHT;
newHT._tables.resize(newSize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if(_tables[i]._s == Exist)
newHT.Insert_Linear(_tables[i]._kv);
}
//_tables.swap(newHT._tables);
newHT._tables.swap(_tables);
}
size_t hashi = kv.first % _tables.size();
while (_tables[hashi]._s == Exist)
{
++hashi;
//if (hashi >= _tables.size())
//{
// hashi %= _tables.size();
//}
// 简化
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._s = Exist;
++_n;
return true;
}
// 二次探测
bool Insert_Twice(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
if (_n * 10 / _tables.size() == 7)
{
size_t newSize = _tables.size() * 2;
HashTable<K, V> newHt;
newHt._tables.resize(newSize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._s == Exist)
newHt.Insert_Twice(_tables[i]._kv);
}
newHt._tables.swap(_tables);
}
size_t hashi = kv.first % _tables.size();
size_t hashtwice = 0;
while (_tables[hashi]._s == Exist)
{
hashi = kv.first % _tables.size();
//++hashtwice; // 优化
// Warning: +=: 从double转换到size_t,可能丢失数据
hashi += size_t(pow(++hashtwice, 2));
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._s = Exist;
++_n;
return true;
}
// Delete状态存在的意义:如果没有Delete状态,会导致Empty后面的数据均无法访问
HashData<K, V>* Find(const K& k)
{
size_t hashi = k % _tables.size();
while (_tables[hashi]._s != Empty)
{
// 易错点:如果此值是已经删除的值,那么也会被当作是存在的值,则被删除的值也会被Find,无法重新插入
// 但插入值是只要Find不到就可以插入,所以会无法插入目标值,或把目标值覆盖
// if (_tables[hashi]._kv.first == k)
if(_tables[hashi]._s == Exist
&& _tables[hashi]._kv.first == k)
{
return &_tables[hashi];
}
++hashi;
hashi %= _tables.size();
}
return nullptr;
}
// 伪删除法
bool Erase(const K& k)
{
HashData<K, V>* ret = Find(k);
if (ret)
{
ret->_s = Delete;
--_n;
return true;
}
return false;
}
void Print()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._s == Exist)
{
cout << "[" << i << "]-> " << _tables[i]._kv.first << endl;
}
else if (_tables[i]._s == Empty)
{
cout << "[" << i << "]-> " << "Empty" << endl;
}
else
{
cout << "[" << i << "]-> " << "Delete" << endl;
}
}
cout << endl;
}
private:
vector<HashData<K, V>> _tables;
size_t _n;
};
四.哈希模拟实现测试代码
闭散列 - 线性探测 / 二次探测插入数据测试(是否能正常对应位 / 是否能正常扩容等)- Print用于测试
// 线性探测测试
void TestHashLinear()
{
HashTable<int, int> ht;
int a[] = { 4,14,24,34,5,7,1,16,28,39,3,7 };
for (auto e : a)
{
ht.Insert_Linear(make_pair(e, e));
}
ht.Erase(4);
//ht.Print();
ht.Insert_Linear(make_pair(4, 4));
// 插入数据的方式:make_pair(key, value)
ht.Insert_Linear(make_pair(44, 44));
ht.Print();
}
// 二次探测测试
void TestHashTwice()
{
HashTable<int, int> ht;
// 分别占4号位 / 5号位 / 8号位
//ht.Insert_Twice(make_pair(4, 4));
//ht.Insert_Twice(make_pair(14, 14));
//ht.Insert_Twice(make_pair(24, 24));
int a[] = { 4,14,24,34,5,7,1,16,28,39,3,7 };
for (auto e : a)
{
ht.Insert_Twice(make_pair(e, e));
}
ht.Erase(4);
//ht.Print();
ht.Insert_Twice(make_pair(4, 4));
ht.Insert_Twice(make_pair(44, 44));
ht.Print();
}
End!!!
标签:tables,string,hashi,探测,Exist,kv,哈希,._,size From: https://blog.csdn.net/ZCDL1314/article/details/143439587