SGISTL源码探究-STL中的hashtable(上)

前言

关于哈希表部分,哈希函数以及解决哈希冲突的方法都有很多种,在本文中不会详细去讨论哈希表的大量知识,而是以SGISTL实现的hashtable为主,所以如果对hashtable不太了解,建议先去网上查阅相关的资料。这里只给出基本的概念。

了解哈希表原理

基本概念

哈希表(hashtable,也称散列表):根据key来访问元素的一种数据结构。它可能先经过哈希函数计算出key,然后通过key访问表中对应key的位置的元素,这样的查找速度很快。
下面这张图是维基百科给出的定义:

其实关于hashtable最重要的无非就两样东西:一个是哈希函数、另一个是处理哈希冲突的方法。

哈希函数

目前常用的哈希函数有以下几种方法:
1. 直接定址法:取关键字或者关键字的某个线性函数为散列地址,比如hash(k) = khash(k) = a * k + b,a、b为常数。
2. 数字分析法:假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
3. 平方取中法:取关键字平方后的中间几位为哈希地址。通常在选定哈希函数时不一定能知道关键字的全部情况,取其中的哪几位也不一定合适,而一个数平方后的中间几位数和数的每一位都相关,由此使随机分布的关键字得到的哈希地址也是随机的。取的位数由表长决定。
4. 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。
5. 随机数法
6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即hash(k) = k mod p,p <= m。不仅可以对关键字直接取模,也可在折叠法、平方取中法等运算之后取模。对p的选择很重要,一般取素数或m,若p选择不好,容易产生冲突。

解决冲突的方法

当两个不同的关键字通过哈希函数计算出来的键相同时,此时就产生了哈希冲突,需要采取一些措施来解决这种现象。
1. 开放寻址法:hash(i) = (hash(key) + d_i) mod m,i = 1,2…k(k <= m - 1),hash(key)为哈希函数,m为哈希表的长度,d_i为增量序列,i为发生冲突的次数。关于增量序列,也有它自己的算法。
- 每次和冲突的次数相等或者是一个线性函数,这种呈线性递增的被称为线性探测
- 如果对i进行平方求值然后相加,这种称为平方探测。或者使用伪随机数序列,称为伪随机探测
- 还有一种再哈希,即二次探测,采用h(k,i) = (h(k) + c_1*i + c_2*i^2) mod m,h(k)是辅助哈希函数,c1c2为正的辅助常数。
- 关于开放寻址法的方法确实很多,双哈希 算是比较好的方法之一,它采用h(k, i) = (h_1(k) + i*h_2(k)) mode m这样的哈希函数。

当探测到了一个空的单元时,就会把散列地址存放在该单元。

当时当线性探测/平方探测这种方法时,可能会导致聚集现象,即可能会导致哈希表中某一块区域占满了,但是其他处于空余状态,如果造成这种现象了,会导致冲突大量发生,造成性能的损失。
2. 链接法(叫法可能有所不同):这种做法很简单,当出现哈希冲突时,将该元素与其他会哈希到该位置的元素形成一个链表即可。

查找效率

关于哈希表的效率问题,我们应当尽量采用足够合适的哈希函数来避免冲突,如果冲突了,我们也应该使用合适的解决冲突的方法。如果造成了聚集现象,会导致性能的严重下降,甚至会选择重建哈希表。
考虑的因素除了通过哈希函数产生的元素是否均匀以及解决哈希冲突的方法的优劣之外,还有一个是装载因子(load factor),定义为填入表的元素个数/哈希表的长度,当装载因子越大,证明产生冲突的可能性就越大。

以上便是关于哈希部分的一个大致的说明,参考了维基百科里面的一些资料,关于哈希还有很多地方没有说到,比如完美哈希(无冲突)等。现在已经验证了有几个哈希函数能取得比较好的性能,感兴趣的可以去网上查查。

STL中的hashtable

以上是关于哈希表的概念,接下来我们来看看STL中是如何实现哈希表的。

引入

首先,stl中实现哈希函数部分是放在stl_hash_fun.h文件里的,供hashtable调用。
其次,关于解决哈希冲突的方法,stl中采用的是链接法
最后,它的大致实现是通过vector容器存储哈希函数映射前的元素,并且通过哈希映射到重复的位置的元素将形成链表,而vector上每个单元称作bucket(桶的意思),因为可能不止一个节点,可能是一条链表。
接下来我们就进入到它的源码部分(迭代器、相关的定义及数据结构、构造/析构函数、常用操作)。

迭代器
#include <stl_algobase.h>
#include <stl_alloc.h>
#include <stl_construct.h>
#include <stl_tempbuf.h>
#include <stl_algo.h>
#include <stl_uninitialized.h>
#include <stl_function.h>
#include <stl_vector.h>
#include <stl_hash_fun.h>

__STL_BEGIN_NAMESPACE

//链表的节点,当有元素哈希到同一个位置上时,会形成链表
template <class Value>
struct __hashtable_node
{
  __hashtable_node* next;
  Value val;
};  

template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey, class Alloc = alloc>
class hashtable;

template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator;

template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_const_iterator;

//迭代器部分
template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {
  /* 首先我们先来看看对应的模板参数代表什么
   * Value:值的类型
   * Key:键的类型
   * HashFcn:哈希函数的函数型别(对象)
   * ExtractKey:从节点取出键的方法(对象)
   * EqualKey:判断键是否相同的方法(对象)
   * Alloc:空间配置器
   * 关于函数对象,它其实就是重载了()操作符,之后便可以像函数指针一样使用了,并且可以传递附加数据
   */
  typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>
          hashtable;
  typedef __hashtable_iterator<Value, Key, HashFcn,
                               ExtractKey, EqualKey, Alloc>
          iterator;
  typedef __hashtable_const_iterator<Value, Key, HashFcn,
                                     ExtractKey, EqualKey, Alloc>
          const_iterator;
  //声明链表节点的别名
  typedef __hashtable_node<Value> node;

  //声明该迭代器的五种相应型别以及difference_type(如果忘了建议翻到迭代器部分再看看)
  typedef forward_iterator_tag iterator_category;
  typedef Value value_type;
  typedef ptrdiff_t difference_type;
  typedef size_t size_type;
  typedef Value& reference;
  typedef Value* pointer;

  //迭代器目前指向的节点
  node* cur;
  /* 迭代器指向的节点所属的容器节点
   * 因为vector上存储的可能是一个链表,所以指向了链表的节点还不够
   * 还需要知道是哪个bucket,因为涉及到跳bucket的情况
   * 这个时候就需要知道迭代器指向是哪个容器,所以需要hashtable* ht这个变量
   */
  hashtable* ht;

  /* 构造函数,指名当前指向的节点,以及指向的hashtable */
  __hashtable_iterator(node* n, hashtable* tab) : cur(n), ht(tab) {}
  /* 默认构造函数 */
  __hashtable_iterator() {}

  //重载*操作符,返回当前节点的value  
  reference operator*() const { return cur->val; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
  //重载->操作符
  pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */
  iterator& operator++();
  iterator operator++(int);
  //重载==和!=操作符,比较节点的地址
  bool operator==(const iterator& it) const { return cur == it.cur; }
  bool operator!=(const iterator& it) const { return cur != it.cur; }
};
/* 以下是const_iterator部分
 * 和上面的iterator实现类似
 * 只是内部的数据成员换成了const之类的改变
 */
template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_const_iterator {
  typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>
          hashtable;
  typedef __hashtable_iterator<Value, Key, HashFcn,
                               ExtractKey, EqualKey, Alloc>
          iterator;
  typedef __hashtable_const_iterator<Value, Key, HashFcn,
                                     ExtractKey, EqualKey, Alloc>
          const_iterator;
  typedef __hashtable_node<Value> node;

  typedef forward_iterator_tag iterator_category;
  typedef Value value_type;
  typedef ptrdiff_t difference_type;
  typedef size_t size_type;
  typedef const Value& reference;
  typedef const Value* pointer;

  //将迭代器指向的节点以及bucket定义成常量
  const node* cur;
  const hashtable* ht;

  /* 构造函数部分,多了个拷贝构造函数 */
  __hashtable_const_iterator(const node* n, const hashtable* tab)
    : cur(n), ht(tab) {}
  __hashtable_const_iterator() {}
  __hashtable_const_iterator(const iterator& it) : cur(it.cur), ht(it.ht) {}
  reference operator*() const { return cur->val; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
  pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */
  const_iterator& operator++();
  const_iterator operator++(int);
  bool operator==(const const_iterator& it) const { return cur == it.cur; }
  bool operator!=(const const_iterator& it) const { return cur != it.cur; }
};
定义部分及数据结构
// Note: assumes long is at least 32 bits.
/* STL要求vector的大小为质数,于是先预置了28个质数
 * 当我们创建的hashtable的大小为50时,它会向上选择最靠近的质数作为容量大小
 */
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53,         97,           193,         389,       769,
  1543,       3079,         6151,        12289,     24593,
  49157,      98317,        196613,      393241,    786433,
  1572869,    3145739,      6291469,     12582917,  25165843,
  50331653,   100663319,    201326611,   402653189, 805306457,
  1610612741, 3221225473ul, 4294967291ul
};
/* 调整n,将其调整成最接近上面数组中的某个质数的值(注意只会向上取或者不变,不可能缩小) */
inline unsigned long __stl_next_prime(unsigned long n)
{
  //指向__stl_prime_list数组起始元素
  const unsigned long* first = __stl_prime_list;
  //指向__stl_prime_list数组的末尾元素后一个位置
  const unsigned long* last = __stl_prime_list + __stl_num_primes;
  //lower_bound的作用在[first, last)范围内寻找第一个比n大的元素并返回其迭代器
  const unsigned long* pos = lower_bound(first, last, n);
  /* 要是n太大了导致[first, last)中没有比n大的元素
   * 则返回数组中最后一个元素(最大的了)
   * 否则返回指向的合适的值
   */
  return pos == last ? *(last - 1) : *pos;
}

//以下正式来到hashtable的实现部分
template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey,
          class Alloc>
class hashtable {
public:
  /* 一些别名
   * 关于Key、Value、HashFcn、EqualKey前面讲解hashtable的迭代器已经提到过
   */
  typedef Key key_type;
  typedef Value value_type;
  typedef HashFcn hasher;
  typedef EqualKey key_equal;

  typedef size_t            size_type;
  typedef ptrdiff_t         difference_type;
  typedef value_type*       pointer;
  typedef const value_type* const_pointer;
  typedef value_type&       reference;
  typedef const value_type& const_reference;

  //返回哈希函数对象
  hasher hash_funct() const { return hash; }
  //返回判断key是否相同的函数对象
  key_equal key_eq() const { return equals; }

private:
  /* 这三个成员都是函数对象
   * 关于它们的具体实现,都在stl_hash_fun.h中
   */
  hasher hash;
  key_equal equals;
  ExtractKey get_key;

  //节点以及以节点为分配单位的空间配置器
  typedef __hashtable_node<Value> node;
  typedef simple_alloc<node, Alloc> node_allocator;

  /* 这便是hashtable最核心的数据结构了
   * 注意它存储的元素是node*的
   * 因为每个元素都可能是一个链表的头节点
   */
  vector<node*,Alloc> buckets;
  //记录元素(节点)的个数
  size_type num_elements;

public:
  //迭代器
  typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey,
                               Alloc>
  iterator;

  typedef __hashtable_const_iterator<Value, Key, HashFcn, ExtractKey, EqualKey,
                                     Alloc>
  const_iterator;

  friend struct
  __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>;
  friend struct
  __hashtable_const_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>;
构造/析构函数
public:
  /* 构造函数
   * 设置哈希函数等
   * 最后调用initialize_buckets初始化hashtable,这个函数放在下面讲
   */
  hashtable(size_type n,
            const HashFcn&    hf,
            const EqualKey&   eql,
            const ExtractKey& ext)
    : hash(hf), equals(eql), get_key(ext), num_elements(0)
  {
    initialize_buckets(n);
  }

  /* 构造函数,默认的节点取key方法 */
  hashtable(size_type n,
            const HashFcn&    hf,
            const EqualKey&   eql)
    : hash(hf), equals(eql), get_key(ExtractKey()), num_elements(0)
  {
    initialize_buckets(n);
  }

  /* 拷贝构造函数
   * 设置其哈希函数等
   * 然后调用copy_from函数,这个也放在后面分析
   */
  hashtable(const hashtable& ht)
    : hash(ht.hash), equals(ht.equals), get_key(ht.get_key), num_elements(0)
  {
    copy_from(ht);
  }

  /* 重载=操作符 */
  hashtable& operator= (const hashtable& ht)
  {
    if (&ht != this) {
      clear();
      hash = ht.hash;
      equals = ht.equals;
      get_key = ht.get_key;
      copy_from(ht);
    }
    return *this;
  }
  //析构函数,调用clear()回收空间
  ~hashtable() { clear(); }
部分成员函数
  //返回节点个数
  size_type size() const { return num_elements; }
  size_type max_size() const { return size_type(-1); }
  //判断hashtable是否为空
  bool empty() const { return size() == 0; }

  void swap(hashtable& ht)
  {
    __STD::swap(hash, ht.hash);
    __STD::swap(equals, ht.equals);
    __STD::swap(get_key, ht.get_key);
    buckets.swap(ht.buckets);
    __STD::swap(num_elements, ht.num_elements);
  }

  /* 遍历,返回指向第一个不为空的元素的迭代器
   * 否则返回end()
   */
  iterator begin()
  {
    for (size_type n = 0; n < buckets.size(); ++n)
      if (buckets[n])
        return iterator(buckets[n], this);
    return end();
  }
  /* 返回指向空节点的迭代器 */
  iterator end() { return iterator(0, this); }

  const_iterator begin() const
  {
    for (size_type n = 0; n < buckets.size(); ++n)
      if (buckets[n])
        return const_iterator(buckets[n], this);
    return end();
  }

  const_iterator end() const { return const_iterator(0, this); }

  friend bool
  operator== __STL_NULL_TMPL_ARGS (const hashtable&, const hashtable&);

public:
  //返回hashtable的节点总数
  size_type bucket_count() const { return buckets.size(); }
  //返回__stl_prime_list最大的质数
  size_type max_bucket_count() const
    { return __stl_prime_list[__stl_num_primes - 1]; }
  //返回当前bucket拥有的节点个数
  size_type elems_in_bucket(size_type bucket) const
  {
    size_type result = 0;
    for (node* cur = buckets[bucket]; cur; cur = cur->next)
      result += 1;
    return result;
  }
  //省略的部分是关于插入的一些操作,我们放在下一小节
  ......
  /* 根据key返回指向该节点的迭代器
   * 用到了bkt_num_key函数,作用是通过key得到它放在hashtable的位置
   * 首先我们根据该key得知它处于的bucket
   * 然后遍历该链表,取得该key对应的具体的节点
   * 最后构造指向该节点的迭代器返回
   */
  iterator find(const key_type& key)
  {
    size_type n = bkt_num_key(key);
    node* first;
    for ( first = buckets[n];
          first && !equals(get_key(first->val), key);
          first = first->next)
      {}
    return iterator(first, this);
  }


  const_iterator find(const key_type& key) const
  {
    size_type n = bkt_num_key(key);
    const node* first;
    for ( first = buckets[n];
          first && !equals(get_key(first->val), key);
          first = first->next)
      {}
    return const_iterator(first, this);
  }

  /* 返回与key相同的节点个数
   * 里面用到了equals这个函数对象
   */
  size_type count(const key_type& key) const
  {
    const size_type n = bkt_num_key(key);
    size_type result = 0;

    for (const node* cur = buckets[n]; cur; cur = cur->next)
      if (equals(get_key(cur->val), key))
        ++result;
    return result;
  }
  //省略的是equal_range及删除操作的定义
  //关于它们的实现,我们放在下一小节分析
  ......

private:
  //next_size通过调用前面分析的__stl_next_prime函数调整n的大小
  size_type next_size(size_type n) const { return __stl_next_prime(n); }

  /* 该函数在构造函数里面调用的初始化函数
   * 首先通过next_size调整n的大小
   * 接着使用vector中的reserve函数,预先告知vector容器有n_buckets个元素(如果忘了建议翻到之前讲vector容器部分复习下)
   * 然后把所有元素都填成null
   * 设置num_elements为0
   */
  void initialize_buckets(size_type n)
  {
    const size_type n_buckets = next_size(n);
    buckets.reserve(n_buckets);
    buckets.insert(buckets.end(), n_buckets, (node*) 0);
    num_elements = 0;
  }

  /* 以下bkt_num*部分,用于对key进行哈希判断该key应该落在vector中哪个位置 */
  size_type bkt_num_key(const key_type& key) const
  {
    return bkt_num_key(key, buckets.size());
  }

  size_type bkt_num(const value_type& obj) const
  {
    return bkt_num_key(get_key(obj));
  }

  //这个函数调用了哈希函数,这些判断位置的函数最后都是调用它。
  size_type bkt_num_key(const key_type& key, size_t n) const
  {
    return hash(key) % n;
  }

  size_type bkt_num(const value_type& obj, size_t n) const
  {
    return bkt_num_key(get_key(obj), n);
  }

  /* 节点配置函数
   * 功能是申请一个节点的空间,并初始化值为obj
   */
  node* new_node(const value_type& obj)
  {
    node* n = node_allocator::allocate();
    n->next = 0;
    __STL_TRY {
      construct(&n->val, obj);
      return n;
    }
    __STL_UNWIND(node_allocator::deallocate(n));
  }

  /* 节点释放函数
   * 先析构val
   * 然后释放空间
   */
  void delete_node(node* n)
  {
    destroy(&n->val);
    node_allocator::deallocate(n);
  }

  void erase_bucket(const size_type n, node* first, node* last);
  void erase_bucket(const size_type n, node* last);

  void copy_from(const hashtable& ht);

};

小结

关于hashtable部分,它采用的解决冲突的方法是链接法,并且使用vector容器存储,这是为了便于扩展大小。
我们已经分析了大部分源码,剩下的就是一些操作需要继续分析,我们放在下一节分析,主要就是插入、删除、拷贝等操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值