目录
哈希表简介
首先我们需要明白的是,哈希是一种思想
正常情况下,我们在面对一些算法题的时候,如果数据范围较小,比如英文26个字母小写,这种情况下我们就可以直接开一个简单数据,让每一个字母代表一个位置,这叫做映射
而我们的哈希表,是一种数据结构,是用来处理那些数据范围很大但是数据量不一定大的情况
假设我们存了一组数:1,331,200000,630
我们能看到,这只有四个数,但是范围却是1~2000000,所以我们就会用到哈希表
而哈希表的原理就是,让每一个数据代表一个位置(通过计算),再将那个数据放到相应的位置,这样,我们通过下标就能找到这个数据,效率非常高
而我们要在哈希表里面找到每个元素的相应位置也较为简单:
我们会通过哈希函数(下文会讲)将传过来的数据转换为整形,然后这个整形的大小可能会比数据大,所以我们就将这个数据模上一个数组大小
因为是取模,所以数据绝对不会超过数组的大小,这样我们就保证了每个数据都能在哈希表里面有一个位置
key模型和key_value模型讲解
在哈希表中有这两个模型,一个叫做key,一个是key_value
1. key是什么呢,这个key是数据,表示这个数据在不在数组里面,举个例子:我们的将 a~z 26个字母按照各自的ASCII码放置好,这就是key,通过ASCII码也能访问回去
2. key_value是什么呢,其实就是我们通过一个key,就能找到对应的value
比如我们生活中很常见的英文字典,我们通过找一个英文,就能找到对应的中文解释
再比如车库停车系统(key为车牌,value为状态判断),一辆车过来了,通过扫描就能得知这个车牌有没有录入系统里面
今天我们的哈希表会有相关的应用,也就是将key模型与key_value模型与哈希表相结合
哈希冲突
其实哈希冲突就是我们算出来的两个不同数据最后要存放的位置是一样的
举个例子:10和100这两个数字,如果size为10,那么我们的数据就应该模10(size)处理
但是10和100模了10之后都是0,也就是说这两个理论上来说应该在哈希表里面放同一个位置
这时我们有两种方法可以解决这个问题:
1. 闭散列(开放定址法)
这种方法的本质就是:我所在的位置已经被人霸占了,那我就往后找一个位置
所以就是10先算然后放在下标为0的位置,然后100算出来也是0,但是下标为0的位置已经被10占了,拿100就往后一个放在下标为1的位置
举个有味道的例子:假设你们隔壁宿舍A厕所坏了,一个宿舍只有一个厕所
这时隔壁宿舍A有人想上厕所,但是他们宿舍厕所坏了,这时就跑来你们宿舍上
但是上一半你也想上了,那你就只能跑隔壁宿舍B上
如果宿舍B也有人在你上一半的时候想上呢?那是不是又得去另一个宿舍啊
这个方法有很大的弊端,如果数据都连在一起时,那要找数据其实就相当于遍历一遍数组(虽然不会有这种情况,因为有一个叫做负载因子的东西控制着)
但是光这么想一想,如果一串数据连一块,是不是有点效率低啊
所以这时又有两种解决方法:
一种是设置一个负载因子,这个负载因子其实就是计算一遍:插入数据总量 / 总空间大小
我们能看到,这其实就是一种比重的计算,当存储的数据达到空间大小的70%时,就扩容
负载因子大小一般在0.7(70%)~0.8(80%)之间
而另一种方法就是另一种处理哈希冲突的方式了
2. 开散列(拉链法)
拉链法的本质就是:将数组里面存的值改为存一个链表
这时候还是两个值:10和100,这两个值最后都要放在下表为0的位置
但是10先放,后面100放的时候发现那个位置已经有值了,所以就链接在10的后面
而如果连续很多数据都插入在同一个地方的话,那其实找数据的时间复杂度就相当于找一条链表了
为了防止这种情况,所以我们需要一个控制一条链表的最长长度
而一个哈希表最好的情况就是:每个位置下面都是只插入一个值,所以当插入元素个数等于空间大小的时候,我们就需要扩容
而扩容并不是简单地将原空间变2倍就行,而是需要开一个新的空间,然后将原哈希表里面的数据一个一个找好新的位置
试想一下:原来空间总大小为10,这时10和11分别放在0和1的位置,但如果现在扩容成20了,我们还应该这样放吗?应该变成10和11对吧,因为扩容之后每一个的位置都会相应地变化
最后,我们要找下标的话,我们还需要写一个哈希函数
什么是哈希函数?
简单来说,哈希函数就是重载了operator()的一个类,然后这个类里面实现的方法是帮助我们将一个数转化为整形的(因为只有整形才能在数组里面找到下标)
这个我们后续实现的时候会对其进行讲解
哈希表底层实现(无封装)
在这一小节里面,我们会通过底层的实现,带领各位读者深度理解哈希表
哈希函数及其特化(string)
在上文中提到,哈希函数就是能将任意值转化为整数的一个重载了operator()的类
而一般情况下,我们直接将传过来的值强转为size_t(无符号整形)即可
但是我们还有一种很常用的情况,就是传一个string过去,因为在日常中,对整数的处理并没有对字符的处理来得频繁
无论是字典,车牌号,身份证号等等,这些都是字符串,所以为了应对这种情况吗,我们可以特化一下这个类,至于如果传过来的是一个结构体,这种情况我们就需要自己在使用的时候自行编写
我们先来实现非string的情况:
template<class k>
struct HashFunc
{
size_t operator()(const k& key)
{
return (size_t)key;
}
};
相对来说还是较为简单的,这时我们再来想一想string的该怎么实现
首先,我们将string里面的成员一个一个拿出来,那就是一个一个的字符,每一个字符都对应了一个ASCII码,我们可以直接用这些ASCII码相加
另外我们还会遇到这种情况:cat、act,这两个单词一个是猫,一个是行动,这两个的ASCII码相加的结果理应是相同的
但是如果相同的话,就会产生哈希冲突,我们需要尽量避免不必要的哈希冲突
所以我们可以在每一次加完ASCII码之后,直接乘等31,然后再加下一个字符的ASCII码
至于为什么是31,其实这是人家经过严谨的数学证明的来的,这里并不是我们的重点,而且,不仅仅是31,131、1313、131313、131313......这些数字都是可以的
这里我们就以31为例
string特化的代码如下:
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hashi = 0;
for (const auto& e : key)
{
hashi *= 31;
hashi += e;
}
return hashi;
}
};
哈希表框架构建
哈希表节点类
要写这个类是因为,我们的哈希表的每一个节点都包含两个东西,一个是pair类型的数据(这里不讲封装,所以我们将数据固定为pair)
然后除此之外,我们还要包含一个链表,但是我们这里存一个_next指针即可,因为如果list的话,后面的迭代器实现会挺复杂,就没那个必要直接实现一个单链表即可
代码如下:
template<class k, class v>
struct hashNode
{
hashNode<k, v>* _next;
pair<k, v> _data;
hashNode(const pair<k, v>& data)
:_data(data)
, _next(nullptr)
{}
};
成员变量与构造析构
首先我们实现一个类,这个类代表的就是哈希表
但是需要注意的是,我们还需要传一个哈希函数做为模板参数
template<class k, class v, class Hash = HashFunc<k>>
class HashTable
{
typedef hashNode<k, v> Node;
public:
private:
vector<Node*> _tables;
size_t _n = 0;
};
然后我们的哈希表底层就是一个数组,只不过我们通过哈希函数和特殊的定址法可以让每一个数据都在哈希表里面都找到位置
另外我们还需要一个变量_n来记录我们插入了几个数据
别忘了,我们的扩容条件是:插入的数据个数等于空间大小的时候,我们就扩容
接着我们再来看看构造与析构
构造函数
这里其实我们的构造函数并不需要写什么,因为即使我们不写,我们的成员是一个vector,默认生成的构造函数会去调用vector的构造函数
但是我们为了避免不必要的消耗(扩容),我们可以提前开好一块空间,这时我们直接调用vector的resize即可,代码如下:
HashTable()
{
_tables.resize(50, nullptr);
}
析构函数
析构函数其实就是将我们开好的空间一个一个销毁就行
但是我们是一个数组里面嵌套了链表的结构
所以这就意味着,我们要实现析构,只能写一个循环嵌套
外层的循环代表遍历这个数组的每一个节点,因为不一定该节点会映射值,所以我们可以判断一个,如果这个地方的值存在,我们就进入第二层循环
而当我们进入第二层循环之后,就意味着我们进入了一个链表,我们这时就需要将这个链表里面的内容一个一个删掉即可
代码如下:
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
迭代器
首先,我们如果要找数据的话,我们就需要有对应的哈希表
但是我们的哈希表里面也要用到迭代器的数据,这时我们就面临一个问题:
我们如果要使用哈希表,就需要先有迭代器
同时我们要使用迭代器,就需要先有哈希表
这就像是先有鸡还是先有蛋一样,所以我们的解决方法是:类声明
我们将迭代器实现在哈希表前面,但是我们先声明一份哈希表,告诉编译器,我们后面有这么一个东西,是对的,你不要管那么多,自己到后面去找定义去
// 前置声明,因为后续的operator++需要用到hashtables
// 但是我们的hashtables也要用到迭代器
// 这就造成了一个先有鸡还是先有蛋的问题
// 所以我们在这里的解决方法是前置声明
template<class k, class v, class Hash>
class HashTable;
然后就是我们的迭代器:
// 迭代器
template<class k, class v, class Hash, class Ptr, class Ref>
struct HTIterator
{
typedef hashNode<k, v> Node;
typedef HTIterator<k, v, Hash, Ptr, Ref> Self;
}
上面的代码写了两个typedef,这样方便我们后续的使用
然后是我们的成员变量,我们的成员只有两个,一个是哈希节点,一个是哈希表
哈希节点代表的是我迭代器现在对应的是这个节点,而我们的哈希表传过来则是为了后面的operator++,我们的这个函数实现会用到哈希表,因为如果迭代器当前指向的节点是一条链的最后一个值,那么我们这时候的++应该对应到下一个链表的开头,需要while循环遍历,需要哈希表
成员与构造函数代码如下:
const HashTable<k, v, Hash>* _ht;
Node* _node;
HTIterator(Node* node, const HashTable<k, v, Hash>* ht)
:_node(node)
, _ht(ht)
{}
然后就是正常的 *、->、!=、==
这里我们就不做过多介绍了,因为我们前面讲过太多了,而且都较为简单
代码如下(可以看注释讲解):
Ref operator*()
{
其实就是返回迭代器指向的节点的值
直接返回即可
return _node->_data;
}
Ptr operator->()
{
->的话是返回迭代器指向节点的值的地址
为什么是地址,这是因为外面当我们使用->的时候
编译器默认会为了美观隐藏掉一个->
这时候就相当于我们使用了两个->,所以就能达到对应的效果
return &_node->_data;
}
bool operator!=(const Self& s)
{
不等直接判断即可
因为都是指针,判断的就是地址
return _node != s._node;
}
bool operator==(const Self& s)
{
等于同理
return _node == s._node;
}
接下来我们来讲一讲operator++
我们的operator++的只要原理就是分情况讨论:
- 如果迭代器指向的节点为一条链表的尾巴,那就意味着我们++之后指向的应该是下一条链表的头,该节点的位置就是:指向的值(pair)的first % 数组的大小,然后从这个位置的下一个位置开始找起,直到找到下一条链表的头
- 如果下一个位置节点存在,我们就++到下一个节点即可
但有一点需要注意,我们的operator++会用到哈希表的私有成员,所以我们需要在哈希表内部给一个友元
代码如下:
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
Hash hs;
size_t hashi = hs(_node->_data.first) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
if (_ht->_tables[hashi]) { break; }
++hashi;
}
if (_ht->_tables.size() == hashi)
_node = nullptr;
else
_node = _ht->_tables[hashi];
}
return *this;
}
迭代器完整代码如下:
// 前置声明,因为后续的operator++需要用到hashtables
// 但是我们的hashtables也要用到迭代器
// 这就造成了一个先有鸡还是先有蛋的问题
// 所以我们在这里的解决方法是前置声明
template<class k, class v, class Hash>
class HashTable;
// 迭代器
template<class k, class v, class Hash, class Ptr, class Ref>
struct HTIterator
{
typedef hashNode<k, v> Node;
typedef HTIterator<k, v, Hash, Ptr, Ref> Self;
const HashTable<k, v, Hash>* _ht;
Node* _node;
HTIterator(Node* node, const HashTable<k, v, Hash>* ht)
:_node(node)
, _ht(ht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
Hash hs;
size_t hashi = hs(_node->_data.first) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
if (_ht->_tables[hashi]) { break; }
++hashi;
}
if (_ht->_tables.size() == hashi)
_node = nullptr;
else
_node = _ht->_tables[hashi];
}
return *this;
}
};
实现迭代器后哈希表的相应调整
由于实现了迭代器,所以我们哈希表的内部也要相应地调整一下
因为迭代器会用到哈希表的私有成员,所以我们需要给一个友元
template<class k, class v, class Hash, class Ptr, class Ref>
friend struct HTIterator;
然后我们的两个迭代器类型太长了,我们可以给两个typedef(一个const,一个非const)
typedef HTIterator<k, v, Hash, pair<k, v>*, pair<k, v>&> iterator;
typedef HTIterator<k, v, Hash, const pair<k, v>*, const pair<k, v>&> const_iterator;
接着是我们的begin和end
我们实现了这两个之后,我们才能使用迭代器遍历哈希表
begin的主逻辑就是:从头开始找,直到遇到一个有值的链表头节点,如果遇不到我们就返回空迭代器
end的主逻辑则是:直接返回空迭代器,因为我们遍历到最后,都是空,直接返回即可
代码如下:
iterator begin()
{
if (_n == 0)return end();
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])return iterator(_tables[i], this);
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator begin()const
{
if (_n == 0)return end();
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])return const_iterator(_tables[i], this);
}
}
const_iterator end()const
{
return const_iterator(nullptr, this);
}
哈希表的查找 Find
这里我们要传一个key过去(节点中存储的是pair,key是pair的第一个值)
有了这个key,我们可以通过运算得到待查找数据对应的下标
size_t hashi = hs(key) % _tables.size();
然后我们就在这个下标位置对应的链表里面查找即可
一直往下找,直到这个链表为空,如果找到了,就返回一个指向这个节点的迭代器
如果没有找到,就返回一个end
代码如下:
iterator Find(const k& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_data.first == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
return end();
}
哈希表的插入 Insert
插入较为复杂
插入的返回值是 pair<iterator, bool>,返回值是这个也是方便我们后续如果需要封装一个operator[],然后参数是一个pair,因为我们要插入的是一个pair
pair<iterator, bool> Insert(const pair<k, v>& data)
{
}
首先我们要做的第一步就是先用一下Find查找一下,如果这个待插入的值已经存在了,那我们还插入什么,返回这个迭代器就行了
Hash hs;
// 用find找这个节点有没有出现过
iterator it = Find(data.first);
if (it != end())return { it, false };
然后就是判断扩容了,我们在上文中提到,当插入数据个数等于空间大小的时候,我们就该扩容了
然后就是扩容的逻辑
我们扩容其实大体逻辑并不复杂:
- 新建一个vector,将其空间的大小开到原哈希表大小的2倍
- 将原哈希表中的数据一个一个重新找位置,重新头插到新的vector中
- 使用vector中的swap函数,将新开的vector变为哈希表
首先第一个步骤自不必说:
if (_n == _tables.size())
{
// 进入判断逻辑
vector<Node*> newtab(_tables.size() * 2, nullptr);
}
然后是第二个步骤
第二个步骤究其根本就是一个循环嵌套
外层的for循环代表这个数组的每一个位置
里面的while循环代表每一个位置里面的链表
我们找到一个数据,就直接头插到新开的vector中即可,代码如下:
// 拷贝数据
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hasii = hs(cur->_data.first) % newtab.size();
// 头插
cur->_next = newtab[hasii];
newtab[hasii] = cur;
cur = next;
}
_tables[i] = nullptr;
}
最后是步骤三,使用一个swap进行交换即可:
_tables.swap(newtab);
在扩容完了之后,我们就正式进入插入的逻辑
同样的,我们还是需要先把插入位置在哈希表中的对应下标给算出来
size_t hashi = hs(data.first) % _tables.size();
然后我们需要new一个节点,最后执行一个头插操作,++_n(哈希表中记录插入数据个数的变量)即可,代码如下:
// 插入逻辑,头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return { iterator(newnode, this), true };;
插入完整代码如下:
pair<iterator, bool> Insert(const pair<k, v>& data)
{
Hash hs;
size_t hashi = hs(data.first) % _tables.size();
// 用find找这个节点有没有出现过
iterator it = Find(data.first);
if (it != end())return { it, false };
// 判断扩容
if (_n == _tables.size())
{
// 进入判断逻辑
vector<Node*> newtab(_tables.size() * 2, nullptr);
// 拷贝数据
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hasii = hs(cur->_data.first) % newtab.size();
// 头插
cur->_next = newtab[hasii];
newtab[hasii] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtab);
}
// 插入逻辑,头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return { iterator(newnode, this), true };;
}
哈希表的删除 Erase
其实删除也并不困难,我们要做的就是先将待删除位置在哈希表中的下标算出来:
Hash hs;
size_t hashi = hs(key) % _tables.size();
然后我们定义两个指针prev、cur,这两个指针的作用是:
prev负责记录上一个节点的位置,cur负责记录当前节点
当我们要删除节点的时候,我们删了节点之前,我们需要让前后链接起来,这样我们才能安心删除节点
Node* cur = _tables[hashi];
Node* prev = nullptr;
然后我们就执行一个while循环的逻辑,这个while循环代表的是查找这条链表的每一个节点
如果这个节点就是要删的节点,并且prev为空,就代表要删的是头节点,头删即可
如果找到了这个节点并且prev不为空,那我们就正常链接之后删除
如果while循环还在进行,并且我们也没找到要删除的节点,我们就让prev等于cur,然后cur变成下一个节点
如果我们找不到节点的话,我们就直接返回false即可
代码如下:
while (cur)
{
if (cur->_data.first == key)
{
if (!prev)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
删除完整代码如下:
bool Erase(const k& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_data.first == key)
{
if (!prev)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
未封装完整代码
#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<string>
#include<assert.h>
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 hashi = 0;
for (const auto& e : key)
{
hashi *= 31;
hashi += e;
}
return hashi;
}
};
template<class k, class v>
struct hashNode
{
hashNode<k, v>* _next;
pair<k, v> _data;
hashNode(const pair<k, v>& data)
:_data(data)
, _next(nullptr)
{}
};
// 前置声明,因为后续的operator++需要用到hashtables
// 但是我们的hashtables也要用到迭代器
// 这就造成了一个先有鸡还是先有蛋的问题
// 所以我们在这里的解决方法是前置声明
template<class k, class v, class Hash>
class HashTable;
// 迭代器
template<class k, class v, class Hash, class Ptr, class Ref>
struct HTIterator
{
typedef hashNode<k, v> Node;
typedef HTIterator<k, v, Hash, Ptr, Ref> Self;
const HashTable<k, v, Hash>* _ht;
Node* _node;
HTIterator(Node* node, const HashTable<k, v, Hash>* ht)
:_node(node)
, _ht(ht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
Hash hs;
size_t hashi = hs(_node->_data.first) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
if (_ht->_tables[hashi]) { break; }
++hashi;
}
if (_ht->_tables.size() == hashi)
_node = nullptr;
else
_node = _ht->_tables[hashi];
}
return *this;
}
};
template<class k, class v, class Hash = HashFunc<k>>
class HashTable
{
typedef hashNode<k, v> Node;
template<class k, class v, class Hash, class Ptr, class Ref>
friend struct HTIterator;
public:
typedef HTIterator<k, v, Hash, pair<k, v>*, pair<k, v>&> iterator;
typedef HTIterator<k, v, Hash, const pair<k, v>*, const pair<k, v>&> const_iterator;
iterator begin()
{
if (_n == 0)return end();
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])return iterator(_tables[i], this);
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator begin()const
{
if (_n == 0)return end();
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])return const_iterator(_tables[i], this);
}
}
const_iterator end()const
{
return const_iterator(nullptr, this);
}
HashTable()
{
_tables.resize(50, nullptr);
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
pair<iterator, bool> Insert(const pair<k, v>& data)
{
Hash hs;
size_t hashi = hs(data.first) % _tables.size();
// 用find找这个节点有没有出现过
iterator it = Find(data.first);
if (it != end())return { it, false };
// 判断扩容
if (_n == _tables.size())
{
// 进入判断逻辑
vector<Node*> newtab(_tables.size() * 2, nullptr);
// 拷贝数据
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hasii = hs(cur->_data.first) % newtab.size();
// 头插
cur->_next = newtab[hasii];
newtab[hasii] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtab);
}
// 插入逻辑,头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return { iterator(newnode, this), true };;
}
iterator Find(const k& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_data.first == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
return end();
}
bool Erase(const k& key)
{
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_data.first == key)
{
if (!prev)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
unordered_map、unordered_set封装
我们来理一下,如果要封装的话,底层就是哈希表
但是map要传的是pair(key_value)
set要传的是key
我们先从节点类开始改起
这是节点存储的应该各是各的,所以模板就变成了一个T
如果set就传一个key,map就传一个pair
template<class T>
struct hashNode
{
hashNode<T>* _next;
T _data;
hashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
};
然后我们再来改一改迭代器
我们的map在哈希表里面存到是pair,在set里面存的是key,两个是不一样的,而且查找、删除要的是key,而插入要的各自不同
所以我们哈希表的模板需要改一改:
要变成 template<class k, class T, class KeyOfT, class Hash>
第一个是为了我们的查找和删除,T代表的各自存储的数据,而我们的KeyOfT则是一个类,这个类重载了一个operator(),如果是map的话就返回map里面的key,set的话直接返回key即可
同时,我们的迭代器需要用到哈希表,我们需要在迭代器里面定义一个哈希表对象,然后对其初始化,那么哈希表的模板参数该有的迭代器就都要有
除此之外,我们还需要另给两个模板参数 Ptr 和 Ref,这两个是给operator* 和 operator-> 做返回值的
我们先来改一下除了operator++之外的代码:
// 迭代器
template<class k, class T, class KeyOfT, class Hash, class Ptr, class Ref>
struct HTIterator
{
typedef hashNode<T> Node;
typedef HTIterator<k, T, KeyOfT, Hash, Ptr, Ref> Self;
const HashTable<k, T, KeyOfT, Hash>* _ht;
Node* _node;
HTIterator(Node* node, const HashTable<k, T, KeyOfT, Hash>* ht)
:_node(node)
, _ht(ht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
};
接着我们来看一看operator++
其实更新的点就在一个KeyOfT,我们需要通过这个KeyOfT拿到节点里面存储的值的 key
除此之外,并无什么不同
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
Hash hs;
KeyOfT kot;
size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
if (_ht->_tables[hashi]) { break; }
++hashi;
}
if (_ht->_tables.size() == hashi)
_node = nullptr;
else
_node = _ht->_tables[hashi];
}
return *this;
}
最后我们来改一改哈希表
其实并没有什么太大的改变
只不过我们原先的 k,v 变成了T,我们如果要取到 T 里面的 k 的话,就用KeyOfT即可
重点就在Insert、Find、Erase这三个,只不过都是加了一个KeyOfT的逻辑进去而已
代码如下:
template<class k, class T, class KeyOfT, class Hash>
class HashTable
{
typedef hashNode<T> Node;
template<class k, class T, class KeyOfT, class Hash, class Ptr, class Ref>
friend struct HTIterator;
public:
typedef HTIterator<k, T, KeyOfT, Hash, T*, T&> Iterator;
typedef HTIterator<k, T, KeyOfT, Hash, const T*, const T&> ConstIterator;
Iterator Begin()
{
if (_n == 0)return End();
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])return Iterator(_tables[i], this);
}
return End();
}
Iterator End()
{
return Iterator(nullptr, this);
}
ConstIterator Begin()const
{
if (_n == 0)return End();
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])return ConstIterator(_tables[i], this);
}
}
ConstIterator End()const
{
return ConstIterator(nullptr, this);
}
HashTable()
{
_tables.resize(50, nullptr);
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
pair<Iterator, bool> Insert(const T& data)
{
Hash hs;
KeyOfT kot;
size_t hashi = hs(kot(data)) % _tables.size();
// 用find找这个节点有没有出现过
Iterator it = Find(kot(data));
if (it != End())return { it, false };
// 判断扩容
if (_n == _tables.size())
{
// 进入判断逻辑
vector<Node*> newtab(_tables.size() * 2, nullptr);
// 拷贝数据
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hasii = hs(kot(cur->_data)) % newtab.size();
cur->_next = newtab[hasii];
newtab[hasii] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtab);
}
// 插入逻辑,头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return { Iterator(newnode, this), true };;
}
Iterator Find(const k& key)
{
Hash hs;
KeyOfT kot;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return Iterator(cur, this);
}
cur = cur->_next;
}
return End();
}
bool Erase(const k& key)
{
Hash hs;
KeyOfT kot;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (kot(cur->_data) == key)
{
if (!prev)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
接着我们来看一看unordered_map、unordered_set这两个的具体实现:
unordered_map
首先是一个类的创建,这里为了防止与标准库里的命名冲突,我们用了一个命名空间包起来
然后就是KeyOfT的编写
namespace hjx
{
template<class k, class v, class Hash = HashFunc<k>>
class unordered_map
{
struct KeyOfT
{
const k& operator()(const pair<k, v>& kv)
{
return kv.first;
}
};
}
}
接着就是迭代器的typedef
这里需要说明一下的是,迭代器的typedef要用到typename,因为编译器没法区分类模板里面是否有错误(不会进入内部编译),所以我们加了一个typename就相当于告诉编译器,我们后面跟的是正确的,你只管使用就是
typedef typename study::HashTable<k, pair<const k, v>, KeyOfT, Hash>::Iterator iterator;
typedef typename study::HashTable<k, pair<const k, v>, KeyOfT, Hash>::ConstIterator const_iterator;
接着就是普通函数的实现,这些其实都可以复用哈希表里面实现的函数,这里就不一一讲解了
代码如下:
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin()const
{
return _ht.Begin();
}
const_iterator end()const
{
return _ht.End();
}
pair<iterator, bool> insert(const pair<k, v>& kv)
{
return _ht.Insert(kv);
}
study::hashNode<pair<k, v>> Find(const k& key)
{
return _ht.Find(key);
}
bool Erase(const k& key)
{
return _ht.Erase(key);
}
v& operator[](const k& key)
{
pair<iterator, bool> ret = _ht.Insert({ key, v() });
return ret.first->second;
}
unordered_map 总代码如下:
namespace hjx
{
template<class k, class v, class Hash = HashFunc<k>>
class unordered_map
{
struct KeyOfT
{
const k& operator()(const pair<k, v>& kv)
{
return kv.first;
}
};
public:
typedef typename study::HashTable<k, pair<const k, v>, KeyOfT, Hash>::Iterator iterator;
typedef typename study::HashTable<k, pair<const k, v>, KeyOfT, Hash>::ConstIterator const_iterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin()const
{
return _ht.Begin();
}
const_iterator end()const
{
return _ht.End();
}
pair<iterator, bool> insert(const pair<k, v>& kv)
{
return _ht.Insert(kv);
}
study::hashNode<pair<k, v>> Find(const k& key)
{
return _ht.Find(key);
}
bool Erase(const k& key)
{
return _ht.Erase(key);
}
v& operator[](const k& key)
{
pair<iterator, bool> ret = _ht.Insert({ key, v() });
return ret.first->second;
}
private:
study::HashTable<k, pair<const k, v>, KeyOfT, Hash> _ht;
};
void test_map()
{
unordered_map<string, string> dict;
dict.insert({ "sort", "排序" });
dict.insert({ "left", "左边" });
dict.insert({ "right", "右边" });
dict["left"] = "左边,剩余";
dict["insert"] = "插入";
dict["string"];
//unordered_map<string, string>::iterator it = dict.begin();
//while (it != dict.end())
//{
// // 不能修改first,可以修改second
// //it->first += 'x';
// it->second += 'x';
// cout << it->first << ":" << it->second << endl;
// ++it;
//}
for (auto e : dict)
{
cout << e.first << " " << e.second << endl;
}
dict.Erase("left");
dict.Erase("right");
dict.Erase("sort");
dict.Erase("insert");
for (auto e : dict)
{
cout << e.first << " " << e.second << endl;
}
dict.Erase("string");
cout << endl;
}
}
unordered_set
unordered_map其实大差不差,这里就不进行深入讲解了
代码如下:
#pragma once
#include"HashTable.h"
namespace hjx
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename study::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
typedef typename study::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin() const
{
return _ht.Begin();
}
const_iterator end() const
{
return _ht.End();
}
pair<iterator, bool> insert(const K& key)
{
return _ht.Insert(key);
}
iterator Find(const K& key)
{
return _ht.Find(key);
}
bool Erase(const K& key)
{
return _ht.Erase(key);
}
private:
hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
};
}
结语
看到这里,这篇博客有关哈希表的相关内容就讲完啦~( ̄▽ ̄)~*
如果觉得对你有帮助的话,希望可以多多支持博主喔(○` 3′○)