–本文章来自侯捷老师的STL课程和自己的理解
若有错误,欢迎指正
除了红黑树可以作为set与map的底层结构之外,还有一种结构叫做哈希表也可以。
哈希表可以说是十分熟悉的一个结构了,很多地方都有使用到这个结构。操作系统(内存分配那一块),算法与数据结构,基本都有提及。
哈希表是怎么一回事呢?
首先我们需要知道什么是哈希函数。
所谓哈希函数,就是你不论扔多少长度的东西进去,总能完成一个编码过程。这个编码过程会根据你输入的东西来得到一个值,我们一般叫做哈希值。
这个值有什么作用呢?
举个很简单的例子。
比如我输入我的英文名字Peter,那么通过哈希函数,计算出了一个值假如是3。那么我就可以说,Peter在这个哈希函数(哈希表)下的hashcode,即Key值是3。
假如哈希表又是一个线性表H的话,那么就可以直接得到H【3】的data值为“Peter”。
这样,我们就把一个查找操作缩短在了O(1)的时间内可以查找到。大大缩短了查找效率。
假如又有一个人的名字叫做Tom,如果它通过哈希函数计算出的值也是3,那么就产生了冲突,这个时候,就需要方案来解决冲突。
解决冲突的方法有很多种。
C++的哈希表使用的是链表的方式来解决冲突。
即,将所有哈希值相同的元素,放在一个Key值下,用单向链表串在一起。
这样又会引出一个新的问题。
假如这个单向链表过长的话,查询时间又会回到O(n)左右。
那么,这个时候就有一个经验性的做法。
当元素的个数超过哈希表的长度时,就重新申请空间,再重新做哈希运算。
以上图为例,当元素总量达到54>哈希表长度53时,就会成倍的增长空间,然后重新计算哈希。哈希函数选的是和长度求模。
这里还需要注意的是:
哈希表成倍增长之后,还会进行一次调整,刻意的让其空间大小为质数。
这些数值由于比较特殊,为了节省性能,都会进行预先的计算,如上图的右上角。存储在了一个数组里面,这样申请的时候,就不用再去计算质数,只用直接判断下一个质数是哪个就行了。
哈希表的模板类如下:
而内部的哈希函数则采用的是偏特化去处理丢入的不同的数据结构:
一般来说,需要更精细处理的一般是字符串之类的数据。
而数值一般只需要经过简单的处理就可以称为hashcode编码了。
大致特化如下:
这里直接将侯捷老师后面提到的一个万用哈希函数放到前面来讲。
注意上图的hash_val函数。这是一个模板函数,使用了语法"…",代表可以传入多个模板参数。那么它是怎么逐一处理这些模板参数的呢?
首先接受整体的模板参数,让其第一个模板参数成为seed,即种子。随后再一次调用hash_val。但是这次一调用的hash_val是第一参数为seed,后面参数为剩下的其余参数的参数列表。然后不断分离,直到只有一个seed和一个参数列表val的时候,在进行进一步的计算返回。
本质上是一个自己调用自己不同版本进行递归分离参数再计算的过程。
这里的seed的计算没有什么数学性的原理,只是单纯用到了黄金分割让其产生的编码更离散,更不容易产生冲突。
然后就是借用hashtable实现的set和map了,除了底层支撑的数据结构不同以外,其余的操作方式与红黑树支撑的大致相同。C++11中采用如下的名称:
使用方法可以参考这里
最后多提一句。
前两天和舍友聊天聊到了JAVA和C++hashtable的异同之处。我发现JAVA的哈希表的设计思路也很巧妙。
它和C++大致相同,只是在解决链表过长的问题上,
选择将超过阈值的链表转变为红黑树。
这样,不仅可以有效避免像C++中因为扩展哈希表而产生的很多闲置内存位置,也可以保证查找时间不至于达到O(n)。我觉得这是一个非常不错的设计思路。改天有空的话看看能不能魔改一下C++的hashtable,对比一下两个的时间效率和空间开销。
当然,最近为了准备实习的面试可能没有什么时间。想看的读者可以自行实现或者等我哪天空闲下来想起来了再说。哈哈哈哈。