紅黑樹是一種平衡二叉搜索樹,它的自平衡機制簡單高效,所以常被用在許多底層設計當中,他的發明者之一Robert Sedgewick正是經典算法書籍《Algorithms》的做者。java
首先紅黑樹是二叉搜索樹,因此它知足二叉搜索樹性質,除此以外紅黑樹還有下面5個性質:node
還能夠推出下面這條性質:算法
MAX <= 2 * MIN
(不考慮NIL節點,這點很容易理解,假設根節點的黑高度是BH,由性質5可知MIN等於BH,結合性質二、三、4可得出,根節點到葉子節點的紅高度RH一定小於等於黑高度BH,全部MAX = BH + RH <= 2 * BH = 2 * MIN
)從性質6能夠知道,紅黑樹只是一種接近平衡的二叉搜索樹,它不是嚴格的平衡二叉樹。因此紅黑樹的查詢要比AVL樹稍慢一丟丟,可是因爲紅黑樹維護平衡只須要旋轉和着色這兩種簡單操做,因此它的插入、刪除操做要比AVL樹快,綜合的統計效率要高於AVL樹。spa
下面這幅圖片展現了一顆紅黑樹,其中黑色的點表示葉子節點NIL。設計
旋轉操做其實並非紅黑樹特有的操做,早在AVL樹中就開始使用旋轉操做來維護樹的平衡了。我以爲旋轉操做的本質就是使樹傾斜,對節點X作左旋操做,其實就是使以X爲根節點的子樹向左傾斜,右旋就是向右傾斜,這樣的話,本來向右傾斜的樹,通過左旋以後就會變得平衡了,右旋也是一樣的道理。3d
這幅圖是上面那顆紅黑樹的一部分,展現了先對節點30作左旋操做,而後對節點38作右旋操做的過程 code
紅黑樹的查詢操做和二叉搜索樹沒什麼區別,這裏就不贅述了,直接上一段代碼,關於紅黑樹的代碼我推薦直接看TreeMap的代碼,比較簡單明瞭。cdn
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
複製代碼
紅黑樹的插入和刪除操做比較複雜,要考慮不少種狀況,爲了描述方便,我在這裏首先作一些約定,假設要插入或刪除的當前節點爲C,其父節點爲P,祖父節點爲G,叔叔節點爲U,兄弟節點爲S,兄弟節點的左子節點爲SL,兄弟節點的右子節點爲SR遞歸
咱們約定插入的節點C是紅色的,爲何這樣呢?不妨思考一下,若是C是黑色的,一定會使樹傾斜。
此時咱們考慮兩種狀況,首先,若是P是黑色,直接插入節點就好了,不會違背紅黑樹的任何性質;若是P是紅色,插入節點後會有兩個連續的紅色節點C和P,會違背性質4,因此咱們須要作一些操做,從新平衡這棵樹。
很好,咱們一下就去除了一半的可能性,只須要考慮P是紅色狀況,一共有3個維度:圖片
這裏咱們只討論P是G的左孩子的狀況
這種狀況下,只須要將P變爲黑色,G變爲紅色,對G作右旋操做便可。
下圖考慮了一種更通常的狀況(其實真正插入的時候,x、y、z和U都是NIL節點),很容易能夠看出子樹x、y、z和U三者的黑高度是相等的,此時並不違背性質5,因此單靠旋轉不能解決問題,須要從新着色,從新着色以後,P的左子樹黑高度變大了,即向左傾斜了,因此再對P作右旋轉操做,整棵樹恢復平衡,而且知足了性質4
這種狀況下,只須要對P作左旋操做,而後就變成了狀況1
這種狀況下,因爲U是紅色的,因此咱們不能像狀況1那樣直接把G變成紅色(這也是U分紅兩種狀況討論的緣由)。
此時須要將P和U變成黑色,而且將G變成紅色,作完這個操做以後,對於以G爲根節點的子樹來講,已是平衡的了,而且它的黑高度沒有發生變化。不須要考慮C是P的左孩子仍是右孩子,由於P變成了黑色,C是紅色,不會違背紅黑樹的性質。
那麼惟一會影響總體樹性質的只有節點G了,由於G變成了紅色,若是G的父節點也是紅色,就違背了性質4。此時咱們能夠發現,G變成了和C剛插入時相似的狀況(這也是我在狀況1中直接討論通常狀況的緣由),咱們只須要以G做爲當前節點再進行以上的操做便可,很明顯這是一個遞歸的操做。
上述3的遞歸操做可能會把根節點設置爲紅色,因此在平衡完整棵樹以後,將根節點設置爲黑色便可。
下面是TreeMap在插入節點後的平衡操做的代碼,我在覈心的地方添加了註釋:
private void fixAfterInsertion(Entry<K,V> x) {
// 插入節點是紅色的
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
// P是G的左孩子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// y是x的叔叔節點
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
// 狀況3:P是G的左孩子,且U是紅色,且C是P的左孩子
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
// 將當前節點指向祖父節點G,向上循環
x = parentOf(parentOf(x));
} else {
// 狀況2:P是G的左孩子,且U是黑色,且C是P的右孩子
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
// 狀況1:P是G的左孩子,且U是黑色,且C是P的左孩子
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
// P是G的右孩子和上面是相似的過程
// ......
}
}
// 最後面把根節點設置爲黑色
root.color = BLACK;
}
複製代碼
刪除節點的操做比插入更加複雜,須要考慮更多種狀況。
首先從刪除節點的種類上分爲如下三種狀況:
其中狀況3可使用直接後繼法,轉換爲狀況1或2,就是將刪除節點C的直接後繼X的值拷貝到C,而後刪除X。這是二叉搜索樹刪除節點的通用作法。
下圖是三種查詢直接後繼的路徑圖,這裏只是爲了說明直接後繼,實際在本文中只會用到第一種狀況。
代碼以下:
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
// 圖中的狀況一
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
// 若是未進入循環體就是狀況二,不然就是狀況三
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
複製代碼
此時刪除的節點C一定是黑色的,且它的非NIL孩子必定是紅色。不然以C爲根節點的子樹就違背了性質5。此時刪除C很簡單,直接將C的孩子設置爲黑色,替換C便可。
此時若是C是紅色,這種狀況也很簡單,直接刪除C便可。
若是C是黑色,這是最複雜的一種狀況,由於C刪除以後,沒有子節點來補充C本來的位置,會影響到總體的平衡,下面討論一下這種狀況下不一樣的處理方法。這裏咱們只考慮C是P的左孩子的狀況,同理能夠得出C是P右孩子的狀況。
由於C要被刪除,P的左邊黑高度會減1,因此咱們須要對P作左旋操做,而後S就到了P原來的位置,因此咱們把S設置爲P的顏色,將P設置爲黑色,將SR設置爲黑色。此時原來以P爲根節點的樹在C刪除後依舊保持平衡。這裏爲啥沒考慮S的左孩子SL呢?由於SL只能是紅節點或者NIL節點,不會影響平衡。下面圖中白色的節點表示能夠爲紅色也能夠爲黑色的意思。
這種狀況能夠將SL設置爲黑色,S設置爲紅色,而後對S作右旋操做,就變成了2.1
這種狀況下,若是P是紅色的,就比較簡單,直接刪除C,而後將S設置爲紅色,將P設置爲黑色就好了。
若是P是黑色的,就稍微麻煩一些,若是刪除了C,以P爲根節點的子樹的黑高度必然會下降1,此時只能向P的父輩節點尋求幫助,才能保持平衡了。
具體作法是,將C刪除,將S設置爲紅色,而後將P做爲當前節點,向上遞歸的考慮2.1和2.2就能夠了。
這種狀況下,SL和SR一定是黑色的非NIL節點。此時能夠先對P作左旋操做,由於P是黑色的,S是紅色的,因此須要從新着色,將P設置爲紅色,S設置爲黑色。而後咱們再來觀察節點C,發現C的兄弟變成了SL,而SL是黑色的,很顯然咱們已經將C的兄弟S爲紅色的狀況轉換成了S是黑色的狀況,根據SL的子節點X、Y的不一樣,會轉換成2.1、2.2和2.3三種狀況,以下圖所示:
下面看一下Java的TreeMap的代碼實現:
private void deleteEntry(Entry<K,V> p) {
// ...
// 對於嚴格的內部節點,使用直接後繼法替換成另外兩種狀況
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {
// 狀況1:刪除的節點有且只有一個非NIL子節點
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
p.left = p.right = p.parent = null;
if (p.color == BLACK)
fixAfterDeletion(replacement); // 這裏其實只會設置replacement爲黑色
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
// 狀況2: 刪除的節點沒有非NIL子節點
if (p.color == BLACK)
fixAfterDeletion(p);
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
複製代碼
下面是刪除後的平衡操做:
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
// 狀況2.4:C的兄弟S爲紅色
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
// 狀況2.3:C的兄弟S爲黑色,且S的左右孩子都爲黑色
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
// 狀況2.2:C的兄弟S爲黑色,且S的右孩子SR爲黑色,且SL爲紅色
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// 狀況2.1:C的兄弟S爲黑色,且S的右孩子SR爲紅色
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
// ......
}
}
setColor(x, BLACK);
}
複製代碼
2-3樹、2-3-4樹以及B類樹都是常見的多路查找樹,它們經過分裂和合並節點來維持絕對的平衡(這類樹的根節點到全部葉子節點的簡單距離都相等)。固然本文的重點不在此,這裏只是提一下紅黑樹和2-3-4樹的關係。
紅黑樹其實就是從2-3-4樹演變過來的,咱們將通常紅黑樹的黑色節點和其紅色子節點合併爲一個節點後,就能獲得一顆標準的2-3-4樹,相似的左傾紅黑樹和右傾紅黑樹都能轉換爲2-3樹,這裏就不展開討論了。
下圖就是本文最開始展現的那顆紅黑樹的2-3-4樹形態。