目录
哈希的介绍
哈希冲突
原因
影响
解决方法
实例
哈希函数
哈希函数设计原则:
常见哈希函数
闭散列
线性探测的实现
代码解读
1. 命名空间和枚举定义
2. 哈希表节点结构体
3. 哈希函数模板
4. 哈希表类
5. 插入、查找和删除逻辑
二次探测
哈希的介绍
O(logN),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
这就是哈希思想的体现。
哈希冲突
哈希冲突(哈希碰撞)是指在使用哈希表(或哈希函数)的过程中,两个或多个不同的输入值(键)通过哈希函数映射到同一个输出值(哈希值)的情况。这是哈希表实现中的一个基本问题,因为理想的哈希函数应该能够为每个可能的键生成唯一的哈希值,但实际上这是不可能的,因为键的空间通常远大于哈希值的范围。我们把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
以下是哈希冲突的几个关键点:
原因
-
有限的范围:哈希函数通常将输入映射到一个有限的整数范围,而可能的输入(键)的数量是无限的,这导致必然会有多个输入映射到同一个输出。
-
哈希函数设计:如果哈希函数设计不当,可能会增加冲突的概率。一个好的哈希函数应该尽可能均匀地分布键。
影响
-
性能下降:哈希冲突会导致哈希表的性能下降,因为需要额外的步骤来解决冲突,这可能会增加查找、插入和删除操作的时间复杂度。
-
数据结构复杂化:为了处理冲突,哈希表通常需要额外的数据结构和算法,如链表法(separate chaining)或开放寻址法(open addressing)。
解决方法
-
链表法:每个哈希桶(bucket)维护一个链表,所有映射到同一个哈希值的键都存储在这个链表中。当发生冲突时,只需将新键插入到对应链表中。
-
开放寻址法:当发生冲突时,哈希表会寻找下一个空闲的槽位来存储冲突的键。这可以通过线性探测(linear probing)、二次探测(quadratic probing)或双重哈希(double hashing)等方法实现。
-
再哈希:当哈希表中的元素太多,导致冲突率上升时,可以通过增加哈希表的大小并重新计算所有元素的哈希值来减少冲突。
-
更好的哈希函数:设计或选择能够更均匀分布键的哈希函数,可以减少冲突的概率。
实例
假设有一个简单的哈希函数 h(k) = k % m
,其中 k
是键,m
是哈希表的大小。如果 m = 10
,那么键 15
和 25
都会映射到同一个哈希值 5
,因为 15 % 10 = 5
和 25 % 10 = 5
。
哈希冲突是哈希表实现中不可避免的问题,但通过合理的设计和策略,可以有效地管理和减少它们的影响。
哈希函数
哈希函数设计原则:
常见哈希函数
本文着重介绍闭散列的实现与机制。
闭散列
线性探测的实现
#pragma once
#include <utility>
#include <string>
#include <vector>
using namespace std;
namespace open_address //闭散列:开放寻址法
{
enum Status
{
DELETE,
EMPTY,
EXIST
};
template<class K, class V>
struct HashDate
{
pair<K, V> _data;
Status _status;
};
//可以进行int类型的强转
template<class K>
struct HashFunc
{
size_t operator()(const K& key) //传入键值,返回哈希值(映射到哈希表的位置)
{
return size_t(key);
}
};
//string类型
template<> //模板特化,需要保留<>
struct HashFunc<string> //特化成<string>
{
size_t operator()(const string& key) //传入键值,返回哈希值(映射到哈希表的位置)
{
// BKDR
size_t hash = 0;
for (auto& e : key)
{
hash *= 31;
hash += e;
}
return hash;
}
};
template<class K, class V, class HashFunc = HashFunc<K>>
class HashTable
{
public:
HashTable()
{
_t.resize(10); //初始分配10个HashDate的空间
_n = 0;
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
if (n * 10 / _t.size() >= 7) //装载因子大于70%,扩容
{
//开一个新表,重新映射
HashTable<K, V> newtable;
size_t newsize = _t.size() * 2;
newtable._t.resize(newsize);
//遍历旧表,重新映射
for (size_t i = 0; i < _t.size(); i++)
{
if (_t[i]._status == EXIST)
{
newtable.Insert(_t[i]._data); //调用newtable的Insert
}
}
_t.swap(newtable._t); //交换表
} //先执行扩容,再执行插入,所以不需要++_n
HashFunc hf;
size_t hashi = hf(kv.first) % _t.size(); //除留余数法求哈希值。
/*要保证哈希值在0~_t.size()-1之间,即需要保证哈希值在可用空间范围之内。
如果%capacity,可能导致得到的哈希值大于哈希表的最大长度?*/
while (_t[hashi]._status == EXIST) //出现碰撞,进行探测
{
hashi++;
hashi %= _t.size(); //循环到头,重新开始,避免无限循环、越界
}
_t[hashi]._data = data;
_t[hashi]._status = EXIST;
_n++; //键值个数+1
return true;
}
HashDate<K, V>* Find(const K& key) //1.要么直接找到2.要么出现冲突,再empty之前探测
{
HashFunc hf;
size_t hashi = hf(key) % _t.size();
while (_t[hashi]._status != EMPTY)
{
if (_t[hashi]._data.first == key && _t[hashi]._status == EXIST) //防止伪删除,不能进入DELETE状态查找
return &_t[hashi];
hashi++;
hashi %= _t.size(); //循环到头,重新开始,避免无限循环、越界
}
return nullptr;
}
//伪删除,只是标记状态为DELETE,不释放空间
bool Erase(const K& key)
{
HashDate<K, V>* ret = Find(key);
if (ret) //存在(找不到返回空)
{
ret->_status = DELETE;
_n--; //键值个数-1(删除需要--_n)
return true;
}
else
return false;
}
private:
vector<HashDate<K, V>> _t; //闭散列,存储数据
size_t _n; //键值的个数
};
}
代码解读
这段代码实现了一个基于开放寻址法的哈希表,以下是对代码的详细解读:
1. 命名空间和枚举定义
namespace open_address {
enum Status {
DELETE,
EMPTY,
EXIST
};
open_address
命名空间用于包含所有与开放寻址法哈希表相关的类和函数。Status
枚举用于表示哈希表中每个槽的状态,包括已删除(DELETE)、空(EMPTY)和存在(EXIST)。
2. 哈希表节点结构体
template<class K, class V>
struct HashDate {
pair<K, V> _data;
Status _status;
};
HashDate
是一个模板结构体,用于表示哈希表中的一个节点,包含一个键值对_data
和一个状态_status
。
3. 哈希函数模板
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 hash = 0;
for (auto& e : key) {
hash *= 31;
hash += e;
}
return hash;
};
};
HashFunc
是一个模板结构体,用于生成哈希值。它对K
类型进行特化,默认实现是将键值转换为size_t
。- 对于
string
类型,HashFunc
进行了特化,使用 BKDR 哈希算法来生成哈希值。
4. 哈希表类
template<class K, class V, class HashFunc = HashFunc<K>>
class HashTable {
public:
HashTable() {
_t.resize(10);
_n = 0;
}
bool Insert(const pair<K, V>& kv) {
// ...(插入逻辑)
}
HashDate<K, V>* Find(const K& key) {
// ...(查找逻辑)
}
bool Erase(const K& key) {
// ...(删除逻辑)
}
private:
vector<HashDate<K, V>> _t;
size_t _n;
};
HashTable
是一个模板类,用于实现基于开放寻址法的哈希表。- 构造函数初始化一个大小为10的
vector
来存储哈希表节点,并设置键值对数量_n
为0。 Insert
方法用于插入键值对,如果装载因子超过70%,则进行扩容。Find
方法用于查找键值对,如果找到则返回指针,否则返回nullptr
。Erase
方法用于删除键值对,实际上是进行伪删除,即将状态设置为DELETE
。- 私有成员
_t
是一个vector
,用于存储哈希表节点,_n
用于记录当前键值对的数量。
5. 插入、查找和删除逻辑
- 插入逻辑中,如果找到相同的键,则返回
false
。如果装载因子超过70%,则进行扩容,然后使用哈希函数找到空槽插入新节点。 - 查找逻辑中,使用哈希函数找到键对应的槽,然后进行线性探测直到找到空槽或匹配的键。
- 删除逻辑中,使用查找逻辑找到节点,然后将其状态设置为
DELETE
并减少键值对数量。
这段代码是一个简单的哈希表实现,它通过开放寻址法解决了哈希冲突,并且提供了基本的插入、查找和删除操作。
二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题