java8 HashMap資料結構實現
一、基礎元素Node
static class Node<K,V> implements Map.Entry<K,V> { //key的hash值 final int hash; final K key; V value; //通過next屬性將多個hash值相同的元素關聯起來,形成單向連結串列 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
實現很簡單,關鍵是next屬性,插入、查詢、刪除用法如下:
@Test public void test8() throws Exception { Node<String,Integer> first=new Node<>(1, "1", 1, null); Node<String,Integer> node2=new Node<>(1, "2", 1, null); Node<String,Integer> node3=new Node<>(1, "3", 1, null); Node<String,Integer> node4=new Node<>(1, "4", 1, null); Node<String,Integer> node5=new Node<>(1, "5", 1, null); //插入時建立鏈式關係 first.next=node2; node2.next=node3; node3.next=node4; node4.next=node5; //從頭元素開始遍歷查詢 Node<String,Integer> n=first; do{ if(n.key.equals("5")){ System.out.println("====元素查詢======"); System.out.println(n); } }while ((n=n.next)!=null); //移除元素node2 first.next=node3; node2.next=null; System.out.println("====移除元素======"); n=first; do{ System.out.println(n); }while ((n=n.next)!=null); }
二、紅黑樹元素TreeNode
1、類定義和類屬性
TreeNode繼承自LinkedHashMap.Entry<K,V>,後者繼承自HashMap.Node<K,V>,只是增加了兩個屬性before和after,用於儲存當前節點的前後節點引用,從而形成一條可以雙向遍歷的連結串列。TreeNode繼承自LinkedHashMap.Entry<K,V>是為了方便LinkedHashMap實現,本身並沒有直接使用before和after兩個屬性。LinkedHashMap.Entry<K,V>的定義如下圖:
TreeNode定義的屬性如下圖:
注意TreeNode的實現同時維護了紅黑樹和雙向鏈式兩種應用關係,這樣便於在紅黑樹和連結串列之間做形態轉換。
2、基礎方法:
/**
* 返回當前節點的根節點
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
//該節點的父節點為null時該節點為根節點
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* 遞迴檢查當前節點是否符合雙向連結串列以及紅黑樹規則
* 該方法在紅黑樹發生改變時都會執行一次,確保改變後紅黑樹符合規範
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
//是否符合雙向連結串列規則
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
//是否符合紅黑樹規則
if (tp != null && t != tp.left && t != tp.right)
return false;
//左節點的hash值必須小於根節點hash值
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
//右節點hash值大於根節點hash值
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
//紅色節點的子節點一定不能是紅色
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
//遞迴檢查左節點和右節點
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
/**
* 將根節點變成頭元素,即儲存在Node[]陣列中的元素
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
//計算根元素在陣列中的索引位置
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
//將對應索引的頭元素設定成根元素
tab[index] = root;
//將root前後的元素建立連線,即移除root元素
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
//將root和first元素建立連線,即將root元素插入到first前面
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
/**
* 判斷兩個任意物件大小的通用方法,該方法是正常流程比不出大小時才會使用
*/
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
//identityHashCode是本地方法,忽視物件本身對hashCode()方法的重寫
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
3、紅黑樹插入元素實現
紅黑樹是一種弱平衡的平衡二叉樹,即保證相同的黑色高度並不是一定滿足平衡因子絕對值不超過1的條件。平衡二叉樹查詢很快但是插入/刪除時因為保持平衡需要旋轉的平均次數較多不適應於插入/刪除頻繁的場景,紅黑樹是插入和查詢都能兼顧的平衡方案,因此應用場景更廣泛。
紅黑樹的定義參考:https://www.cnblogs.com/ysocean/p/8004211.html
平衡二叉樹的定義參考:https://blog.csdn.net/qq_23100787/article/details/52506217
此處只是對大神部落格中的講解做補充,只限於紅黑樹的插入操作。
首先,紅黑樹新增的節點一定是紅色的,因為插入黑色節點總會改變黑色高度,但是插入紅色節點只有一半的機會(即父節點是紅色時)會違背規則。如果新增節點的父節點是黑色的,那就沒有任何問題,直接插入即可。如果新增節點的父節點是紅色的,直接插入會違背不能有兩個連續的紅色節點的規則,因此需要通過變色和旋轉來確保這種情形下紅黑樹是符合規則的。新增節點的父節點為紅色包含以下三種情形:
情形一、父節點及父節點的兄弟節點都是紅色節點,如下圖:
這種情況下首先是變色,把P和U變成黑色,G變成紅色。因為無論N是P的左節點還是右節點,P是G的左節點還是右節點,變色後的結果都是新增節點插入到黑色節點上,所以不需要考慮N,P的位置。變色完成後G和2節點成了兩個紅色的節點,因為黑色高度相同,所以1節點必須是黑色的。因為G和2節點都是左右節點都有可能,因此有四種情形需要討論。先考慮2節點為左節點,對應的兩種情形就是情形二和三了。
情形二、父節點是紅色的,父節點的兄弟節點是黑色的,插入的節點為父節點的右節點,如下圖:
這種情況下已經不能再直接變色了,因為根節點必須是黑色的。假如把2節點變成黑色的,11節點變成紅色的,再對2節點右旋呢?這時節點7就成為11的左節點,11節點和7節點都是紅色節點,也不行。只能對2節點做左旋,變成情形三。
情形三、父節點是紅色的,父節點的兄弟節點是黑色的,插入的節點為父節點的左節點
這種情形的處理就很明確了,把節點7變成黑色,節點11變成紅色,在對節點7做右旋,變成如下圖的平衡紅黑樹:
如果2節點是右節點,對情形二和情形三的處理就是相反的,對情形二由左旋變成右旋,情形三由右旋變成左旋。
HashMap中紅黑樹插入節點的程式碼實現如下:
// 對節點P做紅黑樹左旋,返回該節點的根節點
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
//兩個等於號,從右往左計算,即先執行p.right = r.left,後執行rl = p.right,最後判斷rl!=null
//將p的右節點的左節點變成p的右節點
if ((rl = p.right = r.left) != null)
rl.parent = p;
//將p的父節點變成r的父節點,判斷該父節點是否為空
if ((pp = r.parent = p.parent) == null)
//如果父節點為空,則r是根節點,根節點總是黑色的
(root = r).red = false;
//如果父節點不為空,且p是該父節點的左節點
else if (pp.left == p)
//將pp的左節點置為r
pp.left = r;
else
//將pp的右節點置為r
pp.right = r;
//r的左節點變成p,p的父節點變成r
r.left = p;
p.parent = r;
}
return root;
}
/*
對節點P做紅黑樹右旋操作,並返回該節點的根節點,是左旋的逆向操作
*/
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
//將p的左節點由l變成l的右節點
if ((lr = p.left = l.right) != null)
lr.parent = p;
//將p設定成l的右節點,p的父節點設定成l,l的父節點設定成p原來的父節點
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
//root為原來的根節點,因為插入時會旋轉,根節點會發生改變,所以該方法返回的是執行平衡插入
後的根節點
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
//紅黑樹新增節點一定是紅色的,因為這樣違反規則的可能性最小
x.red = true;
//注意此處是for迴圈,即由當前節點逐步往上處理
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//當前節點的父節點為空,即該節點為根節點,根節點一定是黑色的
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//父節點是黑色的或者父節點為根節點
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//父節點是祖父節點的左節點,即上述討論中的2節點是左節點
if (xp == (xppl = xpp.left)) {
//祖父節點的右節點存在且是紅色節點,即是情形一
if ((xppr = xpp.right) != null && xppr.red) {
//將其父節點和父節點的兄弟節點塗成黑色
xppr.red = false;
xp.red = false;
//祖父節點置為紅色,將當前節點置為祖父節點
xpp.red = true;
//注意此處直接跳到祖父節點了,因為祖父節點成紅色的
x = xpp;
}
//如果祖父節點的右節點不存在或者是黑色的
else {
//當前節點是父節點的右節點,即2節點為左節點時的情形二
if (x == xp.right) {
//將當前節點置為父節點,並對父節點左旋
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果父節點存在,則將父節點塗黑,祖父節點塗紅,並將祖父節點右旋
//即2節點為左節點時的情形三
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
//父節點是祖父節點的右節點
else {
//如果祖父節點的左節點存在且是紅色的,即是情形一
if (xppl != null && xppl.red) {
//祖父節點和祖父節點的兄弟節點置為黑色的
xppl.red = false;
xp.red = false;
//父節點置為紅色的,將當前節點置為祖父節點
xpp.red = true;
x = xpp;
}
//如果祖父節點的左節點並存在或者是黑色的
else {
//如果當前節點是父節點的左節點,即2節點為右節點時的情形二
if (x == xp.left) {
//將當前節點置為父節點,並對父節點右旋
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果父節點存在
if (xp != null) {
//將父節點置為黑色,祖父節點置為紅色,對祖父節點做左旋,即2節點為右
節點時的情形三
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
/**
* 如果存在key值相等的則返回該節點,否則插入新節點,返回null
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//獲取根節點
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//比較key的hash值
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//hash相等且key相等則返回當前節點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//hash相等,equals方法不等下看key是否實現Comparable介面,如果沒有實現或者實現了還是跟父節點key一樣
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
//從當前節點的子節點中找到key值相等的節點並返回
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//如果當前節點的子節點還是沒有key值相等的則採用系統預設方法比較key大小
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
//找到待插入的位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
//構造新的節點
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//插入當前節點的左邊或者右邊
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//此處維護的連結串列順序並不是插入順序,新插入的節點並不是在最後一個
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//插入完成後將根節點作為Tab內的第一個節點
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
3、紅黑樹的查詢實現
紅黑樹的查詢跟二叉樹搜尋是一樣的,通過比較目標key的hash值與紅黑樹中節點的hash值,判斷走左子樹還是右子樹,不斷往下查詢直到找到hash值相等且key值相當的節點。
/**
* 根據hash值遍歷當前節點的所有子節點,直到hash值相同且key相同
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
//當前節點hash值大於目標hash值則從左節點查詢
if ((ph = p.hash) > h)
p = pl;
//當前節點hash值小於目標hash值則從右節點查詢
else if (ph < h)
p = pr;
//如果hash值相同比較key是否相同,相同則返回該節點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//hash碰撞下,hash值相同,key不同或者key為null,如果左節點不存在,查詢右節點
else if (pl == null)
p = pr;
//hash碰撞下,hash值相同,key不同或者key為null,如果右節點不存在,查詢右節點
else if (pr == null)
p = pl;
//hash碰撞下,hash值相同,key不同,左右節點都存在的情況下
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
/**
* 從根節點開始查詢元素
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
4、紅黑樹的形態轉換實現
即將紅黑樹轉換成基礎的單向連結串列,或者由單向連結串列轉換成紅黑樹結構。
/**
* 將單向連結串列結構的TreeNode轉換成紅黑樹結構,原有的連結串列結構基本沒有變化,只是將紅黑樹的根節點作為連結串列元素的第一個節點而已
* 在呼叫此方法前,此元素對應的單向連結串列的Node已經全部轉換成TreeNode了
* @return root of tree
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
//root為空時將當前節點置為根節點,置為黑色
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
//比較根節點和目標節點的hash值,如果dir小於等於0則目標節點作為根節點的左節點,否則作為右節點
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
//如果當前根節點的左節點或者右節點為空則將目標節點插入對應的左節點或者右節點
//否則繼續遍歷,直到找到對應的插入位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//將當前節點的父節點設定根節點,根據dir將當前節點設定根節點的左節點或者右節點
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//確保插入後滿足紅黑樹的規則
root = balanceInsertion(root, x);
break;
}
}
}
}
//將紅黑樹的根節點作為連結串列結構的第一個節點
moveRootToFront(tab, root);
}
/**
*將紅黑樹轉換成連結串列
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
//replacementNode是構造一個普通的連結串列Node
Node<K,V> p = map.replacementNode(q, null);
//當前節點是連結串列第一個元素,
if (tl == null)
hd = p;
else
//將當前節點置為上一個節點的下一個節點
tl.next = p;
//tl表示上一個節點
tl = p;
}
return hd;
}
5、紅黑樹的擴容切分實現
具體來說就是將一顆紅黑樹中的元素幾乎均勻的分到兩個紅黑樹中,該實現是基於HashMap的容量的,切分時先將原有的連結串列轉換成兩個連結串列,再判斷兩個連結串列的長度是否做形態轉換。
//此處的bit是陣列長度,即HashMap的容量,是2的n次方,index是陣列的索引
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// 切分成lo 和 hi的兩個連結串列,lc和hc分別表示兩個連結串列的長度
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
//從當前節點開始往下遍歷
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
//根據此條件判斷是hi連結串列還是lo連結串列,此處的位運算需要重點關注
if ((e.hash & bit) == 0) {
//不斷將當前元素追加到lo連結串列後面
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
//不斷將當前元素追加到hi連結串列後面
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
//如果lo連結串列的長度小於閾值
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
//hiHead為null表明當前tab沒有被拆分,依然還是一顆樹,不需要重新樹化
if (hiHead != null)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
重點關注其中的位運算,假如bit是256,二進位制表示為1,0000,0000,計算e.hash & bit時,只有e的hash值的二進位制表示的第9位為1時,該表示式才為1,其他情形都是0,因為第9位1和0的概率是相等的,因此就幾乎均勻的將原有的連結串列分成了兩個不同的連結串列。
算陣列索引的表示式是(n - 1) & hash,n為陣列長度。假如原有的n是256,n-1的二進位制表示就是1111,1111,高位的hash值比如1,0000,1111,對應的陣列索引位置是1111,低位的hash值比如0,0011,1100, 對應的索引位置是11,1100。執行擴容後n變成512,n-1變成1,1111,1111, 這時高位hash對應的索引位置變成1,0000,1111,即比原來的索引位置增加了原有的容量256,低位hash值對應的索引位置沒變依然是11,1100。如此就很巧妙很輕鬆的算出了切分多出來的另一個連結串列在陣列中的位置。