二叉查找樹可能由於屢次插入新節點致使失去平衡,使得查找效率低,查找的複雜度甚至可能會出現線性的,爲了解決由於新節點的插入而致使查找樹不平衡,此時就出現了紅黑樹。java
紅黑樹它一種特殊的二叉查找樹。紅黑樹的每一個節點上都有存儲位表示節點的顏色,能夠是紅(Red)或黑(Black)。它具備如下特色:node
(1)每一個節點或者是黑色,或者是紅色。數組
多線程
(3)每一個葉子節點(葉子節點,是指爲空(NIL或NULL)的葉子節點)是黑色。app
(4)若是一個節點是紅色的,則它的子節點必定是黑色(即從根節點到葉子節點的路徑上不能有兩個重複的紅色節點)。函數
(5)從一個節點到其上每個葉節點的全部路徑都具備相同的黑色節點個數。post
① 將紅黑樹看成一顆二叉查找樹,將節點插入。this
② 將插入的節點着色爲"紅色"。(由於條件5,從一個節點到其中每個節點的的全部路徑都具備相同的黑色節點)。spa
③經過一系列的旋轉(左旋或右旋操做)或着色等操做,使之從新成爲一顆紅黑樹。.net
在java 1.7以前是用數組和鏈表一塊兒組合構成HashMap,在java1.8以後就使用當鏈表長度超過8以後,就會將鏈表轉化爲紅黑樹,縮小查找的時間(紅黑樹維護也會花費大量時間,包含左旋、右旋和變色過程)。
hashmap構造函數會初始化三個值:
在初始化一個hashmap對象的時候,指定鍵值對的同時,也能夠指定初始map的容量大小,假設此處咱們指定大小爲11,則會在構造函數中調用tableSizeFor將容量改成2的n冪次,即比當前容量大,並且必須是2的指數次冪的最小數,就會變成16。這是由於2的指數次冪便於計算進行位運算操做,提高運行效率問題(位運算>加法>乘法>除法>取模)。
hashmap的的默認值是16,負載因子默認是0.75,代碼以下:
//HashMap<String,String> hashMap = new HashMap<String, String>(11); /** * Returns a power of two size for the given target capacity. **/ static final int tableSizeFor(int cap) { int n = cap - 1; //10 防止在cap已是2的n次冪的狀況下 // >>> 表示不關心符號位,對數據的二進制形式進行右移 |表示或運算 n |= n >>> 1; //15 n |= n >>> 2; //15 n |= n >>> 4; //15 n |= n >>> 8; //15 n |= n >>> 16; //15 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; //16 }
//hashMap.put("2020", "good luck"); /** * Implements Map.put and related methods. * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //若是hashtable沒有初始化,則初始化該table數組 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //經過位運算找到數組中的下標位置,若是數組中對應下標爲空,則能夠直接存放下去 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //數組元素對應的位置已經有元素,產生碰撞 Node<K,V> e; K k; //若是插入的元素key是已經存在的,則將新的value替換掉原來的舊值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若是此時table數組對應的位置是紅黑樹結構,則將該節點插入紅黑樹中 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //若是此時table數組對應的位置是鏈表結構 for (int binCount = 0; ; ++binCount) { //遍歷到數組尾端,沒有與插入鍵值對相同的key,則將新的鍵值對插入鏈表尾部 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //鏈表過長,將鏈表轉化爲紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //發現鏈表中的某個節點有與插入鍵值對相同的key,則跳出循環,在循環外部從新賦值 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //該key在hashmap已存在,更新與在鏈表跳出循環節點對應的值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //超過閾值則更新 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
/** * Implements Map.get and related methods. * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //table數組不爲空,且對應的下標位置也不爲空。 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //若是第一個位置是對應的key,則返回 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; }
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //table不爲空,且容量大於0 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 } //閾值大於0,直接使用舊的閾值 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //若是閾值爲零,則使用默認的初始化值 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; //更新數組桶 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //將以前舊數組桶的數據從新移到新數組桶中 if (oldTab != null) { //依次遍歷舊table中每一個數組桶的元素 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //若是數組桶中含有元素 if ((e = oldTab[j]) != null) { //將下標數據清空 oldTab[j] = null; //若是元組的某一桶中只有一個元素,則直接將該元素移到新的位置去 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //若是是紅黑樹結構 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //鏈表 -- 對舊桶裏鏈表中的每個元素從新計算哈希值獲得下標 else { // preserve order //將原先桶中的鏈表分爲兩個鏈表 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; /* * e.hash & oldCap 對hash取模運算, * 雖然數組大小擴大了一倍, * 可是同一個key在新舊table中對應的index卻存在必定聯繫: * 要麼一致,要麼相差一個 oldCap。 */ if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
此處在處理鏈表的時候,如何將鏈表中的節點從新分配到新的哈希表須要作一些解釋。在擴容的時候,將原來的哈希表擴大了一倍,原來屬於同一個桶中的數據會被從新分配,此時取模運算時(a mod b),會注意到,b會擴大兩倍(a mod 2b),此時若是該桶中的某一個數據的哈希值是c1(0<c<b),則它一定仍是會落入原來的位置,而若是桶中的某一個數據的哈希值是c2(b<c2<2b),則它會被從新分配到一個新的位置(這個位置是原先的哈希桶位置+舊桶的大小)。
HashMap在多線程的狀況下出現的死循環現象
在某些java版本中擴容機制若是使用鏈表,且再插入時使用尾插法會出現死循環,具體緣由能夠參考老生常談,HashMap的死循環,在本文中所參考的java版本使用了頭插法的方式將元素添加到鏈表當中,能夠避免死循環的出現,可是會出現一部分節點丟失的問題。如圖:
假設原始的哈希map的某個桶的數據以下,此時線程開始擴容,將桶中的數據分配到lo和hi桶的鏈表中。
初始時刻線程1和線程2開始運行,線程1在執行完如下代碼後,線程1的時間片運行結束。線程1運行的結果如圖所示
線程2與線程1同時運行,線程2的時間片未用完,還在繼續執行,根據代碼的分配策略,線程2直到時間片運行結束,出現如圖所示的結果:
此時CPU的時間片又被分配到了線程1,線程1繼續運行,由於此時A所在的鏈表結構已經發生了變化,只能處理A,B,D三個元素。此時線程1建立的hashmap如圖: