文章目录
1. 底层结构
1.1 哈希函数
- 直接定址法–(常用)取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况
关键字-存储位置是一对一的关系,不存在哈希冲突 - 除留余数法–(常用)设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
关键字-存储位置是多对一的关系,存在哈希冲突
1.2 哈希冲突
由于除留余数法会导致不同的值映射到同一个位置上,这种现象就叫做哈希冲突,解决方法:闭散列和开散列
1.3闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
1.3.1 线性探测
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
以下是几个概念
插入: 通过哈希函数获取待插入元素在哈希表中的位置如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
负载因子: 储存的关键字个数 / 空间大小。
负载因子太大,冲突可以会剧增,冲突增加,效率降低
负载因子太小,冲突降低,但是空间利用率就低了。一般设置为0.7
1.3.2 线性探测的简单实现
enum Status
{
/*
* 删除状态的意义:
1、再插入,这个位置可以覆盖值!
2、防止后面冲突的值,出现找不到的情况。遇到删除状态,还是继续往后找
*/
EMPTY,
EXIST,
DELETE
};
template<class K, class V>
struct HashData
{
Status _s;
pair<K, V> _kv;
};
template<class K, class V>
class HashTable
{
public:
HashTable(int sz = 10)
{
_table.resize(sz);
_n = 0;
}
bool Insert(const pair<K, V> kv)
{
// 已经有这个值了,就不需要插入了
if (Find(kv.first)) return false;
size_t sz = _table.size();
// 负载因子到0.7就扩容
if (_n * 10 / sz >= 7) {
/*
* 不能直接扩容,因为值的映射关系已经发生变化
* 而是应该开新空间,重新映射,再释放旧空间
*/
size_t newSz = sz * 2;
HashTable* newHT = new HashTable(newSz);
for (size_t i = 0; i < sz; ++i) {
if (_table[i]._s == EXIST) {
newHT->Insert(_table[i]._kv);
}
}
_table.swap(newHT->_table);
}
// 线性探测
size_t hashI = kv.first % sz;
while (_table[hashI]._s == EXIST) {
hashI++;
hashI %= sz; // 防止越界
}
_table[hashI]._kv = kv;
_table[hashI]._s = EXIST;
++_n;
return true;
}
bool Erase(const K& key) {
HashData<K, V>* ret = Find(key);
if (ret) {
// 伪删除法
--_n;
ret->_s = DELETE;
return true;
}
// 没有找到该值,直接返回false
return false;
}
HashData<K, V>* Find(const K& key)
{
size_t sz = _table.size();
size_t hashI = key % sz;
while (_table[hashI]._s != EMPTY) {
/*
* 不要这样写,要判断一下这个元素是否是删除的元素。
* 否则将一个删除的元素查找后还存在
*/
/*if (_table[hashI]._kv.first == key)*/
if(_table[hashI]._s != DELETE and _table[hashI]._kv.first == key)
return &_table[hashI];
hashI++;
hashI %= sz;
}
// 没有找到
return nullptr;
}
// 这个函数仅仅是为了方便Debug
void Print()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._s == EXIST) {
printf("[%d]->%d\n", i, _table[i]._kv.first);
}
else if (_table[i]._s == EMPTY) {
printf("[%d]->EMPTY\n", i);
}
else {
printf("[%d]->DELETE\n", i);
}
}
}
private:
vector<HashData<K, V>> _table;
size_t _n; // 哈希表中储存的关键字的个数
};
线性探测优点:实现简单
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?可以使用二次探测
1.3.3 字符串哈希
当key为string时,取模就会有问题,此时需要将字符串映射为某一个数字。修改原来的代码,加上仿函数
// 能被直接转成size_t的仿函数
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// string的仿函数
struct HashFuncString
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (const auto& e : key) {
hash = hash * 131 + e;
}
return hash;
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
// ...
Hash hs;
size_t hashI = hs(kv.first) % sz;
// ...
}
此时,若有如下的代码,就可以正常运行(修改了一下Print()函数)
// 统计次数
void test()
{
string a[] = { "苹果", "苹果", "菠萝", "葡萄", "西瓜", "菠萝", "菠萝", "葡萄" , "葡萄" , "葡萄" };
HashTable<string, int, HashFuncString> ht;
for (const auto& e : a) {
// auto ret = ht.Find(e);
HashData<string, int>* ret = ht.Find(e);
if (ret) ret->_kv.second++;
else ht.Insert({ e, 1 });
}
ht.Print();
}
但是这样传string就要多传一个参数,很烦,所以可以使用类模版的全特化。
// 能被直接转成size_t的仿函数
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 (const auto& e : key) {
hash = hash * 131 + e;
}
return hash;
}
};
这样,当定义一个key为string的HashTable
对象时,就只需要传递两个参数,而不需要传递第三个仿函数了
1.4 开散列
1.4.1 开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
1.4.2 开散列简单实现
namespace OpenHashing
{
template <class K, class V>
struct HashNode
{
HashNode(const pair<K, V>& kv)
: _kv(kv)
, _next(nullptr)
{}
HashNode* _next;
pair<K, V> _kv;
};
template <class K, class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable(int sz = 10)
{
_table.resize(sz);
_n = 0;
}
HashTable(const HashTable& ht)
{
size_t sz = ht._table.size();
_table.resize(sz, nullptr);
for (size_t i = 0; i < sz; i++) {
Node* cur = ht._table[i];
while (cur) {
Insert(cur->_kv);
cur = cur->_next;
}
}
}
~HashTable()
{
// 链表的空间需要手动释放,所以需要手动写析构函数
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
bool Insert(const pair<K, V>& kv)
{
// 如果已经有该元素了,返回false
if (Find(kv.first)) return false;
size_t sz = _table.size();
// 负载因子设置到1
if (_table.size() == _n)
{
size_t newSz = sz * 2;
HashTable* newHT = new HashTable(newSz);
for (size_t i = 0; i < sz; ++i) {
Node* cur = _table[i];
while (cur) {
newHT->Insert(cur->_kv);
cur = cur->_next;
}
}
_table.swap(newHT->_table);
}
size_t hashI = kv.first % sz;
Node* newNode = new Node(kv);
// 头插
newNode->_next = _table[hashI];
_table[hashI] = newNode;
++_n;
return true;
}
Node* Find(const K& key)
{
return nullptr;
}
private:
vector<Node*> _table;
size_t _n;
};
}
1.4.3 扩容
上面写的扩容效率太低,每次都需要调用析构函数释放之前链表的节点,很麻烦,所以可以让之前链表的节点“挪动”下来、
if (_table.size() == _n)
{
size_t newSz = sz * 2;
vector<Node*> newTable;
newTable.resize(newSz, nullptr);
for (size_t i = 0; i < sz; ++i) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
size_t hashI = cur->_kv.first % newSz;
// 头插到新链表
cur->_next = newTable[hashI];
newTable[hashI] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
1.4.4 查找和删除
比较好写
Node* Find(const K& key)
{
size_t hashI = key % _table.size();
Node* cur = _table[hashI];
while (cur) {
if (cur->_kv.first == key) return cur;
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
size_t hashI = key % _table.size();
Node* cur = _table[hashI], *prev = nullptr;
while (cur) {
if (cur->_kv.first == key) {
if (prev == nullptr) _table[hashI] = cur->_next;
else prev->_next = cur->_next;
}
delete cur;
prev = cur;
cur = cur->_next;
}
}
1.4.5 完整代码
namespace OpenHashing
{
template <class K, class V>
struct HashNode
{
HashNode(const pair<K, V>& kv)
: _kv(kv)
, _next(nullptr)
{}
HashNode* _next;
pair<K, V> _kv;
};
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable(int sz = 10)
{
_table.resize(sz);
_n = 0;
}
~HashTable()
{
// 链表的空间需要手动释放,所以需要手动写析构函数
for (size_t i = 0; i < _table.size(); i++) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
bool Insert(const pair<K, V>& kv)
{
// 如果已经有该元素了,返回false
if (Find(kv.first)) return false;
size_t sz = _table.size();
// 负载因子设置到1
if (_table.size() == _n)
{
size_t newSz = sz * 2;
vector<Node*> newTable;
newTable.resize(newSz, nullptr);
for (size_t i = 0; i < sz; ++i) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
size_t hashI = hf(cur->_kv.first) % newSz;
// 头插到新链表
cur->_next = newTable[hashI];
newTable[hashI] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
size_t hashI = hf(kv.first) % sz;
Node* newNode = new Node(kv);
// 头插
newNode->_next = _table[hashI];
_table[hashI] = newNode;
++_n;
return true;
}
Node* Find(const K& key)
{
size_t hashI = hf(key) % _table.size();
Node* cur = _table[hashI];
while (cur) {
if (cur->_kv.first == key) return cur;
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K& key)
{
size_t hashI = hf(key) % _table.size();
Node* cur = _table[hashI], *prev = nullptr;
while (cur) {
if (cur->_kv.first == key) {
if (prev == nullptr) _table[hashI] = cur->_next;
else prev->_next = cur->_next;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _table;
size_t _n;
Hash hf;
};
}