什么是Hash算法:#####
简单的说,hash算法就是将字符串转化为数字的算法。
用一个例子说Hash的优势#####
试想如果我们对一个数组进行Query,这个数组里,每一个元素都是一个字符串。我们知道数组最快的检索办法是通过数组的下标进行检索,但是对于这种场景,我们无能为力,只能从头查到尾,从而查询出目标元素。
假设,我要找gaofei,那就需要遍历整个数组,十分的低效。这样最坏情况下时间复杂度是O(n)的,但是数组的查询时间复杂度是O(1)的(下标查询),那有没有一种方法能达到O(1)的时间复杂度呢?有!那就是用 Hash。
我们需要达到O(1)的时间复杂度,那无疑使用数组的下标查找。那么我们就需要将存储的元素转化为数组的下标。
那怎么设计hash函数呢?我们用一种最笨的方法,将所有字符串中的字符转化为数字后相加。那ok,我们简单的实现下。
zhangsan=>hash()=>858
lisi=>hash()=>433
wanger=>hash()=>644
wangwu=>hash()=>665
zhangsi=>hash()=>756
gaofei=>hash()=>619
这样,我们就计算出来了每一个字符串对应的数字映射了,我们叫这个数字为hash值,接下来我们再放到数组里:
上图中数组的下标就是字符串对应的数字值。这下我们查找gaofei就很容易了,首先我们根据hash函数计算出gaofei的位置为619,然后去数组的619中找到gaofei,这样时间复杂度就是O(1)啦。
hash冲突(拉链法):#####
但是上面的实现是存在一个问题的,如果还有一个元素叫feigao会怎么样?
我们首先计算下feigao的hash值,计算结果竟然和gaofei一样,也是619。这下产生了Hash冲突了,怎么办?619已经有gaofei了,feigao放在哪?所以接下来我们只能改变数组的结构了,怎么改变?我们将数组内的元素改变为一个链表,这样就能装下足够多的元素了。这样就能解决hash冲突的问题了:
如上图,我们将冲突的元素变为了链表结构,这样我们就能把feigao也放在了这个table结构里面,其实这个数据结构就叫做Hash表(HashTable)。
我们在检索的时候可以这样检索
找到gaofei后,我们便可以遍历链表,找到feigao了。是不是很开森?
怎样压缩hash表#####
但是,问题又来了,上面的数组好像不是很连续啊,从433直接跳到了619.中间的数组都浪费了啊!!!。其实也不能说是浪费,只不过暂时没有用啊,如果没有对应的元素出现,就这样一直浪费着?显然是不可取的。那我们想办法压缩下这个数组。
其实很简答,只要减小hash值就行了嘛,那么我们对hash值取模,这样就能保证所有的hash值落在模值之内了。
但是对于取模的运算计算机通常用位运算是更快的,例如java的HashMap的默认容量是16,每次扩容之后也都维持2^n,为什么呢?
对于取模运算hashMap是这样实现的hashcode& (length-1)。如果length为16,那么正好是hashcode&15,例如feigao这个计算吧:
hash值:619 => ...0001001101011
length-1:15 => ...0000000001111
二者做与运算结果为:...000000001011 => 11(十进制)
其实可以口算一下结果为38余11(注意这里是除16而不是15)。同理对于32也是一样的。但是这里有一个很著名的问题,就是因为数字的不均匀会导致hash值的二进制数末尾都是1的这种场景。这种场景会导致很多的值集中在最后一个数组元素,从而分布不均匀。解决方案是对hash值进行重新计算hash。这种机制比较复杂。至今没搞懂。
这里主要是考虑这样设计主要考虑计算速度会十分的快,根据不同实现这个容量也是不固定的,这里只是以java的HashMap为例。
用几个例子说一下HashTable的用处:#####
Case1:两文件找出重复的元素#####
问题是这样的,有两个文件,文件中有一些短字符串,字符串个数分别为n。它们是有重复的字符串,现在需要找出所有重复的字符串。
首先我们考虑最笨的方法,遍历文件1中的每个元素,取出每一个元素分别去文件2中进行查找,这样的时间复杂度为O(n^2)
接下来我们使用HashTable,我们遍历文件1中的元素和文件2中的元素,放入HashTable中,对于重复的字符串我们做计数处理:
接下来遍历整个列表,找出所有个数大于1的即为重复的元素。
Case2:两文件找出出现次数最多的元素#####
和上面是同理的,但是在遍历的时候需要找计数最大的元素,即为出现次数最多的元素。
Case3:路由算法#####
在一个多线程处理数据的场景下,我们需要将一个数据集分给不同的处理线程,同时要保证,相同的元素需要分到相同的处理线程上去,那么我们怎么处理呢?
其实这个就是一个很典型的Hash值应用场景,对于很多的计算引擎默认都是用hash算法去解决这个问题。因为相同元素的Hash值相同,那么我们可以取hash之后进行模运算,运算结果分配到不同的线程:
Hash算法的要点#####
在设计Hash算法的时候。一定要保证相同字符串产生的Hash值相同,同时要尽量的减小Hash冲突的发生(虽然避免不了)。这样才是一个好的hash算法。目前,MurmurHash是一种冲突率最低的Hash算法。如果有需要,可以优先考虑此算法。