聊聊C++的Hashtable

        相信大家对哈希表都不陌生,其本质和数组类似。下标通过hash函数计算;当出现冲突时,使用链表解决;当元素超过一定数量时触发rehash,减少冲突链表的长度,使数据分布更加均衡、查找更加迅速,诸如此类大家可能能说出来很多。

        那么实际情况下,C++中哈希表是如何实现的、其细节有哪些呢?了解这些有助于我们理解系统库的实际实现,让我们在开发项目的过程中更有底气,接下来我们就一起看看。

        在开始之前,我把Hashtable的主要数据结构画出来,方便大家有个宏观印象。

图片

        总体来说:

        所有bucket首节点构成一个连续的节点指针数组,其大小为bucket_count,buckets指向第一个bucket。空白的矩形代表空指针。

        除了每个bucket之内的节点构成链表之外,整个hashtable的节点也构成了一个链表,链表头为&_M_bbegin._M_node,_M_bbegin是一个虚拟头节点,这样就能支持迭代器遍历整个hashtable。

       每个bucket的第一个元素相当于一个哑结点,其本身不属于这个bucket,哑结点后的第一个节点,才是这个bucket的第一个节点,同时也可以看到,这个哑结点也是另一个bucket的最后一个数据节点。这样一个好处就是除了第一个头节点之外,整个hashtable中都是数据节点,没有多余的哑结点,提高了内存利用率。

        上图展示了某一时刻Hashtable的内部结构,那么随着数据的增加和删除,其内部结构会怎样变化呢?下面我们看一下相关的核心逻辑,为了方便大家理解,我在关键地方加了注释。

        哑结点的获取:

__node_base& _M_before_begin(){ 
    return _M_bbegin._M_node; 
}

        Hashtable首数据节点的获取:

__node_type* _M_begin() const{ 
    return static_cast<__node_type*>(_M_before_begin()._M_nxt); 
}

        每个bucket首数据节点的获取:

__node_type* _M_bucket_begin(size_type __bkt) const{
    __node_base* __n = _M_buckets[__bkt];
    return __n ? 
        //取bucket节点中的下一项
        static_cast<__node_type*>(__n->_M_nxt) : nullptr;
}

        构造函数:

// 第一种
//没指定元素,只有大小,那就只分配bucket数组,并初始化为空。
_M_bucket_count = _M_rehash_policy._M_next_bkt(__bucket_hint);
_M_buckets = _M_allocate_buckets(_M_bucket_count);

// 第二种
//指定了一个初始化容器,那么依次插入。__f代表初始化序列开始,__l代表
//初始化序列结束
auto __nb_elems = __detail::__distance_fw(__f, __l);

//决定bucket大小
_M_bucket_count =
  _M_rehash_policy._M_next_bkt(
    std::max(_M_rehash_policy._M_bkt_for_elements(__nb_elems),
       __bucket_hint));
//分配bucket数组
_M_buckets = _M_allocate_buckets(_M_bucket_count);
//依次插入
for (; __f != __l; ++__f)
        this->insert(*__f);

// 第三种   
//拷贝构造 ht代表另一个Hashtable
//分配bucket数组
_M_buckets = _M_allocate_buckets(_M_bucket_count);
//如果ht数据元素为空,不做后续操作
if (!__ht._M_before_begin()._M_nxt)
  return;

//拷贝ht的首数据节点
const __node_type* __ht_n = __ht._M_begin();
__node_type* __this_n = _M_allocate_node(__ht_n->_M_v);
this->_M_copy_code(__this_n, __ht_n);
//把hashtable首数据节点链到hashtable哑结点后面
_M_before_begin()._M_nxt = __this_n;
//更新首数据节点对应的bucket项,这里更新为hashtable哑结点
_M_buckets[_M_bucket_index(__this_n)] = &_M_before_begin();

// Then deal with other nodes.
__node_base* __prev_n = __this_n;
for (__ht_n = __ht_n->_M_next(); __ht_n; __ht_n = __ht_n->_M_next()){
    __this_n = _M_allocate_node(__ht_n->_M_v);
    __prev_n->_M_nxt = __this_n;
    this->_M_copy_code(__this_n, __ht_n);

    //确定当前节点的bucket,如果bucket为空,把当前节点的上一个节点放到
    //bucket中
    size_type __bkt = _M_bucket_index(__this_n);
    if (!_M_buckets[__bkt])
        _M_buckets[__bkt] = __prev_n;

    __prev_n = __this_n;
}

        hashtable的clear操作:

void clear() noexcept{
  //从首数据节点开始,释放每个数据节点
  _M_deallocate_nodes(_M_begin());
  //置空各bucket
  __builtin_memset(_M_buckets, 0, _M_bucket_count * sizeof(__bucket_type));
  _M_element_count = 0;
  //置空数据节点
  _M_before_begin()._M_nxt = nullptr;
}

        rehash操作:


void _M_rehash_aux(size_type __n, std::true_type){
    //按照新大小,重新分配bucket数组
    __bucket_type* __new_buckets = _M_allocate_buckets(__n);
    __node_type* __p = _M_begin();
    _M_before_begin()._M_nxt = nullptr;
    std::size_t __bbegin_bkt = 0;
    while (__p){
      //next为当前要插入的数据节点,p为上一数据节点
      __node_type* __next = __p->_M_next();
      //计算新的bucket下标
      std::size_t __bkt = __hash_code_base::_M_bucket_index(__p, __n);
      if (!__new_buckets[__bkt]){
        //当数据节点对应的bucket为空时,要将哑结点移动到此bucket
        //表示整个hashtable的首节点
        __p->_M_nxt = _M_before_begin()._M_nxt;
        _M_before_begin()._M_nxt = __p;
        __new_buckets[__bkt] = &_M_before_begin();
        //移东哑结点后,假如之前哑结点所在的bucket有数据节点,那么
        //这个bucket不能为空,填入当前节点的上一节点。
        //__bbegin_bkt记录哑结点所在上一个bucket index。
        //初始时,nxt为空,开头已置空。
        if (__p->_M_nxt)
          __new_buckets[__bbegin_bkt] = __p;
        __bbegin_bkt = __bkt;
    }
    else{
      __p->_M_nxt = __new_buckets[__bkt]->_M_nxt;
      __new_buckets[__bkt]->_M_nxt = __p;
    }
    __p = __next;
}
//释放老的bucket数据
_M_deallocate_buckets(_M_buckets, _M_bucket_count);
_M_bucket_count = __n;
//指向新的bucket数组
_M_buckets = __new_buckets;

        可以看到rehash时,只是重新分配了bucket数组,数据节点重新做了链接,没有重新分配。相比起vector的自动扩容,少了重新分配数据节点内存和初始化的开销。

        insert操作:​​​​​​​

//bucket不为空,正常在链表头插入。
if (_M_buckets[__bkt]){
  // Bucket is not empty, we just need to insert the new node
  // after the bucket before begin.
  __node->_M_nxt = _M_buckets[__bkt]->_M_nxt;
  _M_buckets[__bkt]->_M_nxt = __node;
}
else{
  // The bucket is empty, the new node is inserted at the
  // beginning of the singly-linked list and the bucket will
  // contain _M_before_begin pointer.
  __node->_M_nxt = _M_before_begin()._M_nxt;
  _M_before_begin()._M_nxt = __node;
  if (__node->_M_nxt)
    // We must update former begin bucket that is pointing to
    // _M_before_begin.
    //哑节点可能发生变化,如果哑结点后面还有节点,那么需要更新后面
    //节点对应的bucket
    _M_buckets[_M_bucket_index(__node->_M_next())] = __node;
  //bucket为空时,其第一个元素为哑结点,也是整个Hashtable的首节点
  _M_buckets[__bkt] = &_M_before_begin();
}

        放几张插入的变化图,供大家参考。

图片

图片

图片

图片

图片

        erase操作:​​​​​​​


//删除时需要prev节点,因为需要修改prev的next指针
iterator _M_erase(size_type __bkt, __node_base* __prev_n, __node_type* __n){
  //删除bucket头数据节点的情况
  if (__prev_n == _M_buckets[__bkt])
    _M_remove_bucket_begin(__bkt, __n->_M_next(),
    __n->_M_nxt ? _M_bucket_index(__n->_M_next()) : 0);
  else if (__n->_M_nxt){
    size_type __next_bkt = _M_bucket_index(__n->_M_next());
    //不等时,说明删除的是bucket的最后一个节点
    if (__next_bkt != __bkt)
      _M_buckets[__next_bkt] = __prev_n;
  }

  //考虑完上面两个情况后,删除只需要修改prev的next指针
  __prev_n->_M_nxt = __n->_M_nxt;
  iterator __result(__n->_M_next());
  _M_deallocate_node(__n);
  --_M_element_count;

  return __result;
}

//删除bucket头数据节点的情况
void _M_remove_bucket_begin(size_type __bkt, __node_type* __next,size_type __next_bkt){
  if (!__next || __next_bkt != __bkt){
    // Bucket is now empty
    // First update next bucket if any
    //说明__next_bkt == __bkt,要删除的既是头结点,又是尾节点
    if (__next)
    _M_buckets[__next_bkt] = _M_buckets[__bkt];

    // Second update before begin node if necessary
    //要删除的是hashtable头结点所指向的bucket的唯一一个节点,此时需要
    //更新头节点
    if (&_M_before_begin() == _M_buckets[__bkt])
    _M_before_begin()._M_nxt = __next;
    _M_buckets[__bkt] = nullptr;
  }
}

        可以看到删除时会涉及prev指针的修改,Hashtable的哑结点就方便了此类操作。放几张删除的变化图,供大家参考:

图片

图片

图片

        经过实际阅读源码发现,Hashtable内部的实现和我们平时想的还是有些差别,大家看了之后有什么想法呢,欢迎留言评论。

        原文链接:聊聊C++的Hashtable

山重水复疑无路,柳暗花明又一村

相关推荐

  1. C# —— HashTable

    2024-07-12 02:04:01       22 阅读
  2. C#中哈希表(Hashtable

    2024-07-12 02:04:01       50 阅读
  3. C#面:介绍 Hashtable 和 Dictionary区别

    2024-07-12 02:04:01       33 阅读
  4. Hashmap和Hashtable区别

    2024-07-12 02:04:01       35 阅读
  5. HashTable和ConcurrentHashMap区别

    2024-07-12 02:04:01       32 阅读
  6. TensorFlow 用 hashtable 意义

    2024-07-12 02:04:01       29 阅读
  7. HashMap 和 HashTable异同

    2024-07-12 02:04:01       30 阅读
  8. HashMap和Hashtable区别(绝对经典)

    2024-07-12 02:04:01       56 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-12 02:04:01       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-12 02:04:01       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-12 02:04:01       58 阅读
  4. Python语言-面向对象

    2024-07-12 02:04:01       69 阅读

热门阅读

  1. leetcode秋招冲刺 (专题16--18)

    2024-07-12 02:04:01       22 阅读
  2. 日常的网络杂记

    2024-07-12 02:04:01       20 阅读
  3. 设计模式之单例模式

    2024-07-12 02:04:01       23 阅读
  4. 软件架构之测评方法

    2024-07-12 02:04:01       16 阅读
  5. Webpack打包生产环境进行优化处理

    2024-07-12 02:04:01       21 阅读
  6. 【深度学习】关于模型加速

    2024-07-12 02:04:01       23 阅读
  7. k8s 部署RuoYi-Vue-Plus之mysql搭建

    2024-07-12 02:04:01       23 阅读
  8. 大数据面试题之Hudi(1)

    2024-07-12 02:04:01       19 阅读
  9. ES6 Iterator 与 for...of 循环(五)

    2024-07-12 02:04:01       24 阅读
  10. 对素数的一种新理解

    2024-07-12 02:04:01       22 阅读
  11. 力扣 454四数相加

    2024-07-12 02:04:01       21 阅读
  12. 十大排序算法(慢慢更新)

    2024-07-12 02:04:01       23 阅读
  13. 简谈设计模式之建造者模式

    2024-07-12 02:04:01       18 阅读
  14. 力扣题解(乘积最大子数组)

    2024-07-12 02:04:01       24 阅读