STL关联容器值hashtable

hashtable(散列表)是一种数据结构,在元素的插入,删除,搜索操作上具有常数平均时间复杂度O(1);

hashtable名词

散列函数:负责将某一元素映射为索引。
碰撞(collision):不同的元素被映射到相同的位置。
解决碰撞的方法:线性试探法,二次试探法,开链等。
负载系数:元素个数除以表格大小。
主集团:平均插入成本的增长幅度,远高于负载系数的成长幅度。
次集团:hashtable若采用二次探测,则若两个元素经hash function计算出来的位置相同,则插入时所试探的位置也相同,造成某种浪费。
开链:在每个表格元素中位置一个list。
桶(bucket):hashtable表格内的每个元素。

hashtable组织方式

节点类

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

hashtable的节点和list节点相类似。
一个Node的指针指向每个桶上列表的首个元素,而桶上的链表首地址存放在vector向量中。

typedef _Hashtable_node<_Val> _Node;
vector<_Node*,_Alloc> _M_buckets;

hashtable迭代器的自增操作符定义如下:

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;
}

hashtable类定义有五个模板参数

template <class _Val, class _Key, class _HashFcn,
          class _ExtractKey, class _EqualKey, class _Alloc>
class hashtable;

_Val 值类型
_Key 键类型
_HashFcn 哈希函数
_ExtractKey 提取键的方法
_EqualKey 判断键相等的方法
_Alloc 分配器类型

hashtable插入操作

insert_unique不允许值重复的插入:

 pair<iterator, bool> insert_unique(const value_type& __obj)
  {
    resize(_M_num_elements + 1);
    return insert_unique_noresize(__obj);
  }

该函数首先调用resize函数,传入当前元素加一的值,看是否需要进行扩容。处理完成后,再将值__obj插入到hashtable中。
resize函数的定义如下:

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) {            //传入的新值大于旧值
    //从素数表中查找最接近新值的桶数量__n
    const size_type __n = _M_next_size(__num_elements_hint);
    if (__n > __old_n) {                          //得到的桶数量大于旧值:需要扩容
      //分配容量为__n(新桶数量)的桶向量  
      vector<_Node*, _All> __tmp(__n, (_Node*)(0),
                                 _M_buckets.get_allocator());
      __STL_TRY {
        //对旧hashtable中的每个桶
        for (size_type __bucket = 0; __bucket < __old_n; ++__bucket) {
          _Node* __first = _M_buckets[__bucket];        //取得桶中链表首地址
          while (__first) {                             //未到达旧桶中链表的末尾
            //根据链表节点中存放的元素数值_M_val,以新的桶数量__n,
            //计算其在新的hashtable中的桶编号(rehash)
            size_type __new_bucket = _M_bkt_num(__first->_M_val, __n);
            //更新当前桶中节点链表头的指向,使其指向链表中的下一个元素
            _M_buckets[__bucket] = __first->_M_next;
            
            //以下两句是增长新桶中的链表,把新节点加进去。
            //链表首地址的后继指向新hashtable中计算出来的新桶中的链表开头
            __first->_M_next = __tmp[__new_bucket];
            //新hashtable,新桶的链表开头指向__first
            __tmp[__new_bucket] = __first;

            //更新__first至原有的__first->_M_next,即在原hashtable原桶中的链表中前进一个元素。
            __first = _M_buckets[__bucket];          
          }
        }
        //当旧hashtable中的元素都重新hash到新桶向量后
        _M_buckets.swap(__tmp);    //将新桶向量与旧桶向量相互交换。
        //旧桶数据存放在__tmp向量中,当离开此范围时,__tmp作为一个局部变量,其空间会被自动释放。
      }
      //发生异常后进行的回滚操作 
#         ifdef __STL_USE_EXCEPTIONS
      catch(...) {
        //将新的桶向量中的每个桶
        for (size_type __bucket = 0; __bucket < __tmp.size(); ++__bucket) {
          //对桶中链表的每个节点
          while (__tmp[__bucket]) {
            _Node* __next = __tmp[__bucket]->_M_next;
            _M_delete_node(__tmp[__bucket]);    //删除当前节点
            __tmp[__bucket] = __next;           //前进一个节点
          }
        }
        throw;
      }
#         endif /* __STL_USE_EXCEPTIONS */
    }
  }
}

insert_unique_noresize的函数定义如下:

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) 
    //链表中存在与__obj值相同的节点
    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;                    //增加hashtable元素数量
  //返回插入位置的迭代器,并附加插入成功标识。
  return pair<iterator, bool>(iterator(__tmp, this), true);
}

结合insert_unique函数中调用的两个函数接口的分析,可知:

  • 在进行insert_unique函数执行时,首先尝试对桶向量进行扩容,然后再向hashtable中插入元素。
  • hashtable的扩容操作类似于vector的自增长过程,都经过1.申请更大空间;2.将原容器中的数据转移到新的容器中;3.清理原容器的空间;三部曲。
  • 与vector容器的自增长所不同的是,在数据转移的过程中,hashtable需要根据新的桶数量,对数据进行重新映射。

insert_equal允许重复的插入:

  iterator insert_equal(const value_type& __obj)
  {
    resize(_M_num_elements + 1);
    return insert_equal_noresize(__obj);
  }

与insert_unique相比较,函数在扩容后调用允许重复值的非扩充插入函数insert_equal_noresize:

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) 
    //链表中存在与__obj值相同的节点,则执行插入操作并返回
    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);                 //完成插入操作,返回
}

insert_unique_noresize与insert_equal_noresize的比较:

  • insert_unique_noresize先在hashtable的某个桶中查找与插入值相同的节点,若找到,则直接返回,插入失败。否则,在桶的链表的头部插入新节点。
  • insert_equal_noresize先在hashtable的某个桶中查找与插入值相同的节点,若找到,则在其后插入新节点。否则,在桶的链表的头部插入新节点。

hashtable的删除和复制

hashtable删除

template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::clear()
{
  //遍历桶向量  
  for (size_type __i = 0; __i < _M_buckets.size(); ++__i) {
    _Node* __cur = _M_buckets[__i];    //当前桶
    while (__cur != 0) {               //遍历桶中链表 
      _Node* __next = __cur->_M_next;  //保存后继节点地址  
      _M_delete_node(__cur);           //析构指针所指向的对象,并释放节点 
      __cur = __next;                  //指针前进
    }
    _M_buckets[__i] = 0;               //链表开头指向空 
  }
  _M_num_elements = 0;                 //更新元素数量为0   
}

hashtable复制

template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
  ::_M_copy_from(const hashtable& __ht)
{
  _M_buckets.clear();            //清空原桶向量
  _M_buckets.reserve(__ht._M_buckets.size());    //为桶向量重新分配内存,是其能够容纳被复制对象
  //初始化桶向量中的列表开头为空  
  _M_buckets.insert(_M_buckets.end(), __ht._M_buckets.size(), (_Node*) 0);
  __STL_TRY {
    //遍历桶
    for (size_type __i = 0; __i < __ht._M_buckets.size(); ++__i) {
      const _Node* __cur = __ht._M_buckets[__i];    //被复制散列(源散列)的某个桶中链表开头
      if (__cur) {                                  //链表非空
        _Node* __copy = _M_new_node(__cur->_M_val); //创建链表开头节点副本   
        _M_buckets[__i] = __copy;                   //更新目标散列对应桶的链表开头 

        for (_Node* __next = __cur->_M_next;        //对链表中剩下的元素,依次 
             __next; 
             __cur = __next, __next = __cur->_M_next) {
          __copy->_M_next = _M_new_node(__next->_M_val); //创建节点副本
          __copy = __copy->_M_next;                      //在列表中前进  
        }
      }
    }
    _M_num_elements = __ht._M_num_elements;              //更新元素数量  
  }
  __STL_UNWIND(clear());                                 //复制发生异常时回滚操作   
}

小结

  • hashtable不依赖元素的随机性,假设元素分布在一个相对固定的范围内,类似于一种字典结构。
  • hashtable可提供常数复杂度的插入,删除,搜索操作,但是需要创建哈希表,是以空间的代价换取时间的高效。
  • 树结构提供对数时间复杂度的元素搜索操作,依赖于元素的随机性。
posted @ 2019-11-25 21:04  技术狂人djc  阅读(236)  评论(0编辑  收藏  举报