当前位置: 代码迷 >> 综合 >> STL hashtable
  详细解决方案

STL hashtable

热度:52   发布时间:2023-09-29 14:46:51.0

SGI版本的STL中的哈希表以及hash_map和hash_set都没有被算入C++标准中的,所以在C++标准中的unorder_map和unorder_set才是最标准化的,其中底层使用的hashtable和我这篇文章中讲的hashtable区别不大,所以在文章的最后,我会对比一下SGI版本的哈希表和当下C++标准中的哈希表的区别

一、哈希表概述

哈希表同样是一个键值对应一个实值,但是哈希表找到表中想要的实值并不需要像红黑树一般使用对数时间搜索,因为哈希表中实值和键值是有一个对应函数的,这种函数被称为散列函数,通过实值可以求得键值。如果知道了键值,也不需要想红黑树一样从根节点开始往下查找,而是可以像数组一样直接找到键值对应的内存。其实也就是使用了连续内存的功劳。个人理解哈希表相比于红黑树,是用空间来换取时间。
在哈希表中有两个问题比较核心,一个问题是键值和实值如何对应,也就是散列函数是何种形式,能够让散列表中的空间利用率更高,或者让哈希表占用空间更小。
第二个问题就是如何解决碰撞问题。如果不同的实值被散列函数映射到同一个键值,如何解决?
①散列函数
SGI的散列函数很简单,就是实值%TableSize。
而其他常用的散列函数可以去网上搜一搜

②碰撞问题解决
在书中主要提到了三种解决方式,线性探测,二次探测和开链。

  • 线性探测

就是当发生碰撞时,就自动往后移一格,如果还碰撞,继续往后移,直到没有碰撞为止。
以一个例子来看,注意这里使用的散列函数就是实值%TableSize。
STL hashtable

  • 二次探测

STL hashtable

  • 开链

这是最常用的,也是SGI使用的方法,就是把相同键值的元素放到一起,放在一张链表上,形式如下:
其中把hashtable内的元素称为桶子。

STL hashtable
注意以下所说的实值和键值,分别指用户层的键值和和哈希表桶数组的索引值。如果指其他,会有特殊说明

二、SGI中哈希表源码分析

①哈希表中每一个存放数据的节点的数据结构

template <class _Val>
struct _Hashtable_node
{
    _Hashtable_node* _M_next;_Val _M_val;
};

并没有用使用list或者slist,而是单独又创建了一个链表节点

②哈希表的迭代器
哈希表的迭代器时forward_iterator单向迭代器,只有++运算符,没有–运算符。
迭代器中存储了两个成员变量

class _Hashtable_iterator
{
    
...typedef _Hashtable_node<_Val> _Node;_Node* _M_cur;//哈希表中一个元素的指针_Hashtable* _M_ht;//指向哈希表的桶数组的指针
...
}

哈希表迭代器中的++重载函数:

template <class _Val, class _Key, class _HF, class _ExK, class _EqK, class _All>
_Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>&
_Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>::operator++()
{
    const _Node* __old = _M_cur;_M_cur = _M_cur->_M_next;//如果在一条链表上还有下一个元素,那就返回下一个if (!_M_cur) {
    size_type __bucket = _M_ht->_M_bkt_num(__old->_M_val);//找到节点元素对应的桶索引,沿桶数组找到第一个非零桶元素,该桶元素中指向的节点就是++的结果while (!_M_cur && ++__bucket < _M_ht->_M_buckets.size())_M_cur = _M_ht->_M_buckets[__bucket];}return *this;//可能会返回NULL,如果返回NULL,就是后面再也没找到下一个值
}

③哈希表的数据结构

hasher                _M_hash;//散列函数(哈希映射函数)
key_equal             _M_equals;//判断键值相同与否的函数
_ExtractKey           _M_get_key;//从节点中取出键值的函数,类似于红黑树的KeyofValue
vector<_Node*,_Alloc> _M_buckets;//桶数组,注意是用的vector实现的
size_type             _M_num_elements;//哈希表中元素个数

④桶数组的大小
在SGI版本中,桶数组的大小只从28个质数当中去挑选,即便你选择了桶数组的数目,仍然会改变为最靠近的那28个质数,其实没什么用处。在C++标准中,就没有这个限制了。

⑤桶数组的插入操作

pair<iterator, bool> insert_unique(const value_type& __obj)
{
    resize(_M_num_elements + 1);//判断是否需要重建桶数组,需要就扩充return insert_unique_noresize(__obj);//插入元素obj
}template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::resize(size_type __num_elements_hint)
{
    const size_type __old_n = _M_buckets.size();if (__num_elements_hint > __old_n) {
    //当哈希表中的元素个数大于桶数组的个数时,就会重建桶数组const size_type __n = _M_next_size(__num_elements_hint);//找出下一个质数来确定桶数组的尺寸if (__n > __old_n) {
    //当下一个质数大于当前质数时,才会重建桶数组,如果是最大的了,就不会改变了vector<_Node*, _All> __tmp(__n, (_Node*)(0),_M_buckets.get_allocator());//新建一个桶数组,并且数组中每个元素都是0__STL_TRY {
    for (size_type __bucket = 0; __bucket < __old_n; ++__bucket) {
    _Node* __first = _M_buckets[__bucket];//原桶数组从左往右,从第一个链表往下,把每一个元素重新找的//到在新桶数组中对应的键值,然后移过去,并且每次新插入的元素都是插在头元素下面while (__first) {
    size_type __new_bucket = _M_bkt_num(__first->_M_val, __n);_M_buckets[__bucket] = __first->_M_next;__first->_M_next = __tmp[__new_bucket];__tmp[__new_bucket] = __first;__first = _M_buckets[__bucket];//这里具体可过程以查看书260页,我认为都是繁琐的指针操作而已,没有什么技术含量 }}_M_buckets.swap(__tmp);}}}
}
//这个插入操作也很简单,就是先找到对应的键值,然后在那个桶下找是否有相同实值的元素,有就立刻返回,
//没有就将元素obj插在链表的首节点
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
pair<typename hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::iterator, bool> 
hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::insert_unique_noresize(const value_type& __obj)
{
    const size_type __n = _M_bkt_num(__obj);_Node* __first = _M_buckets[__n];for (_Node* __cur = __first; __cur; __cur = __cur->_M_next) if (_M_equals(_M_get_key(__cur->_M_val), _M_get_key(__obj)))return pair<iterator, bool>(iterator(__cur, this), false);_Node* __tmp = _M_new_node(__obj);__tmp->_M_next = __first;_M_buckets[__n] = __tmp;++_M_num_elements;return pair<iterator, bool>(iterator(__tmp, this), true);
}

而insert_equal只有最后一个插入函数不同,源码如下:

//就是先找到对应的键值,然后在那个桶下找是否有相同实值的元素,有插入到相同节点下方,
//没有就将元素obj插在链表的首节点
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
typename hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::iterator 
hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::insert_equal_noresize(const value_type& __obj)
{
    const size_type __n = _M_bkt_num(__obj);_Node* __first = _M_buckets[__n];for (_Node* __cur = __first; __cur; __cur = __cur->_M_next) if (_M_equals(_M_get_key(__cur->_M_val), _M_get_key(__obj))) {
    _Node* __tmp = _M_new_node(__obj);__tmp->_M_next = __cur->_M_next;__cur->_M_next = __tmp;++_M_num_elements;return iterator(__tmp, this);}_Node* __tmp = _M_new_node(__obj);__tmp->_M_next = __first;_M_buckets[__n] = __tmp;++_M_num_elements;return iterator(__tmp, this);
}

⑥如何从实值找到键值

size_type _M_bkt_num(const value_type& __obj) const
{
    return _M_bkt_num_key(_M_get_key(__obj));
}//由实值找到对应的键值,这是用户层的实值和键值,比如set中实值和键值是一样的,map中键值是实值的first参数size_type _M_bkt_num_key(const key_type& __key) const
{
    return _M_bkt_num_key(__key, _M_buckets.size());
}
//个人认为下面这个函数才是完整的散列函数,将用户层的键值转化成哈希表中桶数组的索引键值
//其中_M_hash就是hash functions,只不过SGI中的hash function很少,并且只给简单的几个类型提供函数,很多函数没有提供函数,如果要使用,需要自定义。
size_type _M_bkt_num_key(const key_type& __key, size_t __n) const
{
    return _M_hash(__key) % __n;
}

三、SGI和C++标准中哈希表有什么不一样

①C++标准中桶数组的尺寸不再是质数,而是如下的源码:

void _Check_size()
{
    	// grow table as needed
if (max_load_factor() < load_factor()){
    	// rehash to bigger tablesize_type _Newsize = bucket_count();if (_Newsize <  512)_Newsize *= 8;	// 小于512就是乘以8else if (_Newsize < _Vec.max_size() / 2)_Newsize *= 2;	// 大于512就是乘以2_Init(_Newsize);_Reinsert();}
}

②散列函数不一样
源码如下:


???由于看不到_Mask的值,我也不知道这个原理,有待理解


size_type _Hashval(const key_type& _Keyval) const
{
    	// return hash value, masked and wrapped to current table size
size_type _Num = ((_Traits&)*this)(_Keyval) & _Mask;
if (_Maxidx <= _Num)_Num -= (_Mask >> 1) + 1;
return (_Num);
}

③SGI模板中,只有几种类型可以当键值,但在C++标准中,基本所有的类都可以当键值,也就是hash function补充更完整了

四、利用hashtable实现的容器

hash_set:键值和实值(这都是应用层的键值实值)是一样的(实现机制和set一样,都是identity函数)。是排列没有顺序的,键值不能重复。
hash_map:键值和实值是不一样的(实现机制和map一样,都是select1st函数),并且每个元素的数据都是pair。是排列没有顺序的,键值不能重复。
hash_multiset:键值可以重复。
hash_multimap:键值可以重复

在C++标准中对应的容器:
hash_set->unordered_set
hash_map->unordered_map
hash_multiset->unordered_multiset
hash_multimap->unordered_multimap

  相关解决方案