散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它经过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫作散列函数,存放记录的数组叫作散列表。java
从这个定义就能够得知,散列表实际上存储数据的仍然是数组。程序员
假设咱们要存储一组电话簿:算法
Bob 13113113111
Lany 13113113113
Mary 13113113112数组
数组查询数据靠的是数组下标,当咱们要查询Mary的电话号码,那得先从下标0开始查询,取出来发现是Bob,再取下标1的数据,取出来也不对,再取下标2的数据,取出来才是Mary的数据,这样遍历了整个数组才找到Mary的电话号码。数据结构
设计者的想法就是将Bob、Lany、Mary设计成key,可以用key来直接取出value值,而且不用遍历整组数据。函数
实现方式也是选择用数组来实现,利用数组高效查询的特性,在存取数据的时候使用了一个中转,将key转化成数组下标。性能
这个中转站,也就是哈希函数,这是一个加密函数,能够将任何数据加密成一串整型数字,虽然是数字,但通常状况下都是以十六进制来表示的,因此咱们日常看到的都是字符串的样子。this
哈希算法有不少种,目前应用最普遍的差很少是MD四、MD五、SHA1等加密算法。加密
将哈希映射到数组下标的实现方式也有不少,直接定址法、数字分析法、平方取中法、折叠法等等,在不一样的语言中,实现方式是不太同样的。spa
最多见的实现就是将哈希函数的结果对数组长度进行取模,获得一个0到数组长度之间的数字,将此数字做为下标。
存电话簿:
Bob 13113113111
Lany 13113113113
Mary 13113113112
咱们先建立一个长度为10的数组:
假设 Bob的哈希值为51246,那么51246%10=6,就将Bob的电话号码放到下标为6的空间中。
Mary的哈希值为26154,那么取模后获得的值为4,就将Mary的电话号码放到下标为4的空间中。
可是因为数组的长度是有限的,系统建立数组的时候也没法预测程序员要存储的数据有多大,因此当添加的数据愈来愈多的时候,数组的空间就会逐渐填满,即便没填满,取模后下标相同的概率也会愈来愈大。
好比Lany哈希值为33724,取模后值也是:
这种状况,就叫哈希冲突。
不管哈希函数设计有多么精细,都会产生冲突现象,也就是2个key的处理结果映射在了同一位置上,避免冲突的办法也有多种。
多哈希法,设计多种哈希函数。
开放地址法,若是哈希冲突,马上计算出一个候补地址尝试存储数据,若是仍有冲突便继续计算下一个候补地址。
拉链法,若是哈希冲突,则在目标地址中将数据换成链表存储,不管多少数据,均可以在链表末尾进行链接。
这里咱们以拉链法来理解:
查找和添加也是同样的,将key进行哈希处理,取模,获得的值就是数组下标。
若是我取Lany的电话号码呢?
在上面咱们已经知道了Lany和Mary是有哈希冲突的,在内存中已经拉出了一个动态链表,当我要查询Lany的电话号码的时候,找到了arr[4]这个元素,因为arr[4]这个元素与链表链头相对应,因此就顺着链表往下找。
可能你们会奇怪,由于我也奇怪,它是怎么肯定哪个元素是Lany的电话号码呢?
这里能够看一段Java中HashMap的源码:
JDK8,HashMap.java 第285行:
Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
数组中的每一个元素,存的并非单纯的咱们想存的数据,而是存了一个Node对象,同时将hash、key、和next也一块儿存了。
当添加的数据多了(HashMap初始长度只有16),散列表达到必定的饱和度,出现哈希冲突的概率会逐渐增高,可能会致使大量数据挤压在同一个数组下标上,造成长链,链表的查询无疑的低效率的,严重影响整个散列表的插入读取性能。
因此散列表必然会有一个扩容的操做:新建立一个2两倍长的空数组,再遍历原数组,将全部的节点从新进行哈希处理,后将新节点放入新数组。