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()方法添加新元素時,老是將新元素添加在鏈表的表頭。而不是鏈表的其它位置。
完。