續哈希表那兩篇博客
我們已經知道了哈希表有多麼多麼的牛逼,但是呢,有沒有想過一個問題:對於用拉鍊法構造出來的哈希表,我們在查找一個元素時,還是要遍歷後面的拉鍊。解決這個問題的一個比較好的方法就是紅黑二叉樹。
這篇博客分析紅黑二叉樹的
- 左旋
- 右旋
- 插入
參考文章:
- https://www.cnblogs.com/lycroseup/p/7325668.html
- https://blog.csdn.net/sd09044901guic/article/details/84955116#commentBox
- https://blog.csdn.net/x763795151/article/details/86583800
- https://www.cnblogs.com/skywang12345/p/3624343.html
一、紅黑二叉樹簡介
紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹
紅黑樹和AVL樹類似,都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能。
它可以在O(log n)時間內做查找,插入和刪除,這裏的n 是樹中元素的數目。 ——百度百科
紅黑二叉樹的五條性質(五條全部滿足就是紅黑二叉樹,只要有一條不符則不是)
1. 每個結點的顏色只能是紅色或黑色。
2. 根結點是黑色的。
3. 每個葉子結點都帶有兩個空的黑色結點(被稱爲黑哨兵),如果一個結點n的只有一個左孩子,那麼n的右孩子是一個黑哨兵;如果結點n只有一個右孩子,那麼n的左孩子是一個黑哨兵。
4. 如果一個結點是紅的,則它的兩個兒子都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。
5. 對於每個結點來說,從該結點到其子孫葉結點的所有路徑上包含相同數目的黑結點。
二、定義結點、顏色等
public class RBNode<T extends Comparable<T>> {
RBNode<T> left; // 左孩子
RBNode<T> right; // 右孩子
RBNode<T> parent; // 父節點
boolean color; // 顏色
T key; // 關鍵字
public RBNode(T key, boolean color, RBNode<T> left, RBNode<T> right, RBNode<T> parent) {
this.key = key;
this.color = color;
this.left = left;
this.right = right;
this.parent = parent;
}
}
左旋和右旋比較繞,需要仔細慢慢消化理解,我花了很長時間才搞明白。
一定要結合圖去寫代碼,否則必出錯(大神除外)
三、左旋
圖片源:https://blog.csdn.net/sd09044901guic/article/details/84955116#commentBox
private void leftRotate(RBNode<T> x) {
// 首先設置x的右孩子y
RBNode<T> y = x.right;
y.parent = x;
// 判斷x是否有父親
if (x.parent == null) { // 沒有就把y變成父親
y = root;
} else {
if (x == x.parent.left) { // 如果x是父親的左孩子,就把y變成父親的左孩子
x.parent.left = y;
} else if (x == x.parent.right) { // 同上
x.parent.right = y;
}
}
x.parent = y;
x.right = y.left; // y的左孩子變成x的右孩子
y.left = x; // x變成y的左孩子
}
四、右旋
private void rightRotate(RBNode<T> y) {
// 右旋,y是x的父親,y變成x的兒子,x右兒子變成y的左兒子
// 設置y的左兒子x
RBNode<T> x = y.left;
x.parent = y;
if (y.parent == null) { // y沒爸,就把x變成根節點
x = root;
} else {
if (y == y.parent.left) { // 如果y是它爸的左兒子,就把x變成y爸的左兒子
x = y.parent.left;
} else if (y == y.parent.right) { // 同上
x = y.parent.right;
}
}
y.parent = x; // 把x變成y的父親
y.left = x.right; // x右兒子給y做左兒子
x.right = y; // y變成x的右兒子
}
五、插入
首先要注意一個問題,插入的元素初始顏色都是紅色,因爲紅色不會違背性質五。這樣使需要討論的情況更少,問題更簡單,
插入要分情況討論,每種情況都要去滿足紅黑二叉樹的五條性質
-
父親結點是黑色
- 此時直接滿足五條性質 -
父親節點是紅色(此時考慮叔叔結點)
叔叔結點爲紅1、父親、叔叔變黑 2、新節點、祖父變紅 3、由於祖父變紅,兩紅不相鄰,往上查找,循環使其滿足性質四
叔叔結點爲黑
①父親是祖父右孩子,新節點是父親右孩子 1、父變黑 2、祖父變紅 3、祖父右旋 ②父親是祖父左孩子,新節點是父親右孩子 1、父親左旋 2、交換新節點與父親的位置 3、經過上兩步就變成了①,再用①操作即可 ③父親是祖父右孩子,新節點是父親右孩子 1、父變黑 2、祖父變紅 3、祖父左旋 ④父親是祖父右孩子,新節點是父親左孩子 1、父右旋 2、交換新節點與父親位置 3、經過上兩步就變成了③,再用③操作即可
//定義一些常用的方法
private RBNode<T> parentOf(RBNode<T> node) {
return node != null ? node.parent : null;
}
private boolean isRed(RBNode<T> node) {
return ((node != null) && (node.color == RED)) ? true : false;
}
private void setBlack(RBTree<T>.RBNode<T> node) {
if (node != null) {
node.color = BLACK;
}
}
private void setRed(RBNode<T> node) {
if (node != null) {
node.color = RED;
}
}
新插入的結點都先插到葉子節點處
//先進行插入,再去滿足性質
private void insert(RBNode<T> node) {
int cmp;
RBNode<T> y = null;
RBNode<T> x = this.root;
// 1、將紅黑樹當作一顆二叉查找樹,將結點添加到二叉查找樹中
while (x != null) {
y = x;
cmp = node.key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else
x = x.right;
}
node.parent = y;
if (y != null) {
cmp = node.key.compareTo(y.key);
if (cmp < 0) {
y.left = node;
} else {
y.right = node;
}
} else {
this.root = node;
}
// 2、設置節點的顏色爲紅色
node.color = RED;
// 3、將他重新修正爲一顆二叉查找樹
insertFixUp(node);
}
下面對插入結點後的樹進行修正,使其變成合法的紅黑二叉樹
private void insertFixUp(RBNode<T> node) {
RBNode<T> parent, gparent;
// 當父親節點存在並且父親節點是黑色,則不用考慮,不影響
// 若父節點存在並且父節點是紅色
while (((parent = parentOf(node)) != null) && isRed(parent)) {
/*
* 分幾種情況考慮:
* 1、叔叔紅色 把叔叔、父親變成黑,祖父變紅(再往上判斷祖父的父親,循環)
* 2、叔叔黑色
* (1.1)父親是左孩子,新插入結點是左孩子
* (1.2)父親是左孩子,新節點是右孩子
* (2.1)父親是右孩子,新插入結點是右孩子
* (2.2)父親是右孩子,新插入結點是左孩子
*/
gparent = parentOf(parent);
//當父親是左孩子
if (parent == gparent.left) {
RBNode<T> uncle = gparent.right;
// case 1:叔叔結點是紅色
if ((uncle != null) && isRed(uncle)) {
setBlack(uncle);
setBlack(parent);
setRed(gparent);
node = gparent;
continue;
}
//case2:叔叔是黑色,新節點是右孩子
if(parent.right == node){
RBNode<T> tmp;
leftRotate(parent);
tmp = parent;
parent = node;
node = tmp;
} //把這種情況變成case3
// case3:叔叔是黑色,新節點是左孩子
setBlack(parent);
setRed(gparent);
rightRotate(gparent);
}else{
//當父親是右孩子
RBNode<T> uncle = gparent.left;
//case1:叔叔結點是紅色
if((uncle!=null) && isRed(uncle)){
setBlack(uncle);
setBlack(parent);
setRed(gparent);
node = gparent;
continue;
}
//case2:叔叔是黑色,當前結點是左孩子
if(parent.left == node){
RBNode<T> tmp;
rightRotate(parent);
tmp = parent;
parent = node;
node = tmp;
}
//case3:叔叔是黑色,當前節點是右孩子
setBlack(parent);
setRed(gparent);
leftRotate(gparent);
}
}
//將根節點設置爲黑色
setBlack(this.root);
}
插入操作相對於刪除操作還要簡單一點,這篇就到這,下篇學習刪除操作