前言
從jdk8.0開始HashMap進行了一次革新,為了提高查找效率在哈希桶內元素超過8個時自動變為紅黑樹結構。
紅黑樹到底是為了解決什么問題存在的?它又是怎么出現的?我們首先要理解一顆二叉查找樹。
二叉查找樹或者是一棵空樹,或者是具有下列性質的二叉樹:
(1)若左子樹不空,則左子樹上所有結點的值均小于或等于它的根結點的值;
(2)若右子樹不空,則右子樹上所有結點的值均大于或等于它的根結點的值;
(3)左、右子樹也分別為二叉查找樹;
.
當然如果要說二叉查找樹的話我們必須從符號表說起,可我們今天的目的是為了簡述紅黑樹,所以二叉查找樹的各種實現就當大家都已經懂得。那為什么會出現紅黑樹呢,我們發現一種情況當輸入的鍵值對鍵值按升序或者降序插入的時候,會有下圖的情況出現
紅黑樹.png而這種情況也是最糟糕的情況假設我們要查找鍵為5的結點,那么花費的時間就是最長的了,我們發現其實查找時間其實跟樹的高度應該是呈現一種正比的關系。
那么可能有些同學想到了平衡二叉查找樹,也就是任何節點的兩個子樹的高度最大差別為一。但是我們發現一個問題,那就是每次插入或者刪除需要平衡次數非常多。那么有什么什么折中的方案既能保證能正常二叉查找樹的性質又能保證查找的效率。
紅黑樹
沒錯就是紅黑樹,不過為了理解紅黑樹我們需要做一些準備工作,首先我們要了解一下B-樹中最簡單的2-3樹。
這是一顆很神奇的樹,包含2-結點(1個鍵兩個鏈接)和3-結點(2個鍵3個鏈接)。
我們只要記住幾點就是一個新的鍵插入2-結點直接變3-結點,
而當要插入到3-結點時,那么變成4-結點,4-結點中間的鍵值上升到父結點直到父節點是一個2-結點
或者到了根節點還不是2-結點那么根節點直接分解使高度加一
可能覺得2-3樹已經完美解決我們的問題了,但現實往往不盡人意,為什么呢??
首先表示一顆2-3樹光表示2-結點和3-結點和它們之間的轉換就需要花費力氣,而且我們可能需要要跟結點之內的鍵值進行比較,而它實際插入情況7種,代表著光判斷起碼寫7個if,2-3樹解決我們的問題,可是實際書寫顯得比較復雜
我們需要一種簡單的數據結構來解決我們的問題對了就是紅黑樹,它其實就是2-3樹的變形.它其中有兩種鏈接一種是紅鏈接一種是黑鏈接(指向的結點顏色就是鏈接顏色),用這兩種鏈接就能表示我們2-3樹。如下圖。
當然紅黑樹有以下幾個特點:
1.紅鏈接均為左鏈接。
2.沒有任何一個結點和兩條紅鏈接相連。
3.而且它是完美黑色平衡的(葉子結點到根結點黑色鏈接數量都是相同的)。
但是現實是殘酷的,我們在對紅黑樹做操作的時候是有可能出現紅色右鏈接或者連續兩條紅色鏈接。我們這時候可以通過旋轉或者上移中鍵進行操作而對其進行操作
public class RBBST<Key extends Comparable<Key>, Value> {
private Node root;
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
private Node left;
private Node right;
private boolean color;
private Key key;
private Value val;
private int N;
public Node(boolean color, Key key, Value val, int N) {
this.color = color;
this.key = key;
this.val = val;
this.N = N;
}
}
public int size() {
return size(root);
}
private int size(Node n) {
if(n==null) {
return 0;
}
return n.N;
}
private boolean isRed(Node n) {
if(n==null) return false;
return n.color == RED;
}
private Node rotateLeft(Node n) {
Node x = n.right;
n.right = x.left;
x.left = n;
x.color = n.color;
n.color = RED;
x.N = n.N;
n.N = size(n.left)+size(n.right)+1;
return x;
}
private Node rotateRight(Node n) {
Node x = n.left;
n.left = x.right;
x.right = n;
x.color = n.color;
n.color = RED;
x.N = n.N;
n.N = size(n.left)+size(n.right)+1;//變了 變成x了
return x;
}
private void change2Conn(Node n) {
n.left.color = BLACK;
n.right.color = BLACK;
n.color = RED;
//return n;還是原來的
}
public void put(Key key, Value val) {
root = put(root, key, val);
root.color = BLACK;
}
private Node put(Node n, Key key, Value val) {
if(n==null) {
return new Node(RED, key, val, 1);
}
int cmp = key.compareTo(n.key);
if(cmp<0) n.left = put(n.left, key, val);
else if(cmp>0) n.right = put(n.right, key, val);
else n.val = val; //find key
if(!isRed(n.left)&&isRed(n.right)) n = rotateLeft(n);
if(isRed(n.left)&&isRed(n.left.left)) n = rotateRight(n);
if(isRed(n.left)&&isRed(n.right)) change2Conn(n);
n.N = size(n.left) + size(n.right) + 1;
return n;
}
}
你以為到這里就完了嗎,本著求知的態度我們繼續講一講HashMap這個Java里面無比精妙的類
HashMap 紅黑樹實現
就看兩段代碼理解了HashMap的紅黑樹實現也就大致理解了
左旋轉
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) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
略微一看很難懂,這完全與我們自己的實現不同呀,傳兩個結點進來這是干什么?
跳過我們看具體插入的實現
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
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;
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
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 {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
是否感覺略微的不適與暈眩,但是你耐著性子看下去發現邏輯竟是如此的簡單。
root代表著在Node[]這個表中每一個位置上第一個結點,而x就是我們需要插入這個位置的結點當然如果兩者的key.equals(root.key)
其實返回false不然就覆蓋了
當x的父親結點為空時,很明顯它就直接返回。為了幫助更好理解這段代碼我們嘗試閱讀這么兩個函數
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;
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;
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) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
上面這一段是當一個哈希桶元素數超過8且其中表的長度大于64的時候發生的對哈希桶進行樹型化。我們看到一開始是對根結點進行賦值的,但是中間插入了
balanceInsertion()
這個函數意味著root是為了滿足紅黑樹而不斷變化的,而其中的moveRootToFront()
是當本身在哈希桶第一個位置的元素不是root時進行把root賦值到哈希桶的第一個位置的值大概意思就是本來是以鏈表的形式存的,然后不斷循環遍歷已經通過
treeifyBin()
中的replacementTreeNode()
從Node類型變為TreeNode類型的結點然后每次都循環都相當于對紅黑樹插入一個結點,當然插入后需要修復紅黑樹看到這里我們可能明白了
rotateleft()
的作用了,跟普通紅黑樹的左旋差不多,就是把右子樹的左子樹掛在右子樹的父親結點的左子樹的地方,而右子樹又變成它父親結點的父親,成為它父親結點的父親結點的兒子。可是有一點筆者邏輯思維有點混亂的是它的red屬性是怎么變換,好像并不是按照正常方式左旋變換,因為左旋方法里面除了當是根結點的時候會讓其變為false,而沒有任何地方作出改變了。