筆記:紅黑樹旋轉和插入
紅黑樹
紅黑樹是每個節點都帶有顏色屬性的二叉查詢樹,顏色或紅色或黑色。在二叉查詢樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求:
- 性質1. 節點是紅色或黑色。
- 性質2. 根節點是黑色。
- 性質3. 每個葉節點(NIL節點,空節點)是黑色的。
- 性質4. 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
- 性質5. 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
旋轉
左旋
/**
* 左旋,
* @param current
*/
private void rotateLeft(Node current) {
Node right = current.rightChild;
Node temp = right.leftChild;
right.leftChild = current;
current.rightChild = temp;
}
右旋
/**
* 右旋
* @param current
*/
private void rotateRight(Node current) {
Node left = current.leftChild;
Node temp = left.rightChild;
left.rightChild = current;
current.leftChild = temp;
}
紅黑樹的插入
插入過程和二叉樹的插入過程相同,先找到插入的節點然後插入。不同的是在插入後,需要通過旋轉和著色操作,使紅黑樹滿足規則,即滿足黑色平衡。
插入一個節點
/**
* 插入一個節點
* @param node
* @return
*/
public void insertNode(Node node) throws KeyAlreadyExistsException {
if (root == null) {
node.color = NodeColor.BLACK;
root = node;
} else {
Stack<Node> parentStack = new Stack<Node>();
Node current = root;
while (current != null) {
parentStack.push(current);
if (node.isBiggerThan(current)) {
current = current.rightChild;
} else if (node.isSmallerThan(current)){
current = current.leftChild;
} else {
throw new KeyAlreadyExistsException("this key is already exists!!!");
}
}
Node parent = parentStack.peek();
if (node.isBiggerThan(parent)) {
parent.rightChild = node;
} else {
parent.leftChild = node;
}
//調整使樹平衡
insertFixUp(parentStack, node);
}
}
調整和著色
插入新節點時,預設新節點為紅色節點。於是有如下兩種情況:
1. 父節點為黑色,滿足黑色平衡,不需要進行調整。
2. 父節點為紅色,不滿足規則,需要進行調整。
當插入節點的父節點為紅色時,出現連續紅色節點,不滿足性質4,因此需要調整。調整時分為如下兩種情況:
1. 插入節點的叔叔節點為紅色,此時需要通過著色操作,將父節點和叔叔節點變為黑色,並將祖父節點變為紅色,將祖父節點作為“當前節點”。並繼續對“當前節點”進行調整。
2. 插入節點的叔叔節點為黑色,需要進行旋轉,使樹滿足規則。
針對上面的第二種情況,也就是叔叔節點為黑色時,可分為如下兩種情況進行旋轉。
1、插入節點在外側(插入節點和父節點方向相同:同在左側、同在右側),如下圖所示:
對於這種情況,只需要對祖父節點進行一次旋轉即可修正。以祖父節點為軸向插入節點一側的相反方向旋轉一次,然後將原父節點顏色置為黑色,將原祖父節點顏色置為紅色。即可滿足所有的性質。這裡以插入節點在左側為例,先將祖父節點右旋(插入節點的相反方向),然後將原父節點置為紅色,祖父節點置為黑色。調整後,滿足所有性質。
2、 插入節點在內側(插入節點和父節點方向相反),如下圖所示:
對於插入節點在內側的情況,可以以父節點為軸,向相反方向旋轉一次。然後將原父節點作為“當前節點”,於是就變為情況1,進行情況1的操作即可。這裡以插入節點在右側為例,以父節點為軸,左旋一次。然後以父節點為當前節點,變為情況1。
對於以上情況,在旋轉時需要用到父節點和祖父節點,將旋轉過的子樹與原樹連線,還需要用到增祖父節點。因此,如果節點中沒有存放父節點的資訊時,建議將使用棧來存放當前節點的所有祖先節點。調整樹的程式碼如下:
/**
* 調整紅黑樹,使其滿足黑色高度相同
* @param parentStack
* @param newNode
*/
private void insertFixUp(Stack<Node> parentStack, Node newNode) {
Node parent = parentStack.pop();
Node current = newNode;
Node uncle = null;
Node gParent = null;
while (parent != null) {
//父節點黑色
if (parent.isBlack()) {
return ;
}
gParent = parentStack.pop();
uncle = gParent.getAnotherChild(parent);
if (uncle != null && uncle.isRed()) {
uncle.color = NodeColor.BLACK;
parent.color = NodeColor.BLACK;
gParent.color = NodeColor.RED;
current = gParent;
if (parentStack.empty()) {
current.color=NodeColor.BLACK;
parent = null;
} else {
parent = parentStack.pop();
}
} else {
// uncle is null or uncle is black, rotate and break while
//情況1:插入節點在內側時,反向旋轉父節點,使其到外側,變為情況2
if (parent.isLeftOf(gParent) && current.isRightOf(parent)) {
rotateLeft(parent);
gParent.leftChild = current;
current = parent;
parent = gParent.leftChild;
} else if (parent.isRightOf(gParent) && current.isLeftOf(parent)) {
rotateRight(parent);
gParent.rightChild = current;
current = parent;
parent = gParent.rightChild;
}
//情況2:插入節點在外側時,反向旋轉祖父節點,完成調整
if (parent.isRightOf(gParent) && current.isRightOf(parent)) {
rotateLeft(gParent);
gParent.color = NodeColor.RED;
parent.color = NodeColor.BLACK;
current = parent;
} else if (parent.isLeftOf(gParent) && current.isLeftOf(parent)) {
rotateRight(gParent);
gParent.color = NodeColor.RED;
parent.color = NodeColor.BLACK;
current = parent;
}
parent = null;
}
} // end while
if (parentStack.empty()) {
root = current;
}
if (!parentStack.empty()){
if (gParent.isRightOf(parentStack.peek())){
parentStack.peek().rightChild = current;
} else {
parentStack.peek().leftChild = current;
}
}
}