是时候说说哈希结构了。记得以前被人问过哈希结构,问题是:哈希结构最差效率是发生在什么时候?这个问题其实没太多意义,一般只有大学论文喜欢研究这种问题,因为在现实工程中,除非故意或者写错代码才有可能发生最差的效率情况。这篇文章将把哈希结构(我所理解的)来个大起底,照遍哈希结构的各个角落,最后,给出自己的哈希结构类。
一步步开始吧。
l 为什么要有哈希结构?
大学老师最喜欢的事情就是照本宣科,很少老师(甚至程序员)会问为什么有这种数据结构。知道为什么有哈希结构有助于知识连贯性和理解。
那么为什么会有哈希结构呢?在学哈希结构的之前,基本都会先接触树,二叉树,平衡二叉树最后到红黑树,这些数据结构时而为了排序用,时而用来查找用。就我所知,红黑树就主要用于存储结构,方便快速查询存储中的数据。那么作为后续的哈希结构,其主要是更强更快的查询,设计哈希的目的,是为了能尽量逼近O(1)时间的查询。
l 哈希的思想
为了尽量逼近O(1)时间查询,设想查询数据时如同取数组元素一样直接使用数组下标取到数组元素:
Value = Array[key];
这个时候就是O(1)时间。因此哈希的主要思想就是:将查找值转为数组下标值,一般来说就是整型int。这个转化过程就是哈希函数,哈希函数是哈希结构效率的核心,越是能将转化后得到的数组下标值均匀的分散在数组各个地方的哈希函数,查找时间越是逼近O(1)。
为什么只是逼近O(1)而不是O(1)呢?理论上是可以做到O(1)的,但说查找,研究查找,只有超大型数据才有意义,但在有限的数组大小情况下,按O(1)标准存储完所有数据几乎不可能,比如设计一个哈希函数将英文单词转为数组下标int型,若设计一个26(26个字母嘛)进制的数据类型,10个字母长度的单词经过哈希函数转化后得到的整型数据已经是很大的数据了,已经放不下了。
因此,哈希允许有冲突。所谓冲突,就是两个不同的查询数据,经过哈希函数转化后,其数组下标值是一样的,也就是说他们都想放到数组的同一个位置上。
解决冲突的方法一般有两种:
1. 链表法;这个是最最常用的方法,简单也最实用。当发生冲突时,就用一个链表连在发生冲突的位置上,后续冲突的数据都挂在链表上。
2. 线性探测法;当发生冲突时把冲突元素放到下一个相邻位置,若仍然冲突(即下一个位置已经被霸占),则继续找下一个位置。这种方法实现复杂,最重要的是相比第一种,效率比较低,因为发生随着发生冲突的次数增多,数组最终被填满,这个时候必须重新申请一个更大的数组,一般其情况下,为了保证效率,当填入的数据个数达到数组大小的一半时,就要重新生成更大的数组了。
其实还有第三种处理冲突的方法,就是二次哈希,也就是说,发生冲突的时候就用另一个哈希函数算出数组下标值,放到另一个哈希结构中。
说到这,开篇的问题已经有答案了,若哈希函数完全无法将查找数据区分成均匀的数组下标,全部统统冲突在同一个位置,这个时候,哈希结构就会退化为一根链表(或简单的数组),查询的时间就会变成O(N)。
l 如何设计哈希函数
哈希函数必须保证生成的数组下标能均匀分散,一般采取的方法是取模法。取模的原理很简单,就是把查询值转成整型后对某个整数取模。这个整数,就是哈希结构里面的数组大小。根据一些学术论文的统计,这个整数最好是素数,这个时候区分度更好,并且,其越大,区分度也越好。
现在,就开始写一个自己的哈希结构。这个结构是基于链表法解决冲突的,这个方法是最简单也是最实用的。
template <class Key, class Value, class F>
class CListHash
Key就是查询关键字,Value就是对应需要查找的值,F是哈希函数类。F这个类应该提供operator()()函数,在CListHash类里面使用这个函数类对象计算哈希值:
class CGetHashCode{
public:
int operator()(const int M, const std::string& strWord)