我另一篇文章讲了什么是二叉树,二叉树就是对于某一个节点来说,它的左子树要比它小,右子树的值要比它大,二叉树搜索树作为一种数据结构,其查找,插入还有删除的时间复杂度都是O(logn)底数是2,但是我们说的这个时间复杂度是指在二叉平衡数上面体现的,也就是说数据时随机的,则效率很好,但是如果顺序从小到大或者从大到小就会发生另一种情况,如图:
从大到小及时全部在左边,这就和链表没有任何区别了,这种情况下查找的时间复杂度是O(N),而不是O(logN),当然这是在最不平衡的条件下,实际情况下,,二叉搜索树的效率应该在O(N)和O(logN)之间,当然这取决于树的平衡程度,
那么为了能够以较快的时间O(logN)来搜索一棵树,我们需要保证树总是平衡的(或者大部分是平衡的),也就是说每个节点的左子树节点个数和右子树节点个数尽量相等。红-黑树的就是这样的一棵平衡树,对一个要插入的数据项(删除也是),插入例程要检查会不会破坏树的特征,如果破坏了,程序就会进行纠正,根据需要改变树的结构,从而保持树的平衡。
红黑树——介绍
红黑树是一个平衡的二叉树,但不是一个完美的平衡二叉树,虽然我们希望一个所有查找都能在~lgN次比较内结束,但是这样在动态插入中保持树的完美平衡代价太高了,所以我们稍微放松一下限制,希望找出一个能在对数时间内完成查找的数据结构,这个时候,红黑树就站出来了,
红黑树的特征:
- 节点都有颜色,在红黑树中,每个节点的颜色不是黑色就是红色,当然也可以是任意的两种颜色,这里的颜色用于标记,我们可以在节点类Node中增加一个boolean型变量isRed,以此来表示颜色的信息。
- 在插入和删除中,要遵守保持这些颜色的不同排列规则,就是在插入还有删除的时候不需要遵守红黑树的规则
红黑树的规则
- 节点是红色或者是黑色,在树里面的节点不是红色就是黑色,也可以是其他颜色,但是我这里用的是红色和黑色,
- 根节点是黑色,根节点总是黑色,不能为红色
- 每个叶节点(NIL或空节点)是黑色,这个可能有点难理解,但是下面的图形可以完美的表示这一点
这个图就是一个红黑树,NIL节点是空节点,并且是黑色的 - 每个红色节点的两个子节点是黑色,也就是说不能有连续两个相同的红节点
- 从任意点到其没有叶节点的路径都包含相同的黑色节点 如图:
上图可以看到,从根节点到每一个NIL的路径中,会发现都包含了相同的黑色节点。
这五条性质约束了红黑树,可以通过数学证明来证明,满足这五条性质的二叉树可以将查找删除维持在对数时间内,
当我们进行插入或者删除操作时所作的一切操作都是为了调整树使之符合红黑树的这五个性质
基本操作——变色,左旋,右旋
- 改变节点颜色
新插入的节点15,我们会发现违反规则四,因为15,的父节点25也是红色,如果将它15改变成黑色会发现违反规则五,因为每一条路径上的黑色节点不一样,这个时候的另一种方法就是将父节点与叔叔节点(也就是父节点的兄弟节点)变为黑色,这样的话就可以发现满足红黑树的五个规则,
2. 右旋
首先要说明的是节点本身是不会旋转的,旋转改变节点之间的关系,选择一个节点作为旋转的顶端,如果做一次右旋,顶端的节点会向下和向右移动到它右子节点的位置,它的左子节点会移动到它原来的位置,右旋的顶端节点要有左子节点
- 左旋
左旋的顶端必须要有左子节点
注意:我们改变颜色也是为了帮助我们判断何时执行什么旋转,而旋转是为了保持树的平衡,光改变节点颜色是不能起到任何作用的,旋转才是关键的作用,在新增节点或者删除节点之后,可能会破坏红黑树的平衡,那么什么时候执行旋转以及执行什么旋转,这是我们需要重点关注的。
左旋和右旋代码
- 节点类
节点类和二叉树的节点差不多,只不过是在其基础上增加了一个boolean类型的变量来表示节点的颜色
public class RBNode<T extends Comparable<T>> {
//颜色
boolean color;
//关键值
T key;
//左子节点
RBNode<T> left;
//右子节点
RBNode<T> right;
//父节点
RBNode<T> parent;
public RBNode(boolean color, T key, RBNode<T> left, RBNode<T> right, RBNode<T> parent) {
this.color = color;
this.key = key;
this.left = left;
this.right = right;
this.parent = parent;
}
// 获取节点的关键值
public T getKey() {
return key;
}
// 打印节点的关键值和颜色信息
public String toString() {
return "" + key + (this.color == REB ? "R":"B");
}
}
- 左旋代码实现
/*
* 左旋示意图:对节点x进行左旋
* p p
* / /
* x y
* / \ / \
* lx y -----> x ry
* / \ / \
* ly ry lx ly
* 左旋做了三件事
* 1.将y的左子节点赋值给x的右子节点,并将x赋值给y原来的左子节点的父节点(y左子节点非空时)
* 2.将x的父节点p(非空时)赋值给y的父节点,同时更新p的总结点为y(左或右)
* 3.将y的左子节点设置为x,将x的父节点设置为y
*
*/
public void leftRotate(RBNode<T> x) {
// 1.将y的左子节点赋值给x的右子节点,并将x赋值给y原来的左子节点的父节点(y左子节点非空时)
RBNode<T> y = x.right;//获取y节点
// 将y的左子节点设置为x的右子节点
x.left = y.right;
if (y.left != null) {
// 将x赋值给y原来左子节点的父节点
y.left.parent = x;
}
// 将x的父节点赋值给y的父节点,同时更新p的子节点为y(左或右)
y.parent = x.parent;
if (x.parent == null) {
this.root = y;//如果x的父节点为空(即x为根节点),则将y设为根节点
} else {
if (x == x.parent.left) {//如果x是左子节点
x.parent.left = y;//则也将y设置为左子节点
} else {
x.parent.right = y;//负责将其设置为右子节点
}
}
// 3.将y的左子节点设置为x ,将x的父节点设置为y
y.left = x;
x.parent = y;
}
- 右旋代码的实现
/*
* 右旋示意图:对节点y进行右旋
* p p
* / /
* y x
* / \ / \
* x ry -----> lx y
* / \ / \
* lx rx rx ry
*右旋做了三件事
* 1.将x的右子节点赋值给y的左子节点,并将y赋值给x原来的右子节点的父节点(x右子节点非空时)
* 2.将y的父节点p(非空时)赋值给x的父节点,同时更新p的子节点x(左或者右)
* 3.将x的右子节点设为y,将y的父节点设置为x
*
*/
public void rightRotate(RBNode<T> y) {
// 将x的右子节点赋值给y的左子节点,并将y赋值给x原来右子节点的父节点(x右子节点非空时)
RBNode<T> x = y.left;
y.left = x.right;//将x的右节点的值赋值给y的左节点
if (x.right != null) {
x.right.parent = y;
}
// 将y的父节点p(非空时)赋值给x的父节点,同时更新p的子节点x(左或者右)
x.parent = y.parent;//将y的父节点赋值给x的父节点
if (y.parent != null) {
this.root = x;//如果y的父节点为空(即x为很节点)
} else {
if (y == y.parent.left) {//如果y是它父节点的左子节点
y.parent.left = x;//则将x设置为y原来父节点的左子节点
} else {
y.parent.right = x;
}
//将x的右子节点设置为y 将y的父节点设置为x
x.right = y;
y.parent = x;
}
}
插入代码
和二叉树的插入操作一样,都先找到插入位置,然后将节点插入,然后看插入的前段代码
public void insert(RBNode<T> node) {
RBNode<T> current = null;//表示最后node的父节点
RBNode<T> x = this.right;//用来向下搜索
// 1.找到插入的位置
while (x != null) {
current = x;
// compareto就是比较两个数据的大小关系 大于0表示前一个数据比后一个数据大, 0表示相等,小于0表示第一个数据小于第二个数据
int cmp = node.key.compareTo(x.key);
if (cmp < 0) {//插入的数 比当前节点小
x = x.left;
} else {//插入的数 比当前节点大
x = x.right;
}
}
node.parent = current;//找到插入位置 将当前的current作为node的父节点
// 2.判断node是左子节点还是右子节点
if (current != null) {
int cmp = node.key.compareTo(current.key);
if (cmp < 0) {//当前插入的节点小于父节点
current.left = node;
} else {
current.right = node;
}
} else {
this.root = node;//都不满足要求设置为根节点
}
// 利用旋转操作 将其修正为一颗红黑树
insertFixUp(node);
}
与二叉搜索树中的实现的思路一样,主要是看最后一步insertFixUp(node)操作,因为插入后可能会导致树的不平衡,insertFixUp(node)方法里主要的分情况讨论,而且分析的是什么时候变色,什么时候左旋,什么时候右旋,我们先从理论上分析具体的情况,然后再看insertFixUp(node)的具体实现,
在插入之前我们先看一下各个节点的叫法
因为需要满足红黑树的五条性质,如果我们插入的是黑色节点则是违反了性质五,需要进行大规模的调整,如果我们插入的是红色的节点,那就只要在插入节点的父节点也是红色的时候违反性质四或者是当插入的节点是根节点时,违反性质二,所以,我们要把插入的节点变成红色,因为如果父节点是红色,需要改变,但是如果父节点是黑色,则不需要改变,这是一般的几率,如果插入的是黑色,则必须要改变,这就是为什么选择红色的原因。
插入节点可能遇到的几种情况:
- 当插入的节点是根节点的时候,直接涂黑
- 当要插入的节点的父节点是黑色的时候,
可以看出来,这个时候插入一个红色的节点并没有对这五个性质产生破坏,所以可以直接进行插入操作 - 如果要插入的节点的父节点是红色且父节点是祖父节点的左支的时候,
这种要分为两种情况,第一种是叔叔节点为黑的情况,一种是叔叔节点为红的情况
当叔叔节点为黑色的时候,也分为两种情况,一种是要插入的节点是父节点的左支,一种是要插入的节点是父节点的右支,先看一下当要插入的节点是父节点的左支的情况
这个时候违反了规则4,我们要进行调整,使之符合要求,我们可以通过对祖父节点进行右旋同时将祖父节点和父节点的颜色进行互换,就变成了
当插入的节点是父节点的右支的时候:
当要插入的节点是父节点的右支的时候,我们可以先对父节点进行左旋,变成如下:
如果我们把原先的父节点看做是新的插入点,原先的插入的节点看做新的父节点,那就变成了当要插入的节点在父节点的左支的情况,就是按照当要插入的节点在父节点的左支进行旋转,旋转之后的结果如下:
4. 如果插入节点的父节点是红色,且父节点是祖父节点的右支的时候:
这个时候的情况和情况三是一个镜像,将情况三的左和右互换就可以了
5. 如果要插入的节点的父节点是红色,而且叔叔节点也为红色的时候
这个时候只需要将父节点个叔叔节点涂黑,将祖父节点涂红就可以了,
上面就是插入的全部操作,下面就是代码
public void insertFixUp(RBNode<T> node) {
RBNode<T> parent, gparent;//定义父节点和祖父节点
// 需要修正的条件是:父节点存在 且父节点是红色
while (((parent = parentOf(node)) != null) && isRed(parent)) {
gparent = parentOf(parent);//获得祖父节点
// 若父节点在祖父节点的左子节点,
if (parent == gparent.left) {
RBNode<T> uncle = gparent.right;//获得叔叔节点
// case 1 叔叔节点也是红色
if (uncle != null && isRed(uncle)) {
setBlack(parent);//把父节点和叔叔节点涂成红色
setBlack(gparent);
setRed(gparent);//把祖父节点涂成红色
node = gparent;//把位置放到祖父节点处
continue;//继续while循环,重新判断
}
// case 2 叔叔节点是黑色 且当前节点是右子节点
if (node == parent.right) {
leftRotate(parent);//从父节点处左旋
RBNode<T> tmp = parent;//然后将自己和父节点调换位置 和图中说的 为右旋做准备
parent = node;
node = tmp;
}
// case 3 叔叔节点是黑色 且当前是左子节点
setBlack(parent);//将父节点图成黑色
setRed(gparent);//将祖父节点涂成红色
rightRotate(gparent);//右旋
} else {// 若父节点在祖父节点的右子节点,与上面的情况相反 但是本质是一样
RBNode<T> uncle = gparent.left;//获得叔叔节点
//case 1 叔叔节点也是红色
if (uncle != null && isRed(uncle)) {
setBlack(parent);//将父节点涂成黑色
setBlack(uncle);//将叔叔节点涂成黑色
setRed(gparent);//将祖父节点涂成红色
node = gparent;//将位置放到祖父节点 继续循环判断
continue;
}
// case 2 叔叔节点是黑色的 且当前是左子节点
if (node == parent.left) {
rightRotate(parent);//从父节点开始右旋
RBNode<T> tmp = parent;//然后就和开始一样 将父节点看成当前节点,将当前节点看成父节点 为左旋做准备
parent = node;
node = tmp;
}
// case 3 叔叔节点是黑色的,且当前节点是右子节点
setBlack(parent);//将父节点涂成黑色
setRed(gparent);//将祖父节点图成红色
leftRotate(gparent);//左旋
}
}
setBlack(root);//将根节点设置为黑色
}
红黑树的删除
- 当删除的元素是红色时候,对五条性质没有什么影响,直接删除
- 当删除的是根节点的时候,直接删除
- 当删除的元素是黑,且有一个红节点的时候,将红节点涂黑放到被删的元素的位置
变成
4. 当被删的元素是黑色,且兄弟节点是黑色,兄弟节点的两个孩子也是黑色的时候,父节点是红色,此时交换兄弟节点和父节点的。NIL元素是指每个叶节点都有两个空的,颜色为黑NIL元素,需要它的时候可以看成两个黑色的元素,不需要的时候可以忽视他
如图
变成
5. 当删除元素是黑色,并且为父节点的左支,且兄弟颜色是黑色,兄弟节点的右支为红色,这个时候需要交换兄弟节点与父节点的颜色,并把父节点的颜色变黑,兄弟节点的右支涂黑,并以父节点为中心左转
如图
变成
6. .当被删除元素为黑、并且为父节点的左支,且兄弟颜色为黑,兄弟的左支为红色,这个时候需要先把兄弟与兄弟的左子节点颜色互换,进行右转,然后就变成了规则5一样了,在按照规则5进行旋转。如图:
由
先兄弟与兄弟的左子节点颜色互换,进行右转,变成:
然后在按照规则5进行旋转,变成:
- 当被删除元素为黑且为父元素的右支时,跟情况5.情况6 互为镜像。
- 被删除元素为黑且兄弟节点为黑,兄弟节点的孩子为黑,父亲为黑,这个时候需要将兄弟节点变为红,再把父亲看做那个被删除的元素(只是看做,实际上不删除),看看父亲符和哪一条删除规则,进行处理变化如图:
由:
变成:
- 当被删除的元素为黑,且为父元素的左支,兄弟节点为红色的时候,需要交换兄弟节点与父亲结点的颜色,以父亲结点进行左旋,就变成了情况4,在按照情况四进行操作即可,变化如下:
由:
交换兄弟节点与父亲结点的颜色,以父亲结点进行左旋 变成:
在按照情况四进行操作,变成:
删除的步骤也讲完,没有讲到的一点就是,在添加删除的时候,时刻要记得更改根元素的颜色为黑。