本文为转载文章,转载地址:http://blog.csdn.net/picway/article/details/64444922
动机
我们已经有了数组,ArrayList和LinkedList,为什么有需要HashMap?
因为在之前的数据结构中,最好的搜索方法是有序数组的二分查找和AVL树搜索。它们的最坏情况所搜时间都是O(lgn)。是否有更快的算法?散列表数据结构提供了这样的保证——O(1)的平均搜索时间。
散列与散列函数
散列表是存储数组,散列是通过推演出对象的数值散列码,并将这个散列码映射到散列表中的位置来在散列表中存储对象的过程。
将数映射到散列表中未知的映射关系称为散列函数。
构造哈希表的几种方法
1.直接定址法(取关键字的某个线性函数为哈希地址)
2.除留余数法(取关键值被某个不大于散列表长m的数p除后的所得的余数为散列地址)
3.平方取中法
4.折叠法
5.随机数法
6.数学分析法
常用方法是直接定址法和除留余数法。
冲突与解决
不同的Key值经过哈希函数Hash(Key)处理以后可能产生相同的值哈希地址,我们称这种情况为哈希冲突。任意的散列函数都不能避免产生冲突。
闭链法
若key1,key2,key3产生哈希冲突(key1,key2,key3值不相同,映射的哈希地址同为key),用以下方法确定它们的地址
线性探测法
若当前key与原来key产生相同的哈希地址,则当前key存在该地址之后没有存任何元素的地址中
key1:hash(key)+0
key2:hash(key)+1
key3:hash(key)+2
二次探测法
若当前key与原来key产生相同的哈希地址,则当前key存在该地址后偏移量为(1,2,3…)的二次方地址处
key1:hash(key)+0
key2:hash(key)+1^2
key3:hash(key)+2^2
开链法
相同key值的节点存储在一个链表上。key值不同的链表头节点组成一个数组。
HashMap使用的就是这种方法。
HashMap的死循环问题
从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题。后来,我们的程序性能有问题,所以需要变成多线程的,于是,变成多线程后到了线上,发现程序经常占了100%的CPU,查看堆栈,你会发现程序都Hang在了HashMap.get()这个方法上了,重启程序后问题消失。但是过段时间又会来。而且,这个问题在测试环境里可能很难重现。
我们简单的看一下我们自己的代码,我们就知道HashMap被多个线程操作。而Java的文档说HashMap是非线程安全的,应该用ConcurrentHashMap。
HashMap会用一个链表数组来做分散所有的key:
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
当一个key被加入时,会通过Hash算法通过key算出这个数组的下标i,然后就把这个键值对插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链表。
我们知道数组大小是不可变的,当HashMap存放元素增加到一定数目(threshold,可通过负载因子和容量相乘获得,负载因子loadFactor可以在初始化时设置,默认是0.75),为了减少碰撞,会执行重哈希操作reHash(),reHash是一个很笨重的操作,要新建一个更大的链表数组,把原数组中的元素重新计算hash值,填入新数组,然后用新数组替代原数组。
因此,如果并发操作HashMap,会有可能在一个线程执行reHash()的时候get或set,这时有可能会使链表出现循环链表,导致程序出现infinite loop——死循环,这是HashMap的一个缺点,但并不打算改正它,因为有线程安全的ConcurrentHashMap来提供并发下的哈希表实现。