C++ 中的 hashTable

 

1. 什么是hashTable

用来存储数据的最基本的的结构有数组和链表两种,其他结构都是在这两种基础之上的复用与衍生。当用户进行输入时,输入可能有一定的规律,更大的可能性是输入的数据具有很大的随机性,采用数组进行存储的话,索引是一个很大的问题,因为数据的类型不一定为整数;如果采用链表,查找时必须进行遍历,对性能有很大的影响。但我们希望能找到这样的结构,不仅查找时有很高的效率,而且适用于多种结构。

幸运的是现在已经有很多这样的结构了,比如二叉搜索平衡树(红黑树、SB树等)以及这篇文章提到的hashTable。

hashTable希望提供常数时间的基本操作。

当我们要存储一定量(设为N)的数据(设为int类型)时,最先想到的是创建一个大小为N * T的数组,然后将数据依次放入空间(1放入array[0],2数据放在array[1]。。。)。但这样会有两个问题:如果N * T的值太大,我们可能没有这么大的空间供使用;数据不是数字,也没有数字类型的标号(比如数据的类型可能是字符串),就没有办法作为数组的索引。

其中索引问题不难解决,我们可以把字符编码,然后用一些数学公式得出索引值。

第一个问题就需要用到哈希函数了。

哈希函数其实就是一种映射函数,将大数映射为小数(0 -- N - 1之间)的函数。使用哈希函数可能会碰到一个问题,不同的元素可能会被映射到相同的位置,也就是“碰撞”,解决碰撞的方法有很多种,包括线性探测、二次探测、开链等做法。这些做法的效率与负载系数有很大的关系,负载系数是指元素个数除以数组大小,除非使用开链策略,负载系数一般在 0 -- 1 之间。

线性探测

需要注意的是采用线性探测策略的话,在删除元素时采用的是惰性删除,也就是只标记删除记号,实际删除元素要等到hashTable重新整理(rehashing)时再进行(因为hashTable中的每一个元素不仅表示它自己,也关系到其他元素的排列)。

线性探测最大的问题就是cluster,就是当多个元素被哈希函数映射到同一位置后导致该位置后的一大片位置被占用,最后插入的时间成本很高,甚至插入成本的增长幅度远高于负载系数的成长幅度。

二次探测

二次探测主要用来姐解决cluster问题,命名由来是因为解决问题的方程式是个二次方程式。

F(i) = i * i;

开链

这种做法是在每一个bucket中维护一个list。

C++ STL 采用的是开链的做法。

2. C++ STL 中的hashTable

2.1 结构

本文所引用的代码是 gcc-2.95版。

hashTable的图形描述如下

hashTable的主干部分使用vector来实现的,vector上的每一个元素被称为bucket,即桶,而每一个bucket都指向一个list。这里的list并不是 stl 中的list,而是自行维护的 hashTable node。

hashTable node的代码如下

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

hashTable的部分代码如下

template <class _Val, class _Key, class _HashFcn,
          class _ExtractKey, class _EqualKey, class _Alloc>
class hashtable {
public:
  typedef _Key key_type;
  typedef _Val value_type;
  typedef _HashFcn hasher;
  typedef _EqualKey key_equal;

  typedef size_t            size_type;
 
  hasher hash_funct() const { return _M_hash; }
  key_equal key_eq() const { return _M_equals; }

private:
  typedef _Hashtable_node<_Val> _Node;

  //下面三个都是仿函数
  hasher                _M_hash;
  key_equal             _M_equals;
  _ExtractKey           _M_get_key;


  vector<_Node*,_Alloc> _M_buckets;
  size_type             _M_num_elements;

  size_type bucket_count() const { return _M_buckets.size(); }//计算vector的大小
};

hashTable的模板参数非常多,包括:

  • Value:节点的实值类型;
  • Key:节点的键值类型;
  • HashFunc:哈希函数;
  • ExtractKey:从节点中取出键值的方法(函数或仿函数);
  • EqualKey:判断键值相同与否的方法;
  • Alloc:空间配置器

当元素个数要大于bucket个数,也就是负载系数大于1时,会执行rehash ing操作,将vector扩大到两倍附近的一个质数,然后将每个数据重新通过哈希函数计算一遍,然后插入到相应的位置。所以hashtable扩充空间是非常消耗性能的,尤其是在数据很多的情况下。

实际上hashTable扩充时要扩充的到校已经计算好写死在了代码中,如下

static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53ul,         97ul,         193ul,       389ul,       769ul,
  1543ul,       3079ul,       6151ul,      12289ul,     24593ul,
  49157ul,      98317ul,      196613ul,    393241ul,    786433ul,
  1572869ul,    3145739ul,    6291469ul,   12582917ul,  25165843ul,
  50331653ul,   100663319ul,  201326611ul, 402653189ul, 805306457ul, 
  1610612741ul, 3221225473ul, 4294967291ul
};

选择质数的原因是stl的哈希函数是通过计算hashCode 除以 bucket个数的余数得到的,而质数做除数得到的结果更加分散。

需要扩充时通过二分查找确定到底要用这些质数中的哪一个

inline unsigned long __stl_next_prime(unsigned long __n)
{
  const unsigned long* __first = __stl_prime_list;
  const unsigned long* __last = __stl_prime_list + __stl_num_primes;
  const unsigned long* pos = lower_bound(__first, __last, __n);//二分查找
  return pos == __last ? *(__last - 1) : *pos;
}

hashTable的大小为20:vector(12) + num_elements(4) + 3个仿函数 = 19 (分配空间时会按最大变量的大小分配) = 20 

2.2 哈希函数

stl中有一系列内置的哈希函数,全都是仿函数,如下

template <class _Key> struct hash { };//泛化

inline size_t __stl_hash_string(const char* __s)
{
  unsigned long __h = 0; 
  for ( ; *__s; ++__s)
    __h = 5*__h + *__s;
  
  return size_t(__h);
}

__STL_TEMPLATE_NULL struct hash<char*>//特化
{
  size_t operator()(const char* __s) const { return __stl_hash_string(__s); }
};

__STL_TEMPLATE_NULL struct hash<const char*>
{
  size_t operator()(const char* __s) const { return __stl_hash_string(__s); }
};

__STL_TEMPLATE_NULL struct hash<char> {
  size_t operator()(char __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned char> {
  size_t operator()(unsigned char __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<signed char> {
  size_t operator()(unsigned char __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<short> {
  size_t operator()(short __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned short> {
  size_t operator()(unsigned short __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<int> {
  size_t operator()(int __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned int> {
  size_t operator()(unsigned int __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<long> {
  size_t operator()(long __x) const { return __x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned long> {
  size_t operator()(unsigned long __x) const { return __x; }
};

可以看到第一行给出了一个函数模板(泛化),之后全是该模板的特化。

2.3 hashTable 的迭代器

以下是stl中对hashtable_iterator的定义

template <class _Val, class _Key, class _HashFcn,
          class _ExtractKey, class _EqualKey, class _Alloc>
struct _Hashtable_iterator {
  _Node* _M_cur;//当前节点
  _Hashtable* _M_ht;//指向vector

  _Hashtable_iterator(_Node* __n, _Hashtable* __tab) 
    : _M_cur(__n), _M_ht(__tab) {}
  _Hashtable_iterator() {}
  reference operator*() const { return _M_cur->_M_val; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
  pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */
  iterator& operator++();//需要注意的是,这个iterator没有--操作
  iterator operator++(int);
  bool operator==(const iterator& __it) const
    { return _M_cur == __it._M_cur; }
  bool operator!=(const iterator& __it) const
    { return _M_cur != __it._M_cur; }
};

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;//node后面仍有元素,直接后移
  if (!_M_cur) {//否则下一个bucket的头节点就是返回值
    //计算出下一个bucket在vector中的位置,下面会列出源码
    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;
}

template <class _Val, class _Key, class _HF, class _ExK, class _EqK, 
          class _All>
inline _Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>
_Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>::operator++(int)
{
  iterator __tmp = *this;
  ++*this;//直接调用前++
  return __tmp;
}

hashTable的迭代器有两个指针,指针cur指向当前节点,ht指向hashTable。当cur后移时,首先判断它的下一个节点是不是为null,不是则cur直接++,否则就需要先通过哈希函数计算出cur在vector中的位置(设为i),然后返回vector[i + 1]的头节点。

需要注意的是,hashTable的迭代器中没有--的操作,也没有定义逆向迭代器(reverse iterator)。

上面引用的函数 _M_bkt_num()在求特定元素在vector中的位置很简单,就是对vector_size 取余而已。

size_type _M_bkt_num(const value_type& __obj) const
  {
    return _M_bkt_num_key(_M_get_key(__obj));
  }

  size_type _M_bkt_num_key(const key_type& __key, size_t __n) const
  {
    return _M_hash(__key) % __n;
  }

 

2.4 构造与内存管理

当我们初始化一个hashTable时

调用的函数如下


  void _M_initialize_buckets(size_type __n)
  {
    const size_type __n_buckets = _M_next_size(__n);//计算出vector应该有多长
    _M_buckets.reserve(__n_buckets);//给vector分配空间
    _M_buckets.insert(_M_buckets.end(), __n_buckets, (_Node*) 0);//初始化vector
    _M_num_elements = 0;
  }

//vector中的reserve函数实现如下,如果vector的容量小于n则扩充否则不变
 void reserve(size_type __n) {
    if (capacity() < __n) {
      const size_type __old_size = size();
      iterator __tmp = _M_allocate_and_copy(__n, _M_start, _M_finish);
      destroy(_M_start, _M_finish);
      _M_deallocate(_M_start, _M_end_of_storage - _M_start);
      _M_start = __tmp;
      _M_finish = __tmp + __old_size;
      _M_end_of_storage = _M_start + __n;
    }
  }

首先通过传入的数字n与__stl_prime_list为vector分配空间,如果这里n = 50,但分配的空间会是53。因为实际分配的空间大小是要根据__stl_prime_list 中列出的数字决定的,系统会选择最接近n的一个数字(一般是>= n的区间内,这里情况特殊)。

hashtable有两种插入操作(insert)时,可分别使用语句

iht.insert_unique(39);
iht.insert_equal(39);

第一条语句所调用的函数的代码如下

pair<iterator, bool> insert_unique(const value_type& __obj)
  {
    resize(_M_num_elements + 1);//判断是否需要扩大vector
    return insert_unique_noresize(__obj);
  }c

//判断是否需要扩大vector,其实就是将hashTable中的元素个数与vector的节点个数进行比较,前者大于后者就扩大
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) {//如果元素个数大于vector的长度,vector扩大
    //根据现在vector的长度返回扩充后vector的长度
    const size_type __n = _M_next_size(__num_elements_hint);/
    if (__n > __old_n) {//如果扩充后的长度比现在的vector长
      vector<_Node*, _All> __tmp(__n, (_Node*)(0),
                                 _M_buckets.get_allocator());//创建新的vector
      __STL_TRY {//这里开始处理旧的vector
        for (size_type __bucket = 0; __bucket < __old_n; ++__bucket) {
          _Node* __first = _M_buckets[__bucket];//获取旧vector上的每一个bucket
          while (__first) {
            size_type __new_bucket = _M_bkt_num(__first->_M_val, __n);//重新调用哈希函数找到新的vector的插入位置
    //接下来四行代码把旧的bucket上的链表转移到新的bucket上
            _M_buckets[__bucket] = __first->_M_next;
            __first->_M_next = __tmp[__new_bucket];
            __tmp[__new_bucket] = __first;
            __first = _M_buckets[__bucket];          
          }
        }
        _M_buckets.swap(__tmp);//通过swap函数回收旧的vector
      }
#         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 */
    }
  }
}


//插入新节点
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);//__obj在vector上的哪一个bucket里
  _Node* __first = _M_buckets[__n];

  //寻找__obj在bucket的链表上的位置
  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);
//发现有重复值,返回该重复值的iterator与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);
}


这四行代码负责将旧的bucket的链表上的节点转移到新的bucket上
            _M_buckets[__bucket] = __first->_M_next;
            __first->_M_next = __tmp[__new_bucket];
            __tmp[__new_bucket] = __first;
            __first = _M_buckets[__bucket];   

过程如下图

第二行代码调用的函数的源码如下

iterator insert_equal(const value_type& __obj)
  {
    resize(_M_num_elements + 1);//判断是否需要扩充vector
    return insert_equal_noresize(__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;//hashTable元素计数器++
      return iterator(__tmp, this);
    }
  //没有发现重复值,将新节点插入到链表头部
  _Node* __tmp = _M_new_node(__obj);
  __tmp->_M_next = __first;
  _M_buckets[__n] = __tmp;
  ++_M_num_elements;//hashTable元素计数器++
  return iterator(__tmp, this);
}

当我们想要删除hashTable时,可以调用clear函数,该函数的实现如下

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) {//循环删除bucket链表的每一个节点
      _Node* __next = __cur->_M_next;
      _M_delete_node(__cur);
      __cur = __next;
    }
    _M_buckets[__i] = 0;
  }
  _M_num_elements = 0;//令hashTable元素计数器为0
}

通过源码可以发现,clear函数并没有删除vector,而是删除了bucket上挂的list,vector的capacity不变。

可以调用copy_from函数函数赋值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();//清除自己的hashTable,造成所有元素为0
  _M_buckets.reserve(__ht._M_buckets.size());//如果目的hashTable要比自己的大,就扩充,否则保持不变
  _M_buckets.insert(_M_buckets.end(), __ht._M_buckets.size(), (_Node*) 0);//从尾端开始给hashTable的vector赋值为0.
  __STL_TRY {
    for (size_type __i = 0; __i < __ht._M_buckets.size(); ++__i) {
      if (const _Node* __cur = __ht._M_buckets[__i]) {
        _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());
}

3. 以hashTable为底层结构的容器

以下容器所供应的操作接口,hashTable几乎都提供了,所以以下容器的操作行为,基本都是在调用hashTable的函数罢了。

3.1 hash_set/unordered_set

运用set是为了更快的搜寻元素,但rb_tree有自动排序功能而hashTable没有,反应到set层面上就是set可以自动排序而hash_set不可以。

hash_set的使用方式与set完全相同。

#include <iostream>
#include <unordered_set>
#include <cstring>

using namespace std;


int main()
{
    unordered_set<int> s;

    s.insert(4);
    s.insert(3);
    s.insert(5);

    unordered_set<int>::iterator ite = s.begin();
    for( ; ite != s.end(); ite++){
        cout << *ite <<" ";
    }
    cout << endl;
    for(int i = 0; i < s.bucket_count(); i++){
        cout <<"bucket: " << i << " 有" <<   s.bucket_size(i)<< "个元素"<<endl;
    }


    cout << s.size() << endl;
    cout << s.bucket_count() << endl;
    cout << s.max_bucket_count() << endl;

    return 0;
}

3.2 hash_multiset/unordered_multiset

hash_multiset与multiset的特性几乎完全相同,唯一的差别在于hash_multiset没有自动排序的功能

3.3 hash_map/unordered_map

3.4 hash_multimap/unordered_multimap

 

 

 

 

 

参考 《STL 源码剖析》 作者:侯捷

  • 9
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值