stl源码分析之hash table

本文主要分析g++ stl中哈希表的实现方法。stl中,除了以红黑树为底层存储结构的map和set,还有用哈希表实现的hash_map和hash_set。map和set的查询时间是对数级的,而hash_map和hash_set更快,可以达到常数级,不过哈希表需要更多内存空间,属于以空间换时间的用法,而且选择一个好的哈希函数也不那么容易。

一、 哈希表基本概念

哈希表,又名散列表,是根据关键字直接访问内存的数据结构。通过哈希函数,将键值映射转换成数组中的位置,就可以在O(1)的时间内访问到数据。举个例子,比如有一个存储家庭信息的哈希表,通过人名查询他们家的信息,哈希函数为f(),数组info[N]用于存储,那么张三家的信息就在info[f(张三)]上。由此,不需比较便可知道张三家里有几口人,人均几亩地,地里有几头牛。很快对不对,不过有时候会出现f(张三)等于f(李四)的情况,这就叫哈希碰撞。碰撞是由哈希函数造成的,良好的哈希函数只能减少哈希碰撞的概率,而不能完全避免。这就需要处理冲突的方法,一般有两种:

1. 开放定址法

先存储了张三的信息,等到存李四的信息时发现,这位置有记录了,怎么办,假如李四这人不爱跟张三一块凑热闹,就重新找了个位置。这个方法就多了,他可以放在后面一个位置,如果这位置还有的话,就再放后面一个位置,以此类推,这就叫线性探测;他可能嫌一个个位置找太慢了,于是就按照12,22,32的间隔找,这就叫平方探测;或者再调用另外一个哈希函数g()得到新的位置,这就叫再哈希…

2. 开链法

如果李四这人嫌重新找个坑太麻烦了,愿意和张三放在一起,通过链表连接,这就是开链法。开链法中一个位置可能存放了多个纪录。

一个哈希表中元素的个数与数组的长度的比值称为该哈希表的负载因子。开放定址法的数组空间是固定的,负载因子不会大于1,当负载因子越大时碰撞的概率越大,当负载因子超过0.8时,查询时的缓存命中率会按照指数曲线上升,所以负载因子应该严格控制在0.7-0.8以下,超过时应该扩展数组长度。 开链法的负载因子可以大于1,插入数据的期望时间O(1),查询数据的期望时间是O(1+a),a是负载因子,a过大时也需要扩展数组长度。

二、 stl哈希表结构

stl采用了开链法实现哈希表,其中每个哈希节点有数据和next指针,

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

哈希表定义时要指定数组大小n,不过实际分配的数组长度是一个根据n计算而来的质数,

void _M_initialize_buckets(size_type __n)
      {
        const size_type __n_buckets = _M_next_size(__n);
        _M_buckets.reserve(__n_buckets);
        _M_buckets.insert(_M_buckets.end(), __n_buckets, (_Node*) 0);
        _M_num_elements = 0;
      }
 inline unsigned long
  __stl_next_prime(unsigned long __n)
  {
    const unsigned long* __first = _Hashtable_prime_list<unsigned long>::_S_get_prime_list();
    const unsigned long* __last = __first + (int)_S_num_primes;
    const unsigned long* pos = std::lower_bound(__first, __last, __n);
    return pos == __last ? *(__last - 1) : *pos;
  }

从 prime_list中找到第一个大于n的数,list是已经计算好的静态数组,包含了29个质数.

template<typename _PrimeType> const _PrimeType
  _Hashtable_prime_list<_PrimeType>::__stl_prime_list[_S_num_primes] =
    {
      5ul,          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
    };

比如指定哈系表长度为50,最后实际分配的是53,指定长度为100,最后实际分配的长度是193.可以发现__stl_prime_list数组中,后一个数总是大约等于前一个数的两倍,这不是巧合。当插入数据时,如果所有元素个数大于哈希表数组长度,为了使哈希表的负载因子永远小于1,就必须调用resize重新分配,增长速度跟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)
        {
          const size_type __n = _M_next_size(__num_elements_hint);
          if (__n > __old_n)
            {
              _Vector_type __tmp(__n, (_Node*)(0), _M_buckets.get_allocator());
              __try
                {
                  for (size_type __bucket = 0; __bucket < __old_n; ++__bucket)
                    {
                      _Node* __first = _M_buckets[__bucket];
                      while (__first)
                        {
                          size_type __new_bucket = _M_bkt_num(__first->_M_val,
                                                              __n);
                          _M_buckets[__bucket] = __first->_M_next;
                          __first->_M_next = __tmp[__new_bucket];
                          __tmp[__new_bucket] = __first;
                          __first = _M_buckets[__bucket];
                        }
                    }
                  _M_buckets.swap(__tmp);
                }
              __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_exception_again;
                }
            }
        }
    }

每次新插入的元素都放在链表的第一个节点前面。

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)
        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;
      return pair<iterator, bool>(iterator(__tmp, this), true);
    }

三、 哈希函数

哈希函数用于计算元素在数组中的位置, M_bkt_num_key简单封装了哈希函数,与数组长度取余得到元素在数组中的位置。

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

_M_hash都定义在<hash_func.h>中,全部是仿函数。除了对字符串设计了一个转换函数之外,其他都是返回原值:

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

stl源码分析之hash table,布布扣,bubuko.com

时间: 2024-08-07 04:15:11

stl源码分析之hash table的相关文章

stl源码分析之vector

上篇简单介绍了gcc4.8提供的几种allocator的实现方法和作用,这是所有stl组件的基础,容器必须通过allocator申请分配内存和释放内存,至于底层是直接分配释放内存还是使用内存池等方法就不是组件需要考虑的事情.这篇文章开始分析gcc4.8 stl的容器源码实现.stl的容器分为序列式容器和关联式容器,前者包括vector,list,queue以及stack等常用数据结构,后者包含了map,set以及hash table等比较高级的结构,本文就从使用最广泛也是最基础的vector开始

java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制

通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的引用放入数组中,每一个数组元素都是一个引用变量. 实际上,HashSet 和 HashMap 之间有非常多相似之处,对于 HashSet 而言.系统採用 Hash 算法决定集合元素的存储位置,这样能够保证能高速存.取集合元素:对于 HashMap 而言.系统 key-value 当成一个总体进行处理

stl源码分析之list

本文主要分析gcc4.8版本的stl list的源码实现,与vector的线性空间结构不同,list的节点是任意分散的,节点之间通过指针连接,好处是在任何位置插入删除元素都只需要常数时间,缺点是不能随机访问,查询复杂度是O(n),n为list中的元素个数.所以list非常适合应用与数据插入删除频繁的场景. 一. list节点 list节点定义如下, struct _List_node_base { _List_node_base* _M_next; _List_node_base* _M_pre

STL源码分析--仿函数 &amp; 配接器

STL源码分析-仿函数 & 配接器 仿函数就是函数对象.就实现观点而言,仿函数其实就是一个"行为类似函数"的对象.为了能够"行为类似函数",其类别定义中必须自定义(或说改写.重载)function call 运算子(operator()),拥有这样的运算子后,我们就可以在仿函数的对象后面加上一对小括号,以此调用仿函数所定义的operator().仿函数作为可配接的关键因素. 配接器在STL组件的灵活组合运用功能上,扮演着轴承.转换器的角色,adapter的定

stl源码分析之priority queue

前面两篇介绍了gcc4.8的vector和list的源码实现,这是stl最常用了两种序列式容器.除了容器之外,stl还提供了一种借助容器实现特殊操作的组件,谓之适配器,比如stack,queue,priority queue等,本文就介绍gcc4.8的priority queue的源码实现. 顾名思义,priority queue是带有优先级的队列,所以元素必须提供<操作符,与vector和list不同,priority queue允许加入元素,但是取出时只能取出优先级最高的元素. 一. pri

STL源码分析--空间配置器的底层实现 (二)

STL源码分析-空间配置器 空间配置器中门道 在STL中的容器里都是使用统一的空间配置器,空间配置器就是管理分配内存和销毁内存的.在STL将在heap空间创建一个对象分为两个步骤,第一是申请一块内存,第二是在这块内存中初始化一个对象.首先申请空间是由malloc提供,初始化一个对象时由constructor管理.销毁一个对象也是由两步骤完成,第一是销毁空间上的对象,第二是释放这块内存. 同时,STL的空间配置器分为两级内存,如果申请的内存空间大于128KB,那么就使用第一级空间配置,如果小于,那

STL源码分析--仿函数 &amp; 模板的模板参数 &amp; 临时对象

STL源码分析-使用的一些特殊语法 关于泛型编程中用到的一些特殊语法,这些语法也适用于平常的模板编程 1.  类模板中使用静态成员变量 Static成员变量在类模板中并不是很特殊,同时这个变量不属于对象,属于实例化以后的这个类类型.每一个实例化对应一个static变量 2.  类模板中可以再有模板成员 3.  模板参数可以根据前一个模板参数而设定默认值 4.  类模板可以拥有非类型的模板参数 所谓非类型的模板参数就是内建型的模板参数 Template <class T,class Alloc =

STL 源码分析 # stl_iterator &amp; stl_iterator_base #

STL 源码分析 # stl_iterator_base && stl_iterator # 这里能很清楚的看到各个基础类型的继承关系 template <class _Tp, class _Distance> struct input_iterator { typedef input_iterator_tag iterator_category; typedef _Tp value_type; typedef _Distance difference_type; typede

STL源码分析--萃取编程(traits)技术的实现

1.为什么要出现? 按照默认认定,一个模板给出了一个单一的定义,可以用于用户可以想到的任何模板参数!但是对于写模板的人而言,这种方式并不灵活,特别是遇到模板参数为指针时,若想实现与类型的参量不一样的实例化,就变得不太可能了!也有时,想禁止此种相同的实例化变得不太可能!故而出现了,Partial Specialization! 同时,在使用void*指针时,可以最大限度的共享代码,减少代码的膨胀! 2.它是什么?其实,就是用户定义的偏特化.用template<>来说明这是一个偏特化,针对任何模板