• 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可提供常数复杂度的插入,删除,搜索操作,但是需要创建哈希表,是以空间的代价换取时间的高效。
    • 树结构提供对数时间复杂度的元素搜索操作,依赖于元素的随机性。
  • 相关阅读:
    分水岭分割算法(watershed segmentation)的C++实现(法2)
    ubuntu16.04下安装opencv3.3
    分水岭分割算法(watershed segmentation)的C++实现(法1)
    dpkg: 处理归档 /var/cache/apt/archives/swig2.0_2.0.12-1ubuntu4_amd64.deb (--unpack)时出错:
    ubuntu16.04安装pycharm
    ImportError: liblapack.so.3: cannot open shared object file问题
    Linux下使用Opencv打开笔记本摄像头
    目标跟踪算法meanshift优缺点
    Jacobian矩阵和Hessian矩阵
    机器视觉中的目标检测
  • 原文地址:https://www.cnblogs.com/SupremeGIS-Developer/p/11924917.html
Copyright © 2020-2023  润新知