什么是哈希
哈希是一种特殊的存储结构,该结构能够通过某种函数使元素的存储位置与它的关键码之间能够建立映射的关系。
哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(散列表)。
哈希冲突
哈希冲突是哈希函数中不可避免的现象。
哈希冲突是指在使用哈希表进行数据的存储和查找时,不同的关键字通过哈希函数计算后得到相同的哈希值。
因为无法彻底解决哈希冲突,所以我们要尽可能设计出一个能将关键字均匀分布到哈希表空间中的的哈希函数。
两种简单的闭散列哈希函数
- 直接定址法:
函数表示: H(key) = x * key + y
优点:简单,均匀
缺点:空间利用率低,在理想状态下即元素是连续的才不会发生碰撞- 除留余数法:
函数表示:Hash(key) = key% p(p<=m),将关键字映射到哈希表地址
m为哈希表中存储元素的数量上限,p为一个不大于哈希表长度m的质数,用于计算余数
优点:简单,适用范围广
闭散列
闭散列也叫开放定址法,是哈希表解决冲突的一种方法。
当发生哈希冲突时,闭散列法使用特定的探测方法来找到下一个可用的空闲位置。探测方法可以是线性的、二次的或伪随机的等。
常用探测方法:
- 线性探测:
从发生冲突的位置开始,依次向后探测,直到找到一个空闲位置为止。线性探测简单易懂,但可能会产生聚集现象(即连续的位置被占用后,后续的插入操作会越来越困难,导致查找时间增加)。- 二次探测:
按照一定的步长进行查找,以减少聚集现象的发生。例如:i + 1²->i + 2²->i + 3²…
如上图所示,当数据插入越多,冲突率越高,所以我们需要扩容,而扩容的判断条件,我们称作负载因子
负载因子越大,产生冲突的概率越高,增删查改的效率越低。负载因子越小,产生冲突的概率越低,增删查改的效率越高。一般负载因子超过0.7就需要进行扩容。
开散列
开散列也叫链地址法(开链法),也是一种解决哈希表的方法。在哈希表中,具有相同地址的关键字归于一个集合,每个集合称为桶,桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中,所以又叫哈希桶。
开散列的负载因子没有限制,可以大于1。
负载因子越大,哈希冲突的概率越高,空间利用率越低。负载因子越小,哈希冲突的概率越低,空间利用率越低。
功能实现
哈希表的主要功能有:
- 插入
- 查找
- 删除
哈希表的结构
闭散列
在闭散列中,删除数据不能用覆盖的方式,应采用状态标记进行伪删除。
如果采用覆盖方式进行删除,即直接删除哈希表中某个位置的元素并用新元素覆盖它,那么将会破坏哈希表的完整性。因为其他元素可能依赖于这个位置的空闲状态来解决冲突,或者这个位置的元素可能与其他元素通过探测过程形成了某种“链式”关系。直接覆盖将会导致这些关系断裂,使得哈希表无法正确地进行查找和插入操作。
enum State
{
EMPTY,//不存在数据
EXIST,//存在数据
DELETE//已删除
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K, class V, class Hash = HashFunc<K>>// 模版特化,插入内容中会有详细介绍
class HashTable
{
typedef HashData<K, V> Node;
public:
//具体实现
private:
vector<Node> _table;//哈希表
size_t _n = 0;//有效数据个数
};
开散列
参考链表结构
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
,_next(nullptr)
{}
};
template<class K, class V>
class HashTable
{
typedef HashData<K, V> Node;
public:
//具体实现
private:
vector<Node*> _table;//哈希表
size_t _n = 0;//有效数据个数
};
插入
闭散列
在插入以前,我们要考虑不同类型的数据插入,全部转换为整数处理,比较特殊的是string类型,因为string类型并不能直接转换为整数。
我采用模版特化的方式,为string类型提供一个特定的哈希函数实现。
template<class K>
struct HashFunc //仿函数,将其他类型转化为整形
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<> //特化
struct HashFunc<string> //string类的不可以直接转化为整形,需要特殊处理
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 31;
hash += e;
}
return hash;
}
};
bool insert(const K& kv)
{
size_t n = _table.size();
//检查是否扩容
if (10 * _n / n >= 7)
{
//直接建立一个新表
HashTable<K, V, Hash> newtable;
newtable._table.resize(n * 2);
//复用插入的逻辑
for (size_t i = 0; i < n; i++)
{
if (_table[i]._state == EXIST)//该位置有元素才转移
{
newtable.insert(_table[i]._kv);
}
}
//交换新旧表
_tables.swap(newtable._tables);;
}
//根据哈希函数,计算位置
Hash hs;
size_t hashi = hs(kv) % n;
//检查是否有哈希冲突问题
while (_table[hashi]._state == EXIST)
{
++hashi;
hashi %= n;//实现下标的环绕
}
//插入元素,修改状态
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
return true;
}
开散列
因为开散列使用的是链表来存储相同哈希值的键值对,每个桶里都有一个指向链表的指针,所以我们对于每个键值对都需要动态地分配内存,那么就需要构造和析构函数来管理内存。
HashTable()
:_tables(11)
, _n(0)
{}
~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;
}
}
bool insert(const pair<K, V>& kv)
{
Hash hs;
size_t size = _table.size();
//检查扩容
if (_n == size)//节点个数等于桶的数量时,进行扩容
{
//为了节省开销,不再重新开辟新节点,直接映射原来的节点,将原来的映射取消
vector<Node*> newtable(size * 2, nullptr);
size_t newsize = newtable.size();
for (size_t i = 0; i < size; i++)
{
Node* cur = _table[i];
while (cur)
{
size_t hashi = hs(cur->_kv) % newsize;//元素对应的新表中的位置
Node* next = cur->_next;//记录当前桶的下一个元素
//头插连接到新桶
cur->_next = newtable[hashi];
newtable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
swap(_table, newtable);
}
size_t hashi = hs(kv) % _table.size();
//头插连接
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return true;
}
查找
闭散列
直接用线性探测一一比较就行,如果查找到空了,就是没找到
Node* find(const K& key)
{
//根据key获取表中的位置
Hash hs;
size_t hashi = hs(key) % _table.size();
while (_table[hashi]._state != EMPTY)
{
if (_table[hashi]._state == EXIST
&& _table[hashi]._data == key)
{
return &_table[hashi];
}
hashi++;
hashi %= _table.size();
}
return nullptr;
}
开散列
开散列就遍历链表好了
Node* find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_data == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
删除
闭散列
进行伪删除
bool erase(const K& key)
{
int hashi = find(key);
if (hashi != -1)
{
_table[hashi]._state = DELETE;//状态设置为删除
--_n;//个数减少
return true;
}
return false;
}
开散列
这里删除要分两种种情况
- 当前桶是否只有这一个元素
- 多个元素时,删除链表中的节点要连接删除节点前后的节点(要记录前驱节点)
bool erase(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_data == key)
{
if (prev == nullptr)//桶中只有一个元素
{
_table[hashi] = nullptr;
}
else
{
prev->_next = cur->_next;//连接前驱和后继节点
}
delete cur;
_n--;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
完整代码
闭散列
enum State
{
EMPTY,//不存在数据
EXIST,//存在数据
DELETE//已删除
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K>
struct HashFunc //仿函数,将其他类型转化为整形
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<> //特化
struct HashFunc<string> //string类的不可以直接转化为整形,需要特殊处理
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 31;
hash += e;
}
return hash;
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashData<K, V> Node;
public:
//具体实现
bool insert(const K& kv)
{
size_t n = _table.size();
//检查是否扩容
if (10 * _n / n >= 7)
{
//直接建立一个新表
HashTable<K, V, Hash> newtable;
newtable._table.resize(n * 2);
//复用插入的逻辑
for (size_t i = 0; i < n; i++)
{
if (_table[i]._state == EXIST)//该位置有元素才转移
{
newtable.insert(_table[i]._kv);
}
}
//交换新旧表
_tables.swap(newtable._tables);;
}
//根据哈希函数,计算位置
Hash hs;
size_t hashi = hs(kv) % n;
//检查是否有哈希冲突问题
while (_table[hashi]._state == EXIST)
{
++hashi;
hashi %= n;//实现下标的环绕
}
//插入元素,修改状态
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
return true;
}
Node* find(const K& key)
{
//根据key获取表中的位置
Hash hs;
size_t hashi = hs(key) % _table.size();
while (_table[hashi]._state != EMPTY)
{
if (_table[hashi]._state == EXIST
&& _table[hashi]._data == key)
{
return &_table[hashi];
}
hashi++;
hashi %= _table.size();
}
return nullptr;
}
bool erase(const K& key)
{
int hashi = find(key);
if (hashi != -1)
{
_table[hashi]._state = DELETE;//状态设置为删除
--_n;//个数减少
return true;
}
return false;
}
private:
vector<Node> _table;//哈希表
size_t _n = 0;//有效数据个数
};
开散列
class HashTable
{
typedef HashData<K, V> Node;
public:
HashTable()
:_tables(11)
, _n(0)
{}
// 拷贝构造和赋值重载也需要
~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;
}
}
//具体实现
typedef HashNode<K, V> Node;
bool insert(const pair<K, V>& kv)
{
Hash hs;
size_t size = _table.size();
//检查扩容
if (_n == size)//节点个数等于桶的数量时,进行扩容
{
//为了节省开销,不再重新开辟新节点,直接映射原来的节点,将原来的映射取消
vector<Node*> newtable(size * 2, nullptr);
size_t newsize = newtable.size();
for (size_t i = 0; i < size; i++)
{
Node* cur = _table[i];
while (cur)
{
size_t hashi = hs(cur->_kv) % newsize;//元素对应的新表中的位置
Node* next = cur->_next;//记录当前桶的下一个元素
//头插连接到新桶
cur->_next = newtable[hashi];
newtable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
swap(_table, newtable);
}
size_t hashi = hs(kv) % _table.size();
//头插连接
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return true;
}
Node* find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi];
while (cur)
{
if (cur->_data == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
bool erase(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_data == key)
{
if (prev == nullptr)//桶中只有一个元素
{
_table[hashi] = nullptr;
}
else
{
prev->_next = cur->_next;//连接前驱和后继节点
}
delete cur;
_n--;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
private:
vector<Node*> _table;//哈希表
size_t _n = 0;//有效数据个数
};