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*) );
_M_num_elements = ;
}
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 - ) : *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*)(), _M_buckets.get_allocator());
__try
{
for (size_type __bucket = ; __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 = ; __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 = ;
for ( ; *__s; ++__s)
__h = * __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的更多相关文章
- java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制
通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的 ...
- STL源码分析《4》----Traits技术
在 STL 源码中,到处可见 Traits 的身影,其实 Traits 不是一种语法,更确切地说是一种技术. STL库中,有一个函数叫做 advance, 用来将某个迭代器(具有指针行为的一种 cla ...
- STL源码分析《3》----辅助空间不足时,如何进行归并排序
两个连在一起的序列 [first, middle) 和 [middle, last) 都已经排序, 归并排序最核心的算法就是 将 [first, middle) 和 [middle, last) 在 ...
- STL 源码分析《1》---- list 归并排序的 迭代版本, 神奇的 STL list sort
最近在看 侯捷的 STL源码分析,发现了以下的这个list 排序算法,乍眼看去,实在难以看出它是归并排序. 平常大家写归并排序,通常写的是 递归版本..为了效率的考虑,STL库 给出了如下的 归并排序 ...
- STL源码分析读书笔记--第二章--空间配置器(allocator)
声明:侯捷先生的STL源码剖析第二章个人感觉讲得蛮乱的,而且跟第三章有关,建议看完第三章再看第二章,网上有人上传了一篇读书笔记,觉得这个读书笔记的内容和编排还不错,我的这篇总结基本就延续了该读书笔记的 ...
- STL 源码分析《2》----nth_element() 使用与源码分析
Select 问题: 在一个无序的数组中 找到第 n 大的元素. 思路 1: 排序,O(NlgN) 思路 2: 利用快排的 RandomizedPartition(), 平均复杂度是 O(N) 思路 ...
- stl源码分析之allocator
allocator封装了stl标准程序库的内存管理系统,标准库的string,容器,算法和部分iostream都是通过allocator分配和释放内存的.标准库的组件有一个参数指定使用的allocat ...
- 第九篇:Spark SQL 源码分析之 In-Memory Columnar Storage源码分析之 cache table
/** Spark SQL源码分析系列文章*/ Spark SQL 可以将数据缓存到内存中,我们可以见到的通过调用cache table tableName即可将一张表缓存到内存中,来极大的提高查询效 ...
- STL源码分析与实现-stl_list容器
1. stl_list 介绍 今天我们来总结一下stl_List, 通过之前介绍单链表的文章,其实对链表的基本操作已经十分熟悉了,那对于stl_list,无非就是链表结构不一样,至于其中的增删改查的细 ...
随机推荐
- hibernate连接mysql,查询条件中有中文时,查询结果没有记录,而数据库有符合条件的记录(解决方法)
今天在另一台服务器上重新部署了网站,结果出现了以下问题: ——用hibernate做mysql的数据库连接时,当查询条件中有中文的时候,查询结果没有记录,而数据库中是存在符合条件的记录的. 测试了以下 ...
- October 2nd 2017 Week 40th Monday
Grown-ups work for things. Grown-ups pay. Grown-ups suffer consequences. 真正的成年人会奋斗.会付出.会承担后果. How to ...
- SAP 前端技术的演化史简介
Jerry之前曾经写过一篇微信公众号文章,题目叫<> 关注我的公号"汪子熙"后,在历史菜单"前端开发相关"里即可找到这篇文章: 该文章简单回顾了SA ...
- 破解myeclipse10失败的一个奇葩原因
昨天开发用的myeclipse10突然弹窗提示我要激活,我清楚的记得安装时候已经破解并且看到激活信息了. 翻遍搜索出来的文章,改systemid之类的也试过了,问题依旧存在,很是绝望. 今早过来机灵了 ...
- [T-ARA][Bye Bye]
歌词来源:http://music.163.com/#/song?id=22704472 사랑하는 그대 Bye Bye, Bye Bye, Bye Bye, [sa-lang-ha-neun geu ...
- Uva10048 Audiophobia (Floyd)
题意:有一个无向带权图,求出两点之间路径的最大边权值最小能为多少. 思路:使用floyd算法跑一边以备查询,每一次跑的过程中dp[i][j]=min(dp[i][j],max(dp[i][k],dp[ ...
- Spark系列-核心概念
Spark系列-初体验(数据准备篇) Spark系列-核心概念 一. Spark核心概念 Master,也就是架构图中的Cluster Manager.Spark的Master和Workder节点分别 ...
- playbook-拓展
一.Handles 1. Handlers 在发生改变时执行的操作 2. handlers也是一些task的列表,通过名字来引用,它们和一般的task并没有什么区别. 3. handlers是由not ...
- virtualbox+vagrant学习-4-Vagrantfile-2-Configuration Version
Configuration Version 配置版本是vagrant 1.1+能够与vagrant 1.0保持向后兼容的机制.同时引入了引人注目的新特性和配置选项. 如果你运行了vagrant ini ...
- Ajax的async属性
Ajax请求中的async:false/true的作用 官方的解释是:http://api.jquery.com/jQuery.ajax/ async Boolean Default: true By ...