Hash表的原理

哈希的概念:Hash,通常翻译作“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫作预映射, pre-image),经过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间一般远小于输入的空间,不一样的输入可能会散列成相同的输出,而不可能从散列值来惟一的肯定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。java

哈希的用途:Hash主要用于信息安全领域中加密算法,它把一些不一样长度的信息转化成杂乱的128位的编码,这些编码值叫作HASH值. 也能够说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。算法

哈希表的概念:哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它经过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫作散列函数(哈希函数),存放记录的数组叫作散列表。数组的各个栏叫作槽(buckets或者slots)。数组

哈希表的模型以下所示:安全

哈希表的过程:key通过hash函数做用后获得一个槽的索引(index),槽中保存着咱们想要获取的值(value)。数据结构

由于哈希表是基于数组实现的,因此能够实现随机存取,访问速度极快。可是在有的时候可能会发生不一样的key通过哈希函数计算后获得同一个槽的索引号的状况(几率很低)。这种状况称为冲突(碰撞)。若是碰撞发生了,采用单纯的数组实现哈希表显然不现实,必须加以解决。对于碰撞的解决方案是采用“拉链法”(open hashing)。函数

拉链法模型以下:性能

在拉链法模型中:槽,也就是数组的每一栏,存储的再也不是value值,而是一个链表的头指针。发生冲突的元素都放在同一张链表中,默认按照插入顺序依次进行链表的头插入。在这种状况下,哈希表就像是一个“链表的数组”。它仍然能够实现快速的访问,同时也解决了冲突。编码

不过若是冲突发生的很是频繁,那么链表长度会很长。不妨考虑极端的状况,全部元素都集中在一个槽中,那么整个哈希表就变成了一个链表!这种状况下,插入和删除操做效率极低,显然不是咱们想看到的,因此一个好的哈希函数必需要求尽可能减小冲突发生的几率,也就是要求数据分布尽可能均匀。加密

在哈希表长度必定的状况下,数据分布均匀的目标是经过哈希算法(散列方法)实现的。翻译

散列方法主要有:

一、除法散列法 :公式: index =hashcode % length

 可是因为位运算速度远快于求模运算,因此通常使用按位与运算进行求模,公式为:index = hashcode &(length-1)。不过这种方法要求length必须为2的整数次方时,两个公式才相等。由于当length为2的整数次方时,length-1的二进制表示所有为1,因此跟hashcode进行按位与运算能够获得槽索引,范围为[0,length)。

二、平方散列法 

求index是很是频繁的操做,而乘法的运算要比除法来得省时,因此咱们考虑把除法换成乘法和一个位移操做。公式: 

index = (hashcode * hashcode) >> 28   (右移,除以2^28。记法:左移变大,是乘。右移变小,是除)

这种方法若是hashcode值不大的话,其平方值也不会很大,那么其二进制高位几乎全为0。最后通过位移运算的结果确定为0。那么hashcode不大的状况下,所有获得索引号为0,这种冲突显然不想看到。因此要求hashcode必须足够大。

三、斐波那契(Fibonacci)散列法

平方散列法的缺点是显而易见的,因此咱们能不能找出一个理想的乘数,而不是拿hashcode自己看成乘数呢?答案是确定的。

对于16位整数而言,这个乘数是40503。

对于32位整数而言,这个乘数是2654435769。

对于64位整数而言,这个乘数是11400714819323198485。

这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。是否是以为很神奇,可能这就是数学之美吧。

 

经过采用适当的散列方法,咱们能够控制数据尽可能均匀地分布在槽中。可是不妨再考虑一个问题:若是一个哈希表被建立了,刚开始全部的槽都是空的。这时候传入一部分数据,数据经过哈希函数应该是能够均匀分布在数组的各个槽中的。偶尔会有小几率的数据发生冲突,被存储在同一个链表中,问题不大。可是随着数据的增多,空槽的数量愈来愈少,发生冲突的几率愈来愈大。为了解决这个问题,咱们引入了负载因子和再哈希的概念。

再哈希:指的是当槽的利用率(已使用槽与总槽数的比值)达到负载因子时,哈希表会就地扩容,具体过程为调用resize()方法,将哈希表的容量变为原来的两倍。以后对全部的数据从新进行散列过程,存储到相应的位置。

负载因子:再哈希发生的阈值。

要注意的是,再哈希的工做量是很大的,由于要对全部数据进行散列过程。因此,哈希表的长度和负载因子选取要合适。在负载因子必定的状况下,若是长度太小,再哈希就会频繁发生,这会严重影响性能;若是长度设置过大,虽然再哈希发生的频率很低,可是会浪费空间。同理,负载因子若是选取过大,那么在再哈希发生以前,就会产生大量的冲突(由于槽位基本已满);若是负载因子选取太小,那么再哈希就会频繁发生,也会影响性能。通常默认长度为16,负载因子为0.75。

哈希表的应用:java.util.HashMap类就是基于哈希表实现的。当经过HashMap对象查找某个key对应的value值过程为:先将传入的键key经过hashCode()方法获得哈希值hash,再经过哈希函数获得槽的索引号,该索引处存储的是指向某一个链表的引用。继续经过equals方法遍历比较链表上的每个对象,便可定位到最终的键值对应的Entry对象(键值对)。因此,HashMap类底层其实就是维护一张哈希表。

相关文章
相关标签/搜索