萌新學習數據結構挺久的了,經常使用數據結構均可以手撕,而平衡樹只是瞭解原理,撕不出來,看各類博客文章也看得暈頭轉向的。
以前看《算法》紅皮書學習了左偏紅黑樹,此次從JDK的TreeMap來分析下常規紅黑樹。 閱讀須要有二叉查找樹的知識背景java
出自《算法導論》node
由於TreeMap中有不少集合相關的操做,原代碼長度上千行,看得眼花了。因此這裏把其中和紅黑樹相關的部分提取出來分析。算法
這裏爲了方便,把其中的泛型部分簡化成int編程
private static class Node implements Comparable<Node> {
int key;
int val;
boolean color = BLACK;
Node left, right, parent;
Node(int key, int val, Node parent) {
this.key = key;
this.val = val;
this.parent = parent;
}
@Override
public int compareTo(Node o) {
return this.key - o.key;
}
}
複製代碼
//紅黑樹
public class RedBlackTree{
//常量定義
private static final boolean RED = false;
private static final boolean BLACK = true;
private Node root;
private int size;
// 以前的節點類
private static class Node implements Comparable<Node> {...}
}
複製代碼
private static boolean colorOf(Node p) {
return (p == null ? BLACK : p.color);
}
private static void setColor(Node p, boolean c) {
if (p != null)
p.color = c;
}
複製代碼
private static Node parentOf(Node p) {
return (p == null ? null : p.parent);
}
private static Node leftOf(Node p) {
return (p == null) ? null : p.left;
}
private static Node rightOf(Node p) {
return (p == null) ? null : p.right;
}
private Node successor(Node tmp) {
//後繼節點的查找
if (tmp == null) {
return null;
}
if (tmp.right != null) {
Node p = tmp.right;
while (p.left != null) {
p = p.left;
}
return p;
} else {
Node p = tmp.parent;
Node ch = tmp;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
複製代碼
所謂左旋,就是對原節點N,讓N的右子節點R代替N的位置,
N成爲R的左子節點,至於他們的其餘子節點,看着辦就好。
從觀感上有N下位成左子節點,N的右子節點上位之感,故謂之左旋。
右旋反之。
很是簡單的操做,無須多言。數據結構
private void rotateLeft(Node tmp) {
if (tmp == null) {
return;
}
Node r = tmp.right;
if (r == null) {
return;
}
tmp.right = r.left;
if (r.left != null) {
r.left.parent = tmp;
}
r.parent = tmp.parent;
if (tmp.parent == null) {
root = r;
} else if (tmp.parent.left == tmp) {
tmp.parent.left = r;
} else {
tmp.parent.right = r;
}
r.left = tmp;
tmp.parent = r;
}
private void rotateRight(Node tmp) {
if (tmp == null) {
return;
}
Node l = tmp.left;
if (l == null) {
return;
}
tmp.left = l.right;
if (l.right != null) {
l.parent = tmp;
}
l.parent = tmp.parent;
if (tmp.parent == null) {
root = l;
} else if (tmp == tmp.parent.left) {
tmp.parent.left = l;
} else {
tmp.parent.right = l;
}
l.right = tmp;
tmp.parent = l;
}
複製代碼
public void put(int key, int val) {
if (this.root == null) {
//對於空樹,咱們直接插入就行了,知足RBT的各類性質
this.root = new Node(key, val, null);
size = 1;
} else {
Node newNode = insert(root, key, val);
//newNode爲空,就是key存在,這時候沒有插入新節點
//非空的話,插入新節點,須要考慮修復被破壞的RBT性質
if (newNode != null) {
fixAfterInsertion(newNode);
}
}
}
複製代碼
其實與常規二叉樹的操做同樣的ide
private Node insert(Node root, int key, int val) {
Node tmp = root, parent = tmp;
while (tmp != null) {
parent = tmp;
int cmp = tmp.key - key;
if (cmp < 0) {
tmp = tmp.left;
} else if (cmp > 0) {
tmp = tmp.right;
} else {
// key已經存在,直接修改val後返回就行了
tmp.val = val;
return null;
}
}
//tmp爲null, parent是上一輪的tmp
//只須要把節點插入到這個parent下面
int cmp = parent.key - key;
Node newNode = new Node(key, val, parent);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
return newNode;
}
複製代碼
方法名分析:插入以後的修復。修復必然是由於某些性質被破壞,這裏須要經過一些操做來還RBT的性質。工具
幾個關鍵點學習
這個操做,能夠參考算法紅皮書中的flip方法this
private void fixAfterInsertion(Node insertNode) {
setColor(insertNode, RED);
Node tmp = insertNode;
while (tmp != null && tmp != root && tmp.parent.color == RED) {
//第一個if
if (parentOf(tmp) == leftOf(parentOf(parentOf(tmp)))) {
Node uncle = rightOf(parentOf(parentOf(tmp)));//tmp的叔節點
if (colorOf(uncle) == RED) {
//第二個if
//可參考紅皮書的翻轉操做
setColor(parentOf(tmp), BLACK);
setColor(uncle, BLACK);
setColor(parentOf(uncle), RED);
tmp = parentOf(uncle);
} else {//第二個else,叔節點爲黑色或不存在
if (tmp == rightOf(parentOf(tmp))) {//第三個if
//若tmp爲右節點,那麼經過旋轉操做,使tmp指向左子節點,方便下面的統一操做
tmp = parentOf(tmp);
rotateLeft(tmp);
}
setColor(parentOf(tmp), BLACK);
setColor(parentOf(parentOf(tmp)), RED);
rotateRight(parentOf(parentOf(tmp)));
}
}
//else中的內容爲第一個if的鏡像
else {
Node uncle = leftOf(parentOf(parentOf(tmp)));//tmp的左叔節點
if (colorOf(uncle) == RED) {
setColor(parentOf(tmp), BLACK);
setColor(uncle, BLACK);
setColor(parentOf(uncle), RED);
tmp = parentOf(uncle);
} else {
if (tmp == leftOf(parentOf(tmp))) {
tmp = parentOf(tmp);
rotateRight(tmp);
}
setColor(parentOf(tmp), BLACK);
setColor(parentOf(parentOf(tmp)), RED);
rotateLeft(parentOf(parentOf(tmp)));
}
}
}
setColor(root, BLACK);
}
複製代碼
從編程的角度理解,這裏循環的做用,是把「新加了一個紅色節點」這一事件逐層向上傳遞。
在傳遞過程當中,可能某一層能夠處理這一事件,那麼他就處理,而後終止這一事件的傳遞。若是處理不了這一事件,他就經過一系列轉換,把這個事件轉換成上層要處理的問題。
這樣遞歸到root就行了。spa
其實與常規二叉樹查找同樣
public Node get(int key) {
Node tmp = root;
while (tmp != null) {
int cmp = tmp.key - key;
if (cmp > 0) {
tmp = tmp.right;
} else if (cmp < 0) {
tmp = tmp.left;
} else {
return tmp;
}
}
return null;
}
複製代碼
public void delete(int key) {
Node node = get(key);
if (node != null) {
size--;
deleteNode(node);
}
}
複製代碼
關鍵點分析:
刪除紅色節點不會破壞RBT的性質,可是刪除黑色節點會破壞性質5,因此刪除黑色節點後須要調用fixAfterDeletion方法來修復性質5,具體後面分析
先是各類非空判斷。
語句1:對於要刪除有兩個子節點的節點Tmp,咱們先找的其後繼節點s,而後把s提到Tmp的位置,轉爲刪除s就行了。
由於Tmp有兩個子節點,可知s一定存在於Tmp的右子樹中,而且s沒有子節點或者只有一個子節點。
這樣咱們就把全部的刪除操做都概括到刪除葉子節點或者只有一個子節點兩種狀況下了。
對於只有一個子節點的狀況,咱們在語句2中處理。 主要步驟就是找到這個子節點,而後子節點登基大寶,原節點被忘卻。
可是原節點若爲黑色,那麼這條路徑下全部節點的路徑都少了一個黑色節點,不符合性質5了,因此要進行修復。
對於刪除葉子節點的狀況,咱們在語句3中處理。 該節點爲紅色,咱們就直接斷開各類鏈接就行了; 若該節點爲黑色,咱們還須要先進行fixAfterDeletion。
private void deleteNode(Node node) {
if (node == null) {
return;
}
if (node.parent == null) {
root = null;
return;
}
//語句1
if (node.left != null && node.right != null) {
Node s = successor(node);
node.key = s.key;
node.val = s.val;
node = s;
}
//語句2
if (node.left != null || node.right != null) {
//找到這個惟一的子節點
Node replacement = node.left == null ? node.right : node.left;
//把這個子節點頂上去,原節點的各類鏈接都被這個子節點所佔據
replacement.parent = node.parent;
if (node.parent == null) {
root = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else {
node.parent.right = replacement;
}
node.left = null;
node.right = null;
node.parent = null;
//老節點若爲黑色,須要修復
if (node.color == BLACK) {
fixAfterDeletion(replacement);
}
} else {//語句3
if (node.parent == null) {
root = null;
} else {
if (node.color == BLACK) {
fixAfterDeletion(node);
}
//斷開鏈接
if (node.parent != null) {
if (node == node.parent.left) {
node.parent.left = null;
} else if (node == node.parent.right) {
node.parent.right = null;
}
node.parent = null;
}
}
}
}
複製代碼
若是刪除體現了「新王上位老王敗潰」的無情,那麼修復則體現了「兄弟就是拿來坑的」的暖暖親情。
刪除以後,若不知足RBT的性質,只會是不知足性質5。即和其兄弟比起來,在路徑上少了一個黑色節點。
因此這個修復的關鍵是想辦法從兄弟那裏找一個紅色節點變成黑色,經過parent傳遞過來,從而達到平衡。實在搞不到了,那麼就把兄弟也減小一個黑色節點(黑變紅),而後把問題交給parent去處理,充分體現了高超的甩鍋水平。
關鍵點分析:
private void fixAfterDeletion(Node tmp) {
while (tmp != root && colorOf(tmp) == BLACK) {
if (tmp == leftOf(parentOf(tmp))) {
//獲取兄弟節點
Node bro = rightOf(parentOf(tmp));
//代碼1,把兄弟變成黑色
if (colorOf(bro) == RED) {
//parent必定爲黑
setColor(bro, BLACK);
setColor(parentOf(tmp), RED);
rotateLeft(parentOf(tmp));
bro = rightOf(parentOf(tmp));
}
//代碼2,bro的顏色一定爲黑色
if (colorOf(leftOf(bro)) == BLACK && colorOf(rightOf(bro)) == BLACK) {
setColor(bro, RED);
tmp = parentOf(tmp);//向上一層傳遞事件
}
else {
//代碼3,bro爲黑色,而且bro至少有一個紅子節點
if (colorOf(rightOf(bro)) == BLACK) {
setColor(leftOf(bro), BLACK);//這種狀況想bro的左子節點一定爲RED
setColor(bro, RED);
rotateRight(bro);
bro = rightOf(parentOf(tmp));
//成功把bro的right轉成了紅色
}
//代碼4這一步的做用是從bro那裏借來一個黑色節點
setColor(bro, colorOf(parentOf(tmp)));
setColor(parentOf(tmp), BLACK);
setColor(rightOf(bro), BLACK);
rotateLeft(parentOf(tmp));
tmp = root;//退出
}
} else {//鏡像操做,沒啥好說的
Node bro = leftOf(parentOf(tmp));
if (colorOf(bro) == RED) {
setColor(bro, BLACK);
setColor(parentOf(tmp), RED);
rotateRight(parentOf(tmp));
bro = leftOf(parentOf(tmp));
}
if (colorOf(rightOf(bro)) == BLACK && colorOf(leftOf(bro)) == BLACK) {
setColor(bro, RED);
tmp = parentOf(tmp);
} else {
if (colorOf(leftOf(bro)) == BLACK) {
setColor(rightOf(bro), BLACK);
setColor(bro, RED);
rotateLeft(bro);
bro = leftOf(parentOf(tmp));
}
setColor(bro, colorOf(parentOf(tmp)));
setColor(parentOf(tmp), BLACK);
setColor(leftOf(bro), BLACK);
rotateRight(parentOf(tmp));
tmp = root;
}
}
}
setColor(tmp, BLACK);
}
複製代碼
理解這一步的關鍵就是:經過循環,把少了一個黑色節點這一事件逐層向上傳遞,直到被某一層處理。具體處理方法呢?就是在bro的子節點裏找到一個RED。
疑問爲何不在bro爲RED的時候直接把bro變成BLACK,經過parent傳遞過來呢?由於bro的子節點的黑色路徑數目會變,以下圖
問題的等效轉換
初看可能會以爲,各類狀況極其複雜,然而他們經過等效變換(換色+旋轉,保持各個節點路徑的黑色節點數目不變),將各類複雜狀況概括爲兩三種簡單狀況,對這兩三種簡單狀況,又分爲在本層解決,或者在本層局部解決把問題推到上一層這兩種處理方式,從而保持或者修復RBT的各類性質。 插入總結:
刪除總結