想具体实现以下红黑树算法已经有很长一段时间了,但是一直没有抽出比较完整的时间来系统整理和实现以下红黑树算法,下面一起来看看红黑树的具体实现吧,想想也很简单。
1. 首先介绍以下红黑树的性质:
红黑树是一种满足红黑性质的搜索二叉树:
- 红黑树的节点是红色或者黑色
- 根节点是黑色的
- 每个叶子节点是黑色的(这里的叶子节点是指空节点)
- 每个红色节点的孩子节点都是黑色的
- 每个节点到其后代节点的简单路径上,都包含相同数目的黑色节点
2. 左旋和右旋
红黑树维持的性质主要通过两个旋转操作(左旋和右旋)、变色实现,变色很容易理解,就是字面意思将节点颜色从一种颜色修改为另外一种,关键是变色的时机和如何变色,比较复杂,这个问题会在后面的实现步骤讲解中涉及,这里主要针对左旋和右旋进行讲解。
因为后面设计代码具体实现,这里首先给出java红黑树节点类的实现。
- public class RBNode<T extends Comparable<T>> {
- boolean color;//节点颜色
- T key; //关键字(键值)
- RBNode<T> left;//坐孩子节点
- RBNode<T> right;//右孩子节点
- RBNode<T> parent;//父节点
- //节点构造函数
- public RBNode(T key, boolean color, RBNode<T> parent, RBNode<T> left, RBNode<T> right) {
- this.key = key;
- this.color = color;
- this.parent = parent;
- this.left = left;
- this.right = right;
- }
- public T getKey(){
- return key;
- }
- public String toString(){
- return “”+key+“:”+(this.color == true? “R”:“B”);
- }
- }
public class RBNode<T extends Comparable<T>> {
boolean color;//节点颜色
T key; //关键字(键值)
RBNode<T> left;//坐孩子节点
RBNode<T> right;//右孩子节点
RBNode<T> parent;//父节点
//节点构造函数
public RBNode(T key, boolean color, RBNode<T> parent, RBNode<T> left, RBNode<T> right) {
this.key = key;
this.color = color;
this.parent = parent;
this.left = left;
this.right = right;
}
public T getKey(){
return key;
}
public String toString(){
return ""+key+":"+(this.color == true? "R":"B");
}
}
左旋
左旋的意思就是以一个节点为轴,向左旋转,将右节点的左子树作为该节点的右字树连接到该节点上,该节点的右节点作为该节点的父节点替换轴节点原来的位置,如下图所示:
左旋代码具体实现:
- /**
- * 节点左旋
- * @param x 旋转时作为轴的节点
- *
- * X Y
- * / \ / \
- * A Y -> X RB
- * / \ / \ / \
- * LA RA LB RB A LB
- * / \
- * LA RA
- *
- * 节点左旋的主要工作:
- * 1.x节点的左孩子变为y节点的右孩子,y左孩子的父节点变为了x
- * 2.x节点变为y节点的左孩子,y变为x父节点的子节点
- * 3.x变为了y的子孩子,x的父节点变为y
- * 每个节点的父节点,左右孩子节点都要注意维护
- */
- private void leftRotate(RBNode<T> x){
- //将x节点的右孩子修改为y节点的左孩子
- RBNode<T> y = x.right;
- x.right = y.left;
- //如果y的左孩子不是null,修改左孩子的父节点,左孩子其他节点不需要变化
- if(y.left != null)
- y.left.parent = x;
- //y节点的父节点修改为x的父节点,左孩子变为了x,右孩子不需要修改
- y.parent = x.parent;
- if(x.parent == null)
- //如果x的父节点为null,则x原来为根节点,现在根节点修改为y
- this.root = y;
- else {
- //判断x是父节点的左孩子还是右孩子,为父节点的子节点赋值
- if(x == x.parent.left)
- x.parent.left = y;
- else
- x.parent.right = y;
- }
- y.left = x;
- //x的父节点修改为y
- x.parent = y;
- }
/**
* 节点左旋
* @param x 旋转时作为轴的节点
*
* X Y
* / \ / \
* A Y -> X RB
* / \ / \ / \
* LA RA LB RB A LB
* / \
* LA RA
*
* 节点左旋的主要工作:
* 1.x节点的左孩子变为y节点的右孩子,y左孩子的父节点变为了x
* 2.x节点变为y节点的左孩子,y变为x父节点的子节点
* 3.x变为了y的子孩子,x的父节点变为y
* 每个节点的父节点,左右孩子节点都要注意维护
*/
private void leftRotate(RBNode<T> x){
//将x节点的右孩子修改为y节点的左孩子
RBNode<T> y = x.right;
x.right = y.left;
//如果y的左孩子不是null,修改左孩子的父节点,左孩子其他节点不需要变化
if(y.left != null)
y.left.parent = x;
//y节点的父节点修改为x的父节点,左孩子变为了x,右孩子不需要修改
y.parent = x.parent;
if(x.parent == null)
//如果x的父节点为null,则x原来为根节点,现在根节点修改为y
this.root = y;
else {
//判断x是父节点的左孩子还是右孩子,为父节点的子节点赋值
if(x == x.parent.left)
x.parent.left = y;
else
x.parent.right = y;
}
y.left = x;
//x的父节点修改为y
x.parent = y;
}
右旋
右旋的机制和左旋相同,只改变了旋转的方向,如下图所示。
节点右旋实现:
左旋右旋很好理解,关键是节点位置变化后,需要细心维护节点的父亲和孩子节点的变化。
- /**
- * 节点右旋
- * @param x 旋转时作为轴的节点
- *
- * | |
- * X Y
- * / \ / \
- * Y B -> LA X
- * / \ / \ / \
- * LA RA LB RB RA B
- * / \
- * LB RB
- *
- * 节点左旋的主要工作:
- * 1.y节点的右孩子变为x节点的左孩子,y右孩子的父节点变为了x
- * 2.x节点变为y节点的右孩子,x变为y父节点的子节点
- * 3.x变为了y的子孩子,x的父节点变为y
- * 每个节点的父节点,左右孩子节点都要注意维护
- */
- private void rightRotate(RBNode<T> x){
- //将x节点的左孩子修改为y节点的右孩子
- RBNode<T> y = x.left;
- x.left = y.right;
- //如果y的右孩子不是null,修改右孩子的父节点,右孩子其他节点不需要变化
- if(y.right != null)
- y.right.parent = x;
- //y节点的父节点修改为x的父节点,右孩子变为了x,左孩子不需要修改
- y.parent = x.parent;
- if(x.parent == null)
- //如果x的父节点为null,则x原来为根节点,现在根节点修改为y
- this.root = y;
- else {
- //判断x是父节点的左孩子还是右孩子,为父节点的子节点赋值
- if(x == x.parent.left)
- x.parent.left = y;
- else
- x.parent.right = y;
- }
- y.right = x;
- //x的父节点修改为y
- x.parent = y;
- }
/**
* 节点右旋
* @param x 旋转时作为轴的节点
*
* | |
* X Y
* / \ / \
* Y B -> LA X
* / \ / \ / \
* LA RA LB RB RA B
* / \
* LB RB
*
* 节点左旋的主要工作:
* 1.y节点的右孩子变为x节点的左孩子,y右孩子的父节点变为了x
* 2.x节点变为y节点的右孩子,x变为y父节点的子节点
* 3.x变为了y的子孩子,x的父节点变为y
* 每个节点的父节点,左右孩子节点都要注意维护
*/
private void rightRotate(RBNode<T> x){
//将x节点的左孩子修改为y节点的右孩子
RBNode<T> y = x.left;
x.left = y.right;
//如果y的右孩子不是null,修改右孩子的父节点,右孩子其他节点不需要变化
if(y.right != null)
y.right.parent = x;
//y节点的父节点修改为x的父节点,右孩子变为了x,左孩子不需要修改
y.parent = x.parent;
if(x.parent == null)
//如果x的父节点为null,则x原来为根节点,现在根节点修改为y
this.root = y;
else {
//判断x是父节点的左孩子还是右孩子,为父节点的子节点赋值
if(x == x.parent.left)
x.parent.left = y;
else
x.parent.right = y;
}
y.right = x;
//x的父节点修改为y
x.parent = y;
}
3. 红黑树的插入操作
红黑树的插入操作主要分为以下几个步骤:
- 找到插入节点的位置(这里位置的寻找和二叉搜索树的方式相同)
- 插入成功后对红黑树的性质进行维护调整
- public void insert(T key){
- RBNode<T> node = new RBNode<>(key,RED,null,null,null);
- if(node != null){
- insertNode(node);
- }
- }
- private void insertNode(RBNode<T> node){
- //首先需要找到插入节点的位置,这里和二叉搜索树的过程是一样的
- //从根节点开始遍历
- RBNode<T> cur = this.root;
- RBNode<T> p = null;
- while (cur != null){
- //记录最后插入位置的父节点
- p = cur;
- int cmp = node.getKey().compareTo(cur.key);
- if(cmp >0){
- cur = cur.right;
- }else {
- cur = cur.left;
- }
- }
- //修改node的父节点
- node.parent = p;
- //判断node是插在左节点还是右节点
- if(p != null){
- int cmp = node.getKey().compareTo(p.key);
- if(cmp < 0){
- p.left = node;
- }else {
- p.right = node;
- }
- }else {
- this.root = node;
- }
- //插入之后对红黑树进行调整
- insertFixUp(node);
- }
public void insert(T key){
RBNode<T> node = new RBNode<>(key,RED,null,null,null);
if(node != null){
insertNode(node);
}
}
private void insertNode(RBNode<T> node){
//首先需要找到插入节点的位置,这里和二叉搜索树的过程是一样的
//从根节点开始遍历
RBNode<T> cur = this.root;
RBNode<T> p = null;
while (cur != null){
//记录最后插入位置的父节点
p = cur;
int cmp = node.getKey().compareTo(cur.key);
if(cmp >0){
cur = cur.right;
}else {
cur = cur.left;
}
}
//修改node的父节点
node.parent = p;
//判断node是插在左节点还是右节点
if(p != null){
int cmp = node.getKey().compareTo(p.key);
if(cmp < 0){
p.left = node;
}else {
p.right = node;
}
}else {
this.root = node;
}
//插入之后对红黑树进行调整
insertFixUp(node);
}
以上代码比较容易理解,最后主要是讲解insertFixUp(node)函数对红黑树的性质进行维护,通过以上代码我们可以发现新插入节点是红色节点,那么来分析一下什么情况会破坏红黑树的性质,首先如果插入节点的父节点是黑色的,那么很理想,不需要对红黑树进行调整,如果父节点为红色,那么很遗憾与红黑树的性质发生了冲突,所以首要前提是插入节点父节点为红色节点。
在满足父节点为红色的情况下,分为以下三种情况需要对红黑树进行不同的变色和旋转调整(以下情况的前提是插入节点的父节点为左孩子):
- 插入节点(z)的叔叔节点(y)是红色的:这种情况只要把z.parent和y修改为黑色,这样解决了z和z.parent都是红色的问题,同时把z.p.p变为红色,这样保证了树的黑色高度不变,同时将z修改为z.p.p来继续检查。这样操作后,z可能满足1,2,3中的任意一种情况,或者直接z.p为黑色节点直接退出。
- 插入节点(z)的叔叔节点(y)是黑色(或者叔叔节点不存在)并且z是一个右孩子:此时以z.parent为轴左旋,并且z和parent互换,此时的z和z.parent还是都为红色,这是z的特征正好满足3的情况。
- 插入节点(z)的叔叔节点(y)是黑色(或者叔叔节点不存在)并且z是一个左孩子:这时把z.parent修改为黑色,z.p.p修改为红色,再以z.p.p为轴右旋,因为y为黑色,所以并不会产生颜色冲突,同时替换z.p.p的和z.p同为黑色,也不会影响高度(因为z.p开始时是红色才需要调整,所以z.p.p一定是黑色)。
</div>***重点内容***