0.前言
众所周知,哈希表具有非常高效的查询效率,那 你是否清楚哈希表的工作原理呢?你是否想要实现一个自己的哈希表?这篇文章以图片结合代码的方式详细讲解如何通过闭散列的方式实现一个哈希表。带领大家将哈希表设计成类模板的形式,以适应各种类型的数据。
1.总设计图
- 先大概浏览一下整体设计图,做到心中有数
2.设计思路
2.1HashTable的设计
要实现哈希表,首先要清楚哈希表中有什么?哈希表要实现成类,类里面无非就是 成员变量和成员方法,那 我们首先要清楚成员变量和成员方法分别有什么?如下图所示:
对于成员变量:大致有三个成员变量分别是_table、_n、_toInt。
- _table:这是一个vector<HashData> 类型的 _table 来充当哈希表的主体;(HashData 是哈希表中存放的数据类型,后面详细讲解)
- _n:用size_t类型的 _n 记录哈希表中的有效的数据元素的个数;
- _toInt:用_toInt这个仿函数对象将各种各样类型的数据 映射成整数(因为元素到存储位置的映射关系是由 除留余数法 来确定的)
对于成员方法:众所周知,成员方法是对成员变量的操作,这些操作无非就是 增 删 查 改;但是我们并没有实现 修改 操作,这是因为 哈希表中元素的值和其存储位置是 一 一对应的,如果贸然修改某个元素的值,必然导致查找该元素时,会发生错误;如果非要修改某个值,在哈希表中其实相当于 删除旧值,插入新值。
- 查找:HashData<K,V>* Find(const K& key);
- 插入:bool Insert(const std::pair<K,V>& kv_data);
- 删除:bool Erase(const K& key);
2.2HashData的设计
HashData是哈希表中存放的数据类型,为了适应各种各样类型的数据,HasData也是设计成 类模板;那HashData中有什么呢?HashData中主要有_state和_kv_data;HashData结构如下图所示:
_state成员:_state成员主要用来标记当前HashData的状态,有三个不同的取值,代码实现如下图所示:
- EMPTY:标记当前位置为空。
- EXIST:标记当前位置为存在。
- DELETE:标记当前位置已删除。
- 为什么要设计这个枚举类型呢?如下图,当删除14,查找15,会发生什么?
- 由于线性探测的原因,往后找到第一个空的位置,就认为没有找到,因此,删除14,查找15会导致查找出错,状态标记可以解决该问题;使用状态标记之后,找到状态为EMPTY才停止,避免了上述错误。
_kv_data成员:将存储元素设置成 Key-Value 结构,Key_Value 结构会了,实现Key结构并不难!!!
- _kv_data是 std::pair<K,V> 类型的键值对数据。前面所说的元素就是指K类型的 Key 元素,Key元素作为整个哈希表中的主键。
3.各个结构实现代码
3.1哈希表中的数据类型 及 数据的状态实现代码
// 哈希表中数据的状态
enum State
{
EMPTY,
EXIST,
DELETE
};
// 哈希表中的数据类型
template<class K, class V>
struct HashData
{
std::pair<K, V> _kv_data;
State _state = EMPTY;
};
3.2不同类型的数据转整形的仿函数 代码(其他的自定义类型需要自己提供仿函数)
// 默认的数据转int类型的仿函数
template<class K>
struct toIntFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 字符串数据转int类型的仿函数
struct StringToIntFunc
{
size_t operator()(const std::string& str)
{
size_t ret = 0;
for (auto& ch : str)
{
ret += ch;
ret *= 131;
}
return ret;
}
};
3.3哈希表模板代码
// 哈希表
template<class K, class V = std::string, class func_t = toIntFunc<K> >
class HashTable
{
public:
HashTable(size_t size = 10)
{
_table.resize(10);
}
// 查找
HashData<K, V>* Find(const K& key)
{}
// 插入
bool Insert(const std::pair<K, V>& kv_data)
{}
// 删除
bool Erase(const K& key)
{}
private:
std::vector<HashData<K, V> > _table;
size_t _n = 0; // 记录插入的数据个数
func_t _toInt; // 将Key值转换成整数的仿函数对象
};
4.成员方法的具体实现
4.1查找代码
HashData<K, V>* Find(const K& key)
{
int hashi = _toInt(key) % _table.size();
// 只要不为空,就继续往后找
while (_table[hashi]._state != EMPTY)
{
if (key == _table[hashi]._kv_data.first && EXIST == _table[hashi]._state)
{
return &_table[hashi];
}
hashi++;
hashi %= _table.size(); // 当hashi走到末尾时,需要回绕
}
return nullptr;
}
4.2插入代码
bool Insert(const std::pair<K, V>& kv_data)
{
if (Find(kv_data.first))
return false;
// 判断是否需要扩容
if (_n * 10 / _table.size() > 7)
{
HashTable<K, V, func_t> newHashTable(2 * _table.size()); // 二倍扩容
// 遍历旧表,插入新表
for (auto hash_data : _table)
{
newHashTable.Insert(hash_data._kv_data);
}
// 新旧交换
_table.swap(newHashTable._table);
}
// 线性探测找插入位置
size_t hashi = _toInt(kv_data.first) % _table.size();
while (_table[hashi]._state == EXIST)
{
hashi++;
hashi %= _table.size(); // 走到结尾需要往回绕
}
// 一定可以找到一个可以插入的位置,因为负载因子会控制在 0.7左右
// 空间不会满,就一定有插入的位置
_table[hashi]._kv_data = kv_data;
_table[hashi]._state = EXIST;
++_n;
return true;
}
4.3删除代码
bool Erase(const K& key)
{
HashData<K, V>* ptr = Find(key);
if (ptr != nullptr);
{
_n--;
ptr->_state = DELETE; // 删除也只是状态的变化
return true;
}
return false;
}