哈希表(hash table)也叫散列表,是一種很是重要的數據結構
應用場景之一:緩存技術(好比memcached的核心其實就是在內存中維護一張大的哈希表)node
用一段連續的存儲單元來存儲數據。
知道下標進行查找,時間複雜度爲O(1)。
知道value值進行查找,時間複雜度爲O(n),由於須要遍歷數組,逐一比對給定關鍵字和數組元素。
對於有序數組,則可採用二分查找,插值查找,斐波那契查找等方式,可將查找複雜度提升爲O(logn)。
插入、刪除操做,涉及到數組元素的移動,其平均複雜度也爲O(n)。數組
新增,刪除等操做(在找到指定操做位置後),僅需處理結點間的引用便可,時間複雜度爲O(1)。
查找操做須要遍歷鏈表逐一進行比對,複雜度爲O(n)。緩存
一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操做,平均複雜度均爲O(logn)。安全
不考慮哈希衝突的狀況下,添加,刪除,查找等操做,僅需一次定位便可完成,時間複雜度爲O(1)。數據結構
數據結構的物理存儲結構:併發
一、順序存儲結構
二、 鏈式存儲結構app
哈希表的主幹就是數組。利用了數組的特性----根據下標查找某個元素一次定位就能夠找到。
在新增或查找某個元素時,咱們經過把當前元素的關鍵字傳給哈希函數,而後映射到數組中的某個位置,最後經過數組下標一次定位就可完成操做。memcached
存儲位置 = f(關鍵字)
這個函數的設計好壞會直接影響到哈希表的優劣。函數
插入、查找操做,如圖:性能
若是兩個不一樣的元素,經過哈希函數得出的實際存儲地址相同怎麼辦?
好的哈希函數會盡量地保證 計算簡單和散列地址分佈均勻,可是,咱們須要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證獲得的存儲地址絕對不發生衝突。
哈希衝突的解決方案:
一、開放定址法
二、再散列函數法
三、鏈地址法
HashMap便是採用了鏈地址法,也就是數組+鏈表的方式。
JDK 8 中,HashMap的主幹是一個Node數組。
//該table在第一次使用時初始化,並在必要時進行調整。當分配時,長度老是2的冪。 transient Node<K,V>[] table;
Node是HashMap中的一個靜態內部類
//HashMap.Node是LinkedHashMap.Entry的父類 //LinkedHashMap.Entry是HashMap.TreeNode的父類 static class Node<K,V> implements Map.Entry<K,V> { final int hash;//對key的hashcode值進行hash運算後獲得的值,存儲在Entry,避免重複計算 final K key; V value; Node<K,V> next;//存儲指向下一個Node的引用,單鏈表結構 Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
HashMap的總體結構以下:
HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前Node的next爲null),那麼對於查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組位置包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。
HashMap的幾個重要屬性
//實際存儲的key-value鍵值對的個數 transient int size; //閾值; //當table分配內存空間後,threshold通常爲 capacity*loadFactory //HashMap在進行擴容時須要參考threshold int threshold; //負載因子,表明了table的填充度有多少,默認是0.75,超過了負載,就開始擴容 final float loadFactor; //用於快速失敗,因爲HashMap非線程安全,在對HashMap進行迭代時,若是期間其餘線程的參與致使HashMap的結構發生變化了(好比put,remove等操做),須要拋出異常ConcurrentModificationException transient int modCount;
HashMap有4個構造器,若是用戶沒有給構造器傳入initialCapacity 和loadFactor這兩個參數,會使用默認值 initialCapacity默認爲16,loadFactory默認爲0.75。
在常規構造器中,沒有爲數組table分配內存空間(有一個入參爲指定Map的構造器例外),而是在執行put操做的時候才真正構建table數組。
public V put(K key, V value) { return putVal(hash("2"), "2", "ljs", false, true); }
static final int hash(Object key) { int h; //key.hashCode()該對象本身的hashcode //HashMap的哈希函數:(hashcode) ^ (hashcode >>> 16) //hashcode 與 向右無符號移動16位的本身 異或,通常都等於hashcode的值 // >>> 與 >> 都是右移,>>> 是會把符號位也一塊兒移動,就是說負數用 >>> 後,會成爲正數 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
/** * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //若是table數組爲空數組{},爲table分配實際內存空間;----resize() //在構造器中沒有指定threshold的話,就是默認的threshold,16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash key的哈希值 和 數組長度作 與運算,計算出在table數組中的具體下標位置 //該位置沒有數據,就直接插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //該位置有數據,遍歷該數組下標的單鏈表 //找到hash、key相同的,執行覆蓋操做。用新value替換舊value,並返回舊value //沒有hash、key相同的,插入到鏈表尾部 else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //覆蓋操做 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //不是第一次resize(),擴容----Threshold * 2 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //雙倍 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //第一次resize(),爲table分配內存空間,newCap = 16 ; newThreshold = 0.75*16 = 12 else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }
經過以上代碼可以得知,當size大於閾值的時候,須要進行數組擴容,擴容時,須要新建一個長度爲以前數組2倍的新的數組,而後將當前的Node數組中的元素所有傳輸過去,擴容後的新數組長度爲以前的2倍,因此擴容相對來講是個耗資源的操做。
存儲位置的肯定流程:
key.hashcode()-->hash()-->(length - 1) & hash-->最終索引位置,找到對應位置table[i]。
public V get("2") { Node<K,V> e; return (e = getNode(hash("2"), "2")) == null ? null : e.value; }
static final int hash("2") { int h; return ("2" == null) ? 0 : (h = "2".hashCode()) ^ (h >>> 16); }
final Node<K,V> getNode(50, "2") { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
取值位置的肯定流程:
key.hashcode()-->hash()-->(length - 1) & hash-->最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,經過key的equals方法比對查找對應的記錄。
注:存數據須要hashcode(),取數據須要equals();hashcode()、equals()是Object的方法,能夠按照本身的需求,重寫對象的hashcode() 和 equals() 方法。
數組進行擴容,數組長度發生變化,而存儲位置 index = h&(length-1),index也可能會發生變化,須要從新計算index:
將老數組中的數據逐個鏈表地遍歷,扔到新的擴容後的數組中,咱們的數組索引位置的計算是經過 對key值的hashcode進行hash函數運算後,再經過和 length-1進行與運算。
16的二進制表示爲 10000,那麼length-1就是15,二進制爲01111,同理擴容後的數組長度爲32,二進制表示爲100000,length-1爲31,二進制表示爲011111。從下圖能夠咱們也能看到這樣會保證低位全爲1,而擴容後只有一位差別,也就是多出了最左位的1,這樣在經過 h & (length-1)的時候,只要h對應的最左邊的那一個差別位爲0,就能保證獲得的新的數組索引和老數組索引一致(大大減小了以前已經散列良好的老數組的數據位置從新調換)。
數組長度保持2的次冪,length-1的低位都爲1
&運算,高位是不會對結果產生影響的,因此只關注低位,若是低位所有爲1,那麼對於h低位部分來講,任何一位的變化都會對結果產生影響,也就是說,要獲得index=21這個存儲位置,h的低位只有這一種組合。
若是不是2的次冪,也就是低位不是全爲1此時,要使得index=21,h的低位部分再也不具備惟一性了,哈希衝突的概率會變的更大,同時,index對應的這個bit位不管如何不會等於1了,而對應的那些數組位置也就被白白浪費了