红-黑树本质上就是一个二叉搜索树,在二叉树上添加了一些强制性的规则,来维持二叉树的平衡性。红-黑树的平衡是在插入节点的过程中(删除也是,本文只实现插入)取得的。 对一个要插入的数据项,插入过程要一直检查,确保不会破坏红-黑树的规则。如果破坏了,程序就会进行纠正,根据需要更改树的结构。通过维持树的规则,进而保持了树的平衡。
1. 红-黑树规则
(1). 每个节点都有颜色。 为红色或黑色,或其他两种颜色,只要可以区分节点类型即可,程序中用一个boolean类型标识。
(2). 根节点是黑色。
(3). 红色节点的子节点必须是黑色,即不允许父与子两个节点都为红色。
(4). 从根到叶子节点或空子节点的每条路径,必须包含相同数目的黑色节点。
(5). 新插入的节点颜色默认为红色(本文适用)。
2. 红-黑树修正违规情况
在插入节点过程中如果破坏了上述红-黑树的规则,可以通过两种形式来使红-黑树再次符合规则:改变节点颜色、旋转操作。
改变节点颜色不作过多解释,就是将节点的颜色变换(红变黑,黑变红),在程序里体现就是改变代表颜色的布尔字段的值即可。
重点说一下旋转操作,先不考虑颜色问题对于红-黑树规则的影响(不考虑规则3)。
(1). 左旋:
1). 进行一次左旋,A节点旋转到其父节点B的位置上,B节点变为A的左子节点(符合B<A的条件)。
2). 如果旋转前 B节点不为根节点(存在父节点R),则A的父节点从B转变为R,B的父节点从R转为A;
3). 如果旋转前 A有一个左子节点D,旋转之后A的左子节点为变为B,D变为B的右子节点,始终符合B<D<A的条件。
4). 图中C节点在旋转前后均为B节点的左子节点,P节点均为A节点的右子节点,所以C、P两个节点不需要变动。
综上: 在进行一次左旋时,需要改变的引用关系为 A、B,若存在 R、D,则R、D也需要进行变动。
/**
* 左旋:
*
* 元素D可为空,不考虑红黑树特性,只考虑左旋实现, C P D 均可以为空
* 具体的旋转时,旋转的节点下面可以带有其他节点,即 D P C 下面都可以有节点, 整个子树跟着一起转。
* 旋转主要涉及 A B 两个节点, D、B的父节点如果不为空也会涉及
*
* B A
* C A -----> B P
* D P C D
*
* @param node A
*/
private void leftRotate(Node A) {
Node B = A.getParentNode();
Node D = A.getLeftNode();
if (B != root) {
Node ggParentNode = B.getParentNode();
A.setParentNode(ggParentNode);
if (B == ggParentNode.getLeftNode())
ggParentNode.setLeftNode(A);
else
ggParentNode.setRightNode(A);
} else {
A.setParentNode(null);
root = A;
}
B.setParentNode(A);
A.setLeftNode(B);
if (D != null) {
D.setParentNode(B);
}
B.setRightNode(D);
}
(2). 右旋:
1). 进行一次右旋,B节点旋转到其父节点A的位置上,A节点变为B的右子节点(符合B<A的条件)。
2). 如果旋转前 A节点不为根节点(存在父节点R),则B的父节点从A转变为R,A的父节点从R转为B;
3). 如果旋转前 B有一个右子节点D,旋转之后B的右子节点为变为A,D变为A的左子节点,始终符合B<D<A的条件。
4). C、P两个节点仍无需变动。
综上: 在进行一次右旋时,需要改变的引用关系为 A、B、R、D,左旋和右旋过程其实正好相反。
/**
* 右旋:
*
* 元素D可为空,不考虑红黑树特性,只考虑左旋实现, C P D 均可以为空
* 具体的旋转时,旋转的节点下面可以带有其他节点,即 D P C 下面都可以有节点, 整个子树跟着一起转。
* 旋转主要涉及 A B 两个节点, D、A的父节点如果不为空也会涉及
*
* A B
* B P -----> C A
* C D D P
*
* @param node B
*/
private void rightRotate(Node B) {
Node A = B.getParentNode();
Node D = B.getRightNode();
if (A != root) {
Node ggParentNode = A.getParentNode();
B.setParentNode(ggParentNode);
if (A == ggParentNode.getLeftNode())
ggParentNode.setLeftNode(B);
else
ggParentNode.setRightNode(B);
} else {
B.setParentNode(null);
root = B;
}
A.setParentNode(B);
B.setRightNode(A);
if (D != null) {
D.setParentNode(A);
}
A.setLeftNode(D);
}
注意:对于旋转操作,受影响的都是涉及到的几个节点的数据和所在的子树结构,局部旋转不会影响到其他子树,可以放心进行旋转操作:
上图中一次旋转过后,C、D、P下面的子树结构都没有发生变化,所以我们在旋转过程中只关注 B、A、R、D 4个特定的节点即可。
3. 插入过程
3.1 在下行路途中的颜色变换
红-黑树的插入过程的开始时所做的事和普通的二叉搜索树一样:以根节点为起始,朝深处走,在每一个节点处通过比较节点值的相对大小来决定向左走还是向右走。但是,在向下查找过程中,为了不违反红-黑树的颜色规则,也为了更好的插入新节点,我们需要进行颜色变换。
变换的规则:在向下查找路途中,如果遇到一个有两个红色子节点的黑色节点时,它必须把两个子节点变为黑色,把自己变为红色(根节点除外,根节点只把两个子节点置为黑色即可,自身永远都是黑色的)。
那么这样做有什么好处呢? 经过颜色变换之后,首先这种变换没有改变从根节点向下过A节点到叶节点或者空节点的路径上的黑色节点的数目。通俗的讲,虽然A节点不是黑色了,但是两个子节点都变成了黑色,两条路径上面的黑色节点数目仍然不变。规则中规定,红色父节点下面不能连接红色子节点,新增的节点默认为红色,这样做的好处我们将底层的子节点变为黑色之后,会更容易去连接新节点。
下行路途中颜色变换带来的问题: 将A节点置为红色之后,如果恰好A节点的父节点P也是红色,那么违反了规则3,我们需要对此次颜色变换进行旋转操作,得到正确的红-黑树之后,程序才可继续执行,下面是A、P两个节点都为红色的修复方法:
对A节点进行分析,会得到2种情况,每种情况下面分别又有对称的2种情况具体操作如下:
(1). A节点是外侧子孙。 外侧子孙通俗来说就是 A节点与 A的爷爷节点不在同一个边上,如下图:A在P的左边,A的爷爷节点G在P的右边,A是G的外侧子孙节点(A在右边,G在左边也属于外侧子孙)。
(2). A节点是内侧子孙。 如下图:A在P的左边,A的爷爷节点G也在P的左边,A是G的内侧子孙节点(A在右边,G在右边也属于内侧子孙)。
对节点A进行旋转代码如下:
/**
* 在下行途中,进行颜色转换之后遇到, 红-红 连接的 情况需要进行 旋转
* @param node
*/
private void downRotate(Node node) {
//情况1: 外侧子孙, 节点是父节点的左子节点, 父节点是祖父节点的左子节点
// 右旋
Node parentNode = node.getParentNode();
Node gradPaNode = parentNode.getParentNode();
if (node==parentNode.getLeftNode() && parentNode==gradPaNode.getLeftNode()) {
parentNode.setRed(false);
node.setRed(false); // 自身和父节点本来是红色,全都变为黑色
rightRotate(parentNode); // 注意在旋转的时候不要赋错值
}
//情况2:外侧子孙 : 节点是父节点的右子节点, 父节点是祖父节点的右子节点
// 左旋
else if (node==parentNode.getRightNode() && parentNode==gradPaNode.getRightNode()) {
parentNode.setRed(false);
node.setRed(false);
leftRotate(parentNode);
}
// 情况3: 内侧子孙
// 节点是父节点的右子节点, 父节点是祖父节点的左子节点 需要 先左旋,再右旋
else if (node==parentNode.getRightNode() && parentNode==gradPaNode.getLeftNode()) {
node.setRed(false);
parentNode.setRed(false);
// 先左旋
leftRotate(node);
// 再右旋
rightRotate(node);
}
// 情况4: 内侧子孙
// 节点是父节点的左子节点, 父节点是祖父节点的右子节点 需要 先右旋,再左旋
else if (node==parentNode.getLeftNode() && parentNode==gradPaNode.getRightNode()) {
node.setRed(false);
parentNode.setRed(false);
// 先右旋
rightRotate(node);
// 再左旋
leftRotate(node);
}
}
3.2 插入节点之后的转换
在顺着树的路径向下找到合适的位置之后,就可以插入新的节点,但是新插入的节点可能会违反红-黑树规则。因此,在插入之后,必须要进行检测是否违反规则,如果违反规则,需要采取相应的措施。
文章开头已经讲过,本文新插入的节点是红色的,而插入节点后主要有以下3种可能存在的情况:
1). P是黑色的。
如果P是黑色的,什么事情都不需要做,因为新插入的节点是红色的,连接在黑色的父节点上不影响红黑树规则。插入之后就可以返回。
2). P是红色的,X是G的一个外侧子孙节点。
需要一次旋转和一些颜色变化如图所示:
3). P是红色的,X是G的一个内侧子孙节点。
需要两次旋转和一些颜色变化如图所示:
PS:对于其他可能存在的情况,比如X存在一个兄弟节点或者存在一个叔叔节点,这种情况都会违反红-黑树规则,具体原因读者可自行分类思考。
由于程序在下行搜索路途中使用颜色变换已经消除了底层旋转可能造成树的上方发生违反规则的情况,保证了插入节点之后一次或两次旋转便可以使整棵树再次成为正确的红-黑树。在下行途中的颜色变换的插入效率比其他平衡术的效率更高,因为它保证了在下行路途中仅在树上行走了一遍。
注: 本文内容参考于Java数据结构和算法第二版,代码纯手打,如有错误和疑问请留言,共同进步。
附上所有代码,如有错误请指正:
package com.suning.tree.rbtree;
/**
* 树节点实体类<br>
* 〈功能详细描述〉
*
* @author zhujk
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
class Node {
/**
* 数据
*/
private int data;
/**
* 左子节点
*/
private Node leftNode;
/**
* 右子节点
*/
private Node rightNode;
/**
* 父节点
*/
private Node parentNode;
/**
* 颜色
*/
private boolean isRed = true; //新增节点默认为红色
/**
* @return the data
*/
public int getData() {
return data;
}
/**
* @param data the data to set
*/
public void setData(int data) {
this.data = data;
}
/**
* @return the leftNode
*/
public Node getLeftNode() {
return leftNode;
}
/**
* @param leftNode the leftNode to set
*/
public void setLeftNode(Node leftNode) {
this.leftNode = leftNode;
}
/**
* @return the rightNode
*/
public Node getRightNode() {
return rightNode;
}
/**
* @param rightNode the rightNode to set
*/
public void setRightNode(Node rightNode) {
this.rightNode = rightNode;
}
/**
* @return the parentNode
*/
public Node getParentNode() {
return parentNode;
}
/**
* @param parentNode the parentNode to set
*/
public void setParentNode(Node parentNode) {
this.parentNode = parentNode;
}
/**
* @return the isRed
*/
public boolean isRed() {
return isRed;
}
/**
* @param isRed the isRed to set
*/
public void setRed(boolean isRed) {
this.isRed = isRed;
}
}
package com.suning.tree.rbtree;
import com.suning.util.NumUtil;
/**
* 红黑树<br>
* 〈功能详细描述〉
*
* @author zhujk
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
class Tree {
/**
* 红黑树规则:
* 所有的节点不是红色就是黑色
* 根节点是黑色
* 红色的节点的子节点必须是黑色,反之则无所谓
* 任意的父节点到每个叶子节点的路径上的黑色节点总数相等 / 任意一个节点到到NULL(树尾端)的任何路径,所含之黑色节点数必须相同。
*/
public static void main(String[] args) {
Tree tree = new Tree();
int[] result = NumUtil.randomNumber(1, 1000, 20); // 取随机数
for(int k =0 ;k<result.length;k++){
tree.add(result[k]);
}
tree.inorder();
}
/**
* root node
*/
private Node root;
/**
* 红黑树新增一个节点
* @param data
*/
public void add(int data) {
Node node = new Node();
node.setData(data);
if (root == null) {
node.setRed(false); // 根节点是黑色
root = node;
}
else {
// 遍历树,查找到应该插入的位置
Node currentNode = root;
node.setRed(true);
Node lCurrentNode = null;
Node rCurrentNode = null;
// 下行遍历,遇到 父节点为黑,两个子节点都为红的 转换为 父节点为红,两个子节点为黑, 但是可能会出现父节点与祖父节点都为红的情况,需要判断,旋转
while (true) {
lCurrentNode = currentNode.getLeftNode();
rCurrentNode = currentNode.getRightNode();
// 下行过程中进行颜色转换
if (!currentNode.isRed() && lCurrentNode!=null && rCurrentNode!=null
&& lCurrentNode.isRed() && rCurrentNode.isRed())
{
lCurrentNode.setRed(false);
rCurrentNode.setRed(false);
if (currentNode != root) {
currentNode.setRed(true);
// 如果出现两个节点都为红的,需要进行旋转操作
if (currentNode.getParentNode().isRed()) {
downRotate(currentNode);
}
}
}
if (data < currentNode.getData()) {
if (currentNode.getLeftNode() != null) {
currentNode = currentNode.getLeftNode();
continue;
} else {
insert(currentNode, node); // 找到插入位置,插入
root.setRed(false); //根节点置黑
break;
}
}
else {
if (currentNode.getRightNode() != null) {
currentNode = currentNode.getRightNode();
continue;
} else {
insert(currentNode, node); // 找到插入位置,插入
root.setRed(false); //根节点置黑
break;
}
}
}
}
}
/**
* 在下行途中,进行颜色转换之后遇到, 红-红 连接的 情况需要进行 旋转
* @param node
*/
private void downRotate(Node node) {
//情况1: 外侧子孙, 节点是父节点的左子节点, 父节点是祖父节点的左子节点
// 右旋
Node parentNode = node.getParentNode();
Node gradPaNode = parentNode.getParentNode();
if (node==parentNode.getLeftNode() && parentNode==gradPaNode.getLeftNode()) {
parentNode.setRed(false);
node.setRed(false); // 自身和父节点本来是红色,全都变为黑色
rightRotate(parentNode); // 注意在旋转的时候不要赋错值
}
//情况2:外侧子孙 : 节点是父节点的右子节点, 父节点是祖父节点的右子节点
// 左旋
else if (node==parentNode.getRightNode() && parentNode==gradPaNode.getRightNode()) {
parentNode.setRed(false);
node.setRed(false);
leftRotate(parentNode);
}
// 情况3: 内侧子孙
// 节点是父节点的右子节点, 父节点是祖父节点的左子节点 需要 先左旋,再右旋
else if (node==parentNode.getRightNode() && parentNode==gradPaNode.getLeftNode()) {
node.setRed(false);
parentNode.setRed(false);
// 先左旋
leftRotate(node);
// 再右旋
rightRotate(node);
}
// 情况4: 内侧子孙
// 节点是父节点的左子节点, 父节点是祖父节点的右子节点 需要 先右旋,再左旋
else if (node==parentNode.getLeftNode() && parentNode==gradPaNode.getRightNode()) {
node.setRed(false);
parentNode.setRed(false);
// 先右旋
rightRotate(node);
// 再左旋
leftRotate(node);
}
}
/**
*
* @param parentNode 待添加的节点的父节点,已确定
* @param node 待添加的节点
*/
private void insert(Node parentNode, Node node) {
//插在左边,作为左子节点
if (parentNode.getData() > node.getData()) {
parentNode.setLeftNode(node);
node.setParentNode(parentNode);
// 父节点是黑色,直接插入
if (!parentNode.isRed()) {
return;
}
// 父节点是红色
else {
// (1)待插入节点作为外侧子孙, 都位于左侧, 需要右旋
if (parentNode.getParentNode().getLeftNode() == parentNode) {
parentNode.setRed(false);
parentNode.getParentNode().setRed(true);
rightRotate(parentNode);
}
else {
// 待插入节点作为内侧子孙, 位于父节点的左边, 而父节点位于祖父节点的右边,
// 需要先右旋, 再左旋
node.setRed(false);
parentNode.getParentNode().setRed(true);
rightRotate(node);
leftRotate(node);
}
}
}
//插在右边,作为右子节点
else {
parentNode.setRightNode(node);
node.setParentNode(parentNode);
if (!parentNode.isRed()) {
return ;
}
else {
// (1)待插入节点作为外侧子孙, 都位于右侧, 需要左旋
if (parentNode.getParentNode().getRightNode() == parentNode) {
parentNode.setRed(false);
parentNode.getParentNode().setRed(true);
leftRotate(parentNode);
}
else {
// 待插入节点作为内侧子孙, 位于父节点的右边, 而父节点位于祖父节点的左边,
// 需要先左旋, 再右旋
node.setRed(false);
parentNode.getParentNode().setRed(true);
leftRotate(node);
rightRotate(node);
}
}
}
}
/**
* 左旋:
*
* 元素D可为空,不考虑红黑树特性,只考虑左旋实现, C P D 均可以为空
* 具体的旋转时,旋转的节点下面可以带有其他节点,即 D P C 下面都可以有节点, 整个子树跟着一起转。
* 旋转主要涉及 A B 两个节点, D、B的父节点如果不为空也会涉及
*
* B A
* C A -----> B P
* D P C D
*
* @param node A
*/
private void leftRotate(Node A) {
Node B = A.getParentNode();
Node D = A.getLeftNode();
if (B != root) {
Node ggParentNode = B.getParentNode();
A.setParentNode(ggParentNode);
if (B == ggParentNode.getLeftNode())
ggParentNode.setLeftNode(A);
else
ggParentNode.setRightNode(A);
} else {
A.setParentNode(null);
root = A;
}
B.setParentNode(A);
A.setLeftNode(B);
if (D != null) {
D.setParentNode(B);
}
B.setRightNode(D);
}
/**
* 右旋:
*
* 元素D可为空,不考虑红黑树特性,只考虑左旋实现, C P D 均可以为空
* 具体的旋转时,旋转的节点下面可以带有其他节点,即 D P C 下面都可以有节点, 整个子树跟着一起转。
* 旋转主要涉及 A B 两个节点, D、A的父节点如果不为空也会涉及
*
* A B
* B P -----> C A
* C D D P
*
* @param node B
*/
private void rightRotate(Node B) {
Node A = B.getParentNode();
Node D = B.getRightNode();
if (A != root) {
Node ggParentNode = A.getParentNode();
B.setParentNode(ggParentNode);
if (A == ggParentNode.getLeftNode())
ggParentNode.setLeftNode(B);
else
ggParentNode.setRightNode(B);
} else {
B.setParentNode(null);
root = B;
}
A.setParentNode(B);
B.setRightNode(A);
if (D != null) {
D.setParentNode(A);
}
A.setLeftNode(D);
}
/**
* 中序遍历
* 从小到大排序
* @param node
*/
private void inorder(Node node) {
if (node == null)
return ;
System.out.print(" " + node.getData() + " ");
if (node != root)
System.out.println(": 父节点是 " + node.getParentNode().getData() + ", isRed = " + node.isRed());
else
System.out.println();
inorder(node.getLeftNode());
inorder(node.getRightNode());
}
public void inorder() {
inorder(root);
}
/**
* 校验红黑树合法性
*/
public void check() {
if (root == null)
return ;
checkRedRed(root);
}
//校验规则3
private void checkRedRed(Node node) {
if (node == null)
return ;
if (node.getLeftNode()==null || node.getRightNode()==null)
return ;
if (node.isRed() && node.getLeftNode().isRed()) {
System.out.println(node.getData() + "与左子节点都为红色");
}
else if (node.isRed() && node.getRightNode().isRed()) {
System.out.println(node.getData() + "与右子节点都为红色");
}
else {
checkRedRed(node.getLeftNode());
checkRedRed(node.getRightNode());
}
}
}