本文主要分析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; } }; ……