38.C++哈希3(哈希表底层模拟实现 - 开散列拉链法和哈希桶)
⭐上篇文章:37.C++哈希2(哈希表底层分析与模拟实现-闭散列key模型与key-value模型)-CSDN博客
⭐本篇代码:c++学习/20.哈希与哈希表 · 橘子真甜/c++-learning-of-yzc - 码云 - 开源中国 (gitee.com)
⭐标⭐是比较重要的部分
目录
一. 拉链法与哈希桶
二. 开散列哈希表代码实现
2.1 哈希节点
2.2 哈希表框架
2.3 Insert
2.4 Print
2.5 测试1
2.6 Find
2.7 Erase
三. 完整测试
3.1 key模型
编辑 3.2 key-value模型
一. 拉链法与哈希桶
哈希映射是有冲突的,闭散列通过探测的方式,如果发现自己映射的位置被抢占了,就去抢占其他空闲的位置,这会让哈希冲突变多,从而导致效率降低。
而拉链法和哈希桶解决哈希冲突的方式是:将映射相同位置的所有节点以链表的方式连在一起,这样一来就不会去抢占其他人的位置!
将具有相同地址映射的的节点归于一个子集,每一个子集称为一个哈希桶。桶中的元素通过链表或者其他方式连接起来,链表的头节点放在哈希表中。
如下图,将相同冲突的元素通过链表进行连接。
二. 开散列哈希表代码实现
2.1 哈希节点
与闭散列不同,开散列的哈希节点不需要存放该节点的状态,而是需要存放一个next指针。只用指向下一个节点(如果使用红黑树等结构需要更多的指针)。
节点代码如下:
template <class T>
struct HashData
{
T _data;
HashData<T> *_next;
// 构造函数
HashData(const T &data = T())
: _data(data), _next(nullptr) {}
};
2.2 哈希表框架
与闭散列一样,需要插入函数,删除函数,查找函数,遍历,以及获取当前数据的key值
成员变量为:_num表示有效数据,_table表示哈希表,这里使用vector作为哈希桶,内部的数据类型的HashData*
代码如下:
// 使用KeyOfT来获取pair的key值
// unordered_set -> <Key,Key>
// unordered_map -> <Key,Value>
template <class K>
struct SetKeyOfT
{
const K &operator()(const K &key)
{
return key;
}
};
template <class K, class T>
struct MapKeyOfT
{
const K &operator()(const pair<K, T> &kv)
{
return kv.first;
}
};
//哈希表,默认为set(key模型)
template <class K, class T, class KeyOfT = SetKeyOfT<int>>
class OpenHashTable
{
typedef ::HashData<T> HashData;
public:
bool Insert(const T &data) {}
HashData *Find(const K &key) {}
bool Erase(const K &key) {}
void Print() {}
private:
vector<HashData *> _table;
size_t _num = 0;
};
2.3 Insert⭐
与闭散列一样,开散列也有一个负载因子,只要哈希表中的有效数据与哈希表的长度一样多时候。表明这个哈希表中的数据冲突已经达到了理想状态,此时就需要"扩容"。
理想状态:每一个哈希桶桶只有一个数据,效率达到最高为O(1)。
最糟状态:所有的有效数据都集中在一个哈希桶中,效率退化到O(N)。
如何扩容,需要注意什么?扩容的时候需要遍历旧表,将旧表中的所有数据重新映射到新表中。由于取模的数字变大,每一次扩容都会降低哈希冲突!
由于每一个哈希桶中都是链表,插入数据的时候采用头插法更简单!
代码如下:
bool Insert(const T &data)
{
KeyOfT koft{};
// 1. 判断是否需要扩容
if (_table.size() == 0 || _num * 100 / _table.size() > 75)
{
// 开辟新表,减少哈希冲突
size_t newsize = (_table.size() == 0) ? 10 : _table.size() * 2;
vector<HashData *> newtable;
newtable.resize(newsize);
// 遍历旧表,重新哈希映射到新表
for (int i = 0; i < _table.size(); ++i)
{
// 判断每一个哈希桶,通过头插法插入到新表中
HashData *cur = _table[i];
while (cur != nullptr)
{
HashData *next = cur->_next;
int index = koft(cur->_data) % newsize;
cur->_next = newtable[index];
newtable[index] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newtable); // 使用交换快速替换两张表,旧表直接不要了
}
// 2. 插入新的数据
// 首先需要判断表中有无该数据
int index = koft(data) % _table.size();
HashData *cur = _table[index];
while (cur != nullptr)
{
// 只需要判断key值即可,因为key-value。不同的key可能存在value相同的情况
if (koft(cur->_data) == koft(data))
return false;
cur = cur->_next;
}
HashData *newnode = new HashData(data);
newnode->_next = _table[index];
_table[index] = newnode;
++_num; // 别忘了增加有效数据
return true;
}
2.4 Print
既然插入的代码写完了,这时候需要遍历来测试代码是否正确。遍历比较简单,只需要循环遍历哈希表,表头不为空遍历这个链表即可。
代码如下:这里只打印了data的key值
void Print()
{
KeyOfT koft{};
for (int i = 0; i < _table.size(); ++i)
{
HashData *cur = _table[i];
std::cout << i << ": ";
while (cur != nullptr)
{
std::cout << koft(cur->_data) << " -> ";
cur = cur->_next;
}
std::cout << "nullptr" << std::endl;
}
}
2.5 测试1
随机插入20个数据,打印哈希表中的内容。查看有无bug
测试代码如下:
#include <iostream>
#include "HashTable.h"
using namespace std;
void testset()
{
OpenHashTable<int, int> ht;
for (int i = 0; i < 20; i++)
{
ht.Insert(rand() % 100);
}
ht.Print();
}
int main()
{
srand(time(0) ^ rand());
testset();
return 0;
}
测试结果如下:
可以看到,结果是正确的!
2.6 Find
这个函数是用于查找某一个节点是否在哈希表中,不能去遍历查找,这样的效率是O(N)。而应该先求出该数据在哈希表中的映射位置,在这个哈希桶中查找。间效率提升至O(1)。
HashData *Find(const K &key)
{
if (_table.empty())
return nullptr;
KeyOfT koft;
size_t index = key % _table.size();
HashData *cur = _table[index];
while (cur != nullptr)
{
if (koft(cur->_data) == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
2.7 Erase⭐
不可以通过Find查找然后去删除,因为桶中是链表,删除某一个节点之后。需要将该节点的前后节点连接起来!
链表删除头节点,只需要将头节点的next节点赋值给头节点即可
bool Erase(const K &key)
{
if (_table.empty())
return false;
KeyOfT koft;
int index = key % _table.size();
HashData *prev = nullptr;
HashData *cur = _table[index];
while (cur != nullptr)
{
if (koft(cur->_data) == key)
{
// 头节点就是删除的节点,需要将头节点置为next
if (prev != nullptr)
prev->_next = cur->_next;
else
_table[index] = cur->_next;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
三. 完整测试
3.1 key模型
#include <iostream>
#include "HashTable.h"
using namespace std;
void testset()
{
OpenHashTable<int, int> ht;
for (int i = 0; i < 15; i++)
{
ht.Insert(rand() % 100);
}
ht.Print();
while (true)
{
cout << "删除数据输入1, 查找数据输入2, 退出输入0:";
int n = 0;
cin >> n;
if (n == 0)
break;
else if (n == 1)
{
cout << "请输入删除的数字:";
int num;
cin >> num;
bool flag = ht.Erase(num);
if (flag)
{
cout << "删除成功,哈希表如下" << endl;
ht.Print();
}
else
cout << "没有这个数据,删除失败!" << endl;
}
else if (n == 2)
{
cout << "请输入查找的数字:";
int num;
cin >> num;
HashData<int> *data = ht.Find(num);
if (data == nullptr)
cout << "这个数据不存在" << endl;
else
cout << "这个数据存在" << data->_data << endl;
}
else
{
cout << "非法输入" << std::endl;
continue;
}
}
}
int main()
{
srand(time(0) ^ rand());
testset();
return 0;
}
测试结果如下:
3.2 key-value模型
key-value模型需要修改print,让其打印kv键值对
void PrintMap()
{
KeyOfT koft{};
for (int i = 0; i < _table.size(); ++i)
{
HashData *cur = _table[i];
std::cout << i << ": ";
while (cur != nullptr)
{
std::cout << cur->_data.first << ":" << cur->_data.second << " -> ";
cur = cur->_next;
}
std::cout << "nullptr" << std::endl;
}
}
#include <iostream>
#include "HashTable.h"
using namespace std;
void testset()
{
OpenHashTable<int, pair<int, char>, MapKeyOfT<int, char>> ht;
for (int i = 0; i < 15; i++)
{
int t = rand() % 26;
ht.Insert(make_pair(i, (char)(i + 'a')));
}
ht.PrintMap();
while (true)
{
cout << "删除数据输入1, 查找数据输入2, 退出输入0:";
int n = 0;
cin >> n;
if (n == 0)
break;
else if (n == 1)
{
cout << "请输入删除的数字:";
int num;
cin >> num;
bool flag = ht.Erase(num);
if (flag)
{
cout << "删除成功,哈希表如下" << endl;
ht.PrintMap();
}
else
cout << "没有这个数据,删除失败!" << endl;
}
else if (n == 2)
{
cout << "请输入查找的数字:";
int num;
cin >> num;
HashData<pair<int, char>> *data = ht.Find(num);
if (data == nullptr)
cout << "这个数据不存在" << endl;
else
cout << "这个数据存在" << data->_data.first << ":" << data->_data.second << endl;
}
else
{
cout << "非法输入" << std::endl;
continue;
}
}
}
int main()
{
srand(time(0) ^ rand());
testset();
return 0;
}
测试结果如下:
以上已经基本完成了哈希表的简单实现,还需要改进哈希函数(字符串映射),迭代器实现,封装为unordered_set 与 unordered_map