欢迎来到博主的专栏:数据结构
博主ID:代码小豪
开散列式哈希表
前文提到,解决哈希冲突的方法分为闭散列和开散列,由于开散列的底层原理与闭散列的哈希表的底层原理不同,因此博主重新开一篇文章讲解。
开散列法又叫链地址法(开链法),首先对key值进行哈希函数计算出映射地址,具有相同地
址的key值归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
这样说好像有点抽象了,简单来说就是哈希表中存的不再是元素,而是一个指向链表的指针,这个链表存的是具有相同映射地址的key值。若某个地址不存在任何元素,则记为nullptr。
这种将多个相同映射值的数据装在一个集合的哈希结构也叫做哈希桶,即哈希表的每个元素都是一个桶,如果桶不存在任何元素,也就是nullptr。
开散列式哈希表的结构
关于哈希表的底层,博主仍然采用vector作为底层,根据上述的内容,该vector容器应该存指向链表头结点的指针,而节点应该存一个数据,还有指向下一个节点的指针。
节点的结构如下:
template<class T>
struct hash_Node
{
hash_Node(const T& data)
:_data(data)
,_next(nullptr)
{}
T _data;
hash_Node<T>* _next;
};
哈希表的结构如下:
template<class key,class value>
class hash_tab
{
public:
typedef pair<key, value> value_type;
typedef hash_Node<value_type> Node;
hash_tab()
{
_tab.resize(10);
}
private:
vector<Node<value_type>*> _tab;//哈希表
size_t _n;//有效数据个数
};
insert
先使用哈希函数求出待插入元素的映射值,然后将节点头插在该位置。
插入方法如下:
(1)求出映射值
(2)在地址处将节点头插在对应的链表处
可以发现,开散列式的哈希表不需要线性探测,即使发生了哈希冲突,插入的效率依然是O(1)。插入的效率非常高。但是查找的效率却并非如此,比如在上例中查找34,需要遍历链表,因此插入的效率依然与负载因子挂钩,
负载因子=有效数据个数%哈希表的可容纳地址总数
由于开散列式哈希表空间利用率远高于闭散列,因此通常负载因子会控制在1以上。博主采用1作为负载因子的限制值。当负载因子超过1时,就要对哈希表进行扩容。
开散列式的哈希表依然采用异地扩容的战略,理由与闭散列式的哈希表一致:
哈希表扩容之后,key值的映射值也随之改变,因此需要重新插入。
异地扩容的方法如下:
(1)建立新表
(2)将旧表的内容移动到新表当中
(4)将旧表的所有指针置为nullptr
(3)交换新旧两表
bool insert(const value_type& val)
{
if (_n / _tab.size() >= 1)//如果负载因子>=1就要扩容
{
hash_tab<key, value> newtab;
newtab._tab.resize(2 * _tab.size());//2倍扩容
for (int i = 0; i < _tab.size(); i++)//遍历旧表
{
Node* cur = _tab[i];
while (cur != nullptr)
{
Node* next = cur->_next;
newtab.insert(cur);
}
_tab[i] = nullptr;
}
_tab.swap(newtab._tab);//交换新旧两表
}
size_t hashnum = val.first % _tab.size();
Node* newnode = new Node(val);
newnode->_next = _tab[hashnum];
_tab[hashnum] = newnode;
_n++;
}
private:
bool insert(Node* newnode)//重载了一个插入节点的版本
{
size_t hashnum = newnode->_data.first % _tab.size();
newnode->_next = _tab[hashnum];
_tab[hashnum] = newnode;
_n++;
return true;
}
为了支持插入节点,博主重载了一个insert函数,这个函数由于只用于将旧表的节点转移到新表,因此将接口隐藏起来。
find和erase
find函数的原理如下:
通过计算出key值的映射位置,在映射的桶中查找对应key值的数据,返回该节点。
Node* find(const key& key)
{
size_t hashnum = key % _tab.size();
Node* cur = _tab[hashnum];
while (cur != nullptr)
{
if (cur->_data.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
erase函数的原理如下:
通过find函数找到待删除的节点,然后删除该节点,删除节点的操作和链表删除节点的操作一致,博主不多赘述。
析构函数
由于开散列式哈希表的节点是开辟在堆区上的(new出来的),因此需要对这些数据进行销毁,否则就会导致内存泄漏。因此我们需要为哈希表设计一个析构函数。
析构函数的思路如下:
(1)遍历整个表的哈希桶
(2)将每个哈希桶内的数据释放
(3)释放后将哈希桶置为空桶。
~hash_tab()
{
for (size_t i=0;i<_tab.size();i++)
{
Node* cur = _tab[i];
while (cur != nullptr)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tab[i] = nullptr;
_n = 0;
}
}