1,Hashing过程算法
像二分查找、AVL树查找,这些查找算法的时间复杂度为O(logn),而对于哈希表而言,咱们通常说它的查找时间复杂度为O(1)。那它是怎么实现的呢?这就是一个Hashing过程。数组
在JAVA中,每一个对象都有一个散列码,它是由Object类的hashCode()方法计算获得的(固然也能够覆盖Object的hashCode())。而咱们能够在散列码的基础上,定义一个哈希函数,再对哈希函数计算出的结果求余,最终获得该对象在哈希表的位置。函数
1 final int hash(Object k) { 2 int h = hashSeed; 3 if (0 != h && k instanceof String) { 4 return sun.misc.Hashing.stringHash32((String) k); 5 } 6 7 h ^= k.hashCode(); 8 h ^= (h >>> 20) ^ (h >>> 12); 9 return h ^ (h >>> 7) ^ (h >>> 4); 10 }
如上,哈希函数hash(Object k) 中用到了hashCode()。而后再通过进一步的特殊处理,获得一个最终的哈希值。哈希函数的定义是须要技艺的,由于它要保证尽可能地将全部的Key均匀地分布,所以最好借助前人已实践的经验。spa
当获得哈希值以后,根据该哈希值Mod N(求余)计算出其在哈希表的位置。code
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }
indexFor(int h, int length)实际上完成的就是求余操做。只不过求余操做涉及到除法,而这里能够经过移位操做来代替除法。即两者完成的功能都是同样的,移位的效率更高。对象
哈希过程为何须要先根据hashCode获得一个值(又称散列码),而后再对该值求余呢?blog
在JAVA中,Object类的hashCode()方法返回的是由调用对象的内存地址导出的一个值,也即,当没有覆盖Object类中的equals() 和 hashCode()时,只有当两个对象的内存地址同样时,才认为两个对象是相等的。这显然不符合实际状况,好比Person类有 String id、String name.....显然在现实中是根据id(身份证)不一样来判断两我的不一样。所以,须要进一步根据hashCode()值来封装(如上面的 hash(Object k)方法),返回一个合理的散列码。内存
那为何又须要对获得的散列码求余呢?---上面的 indexFor(int h, int length)完成的功能ci
在底层是用数组来存储<key, value>的,而咱们获得的散列码可能很大(事实上散列码的范围很是广)get
而内存是有限的,不能分配为数组分配一块很大很大的空间,所以,存储<key, value>的数组空间相对较小。
从而须要把 全部的散列码都 “约束” 到这个有效的数组空间中。----这也是致使冲突的根源
为何使用HashMap查找是O(1)呢?
T value = hashmap.get(key)
①get(key)时,一步计算出该key所对应的底层数组array的 index (至关于上面 hash(Object k ) 和 indexFor(int h, int length) 这两个函数完成的功能)
②value = array[index]
所以,就认为查找的复杂度为O(1)
2,冲突处理
冲突处理主要分两种,一种是开放定址法,另外一种是链地址法。HashMap的实现中采用的是链地址法。
开放定址法有两种处理方式,一种是线性探测另外一种是平方探测。
线性探测:依次探测冲突位置的下一个位置。如,在哈希表的位置2处发生了冲突,则探测位置3处是否被使用了,若被使用了,则探测位置4……直至下一个被探测的位置为空(意味着还有位置能够插入元素---插入成功)或者探测了N-1(N为哈希表的长度)个元素又回到了原始的冲突位置处(意味着已经没有位置可供新元素插入了---插入失败)
所以,插入一个元素时,最坏状况下的时间复杂度为O(N),由于它有可能探测了N-1个元素!
平方探测:以平方大小来递增下一次待探测的位置。如,在哈希表位置2处发生了冲突,则探测 (1^2=1)位置3(2+1),若位置3被使用了,则探测(2^2=4) 位置6(2+4),若位置6被使用了,则探测(3^2=9)位置11(2+9=11)……平方探测法有一个特色:对于任何一个给定的素数N(假设哈希表的长度设置为素数),当计算( h(k) + i ^2 ) MOD N 时,随着 i 的增加,获得的结果是循环的。
所以,当平方探测重复探测了某一个位置时,说明探测失败即已经没有位置可供新元素插入了,尽管此时哈希表并无满。
平方探测是跳着探测的,它忽略了一些位置,而这些位置多是空的。即在哈希表仍未满的状况下,已经不能再插入新元素了
最坏状况下,平方探测须要检测 N/2个位置,所以插入一个元素的最坏时间复杂度为O(N)。
链地址法
在HashMap的实现中,采用的链地址法来解决冲突,它有一个桶的概念:对于Entry数组而言,数组的每一个元素处存储的是链表,而不是直接的Value。在链表中的每一个元素才是真正的<Key, Value>。而一个链表,就是一个桶!所以HashMap最多能够有Entry.length 个桶。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { static final Entry<?,?>[] EMPTY_TABLE = {}; ..... .....
HashMap中有一个Entry数组,而Entry类是HashMap的内部类。由Entry类来封装实际的<Key, Value>
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash;
HashMap中还有两个变量: int threshold 和 float loadFactor。loadFactor 默认是0.75,threshold做用以下:当HashMap中的元素个数超过threshold时,就会从新调整哈希的大小。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);
而loadFactor做用是:指定threshold,通常状况下,哈希表的大小乘以0.75等于threshold。
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
在HashMap中,addEntry()方法添加新元素时,老是将新元素添加在链表的表头。而不是链表的其它位置。
完。