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,无非就是链表结构不一样,至于其中的增删改查的细 ...
随机推荐
- c# Windows Service 桌面上显示UI
介绍 本文的目的是说明如何从Windows Vista中的服务正确启动交互式进程,以及演示如何以完全管理员权限启动该进程.交互式过程是能够在桌面上显示UI的过程. 本文介绍如何创建一个名为Loader ...
- Windows Server 2008搭建域控制器
前言 1.为什么要建域 工作组的分散管理模式不适合大型的网络环境下工作,域模式就是针对大型的网络管理需求设计的,就是共享用户账号,计算机账号和安全策略的计算机集合.域中集中存储用户账号的计算机就是域控 ...
- fountion 的调用 和 打印返回值 + 占位符
结果: (2) 结果
- 【C#】#102 发送邮件
项目需求:定时的发送邮件,于是学习了如何发送邮件 下面有一个简单的例子.能够实现简单的发送邮件,加上附件可以添加一个属性[Attachment],然后配置上附件的路径 Demo下载 代码总共只有一下这 ...
- php 访问控制和重载
一 php 类中定义的private/protected属性,类外部是无法访问的,但是 我们可以通过public方法来访问设置这些属性 如下 <?php class test{ priv ...
- C++中重载决议与可访问性检查的顺序
http://blog.csdn.net/starlee/article/details/1406781 对于如下的类: class ClxECS{public: double Test(dou ...
- 使用Yarn+Webpack+Babel6搭建React.js环境
使用Yarn+Webpack+Babel6搭建React.js环境 Facebook开源的React.js已经改变了世人对前端UI的思考方式.这种基于组件方式的优势之一,就是使View更加的简单,因为 ...
- django 表结构
django 表结构 一:查看orm写的sq语句:如果对某个语句不清楚的话可以调用queryset的的query方法来查看sql. 1 obj=Hostinfo.objects.filter(id=v ...
- MySQL(五)SELECT语句执行顺序
上一篇讲述了Oracle的SELECT语法的执行顺序,这篇讲述MySQL的SELECT语法的执行顺序.MySQL的SELECT语法的执行顺序和Oracle的基本相同,只是增加了MySQL独有的LIMI ...
- 如何修改macbook的MAC地址
修改命令 sudo ifconfig en0 ether xx:xx:xx:xx:xx:xx 查看 ifconfig en0 | grep ether 随机生产一个MAC地址 openssl rand ...