字典哈希表的实现原理_GCC中unordered_(multi)set/map的实现原理 (Part 2 图解哈希表结构)...

本文深入探讨了GCC中unordered_set/map的实现原理,详细讲解了哈希表的结构、Node节点、迭代器、_ExtractKey方法以及insert操作流程。通过图文并茂的方式展示了哈希表如何存储元素、如何利用迭代器遍历以及cache hash值对提升效率的重要性。
摘要由CSDN通过智能技术生成

8a55759b746f584610a91c30b0092d67.png

写在前面

(本专栏仅是个人笔记本,有胡言乱语和错漏部分)

本文以图文+代码的形式记录了_Hashtable的结构,如何编排每一个bucket的链表,如何将每个bucket的链表串在一起形成一个长链表,如何利用迭代器遍历_Hashtable。

此外,还解释了insert操作的流程,以及为什么cache hash值可以提高_Hashtable的效率。

_Hashtable的数据成员

  312     private:
  313       __bucket_type*            _M_buckets              = &_M_single_bucket;
  314       size_type                 _M_bucket_count         = 1;
  315       __node_base               _M_before_begin;
  316       size_type                 _M_element_count        = 0;
  317       _RehashPolicy             _M_rehash_policy;
  • _Bucket[] _M_buckets 数组,数组元素类型为Node*
  • _Hash_node_base _M_before_begin 容器的哨兵节点
  • size_type _M_bucket_countbucket的数量,若采用Prime_rehash,则这个数一直是个素数
  • size_type _M_element_count 容器中元素的数量

Node

268  template<typename _Value, bool _Cache_hash_code>
 269  struct _Hash_node;

有如下的数据成员:

  • _Hash_node_base* _M_nxt;
  • __gnu_cxx::__aligned_buffer<_Value> _M_storage; 为什么不直接用_Value v呢?
  • (可选,通过特例化添加)std::size_t _M_hash_code; 当cache时,存在这个数据成员。

迭代器

对于(multi)set,返回的迭代器只能是只读的:

341       typedef std::forward_iterator_tag                 iterator_category;
  342 
  343       using pointer = typename std::conditional<__constant_iterators,
  344                                                 const _Value*, _Value*>::type; //根据模板参数__constant_iterators决定是否const
  345 
  346       using reference = typename std::conditional<__constant_iterators,
  347                                                   const _Value&, _Value&>::type;  //根据模板参数__constant_iterators决定是否const

其他部分,跟forward_list的迭代器大同小异。

_ExtractKey

STL预设了两种从_Value中提取key的方法:

88   struct _Identity //for set
   89   {
   90     template<typename _Tp>
   91       _Tp&&
   92       operator()(_Tp&& __x) const
   93       { return std::forward<_Tp>(__x); }
   94   };
   95 
   96   struct _Select1st  // for map
   97   {
   98     template<typename _Tp>
   99       auto
  100       operator()(_Tp&& __x) const
  101       -> decltype(std::get<0>(std::forward<_Tp>(__x)))
  102       { return std::get<0>(std::forward<_Tp>(__x)); }
  103   };
  104

_Hashtable的结构

_Hashtable的可以看作一个vector ,每一个bucket存储的是一个Node* 类型的指针,并具有如下特点:

  • 当该bucket为空时,该bucket指针为null
  • 每个哈希容器有一个哨兵节点(_M_before_begin),作为使用迭代器遍历容器的起点。
  • 每一个bucket链表的最后一个结点,同时又是某一个bucket的哨兵节点。

用图描绘一下就是:

c13bbaa7549038858fe1f0936692242a.png

d11f75173daeb51b0c13ea7a3d032880.png

740c1d57c7d580ac9f537e20abeba3fa.png

insert

insert定义在_Insert_base中,4种哈希容器公用一个insert函数:

710       __ireturn_type  //ireturn_type对每个哈希容器不同
  711       insert(const value_type& __v)
  712       {
  713         __hashtable& __h = _M_conjure_hashtable();
  714         __node_gen_type __node_gen(__h);
  715         return __h._M_insert(__v, __node_gen, __unique_keys()); // false_type or true_type
  716       }

然后根据 __unique_keys(),调用不同重载版本的_M_insert():

//for multiset/multimap
  687       template<typename _Arg, typename _NodeGenerator>
  688         iterator
  689         _M_insert(_Arg&& __arg, const _NodeGenerator& __node_gen,
  690                   std::false_type __uk)
  691         {
  692           return _M_insert(cend(), std::forward<_Arg>(__arg), __node_gen,
  693                            __uk);
  694         }

 //for map/set
 1683   // Insert v if no element with its key is already present.
 1684   template<typename _Key, typename _Value,
 1685            typename _Alloc, typename _ExtractKey, typename _Equal,
 1686            typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
 1687            typename _Traits>
 1688     template<typename _Arg, typename _NodeGenerator>
 1689       auto
 1690       _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
 1691                  _H1, _H2, _Hash, _RehashPolicy, _Traits>::
 1692       _M_insert(_Arg&& __v, const _NodeGenerator& __node_gen, std::true_type)
 1693       -> pair<iterator, bool>
 1694       {
 1695         const key_type& __k = this->_M_extract()(__v);
 1696         __hash_code __code = this->_M_hash_code(__k);
 1697         size_type __bkt = _M_bucket_index(__k, __code);
 1698 
 1699         __node_type* __n = _M_find_node(__bkt, __k, __code);
 1700         if (__n)
 1701           return std::make_pair(iterator(__n), false);
 1702 
 1703         __n = __node_gen(std::forward<_Arg>(__v));
 1704         return std::make_pair(_M_insert_unique_node(__bkt, __code, __n), true);
 1705       }

这里我们暂时只关注unordered_map/set的插入。

在插入之前,首先在bucket中查找,先用 _M_find_node判断是否存在key相同的元素, _M_find_node又是基于_M_find_before_node的,其代码如下。

1413   // Find the node whose key compares equal to k in the bucket n.
 1414   // Return nullptr if no node is found.
 1415   template<typename _Key, typename _Value,
 1416            typename _Alloc, typename _ExtractKey, typename _Equal,
 1417            typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
 1418            typename _Traits>
 1419     auto
 1420     _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
 1421                _H1, _H2, _Hash, _RehashPolicy, _Traits>::
 1422     _M_find_before_node(size_type __n, const key_type& __k,
 1423                         __hash_code __code) const
 1424     -> __node_base*
 1425     {
 1426       __node_base* __prev_p = _M_buckets[__n];
 1427       if (!__prev_p)
 1428         return nullptr;
 1429 
 1430       for (__node_type* __p = static_cast<__node_type*>(__prev_p->_M_nxt);;
 1431            __p = __p->_M_next())
 1432         {
 1433           if (this->_M_equals(__k, __code, __p))
 1434             return __prev_p;
 1435        //由于_Hashtable的结构特殊,每次都要判断是否已经遍历完了当前的bucket
 1436           if (!__p->_M_nxt || _M_bucket_index(__p->_M_next()) != __n)
 1437             break;
 1438           __prev_p = __p;
 1439         }
 1440       return nullptr;
 1441     }

在1436行会频繁调用_M_bucket_index(Node *)这个函数,如果cache了hash值,_M_bucket_index仅需一次mod操作,否则就是extract_key, hash, mod这三个操作都要进行。这就是cache hash值的意义。GCC注释中也是这么解释的:

Walking through a bucket's nodes requires a check on the hash code to see if each node is still in the bucket. Such a design assumes a quite efficient hash functor and is one of the reasons it is highly advisable to set __cache_hash_code to true.

如果不存在相同key的元素,_M_insert进而调用了_M_insert_unique_node执行插入:

_M_insert_unique_node(size_type __bkt, __hash_code __code, __node_type* __node)

函数功能:已经确定表中没有相同key的元素,向表中插入新的元素。

//无特例化
 1581   template<typename _Key, typename _Value,
 1582            typename _Alloc, typename _ExtractKey, typename _Equal,
 1583            typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
 1584            typename _Traits>
 1585     auto
 1586     _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
 1587                _H1, _H2, _Hash, _RehashPolicy, _Traits>::
 1588     _M_insert_unique_node(size_type __bkt, __hash_code __code,
 1589                           __node_type* __node)
 1590     -> iterator
 1591     {
 //交由_RehashPolicy 判断 是否需要rehash,
 1592       const __rehash_state& __saved_state = _M_rehash_policy._M_state();
 1593       std::pair<bool, std::size_t> __do_rehash
 1594         = _M_rehash_policy._M_need_rehash(_M_bucket_count, _M_element_count, 1); //1 表示要插入一个元素
 1595 
 1596       __try
 1597         {
 1598           if (__do_rehash.first) //需要rehash
 1599             {
 1600               _M_rehash(__do_rehash.second, __saved_state);
 1601               __bkt = _M_bucket_index(this->_M_extract()(__node->_M_v()), __code);
 1602             }
 1603 
 1604           this->_M_store_code(__node, __code); //在node中保存哈希值
 1605 
 1606           // Always insert at the beginning of the bucket.
 1607           _M_insert_bucket_begin(__bkt, __node);
 1608           ++_M_element_count; //哈希表中的总元素数目
 1609           return iterator(__node);
 1610         }
 1611       __catch(...)
 1612         {
 1613           this->_M_deallocate_node(__node);
 1614           __throw_exception_again;
 1615         }
 1616     }

_M_insert_unique_node又调用了_M_insert_bucket_begin

_M_insert_bucket_begin(size_type __bkt, __node_type* __node)

函数作用:在bucket处的链表头部插入新节点,若这是该bucket的第一个节点,则会有额外的指针操作。 结合注释并参考上面的图,还是挺好理解的

1443   template<typename _Key, typename _Value,
 1444            typename _Alloc, typename _ExtractKey, typename _Equal,
 1445            typename _H1, typename _H2, typename _Hash, typename _RehashPolicy,
 1446            typename _Traits>
 1447     void
 1448     _Hashtable<_Key, _Value, _Alloc, _ExtractKey, _Equal,
 1449                _H1, _H2, _Hash, _RehashPolicy, _Traits>::
 1450     _M_insert_bucket_begin(size_type __bkt, __node_type* __node)
 1451     {
 1452       if (_M_buckets[__bkt])
 1453         {
 1454           // Bucket is not empty, we just need to insert the new node
 1455           // after the bucket before begin.
 1456           __node->_M_nxt = _M_buckets[__bkt]->_M_nxt;
 1457           _M_buckets[__bkt]->_M_nxt = __node;
 1458         }
 1459       else
 1460         {
 1461           // The bucket is empty, the new node is inserted at the
 1462           // beginning of the singly-linked list and the bucket will
 1463           // contain _M_before_begin pointer.
 1464           __node->_M_nxt = _M_before_begin._M_nxt;
 1465           _M_before_begin._M_nxt = __node;
 1466           if (__node->_M_nxt)
 1467             // We must update former begin bucket that is pointing to
 1468             // _M_before_begin.
 1469             _M_buckets[_M_bucket_index(__node->_M_next())] = __node;
 1470           _M_buckets[__bkt] = &_M_before_begin;
 1471         }
 1472     }

至此,插入完毕。

begin end size

结合图例,很容易可以给出begin, end和size的实现:

368  __node_type*
 369  _M_begin() const
 370  { return static_cast<__node_type*>(_M_before_begin._M_nxt); } //返回哨兵节点的next指针

 481  iterator
 482  begin() noexcept
 483  { return iterator(_M_begin()); }

 489  iterator
 490  end() noexcept
 491  { return iterator(nullptr); }


 505  size_type
 506  size() const noexcept
 507  { return _M_element_count; }

其他成员函数

只要理解了Hashtable的结构,其他成员函数的代码还是比较self-explained的,这里就暂且略过。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值