STL源码剖析(十五)关联式容器之hashtable

hashtable
前面介绍了二叉搜索树、平衡二叉搜索树、以及广泛使用的平衡二叉搜索树:红黑树。
RB-tree红黑树不仅在树形的平衡在表现不错,在效率表现和实现复杂度上也保持相当的平衡,所以运用甚广。
二叉搜索树具有对数平均时间的表现,但输入数据需具有足够的随机性;hashtable散列表在插入、删除、搜寻等操作上也具有常数平均时间表现,且这种表现是以统计为基础,不需要仰赖输入元素的随机性。

hashtable散列表概述
hash table可提供对任何有名项的存取操作和删除操作,因操作对象是有名项,所以hashtable也可视为一种字典结构。这种结构的用意在于提供常数时间之基本操作,类似stack和queue。
举例,如果所有的元素都是16-bits且不带正负号的整数,范围0~65535,简单运用一个array可以满足常数时间的基本操作。配置一个array A,有65535个元素,索引号码0~65535,初值为0,每个元素代表相应元素的出现次数,插入元素i,执行A[i]++,删除元素i执行A[i]- -,搜索元素i,就检查A[i]是否为0,以上每个操作都是常数时间。额外的负担是array的空间和初始化时间。
但是这种解法存在现实问题,如果元素格外多,则array的大小就是个问题。
如何避免使用大的荒谬的array,使用映射函数,将大数映射为小数,将某一元素映射到一个大小可接受的索引,这样的函数称为hash function(散列函数),使用hash function会带来一个问题:可能有不同的元素被映射到相同的位置,这便是所谓的碰撞问题,解决碰撞问题的方法有:线性探测(linear probing)、二次探测(quadratic probing)、开链(separate chaining)等。

线性探测(linear probing)
负载系数(loading factor),意指元素个数除以表格大小,负载系数永远在0~1之间,除非采用开链策略。
当hash function计算出某个元素的插入位置,而该位置上的空间已不再可用时,循序往下寻找(如果到达尾端就绕到头部继续寻找),直到找到一个可用空间为止。进行元素搜寻操作时也一样,如果hash function计算出来的位置上元素值与我们搜寻的目标不符,就循序往下一一寻找,直到找到吻合者,或者遇上空格元素。而元素的删除必须采用惰性删除,只标记删除记号,实际删除操作则待表格重新整理时再进行,这是因为hash table中的每个元素不仅表述它自己,也关系到其他元素的排列。

欲分析线性探测的表现,需要两个假设,1表格足够大,2每个元素都够独立。在此假设下,最坏情况是线性巡访整个表格,平均情况是巡访一半表格。但实际情况更糟糕,举例说明:
新元素第一次插入位置#9,第二次插入位置#8,如果第三个元素插入位置为#9则需要绕到头部插入位置#0,第四个元素如果插入位置为#0、8、9则需要插入位置#1,之后的新元素经过hash function计算后的位置若为#3~#7可直接插入,否则只能插入位置#2。这样的现象在hashing过程中称为主集团primary clustering,之后的插入操作可能会不断解决碰撞问题,主集团也越来越大。

二次探测
二次探测主要解决主集团问题,如果hash function计算出新元素的位置为H,而该位置已被使用,则依序尝试H+1²,H+2²,H+3²,H+4²,…,H+i²,而不是像线性探测依序尝试H+1,H+2,H+3,H+4,…,H+i。
二次探测也会带来一些问题:
线性探测法每次探测必须是不同的位置,二次探测法能否保持如此,能否保证没有位置时插入X一定成功。
二次探测法的运算过程较线性探测法复杂,是否带来执行效率问题。
不论线性探测或二次探测,负载系数过高时,表格能否动态增长。

如果将表格大小设置为质数,并且保持负载系数小于0.5,超过0.5就重新配置整理表格,可保证每插入一个新元素需要的探测次数不超过2。
复杂度的问题,线性探测需要一个加法(加1),一个测试(是否需要回头),以及一个可能的减法(绕转回头)。二次探测需要一个加法(从i-1到i),一个乘法(i²)另一个加法,一个mod运算。
Hi = H0 + i² (mod M)
Hi-1 = H0 + (i-1)² (mod M)
整理
Hi - Hi-1 = i² - (i-1)² (mod M)
Hi = Hi-1 + 2i-1 (mod M)
以前一个H值计算下一个H值,不需要执行二次方的乘法,乘以2可以位移位快速完成,mod运算可证明为非需要。
最后一个array动态增长的问题,扩充表格,需找到一个新的大约两倍质数的表格,表格重建不可能原封不动地拷贝,必须检验旧表格中的所有元素,计算其在新表格中的位置,再插入到新表格中。
二次探测可以消除主集团,却可能造成次集团:两个元素经hash function计算出来的位置若相同,则插入时所探测的位置也相同,形成某种浪费。消除次集团如复式散列。

开链
开链法是在每一个表格元素中维护一个list,hash function分配一个list,在list中执行元素的插入、搜索、删除等操作。虽然针对list的搜索是线性操作,但list够短可保证速度。
使用开链手法,表格的负载系数将大于1。
STL的hashtable便是采用这种做法。

hash_set
STL set以RB-tree为底层机制,hash_set是以hashtable为底层机制,封装hashtable的接口。
RB-tree有自动排序功能而hashtable没有,即set可自动排序hash_set不可。set和hash_set都可完成快速搜索元素。

hash_map
hash_map以hashtable为底层机制。map和hash_map都可完成根据键值快速搜索元素功能,hash_map使用方式与map完全相同,但不可自动排序。

hash_multiset
hash_multiset特性与multiset完全相同,区别为hash_multiset使用hashtable底层机制,不可自动排序。
hash_multiset与hash_set唯一区别是元素插入操作,hash_multiset使用hashtable的insert_equal(),hash_set使用insert_unique()。

hash_multimap
hash_multimap与multimap特性完全相同,区别为hash_multimap使用hashtable底层机制,不可自动排序。
hash_multimap与hash_map实现上的唯一区别为插入操作,hash_multimap使用hashtable的insert_equal(),hash_map使用insert_unique()。

hash_functions
<hash_fun.h>定义中有现成的hash_functions,都是仿函数,在下一篇hashtable设计中计算元素位置_M_bkt_num,调用仿函数,取得可以对hashtable模运算的值。
针对char, int, long等整型,仿函数返回原值,对于字符串设计了一个转换函数。

hashtable无法处理以下类型以外的元素,如string, double, float,需要自定义hash_function。

namespace __gnu_cxx _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION

  using std::size_t;

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

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

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

  template<>
    struct hash<char>
    { 
      size_t
      operator()(char __x) const
      { return __x; }
    };

  template<>
    struct hash<unsigned char>
    { 
      size_t
      operator()(unsigned char __x) const
      { return __x; }
    };

  template<>
    struct hash<signed char>
    {
      size_t
      operator()(unsigned char __x) const
      { return __x; }
    };

  template<>
    struct hash<short>
    {
      size_t
      operator()(short __x) const
      { return __x; }
    };

  template<>
    struct hash<unsigned short>
    {
      size_t
      operator()(unsigned short __x) const
      { return __x; }
    };

  template<>
    struct hash<int>
    { 
      size_t 
      operator()(int __x) const 
      { return __x; }
    };

  template<>
    struct hash<unsigned int>
    { 
      size_t
      operator()(unsigned int __x) const
      { return __x; }
    };

  template<>
    struct hash<long>
    {
      size_t
      operator()(long __x) const
      { return __x; }
    };

  template<>
    struct hash<unsigned long>
    {
      size_t
      operator()(unsigned long __x) const
      { return __x; }
    };

_GLIBCXX_END_NAMESPACE_VERSION
} // namespace
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值