目录
闭散列的实现
结构
①枚举状态常量
每一个位置除了存储数据以外,还需要存储该位置的状态。
enum State
{
EXIST, //存在数据
EMPTY, //无数据
DELETE //原有的数据已被删除
};
目的:为了能够处理删除和查找的情况。
举个例子:比如我们现在将在下标为1的数字1删除并置为空,那么当我们查找11时,根据哈希函数进行位置映射时,会下标1的位置,而此时的位置被置为空,就不会向后继续查找了,最后返回不存在!可是11被放在了5下标的位置呀!
②位置存储结构
存数据和状态,初始状态默认为空!
template <class K, class V>
struct HashDate
{
pair<K, V> _kv;
State _state = EMPTY;//状态
};
③闭散列表结构
template <class K, class V,class Hash=HashFunc<K>>
class HashTable
{
private:
vector<HashDate<K, V>> _t;//表
size_t _n=0;//有效数据个数;
};
这里第三个参数是一个仿函数,因为除留取余法需要整数相除算出位置,那么对于string类型,该如何转化为整数?直接强转并不可行(自定义类型和内置类型不能直接强转,需要通过显示转化函数才能进行),这就是这个仿函数存在的意义,该仿函数就是能够将字符串string类转化成整形!但是如果仅仅只给出string的仿函数,那其他类型怎么办?所以这里就需要使用模板以及模板特化
先给出适合除了string类的其他类型的通用模板,在此基础上特化出仅适合string类的模板
//适合int,int* ,……
template<class K>
struct HashFuc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化出适合string类的类模板
template<>
struct HashFuc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto a : s)
{
hash *= 131;
hash += a;
}
return hash;
}
};
值得一提的是这里将string转化为整形采用的方法是将所有字符串中的字符ASCII码值依次相加,在乘上因子131!这是一种字符串哈希算法,当然还有很多可以点击☞字符串哈希算法
插入
这里的逻辑就是:
- 通过哈希函数获取关键码key的位置
- 若该位置中没有元素则直接插入新元素,若有发生哈希冲突,使用线性探测找到下一个空位置再放入数据
注意:在哈希一文中提到的数据堆积问题,需要扩容操作,对于闭散列,扩容的条件是:负载因子超过0.7时扩容
这里的扩容核心思路就是开一个比旧表大2倍(不严谨)的新表,将旧表内容重新负载到扩容后的新表中,最后让旧表指向新表即可!负载的步骤和插入的类似,都是先算key在新表的位置,在判断是否冲突,所以这里直接采用复用的手段。实现如下
bool Insert(const pair<K, V>& kv)
{
//不允许重复
if (Find(kv.first))
return false;
//可能会扩容
//负载因子在0.7-0.8之间扩容
if (_n*10 / _t.size() >= 7)
{
HashTable<K, V,Hash> newhash;//新的哈希表
newhash._t.resize(_t.size() * 2);
//重新建立映射关系,直接复用Insert
for (size_t i = 0; i < _t.size(); i++)
{
if (_t[i]._state == EXIST)
{
newhash.Insert(_t[i]._kv);//旧表原本数据到插入新表
}
}
_t.swap(newhash._t);//交换
}
Hash hs;//仿函数
//线性探测
size_t hashi = hs(kv.first) % _t.size();//算出位置
//找到空位置为止
while (_t[hashi]._state == EXIST)
{
++hashi;
//防止越界
hashi %= _t.size();
}
++_n;
_t[hashi]._kv = kv;
_t[hashi]._state = EXIST;
return true;
}
删除
步骤:先找到该关键码在哈希表中位置,不存在就返回空,删除失败;若存在,就将值对应位置的状态置为delete即可!
一定要注意:这里的删除是伪删除,数据并没有被抹除掉,还在表中,只不过状态置为已删除!!下次插入数据可直接覆盖掉!(原因看上文枚举常量部分!)
bool Erase(const K& key)
{
//找
HashDate<K, V>* ret = Find(key);
if (ret == nullptr)
{
return false;//找不到就返回空
}
else
{
ret->_state = DELETE;//状态置为删除
--_n;
return true;
}
}
查找
步骤:首先通过传入的关键码key算出位置,从该位置一直先后找,找到就返回对应位置,直到遇到空位置就停止。
需要注意一点细节,因为删除是伪删除,数据还存在表中,需要多加一条判断,即值存在并且位置状态也是存在才可行!若不加,可能就是造成即使值已经删除,但是还是能找到的现象!
HashDate<K, V>* Find(const K& key)
{
Hash hs;
//找位置
size_t hashi = hs(key) % _t.size();
//找到空就停止
while (_t[hashi]._state != EMPTY)
{
//因为删除是伪删除,所以多加了一条判断
if (_t[hashi]._state== EXIST && _t[hashi]._kv.first == key)
{
return &_t[hashi];
}
++hashi;
hashi %= _t.size();
}
return nullptr;
}
完整实现代码
#include <iostream>
#include <vector>
using namespace std;
enum State
{
EXIST, //存在数据
EMPTY, //无数据
DELETE //原有的数据已被删除
};
template <class K, class V>
struct HashDate
{
pair<K, V> _kv;
State _state = EMPTY;//状态
};
//适合int,int* ,……
template<class K>
struct HashFuc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化出适合string类的类模板
template<>
struct HashFuc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto a : s)
{
hash *= 131;
hash += a;
}
return hash;
}
};
template <class K, class V,class Hash=HashFunc<K>>
class HashTable
{
public:
HashTable()
{
_t.resize(10);
}
bool Insert(const pair<K, V>& kv)
{
//不允许重复
if (Find(kv.first))
return false;
//可能会扩容
//负载因子在0.7-0.8之间扩容
if (_n*10 / _t.size() >= 7)
{
HashTable<K, V,Hash> newhash;//新的哈希表
newhash._t.resize(_t.size() * 2);
//重新建立映射关系,直接复用Insert
for (size_t i = 0; i < _t.size(); i++)
{
if (_t[i]._state == EXIST)
{
newhash.Insert(_t[i]._kv);
}
}
_t.swap(newhash._t);
}
Hash hs;//仿函数
//线性探测
size_t hashi = hs(kv.first) % _t.size();
while (_t[hashi]._state == EXIST)
{
++hashi;
//防止越界
hashi %= _t.size();
}
++_n;
_t[hashi]._kv = kv;
_t[hashi]._state = EXIST;
return true;
}
HashDate<K, V>* Find(const K& key)
{
Hash hs;
//找位置
size_t hashi = hs(key) % _t.size();
//找到空就停止
while (_t[hashi]._state != EMPTY)
{
//因为删除是伪删除,所以多加了一条判断
if (_t[hashi]._state== EXIST && _t[hashi]._kv.first == key)
{
return &_t[hashi];
}
++hashi;
hashi %= _t.size();
}
return nullptr;
}
//伪删除,实际上的数据还在表中
bool Erase(const K& key)
{
//找
HashDate<K, V>* ret = Find(key);
if (ret == nullptr)
{
return false;
}
else
{
ret->_state = DELETE;
--_n;
return true;
}
}
private:
vector<HashDate<K, V>> _t;
size_t _n=0;//有效数据个数;
};
开散列(哈希桶)的实现
实际上哈希桶的实现和闭散列的结构上类似,只不过哈希桶中的每一个桶(位置)都相当于一个单链表,将冲突的数据用链表链接起来,并不需要探测寻找下一个位置,此时无需设置任何状态!
整体结构
template<class K>
struct HashFuc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFuc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto a:s)
{
hash *= 131;
hash += a;
}
return hash;
}
};
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 Hash=HashFuc<K>>
class HashTable
{
typedef HashNode<K,V> Node;
public:
HashTable();
// 哈希桶的销毁
~HashTable();
// 插入值为data的元素,如果data存在则不插入
bool Insert(const pair<K,V>& kv);
// 在哈希桶中查找值为key的元素,存在返回true否则返回false
Node* Find(const K& key);
// 哈希桶中删除key的元素,删除成功返回true,否则返回false
bool Erase(const K& key);
private:
vector<Node*> _h;
size_t _n = 0;
};
插入
整体步骤:
- 根据关键码key算位置
- 若该位置没有元素,直接放进,若有,说明发生冲突,直接采用头插法即可!
同样需要注意扩容问题,在上一文中提到,对于开散列,扩容的条件:负载因子等于1时扩容!
这里扩容的操作和闭散列的有所不同,这里不采用复用的手段(不是说不可以),直接采用复用的话涉及到资源浪费问题,因为在这个过程中我们需要创建相同数据的结点插入表中,插入完毕后还需要将旧表的结点给释放,没必要!所以不采用复用的方式,而是重新开比旧表大二倍的新表,将旧表的结点一个个取下来,重新根据哈希函数计算位置,在插入,冲突就头插法,就这样!
bool Insert(const pair<K,V>& kv)
{
//不能存在重复
if (Find(kv.first))
return false;
//负载因子为1时开始扩容
if (_n == _h.size())
{
vector<Node*> newHash(_h.size() * 2, nullptr);
//遍历旧表
for (size_t i = 0; i < _h.size(); i++)
{
//将旧表的结点一个一个取下来重新映射至新表,节省空间
Node* cur = _h[i];
while (cur)
{
Node* next = cur->_next;
//映射至新表中
Hash hs;
size_t newi = hs(cur->_kv.first) % newHash.size();//哈希函数又称散列函数
//头插法
cur->_next = newHash[newi];
newHash[newi] = cur;
cur = next;
}
//旧表置空
_h[i] = nullptr;
}
//交换两个表的内容即可
_h.swap(newHash);
}
Hash hs;
size_t hashi = hs(kv.first) % _h.size();
//头插
Node* newnode = new Node(kv);
newnode->_next = _h[hashi];
_h[hashi] = newnode;
++_n;
return true;
}
查找
先通过哈希函数算出位置,在进行类似遍历单链表的操作。找到就放回结点位置,找不到就返回空!
Node* Find(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _h.size();
Node* cur = _h[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
删除
实际上就是单链表的删除操作而已!需要注意的是,我们要保存待删除结点的前一个结点位置,为了方便删除结点后再一次链接。
bool Erase(const K& key)
{
Hash hs;
size_t hashi = hs(key) % _h.size();//算位置
Node* prev = nullptr;//保存待删除结点的前一个位置
Node* cur = _h[hashi];//遍历链表
while (cur)
{
if (cur->_kv.first == key)//找到目标节点
{
//删除的是第一个
if (prev == nullptr)
_h[hashi] = cur->_next;
else
prev->_next = cur->_next;
delete cur;
--_n;//数据个数-1
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
析构
因为涉及到手动开空间,所以哈希需要手动写析构函数
操作简单,就是遍历结点一个个释放掉即可,最后在置空!
// 哈希桶的销毁
~HashTable()
{
for (size_t i = 0; i < _h.size(); i++)
{
Node* cur = _h[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_h[i] = nullptr;
}
_n = 0;
}
开散列与闭散列的比较
应用链地址法(开散列)处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法(闭散列)必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用开散列反而比闭散列节省存储空间。
所以实际上哈希桶(开散列)用的比较多,比如C++STL库中的unordered系列容器的底层结构就是哈希桶!!!