前面一章我們聊到了平衡二叉樹,它是一種搜索效率極高的樹, 但是它有一個缺點就是建樹成本、 插入節點以及刪除節點都需要進行 左平衡和右平衡旋轉,需要消耗大量的計算資源。所以引進了紅黑樹,它相對平衡二叉樹,犧牲了一小部分的搜索效率,但是換來了在建樹、插入節點和刪除節點的極大的方便。 這就是紅黑樹的由來。
紅黑樹的定義
它是以棵空樹,或者是一個具有以下性質的樹:
- 節點非紅即黑
- 根節點是黑色
- 所有的葉子節點都是NULL節點,且節點顏色是黑色
- 所有的紅節點的子節點必須是黑色節點
- 從任意節點到其葉子節點的所有路徑上都包含有相同數目的黑節點
滿足上面的定義的它就是一棵紅黑樹。
紅黑樹相對另外一個平衡二叉樹,還有另外一個特點:
相對於平衡二叉樹的改進,任意一個節點,他的左右子樹的層次最多不超過一倍。
如上圖:根節點 13,左邊是4層, 那麼右邊最多是7層
如17號節點,左邊是兩層,那麼右邊就最多3層。
紅黑樹的建樹規則
按照二叉排序樹的方式插入節點,初始顏色是紅色。
一、插入的是根節點 直接塗黑
二、如果插入的節點的父節點是黑色,不做任何處理
如上圖, 1 和 2 號位置標記的,選擇爲 1 號節點插入一個 -1節點, 不做任何處理,也滿足上面的五條紅黑樹的規則
同樣的 15號節點插入一個左子節點14,也滿足上面的五條紅黑樹的規則。
三、插入節點的父節點是紅色
如果滿足這種情況,我們需要根據父親節點時祖父節點的左孩子還是右孩子以及祖父節點的另一個子節點(叔叔節點)是紅色還是黑色分情況判斷。
-
父親節點時祖父節點的左孩子
-
祖父節點的另一個子節點(叔叔節點)是紅色
對策: 將當前節點的父節點和叔叔節點塗黑,祖父節點塗紅,把當前節點指向祖父節點,從新的當前節點重新開始判斷( 三這個步驟) -
叔叔節是黑色
-
如果當前節點是其父節點的右孩子
對策:當前節點的父節點做爲新的當前節點,以新當前節點爲支點左旋 在執行情況二 -
如果當前節點是其父節點的左孩子
對策:父節點變爲黑色,祖父節點變紅色,再祖父節點爲支點進行右旋
-
-
-
父親節點時祖父節點的右孩子
- 祖父節點的另一個子節點(叔叔節點)是紅色
對策: 將當前節點的父節點和叔叔節點塗黑,祖父節點塗紅,把當前節點指向祖父節點,從新的當前節點重新開始判斷( 三這個步驟)
如上圖完成變換以後,發現當前節點時紅色且爲根節點,沒有父節點,最後把 當前節點塗黑,即完成紅黑樹的變換
- 叔叔節是黑色
- 情況一、當前節點是其父節點的左孩子
對策:當前節點的父節點做爲新的當前節點,以新當前節點爲支點右旋。然後在執行情況二
- 情況二、當前節點是其父節點的右孩子
對策:父節點變爲黑色,祖父節點變紅色,再祖父節點爲支點進行左旋
- 情況一、當前節點是其父節點的左孩子
- 祖父節點的另一個子節點(叔叔節點)是紅色
通過TreeMap查看紅黑樹的建樹規則
因爲TreeMap的底層就是用的紅黑樹數據結構,使我們非常好的學習例子
public V put(K key, V value) {
TreeMapEntry<K,V> t = root;
// 1、如果根 等於 null,就直接設置根節點
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMapEntry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//如果有設置比較器,if 和else 代碼是相同的 所以
if (cpr != null) {
// 註釋1
do { // while通過比較器從根節點位置做比較,如果比t小,t就往左邊找,反之就往右邊找, 用parent 記錄位置
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
//註釋二
// 和註釋一 一樣,就是創建了一個比較器
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
//跳出wihle循環以後,插入節點,且節點顏色是紅色
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//核心方法,因爲插入的節點就會不滿足,紅黑樹的規則,這個時候就需要對紅黑樹進行調整
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
從上面的註釋可以看出這就是一個 排序二叉樹的插入規則,從根節點開始遍歷,小就往左邊找,大往右邊找,然後把t 指向 t 的做節點或者右節點。直到t等於null,跳出循環,說明已經找到了位置。
/** From CLR */
private void fixAfterInsertion(TreeMapEntry<K,V> x) {
//註釋1 新插入的節點 首先給一個紅色,然後後面對這節點修改顏色,同時 x 的指向也會改變
x.color = RED;
//註釋2
//如果父節點是黑色不做操作,把插入節點改成黑色,即可
//如果父節點是紅色,進入while
while (x != null && x != root && x.parent.color == RED) {
// 註釋2.1 父節點是祖父節點的左孩子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));//叔叔節點
if (colorOf(y) == RED) {//叔叔節點時紅色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//不是紅色 ,就會執行下面這兩步
//變換1
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
//變換2
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else { // 註釋2.2 父節點是祖父節點的左孩子
TreeMapEntry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) { //叔叔節點時紅色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//叔叔節點時黑色
//變換1
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
//變換2
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
其實通過上面的代碼 更加清晰明白紅黑樹的建樹規則。