一、红黑树
红黑树是一种自平衡二叉搜索树,是在计算机科学中哟感到的一种数据结构,红黑树与AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉搜索树的平衡,从而获得较高的查找性能,红黑树虽然比较复杂,但是它在最坏的情况下运行时间也是非常良好的,并且在实践中是高效的:它可以在O(log 2^n)时间内做查找,插入和删除,n是指树中元素的数目。
1、红黑树的五个性质
红黑树的每个节点都带有颜色属性,节点是红色或者黑色,在二叉搜索树(BST)的性质以外,对于任何有效的红黑树我们增加了以下的额外要求:
- 树的节点是红色或者黑色;
- 根节点必须是黑色;
- 所有的叶子节点都是黑色(叶子节点是null节点,不存储实际的数据);
- 不能出现连续的两个红色节点(从根节点到每个叶子节点的路径上不能有两个连续的红色节点);
- 从任一节点到其每个叶子的所有路径上包含的黑色节点的数目都相同;
这些约束强制了红黑树的关键性质:从根节点到叶子节点的最长路径不会超过最短路径的两倍;因为性质4导致了每条路径上不能有连续的两个红色节点,假设最短路径上都是黑色节点,最长路径的最坏可能就是红黑交替,又因为性质5所有路径上的黑色节点数目相同,这就表明了没有一条路径能大于任何其他路径的两倍长度;
这样红黑树大体上是平衡的,比如增删查某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏的情况下都是高效的,而不同于普通的二叉搜索树;
2、操作
红黑树能自平衡依赖的是以下三种操作:
- 变色:节点的颜色由红变黑或者由黑变红;
- 左旋
- 右旋
在红黑树上的只读操作不需要对用于二叉搜索树的操作做出修改,但是在插入、删除之后节点的红黑属性可能不符合红黑树的性质,恢复这些属性需要少量的颜色变更(这在实践中是非常快速的)并且在插入操作中树的旋转不会超过两次,在删除操作中不会超过三次,但是操作相对来说比较复杂;
二、定义数据结构
红黑树的定义先简单写一下,后面的操作我们逐步来实现;
class RBTree<T extends Comparable<T>> {
private RBNode<T> root;
public RBTree() {
this.root = null;
}
}
//用枚举类定义节点的颜色
enum Color {
RED,BLACK;
}
//红黑树的节点类型
class RBNode<T extends Comparable<T>> {
private T data; // 数据
private RBNode<T> left; //左孩子
private RBNode<T> right; //右孩子
private RBNode<T> parent; //父亲节点
private Color color; //节点的颜色
public RBNode(T data, RBNode<T> left, RBNode<T> right, RBNode<T> parent, Color color) {
this.data = data;
this.left = left;
this.right = right;
this.parent = parent;
this.color = color;
}
public RBNode(T data, RBNode<T> parent, Color color) {
this.data = data;
this.left = null;
this.right = null;
this.parent = parent;
this.color = color;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public RBNode<T> getLeft() {
return left;
}
public void setLeft(RBNode<T> left) {
this.left = left;
}
public RBNode<T> getRight() {
return right;
}
public void setRight(RBNode<T> right) {
this.right = right;
}
public RBNode<T> getParent() {
return parent;
}
public void setParent(RBNode<T> parent) {
this.parent = parent;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
}
三、红黑树的旋转操作
在实现红黑树的插入/删除操作时又可能需要对树进行旋转,所以首先应该实现的是红黑树的旋转操作,红黑树的旋转操作跟AVL树类似,但是在上面还定义了每个节点存放父亲节点的指针域,所以在这里要进行处理;
1、左旋
先回忆在AVL树中的左旋操作,对节点40进行左旋,只需改变2、3、5处的指向关系,但在红黑树中增加了parent指针,还要对1、4、6处进行setParent()操作,首先拿到旋转节点node的右孩子child
第一步是做child与node父亲节点之间的连接:
- 修改child的父亲节点,变成了node的父亲节点(1处);
- 在node.parent为null时,让this.root指向child,再修改node.parent.child,还需判断node是其父亲节点的左孩子还是右孩子(2处);
第二步是修改node与其新的右孩子也就是child.left之间的连接:
- 把child的左孩子拿去当做node的右孩子(3处);
- 在child.left不为null时,将其父亲节点改为node,是空则不用做处理(4处);
第三步就是连接node与child:
- 将child的左孩子改为node(5处);
- 将node的父亲修改为child(6处);
编码:
//左旋
public void leftRotate(RBNode<T> node) {
RBNode<T> child = node.getRight(); //拿到node的右孩子
child.setParent(node.getParent()); //把child的父亲改成node的父亲
//让node的父亲节点指向child
if (node.getParent() == null) {
this.root = child; //若node是根节点,则旋转之后新的根节点指向child
} else { //node不是根节点则要判断node是其父亲的左孩子还是右孩子
if (node == node.getParent().getLeft()) {
node.getParent().setLeft(child);
} else {
node.getParent().setRight(child);
}
}
node.setRight(child.getLeft()); //node的右孩子改为child的左孩子
//child左孩子不为null则将其父亲改为node
if (child.getLeft() != null) {
child.getLeft().setParent(node);
}
child.setLeft(node); //将child左孩子改成node
node.setParent(child); //将node的父亲改成child
}
2、右旋
右旋操作跟左旋的步骤是一样的,在AVL树旋转中只修改红色圈部分2、3、4处的指向关系,在这里还要再对1、5、6处节点的父亲节点进行重新指向,首先拿到node的左孩子child:
第一步:连接child和node的父节点;
第二步:连接child.right和node;
第三步:连接node与child;
编码:
//右旋
public void rightRotate(RBNode<T> node) {
RBNode<T> child = node.getLeft();
//连接child和node.parent
child.setParent(node.getParent());
if (node.getParent() == null) {
this.root = child;
} else {
if (node == node.getParent().getLeft()) {
node.getParent().setLeft(child);
} else {
node.getParent().setRight(child);
}
}
//连接node与child.right
node.setLeft(child.getRight());
if (child.getRight() != null) {
child.getRight().setParent(node);
}
//连接node与child
child.setRight(node);
node.setParent(child);
}
四、红黑树的插入操作
在插入节点时,新插入的节点的颜色应该是红色的,因为如果贸然插入一个黑色节点一定会破坏性质5,所以只能让新添加的节点是红色,但这样是有可能破坏了性质4,那这时候就需要我们对树进行调整,来重新满足它的五条性质;
(一)在编码实现之前先做具体的分析,红黑树的插入有这样三种情况
- 树为空,新插入的根节点应该是黑色;
- 树不为空,新插入的节点应该是红色,然后检查parent,parent是黑色则插入完成;
- parent是红色,需要做红黑树的插入调整:
红黑树的插入调整有三种情况,先看叔叔节点的颜色(下面的举例都是假设新插入的结点是其父结点的左孩子,为右孩子时与此相反)
3.1 叔叔节点是红色:
处理方式:先把parent和uncle都涂成黑色,再将祖父节点涂成红色;
3.2 叔叔节点是黑色,node,parent,parent.parent在一侧:
处理方式:先把父结点和祖父结点的颜色交换,,再以祖父结点为根节点右旋;
3.3 叔叔节点是黑色,node和parent,parent.parent不在一侧:
处理方式:先将node 旋转到一侧,再重复第二种情况的操作;
(二)编码
要注意的是,红黑树的调整中应该先改变结点颜色,再进行旋转;
//添加元素
public void insert(T data) {
if (this.root == null) {
this.root = new RBNode<>(data, Color.BLACK);
return;
}
RBNode<T> cur = this.root;
RBNode<T> pre = null;
while (cur != null) { //寻找插入结点的位置
if (cur.getData().compareTo(data) > 0) {
pre = cur;
cur = cur.getLeft();
} else if (cur.getData().compareTo(data) < 0){
pre = cur;
cur = cur.getRight();
} else {
break;
}
}
//pre就是插入结点的父节点,再将新结点node插入
RBNode<T> node = new RBNode<>(data, pre, Color.RED); //创建新节点,直接给node写入父结点parent
if (pre.getData().compareTo(data) > 0) {
pre.setLeft(node); //node < parent.data则node为parent的左孩子
} else {
pre.setRight(node); //反之,node是parent的右孩子
}
if (color(pre) == Color.RED) { //如果当前结点的父结点是红色,需要做插入调整
fixAfterInsert(node);
}
//没有进入if语句,说明当前结点的父结点是黑色,插入完成
}
//插入调整
private void fixAfterInsert(RBNode<T> node) {
/**
* 进入循环的条件是node的父结点是红色
* 说明node一定存在祖父结点
*/
while (color(node.getParent()) == Color.RED) {
if (node.getParent() == node.getParent().getParent().getLeft()) {
//若node的父结点是其祖父节点的左孩子,说明当前结点和父结点在祖父结点的左子树
RBNode<T> uncle = node.getParent().getParent().getRight(); //当前结点的叔叔结点就是其祖父结点的右孩子
if (color(uncle) == Color.RED) { //插入情况1:叔叔结点是红色
node.getParent().setColor(Color.BLACK); //修改当前结点父亲结点和叔叔结点的颜色为黑色
uncle.setColor(Color.BLACK);
node.getParent().getParent().setColor(Color.RED); //再将当前结点的祖父结点涂成红色
node = node.getParent().getParent(); //node指向其祖父结点,循环
} else { //说明叔叔结点是黑色
if (node == node.getParent().getRight()) { //插入情况3的前半部分:叔叔结点是黑色,并且当前结点和父结点、祖父结点不在一侧,以当前结点的父结点为根节点左旋
node = node.getParent(); //先改变node的指向
leftRotate(node); //以node为根节点左旋
}
//插入情况2和插入情况3后半部分:叔叔结点是黑色,当前结点和父结点、祖父结点在同一侧,这里需要交换父结点和祖父结点的颜色,再以当前结点为根节点右旋,插入操作完成
node.getParent().setColor(Color.BLACK);
node.getParent().getParent().setColor(Color.RED);
rightRotate(node.getParent().getParent()); //
break;
}
} else { //反之,当前结点和其父结点在祖父结点的右子树
RBNode<T> uncle = node.getParent().getParent().getLeft(); //当前结点的叔叔结点是其祖父结点的左孩子
if (color(uncle) == Color.RED) { //插入情况1
node.getParent().setColor(Color.BLACK);
uncle.setColor(Color.BLACK);
node.getParent().getParent().setColor(Color.RED);
node = node.getParent().getParent();
} else { //说明叔叔结点是黑色
if (node == node.getParent().getLeft()) { //插入情况3的前半部分
node = node.getParent(); //调整node,旋转之后仍然满足情况2
rightRotate(node);
}
//插入情况2和插入情况3后半部分
node.getParent().setColor(Color.BLACK);
node.getParent().getParent().setColor(Color.RED);
leftRotate(node.getParent().getParent());
break;
}
}
}
//如果在情况1中,node回溯到了根节点时变成了红色,所以此时就将根节点置为黑色,不用再特别加以判断
this.root.setColor(Color.BLACK);
}
五、红黑树的删除操作
在删除的是红色结点时,并没有破坏红黑树的性质,因此不用进行删除调整,但当删除的是黑色结点时,就需要进行一定的调整再次满足红黑树的性质:如果删除结点的孩子补上来是红色,就直接把孩子结点涂黑,如果补上来的孩子结点依然是黑色,那这时就要通过调整使其变成红色,再将其涂黑;这里的思路就是向其兄弟“借”一个红色结点,这就要求兄弟结点必须是黑色,并且兄弟节点有红色的孩子,事实上并不是真的拿过来一个结点,“借”的其实是结点的颜色;
- 删除的是红色节点,删除结束;
- 删除的结点是黑色时,要进行删除调整;
(一)红黑树的删除调整有四种情况(下面的举例都是假设删除的结点是其父结点的左孩子):
2.1 兄弟是黑色,兄弟的孩子也都是黑色:
处理方式:将兄弟结点涂成红色,让node指向父结点,由父结点再去找他的兄弟“借”红色的孩子;
2.2 兄弟是黑色,兄弟的左孩子是黑色,右孩子是红色:
处理方式:交换兄弟结点和父结点的颜色,再将兄弟结点的孩子涂成黑色,再以父结点为根结点左旋;
2.3 兄弟是黑色,兄弟的左孩子是红色,右孩子是黑色
处理方式:
1)交换兄弟结点和其孩子的颜色,再以兄弟节点为根节点右旋,这样就转换成了情况2;
2) 交换兄弟结点和父结点的颜色,再将兄弟结点的孩子涂成黑色,再以父结点为根结点左旋;
2.4 兄弟是红色
处理方式:交换父结点和兄弟结点的颜色,再以父结点为根节点进行左旋;
(二)编码
在删除结点时跟BST树的删除一样,如果待删除结点cur有两个孩子,记录cur结点为old,cur再去寻找前驱结点的值覆盖old,然后删前驱(old):
- 如果cur有一个孩子,就直接删除cur,根据cur的颜色,判断是否需要调整,需要的话就再根据补上来的孩子节点child进行调整;
- 如果cur结点没有孩子,先判断cur的颜色,需要进行插入调整的话,先进行调整再删除cur结点,不需要的话就直接删除cur;
还要注意的是,在上面删除调整的四种情况中,编码时处理的次序应该是情况4 ->情况1 ->情况3的前半段 ->情况2(也是情况3的后半段);
//删除元素
public void remove(T data) {
if (this.root == null) {
return;
}
RBNode<T> cur = this.root;
while (cur != null) { //寻找删除结点的位置
if (cur.getData().compareTo(data) > 0) {
cur = cur.getLeft();
} else if (cur.getData().compareTo(data) < 0) {
cur = cur.getRight();
} else {
break;
}
}
if (cur == null) { //说明data不存在
return;
}
if (cur.getLeft() != null && cur.getRight() != null) { //当前结点有两个孩子
RBNode<T> old = cur; //记录当前结点
cur = cur.getLeft(); //cur去寻找当前结点的前驱
while (cur.getRight() != null) {
cur = cur.getRight();
}
//用前驱的值覆盖old的值,再删掉前驱
old.setData(cur.getData());
}
//删掉cur指向的当前结点
RBNode<T> child = cur.getLeft();
if (child == null) {
child = cur.getRight(); //child指向cur唯一的孩子,如果child为null说明cur没有孩子结点
}
if (child != null) { //cur的有一个不为空的孩子,先删除child再进行调整
child.setParent(cur.getParent()); //修改child的父亲
//当父结点为null时根节点变为child
if (cur.getParent() == null) {
this.root = child;
} else { //父结点不为null则修改父结点的孩子域
if (cur == cur.getParent().getLeft()) { //cur是其父结点的左孩子
cur.getParent().setLeft(child); //将父结点的左孩子修改为child
} else {
cur.getParent().setRight(child);
}
}
if (color(cur) == Color.BLACK) { //删除的cur是黑色,破坏了性质5,需要做删除后的调整
fixAfterRemove(child);
}
} else { //child为null,先调整再进行删除
if (cur.getParent() == null) { //cur的父结点和子结点都是null,说明红黑树只有一个结点root,删除之后树为null,修改this.root
this.root = null;
} else { //cur的父结点不为null
if (color(cur) == Color.BLACK) { //删除的cur是黑色,破坏了性质5,需要做删除后的调整
fixAfterRemove(cur);
}
if (cur == cur.getParent().getLeft()) { //删除cur
cur.getParent().setLeft(null);
} else {
cur.getParent().setRight(null);
}
}
}
}
private void fixAfterRemove(RBNode<T> node) {
while (color(node) == Color.BLACK) {
if (node == node.getParent().getLeft()) { //左子树删除结点
RBNode<T> brother = node.getParent().getRight();
if (color(brother) == Color.RED) { //情况4:兄弟节点是红色
brother.setColor(Color.BLACK); //交换父结点和兄弟结点的颜色
node.getParent().setColor(Color.RED);
leftRotate(node.getParent()); //以父结点为根节点左旋
brother = node.getParent().getRight(); //更新兄弟节点
}
//情况1:兄弟结点的两个子节点都是黑色
if (color(brother.getLeft()) == Color.BLACK && color(brother.getRight()) == Color.BLACK) {
brother.setColor(Color.RED); //将兄弟结点涂成红色
node = node.getParent(); //改变node指针,指向父结点
} else {
//情况3的前半部分
if (color(brother.getLeft()) == Color.RED) {
brother.setColor(Color.RED); //交换兄弟节点和其左孩子的颜色
brother.getLeft().setColor(Color.BLACK);
rightRotate(brother);//义兄弟结点为根节点右旋
brother = node.getParent().getRight(); //更新兄弟结点
}
//统一处理情况2和情况3的后半部分
brother.setColor(color(node.getParent()));
node.getParent().setColor(Color.BLACK);
brother.getRight().setColor(Color.BLACK);
rightRotate(node.getParent());
break;
}
} else { //删除右子树的结点
RBNode<T> brother = node.getParent().getLeft();
if (color(brother) == Color.RED) { //情况4:兄弟节点是红色
brother.setColor(Color.BLACK);
node.getParent().setColor(Color.RED);
rightRotate(node.getParent());
brother = node.getParent().getLeft();
}
//情况1:兄弟结点的两个子节点都是黑色
if (color(brother.getLeft()) == Color.BLACK && color(brother.getRight()) == Color.BLACK) {
brother.setColor(Color.RED); //将兄弟结点涂成红色
node = node.getParent(); //改变node指针,指向父结点
} else {
//情况3的前半部分
if (color(brother.getRight()) == Color.RED) {
brother.setColor(Color.RED); //交换兄弟节点和其左孩子的颜色
brother.getRight().setColor(Color.BLACK);
leftRotate(brother);//义兄弟结点为根节点右旋
brother = node.getParent().getLeft(); //更新兄弟结点
}
//统一处理情况2和情况3的后半部分
brother.setColor(color(node.getParent()));
node.getParent().setColor(Color.BLACK);
brother.getLeft().setColor(Color.BLACK);
leftRotate(node.getParent());
break;
}
}
}
//node为红色就直接涂成黑色
node.setColor(Color.BLACK);
}