目录
本文是我跟着视频大佬一起写的Java红黑树代码,如果想要学习红黑树还是需要自己去动手敲一下,(本文实现了红黑树的查找和插入操作)
1.特性
在敲代码之前我们应该首先了解一下红黑树的几个特性。
- 根节点是黑色。
- 每个节点或者是黑色,或者是红色。
- 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑色节点。
我们看到这些红黑树的特性,可能觉得头大(最初我也是这么觉得),但是只要一条一条分析,其实就完全可以理解,现在有一个红黑树的图,可以稍微了解一下。
红黑树从叶子节点(为Null)从下往根节点数,路过的黑色节点数一定相同。
在进行学习红黑树的前面我们要先导入一个思想,红黑树的插入和删除操作会打乱红黑树的结构,所以为了继续遵守这些特性,我们需要一些操作,来使其一直保持这些特性。
由于查找的操作很简单,它的中序遍历和二分查找的时间复杂度相同,所以我就不过多的解释,直接把方法说出了, 就是从根节点开始判断,要查找的A值和root的大小比较,A值大就与root.right比较,反之和root.left比较,重复操作就可以得到A(部分代码如下)。
private void inOrderPrint(RBNode node){
if (node != null){
inOrderPrint(node.left);
System.out.println("key:"+node.key+",value:"+ node.value);
inOrderPrint(node.right);
}
}
接下来就是比较重要的操作:插入。
2.恢复二叉树特性方法:
由于插入可能会导致红黑树的形状改变,所以我们应该学一下恢复红黑树特性的三个方法。
1.变色
可以将红黑树进行变色处理,黑变红,红变黑。
2.左旋
左旋大致可以分为三个步骤,我可以看一下下面这个图:
根据图所示,看得出来,x和y节点交换了位置,并且y节点的左子节点变为了x的右子节点,根据以上的变换我们可以分为三个步骤进行操作。
- 将y节点的左子节点变为x的右子节点:我们首先将x的右子节点指向y的左子节点(首先这个步骤可以防止y的左子节点为空),然后将y的左子节点的父节点指向x。
- 更新x的父节点的位置,首先判断x有没有父节点,有的话就可以将y的父节点变为x的父节点,将x的父节点变为y。如果x就是root,那么就要将y变为root。
- 最后一步,将x的父节点更新为y,将y的左子节点更新为x。
根据上面的步骤,我们可以得到代码:
private void leftRotate(RBNode x){
RBNode y=x.right;
//将x的右子节点指定为x的左子节点,将y的左子节点的父节点指定为x,
x.right=y.left;
if (y.left!=null) {
y.left.parent=x;
}
//将y的父节点更新为x的父节点,将x的父节点指定为y
if(x.parent!=null){
x.parent=y.parent;
if (x==x.parent.left) {
x.parent.left=y;
}else{
x.parent.right=y;
}
}else{//y现在为根节点
this.root=y;
this.root.parent=null;
}
//将x的父节点更新为Y,将y的左子节点更新为x
x.parent=y;
y.left=x;
}
3.右旋:
与左旋类似,先看图:
根据图所示,看得出来,x和y节点交换了位置,并且x节点的右子节点变为了y的左子节点,根据以上的变换我们可以分为三个步骤进行操作。
- 将x节点的右子节点变为y的左子节点:我们首先将y的左子节点指向x的右子节点(首先这个步骤可以防止x的右子节点为空),然后将x的右子节点的父节点指向y。
- 更新y的父节点的位置,首先判断y有没有父节点,有的话就可以将x的父节点变为y的父节点,将y的父节点变为x。如果y就是root,那么就要将x变为root。
- 最后一步,将y的父节点更新为x,将x的右子节点更新为y。
根据上面的步骤,我们可以得到代码:
private void rightRotate(RBNode y){
RBNode x=y.left;
//将y的左子节点指向x的右子节点,更新x的右子节点的父节点为y
y.left=x.right;
if (x.right!=null) {
x.right.parent=y;
}
//当y的父节点不为空时,更新x的父节点为y的父节点,更新y的父节点指定子节点为x
if(y.parent!=null){
x.parent=y.parent;
if(y==y.parent.left){
y.parent.left=x;
}else{
y.parent.right=x;
}
}else{
this.root=x;
this.root.parent=null;
}
//
y.parent=x;
x.right=y;
}
至此我们已经学会了对二叉树的一些基本操作,现在我们就可以开始学习下一个部分。
3.插入
插入也会有多种情况,主要是判断插入节点的父节点的颜色(插入节点为红色,因为插入的红色节点一定没有子节点,而把它作为其他节点的子节点时,可以将它变为黑色节点以满足上述特性),如果要插入位置的父节点为红色,这样就违反了第四条原则,(也就是两个红色的节点不能相连)。于是根据不同的类型,分为了四大类。
1.红黑树为空树
处理方法:将root着色为黑色
2.插入节点的key已经存在
处理方法:不用处理
3.插入节点的父节点为黑色
处理方法:不用处理
4.插入节点为红色
4.1,判断插入节点的叔叔节点存在,并且是为红色(由于它的父节点为红色,那么我们可以根据第一条特性得到,他一定有爷爷节点,就是它的父节点一定有父节点,而且为黑色)
处理方法:将插入节点的父节点和它的叔叔节点(他们都为红色)着色为黑色,将爷爷节点染色为红色,然后再以爷爷节点为当前节点,继续递归这个操作。
4.2,判断叔叔节点不存在,或者叔叔节点为黑色,如果插入节点的父节点为爷爷节点的左子树(left)
4.2.1,插入节点为父节点的左子节点(LL双红类型)
处理方法:将插入节点的父节点着色为黑色,将爷爷节点染色为红色,然后将爷爷节点进行右旋操作。
4.2.2,插入节点为父节点的右子节点(LR双红类型)
处理类型:将父节点左旋,(然后变为和LL双红类型一样),将插入节点的父节点着色为黑色,将爷爷节点染色为红色,然后将爷爷节点进行右旋操作。
4.3,判断叔叔节点不存在,或者叔叔节点为黑色,如果插入节点的父节点为爷爷节点的右子树(right)
4.3.1,插入节点为父节点的右子节点(RR双红类型)
处理方法:将插入节点的父节点着色为黑色,将爷爷节点染为红色,然后将爷爷节点进行左旋操作。
4.3.2,插入节点为父节点的左子节点(RL双红类型)
处理方法:将父节点右旋,(然后变为和RR双红类型一样),将插入节点的父节点着色为黑色,将爷爷节点染为红色,然后将爷爷节点进行左旋操作。
最终就完成了插入操作的全部,是不是挺简单的,那我们就来写一下源码。
4.源码
首先我们先写一个RBTree类,然后在其中创建一个静态的内部类,存放着父节点,根节点,左节点,右节点,颜色和key,value。生成一个他们的get,set方法,改造一下无参方法(这些方法就不粘贴在下面了)。
static class RBNode<k extends Comparable<k>,v>{
private RBNode parent;
private RBNode left;
private RBNode right;
private boolean color;
private k key;
private v value;
}
然后,我们定义一下颜色,RED和BLACK。
private static final boolean RED=true;
private static final boolean BLACK=false;
然后把上面的参数写一些方法,例如,根的引用,获取当前节点的父节点,判断当前节点是否为红色,黑色等。
/**
* 树根的引用
* */
private RBNode root;
/**
* 获取当前节点的父节点
* @param node
* */
private RBNode parentOf(RBNode node){
if(node!=null){
return node.parent;
}
return null;
}
/**
*节点是否为红色
* @param node
* */
private boolean isRed(RBNode node){
if(node!=null){
return node.color==RED;
}
return false;
}
/**
* 设置节点为红色
* @param node
* */
private void setRed(RBNode node){
if(node != null){
node.color=RED;
}
}
/**
* 设置节点为黑色
* @param node
* */
private void setBlack(RBNode node){
if(node != null){
node.color=BLACK;
}
}
/**
*节点是否为黑色
* @param node
* */
private boolean isBlack(RBNode node){
if(node!=null){
return node.color==BLACK;
}
return false;
}
将中序遍历方法写上,(上面写过了)。
将左旋右旋方法写上。
最后写的就是插入方法,(我将完整代码放在下面,插入方法就在里面,有详细的注释)。
完整代码:
package RBTree;
import javax.print.DocFlavor;
import javax.xml.soap.Node;
public class RBTree<k extends Comparable<k>,v> {
private static final boolean RED=true;
private static final boolean BLACK=false;
/**
* 树根的引用
* */
private RBNode root;
/**
* 获取当前节点的父节点
* @param node
* */
private RBNode parentOf(RBNode node){
if(node!=null){
return node.parent;
}
return null;
}
/**
*节点是否为红色
* @param node
* */
private boolean isRed(RBNode node){
if(node!=null){
return node.color==RED;
}
return false;
}
/**
* 设置节点为红色
* @param node
* */
private void setRed(RBNode node){
if(node != null){
node.color=RED;
}
}
/**
* 设置节点为黑色
* @param node
* */
private void setBlack(RBNode node){
if(node != null){
node.color=BLACK;
}
}
/**
*节点是否为黑色
* @param node
* */
private boolean isBlack(RBNode node){
if(node!=null){
return node.color==BLACK;
}
return false;
}
/**
* 中序打印的方法
* */
public void inOrderPrint(){
inOrderPrint(this.root);
}
private void inOrderPrint(RBNode node){
if (node != null){
inOrderPrint(node.left);
System.out.println("key:"+node.key+",value:"+ node.value);
inOrderPrint(node.right);
}
}
/**
* 左旋
* * 左旋示意图:左旋X节点
* P p
* | |
* x y
* / \ --> / \
* lx y x ry
* / \ / \
* ly ry lx ly
*
*
*
* 1.将y的左子节点变为x的右子节点:将x的右子节点指定为x的左子节点,将y的左子节点的父节点指定为x,
* 2.更新X的父节点的位置:将y的父节点更新为x的父节点,将x的父节点指定为y
* 3.将x的父节点更新为Y,将y的左子节点更新为x
* */
private void leftRotate(RBNode x){
RBNode y=x.right;
//将x的右子节点指定为x的左子节点,将y的左子节点的父节点指定为x,
x.right=y.left;
if (y.left!=null) {
y.left.parent=x;
}
//将y的父节点更新为x的父节点,将x的父节点指定为y
if(x.parent!=null){
x.parent=y.parent;
if (x==x.parent.left) {
x.parent.left=y;
}else{
x.parent.right=y;
}
}else{//y现在为根节点
this.root=y;
this.root.parent=null;
}
//将x的父节点更新为Y,将y的左子节点更新为x
x.parent=y;
y.left=x;
}
/**
* 右旋
* 右旋示意图:右旋X节点
* P P
* | |
* y x
* / \ --> / \
* x ry lx y
* / \ / \
* lx ly ly ry
*
*
*
* ————————————————
* 1.将y的左子节点指向x的右子节点,更新x的右子节点的父节点为y
* 2.当y的父节点不为空时,更新x的父节点为y的父节点,更新y的父节点指定子节点为x
* 3.更新x的父节点,更新y的父节点为x,x的右子节点为y
* */
private void rightRotate(RBNode y){
RBNode x=y.left;
//将y的左子节点指向x的右子节点,更新x的右子节点的父节点为y
y.left=x.right;
if (x.right!=null) {
x.right.parent=y;
}
//当y的父节点不为空时,更新x的父节点为y的父节点,更新y的父节点指定子节点为x
if(y.parent!=null){
x.parent=y.parent;
if(y==y.parent.left){
y.parent.left=x;
}else{
y.parent.right=x;
}
}else{
this.root=x;
this.root.parent=null;
}
//
y.parent=x;
x.right=y;
}
/**
* 公开的插入
*
* */
public void insert(k key,v value){
RBNode node=new RBNode();
node.setKey(key);
node.setValue(value);
node.setColor(RED);
insert(node);
}
private void insert(RBNode node ){
//从根部查找
RBNode parent=null;
RBNode x=this.root;
while(x!=null){
parent=x;
int cmp=node.key.compareTo(x.key);
if(cmp>0){
x=x.right;
} else if (cmp==0) {
x.setValue(node.getValue());
return;
}else{
x=x.left;
}
}
node.parent=parent;
if(parent!=null){
int cmp=node.key.compareTo(parent.key);
if(cmp>0){
parent.right=node;
}else{
parent.left=node;
}
}else{
this.root=node;
}
//调用修复颜色
insertFixUp(node);
}
/**
* 修复红黑树的方法
* 1.红黑树为空树,将root节点染为黑色
* 2.插入节点的key已经存在,不处理
* 3.插入节点父节点为黑色,不用处理
* 4.插入节点为红色:
* 4.1叔叔节点存在并且为红色:(将爸爸和叔叔染为黑色,将爷爷染为红色)(111染色处理),以爷爷节点为当前节点,向上处理。
* 4.2叔叔节点不存在,或者为黑色,父亲节点为爷爷节点的左子树(lift)
* 4.2.1:插入节点为父节点的左子节点(LL双红):先111染色处理,将爷爷右旋
* 4.2.2:插入节点为父节点的右子节点(LR双红):将父节点左旋,先111染色处理,将爷爷右旋
* 4.3叔叔节点不存在,或者为黑色,父亲节点为爷爷节点的右子树(right)
* 4.3.1:插入节点为父节点的右子节点(RR双红):先111染色处理,将爷爷左旋
* 4.3.2:插入节点为父节点的左子节点(RL双红):将父节点右旋,先111染色处理,将爷爷左旋
* */
private void insertFixUp(RBNode node){
this.root.setColor(BLACK);
RBNode parent=parentOf(node);
RBNode gparent=parentOf(parent);
if(parent!=null && isRed(parent)){
RBNode uncle=null;
if(parent==gparent.left){
uncle=gparent.right;
//4.1
if(uncle!=null && isRed(uncle)){
setBlack(parent);
setBlack(uncle);
setRed(gparent);
//递归
insertFixUp(gparent);
return;
}
//4.2叔叔节点不存在,或者为黑色
if (uncle==null || isBlack(uncle)){
//4.2.1
if(node==parent.left){
setBlack(parent);
setRed(gparent);
rightRotate(gparent);
return;
}
//4.2.2
if(node==parent.right){
leftRotate(parent);
insertFixUp(parent);
return;
}
}
}else{
uncle=gparent.left;
if(uncle!=null && isRed(uncle)){
setBlack(parent);
setBlack(uncle);
setRed(gparent);
//递归
insertFixUp(gparent);
return;
}
//4.3
if(uncle==null || isBlack(uncle)){
//4.3.1
if(node==parent.right){
setBlack(parent);
setRed(gparent);
leftRotate(gparent);
return;
}//4.3.2
if (node==parent.left){
rightRotate(parent);
insertFixUp(parent);
return;
}
}
}
}
}
public RBNode getRoot() {
return root;
}
//静态内部类
static class RBNode<k extends Comparable<k>,v>{
private RBNode parent;
private RBNode left;
private RBNode right;
private boolean color;
private k key;
private v value;
public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, k key, v value) {
this.parent = parent;
this.left = left;
this.right = right;
this.color = color;
this.key = key;
this.value = value;
}
public RBNode(){
}
public RBNode getParent() {
return parent;
}
public void setParent(RBNode parent) {
this.parent = parent;
}
public RBNode getLeft() {
return left;
}
public void setLeft(RBNode left) {
this.left = left;
}
public RBNode getRight() {
return right;
}
public void setRight(RBNode right) {
this.right = right;
}
public boolean isColor() {
return color;
}
public void setColor(boolean color) {
this.color = color;
}
public k getKey() {
return key;
}
public void setKey(k key) {
this.key = key;
}
public v getValue() {
return value;
}
public void setValue(v value) {
this.value = value;
}
}
}
然后,我在网上复制粘贴了一个打印方法,建一个TReeOperation类:
package RBTree;
//打印红黑树树形结构
public class TreeOperation {
/*
树的结构示例:
1
/ \
2 3
/ \ / \
4 5 6 7
*/
// 用于获得树的层数
public static int getTreeDepth(RBTree.RBNode root) {
return root == null ? 0 : (1 + Math.max(getTreeDepth(root.getLeft()), getTreeDepth(root.getRight())));
}
private static void writeArray(RBTree.RBNode currNode, int rowIndex, int columnIndex, String[][] res, int treeDepth) {
// 保证输入的树不为空
if (currNode == null) return;
// 先将当前节点保存到二维数组中
res[rowIndex][columnIndex] = String.valueOf(currNode.getKey() + "-" + (currNode.isColor() ? "R" : "B") + "");
// 计算当前位于树的第几层
int currLevel = ((rowIndex + 1) / 2);
// 若到了最后一层,则返回
if (currLevel == treeDepth) return;
// 计算当前行到下一行,每个元素之间的间隔(下一行的列索引与当前元素的列索引之间的间隔)
int gap = treeDepth - currLevel - 1;
// 对左儿子进行判断,若有左儿子,则记录相应的"/"与左儿子的值
if (currNode.getLeft() != null) {
res[rowIndex + 1][columnIndex - gap] = "/";
writeArray(currNode.getLeft(), rowIndex + 2, columnIndex - gap * 2, res, treeDepth);
}
// 对右儿子进行判断,若有右儿子,则记录相应的"\"与右儿子的值
if (currNode.getRight() != null) {
res[rowIndex + 1][columnIndex + gap] = "\\";
writeArray(currNode.getRight(), rowIndex + 2, columnIndex + gap * 2, res, treeDepth);
}
}
public static void show(RBTree.RBNode root) {
if (root == null) System.out.println("EMPTY!");
// 得到树的深度
int treeDepth = getTreeDepth(root);
// 最后一行的宽度为2的(n - 1)次方乘3,再加1
// 作为整个二维数组的宽度
int arrayHeight = treeDepth * 2 - 1;
int arrayWidth = (2 << (treeDepth - 2)) * 3 + 1;
// 用一个字符串数组来存储每个位置应显示的元素
String[][] res = new String[arrayHeight][arrayWidth];
// 对数组进行初始化,默认为一个空格
for (int i = 0; i < arrayHeight; i++) {
for (int j = 0; j < arrayWidth; j++) {
res[i][j] = " ";
}
}
// 从根节点开始,递归处理整个树
// res[0][(arrayWidth + 1)/ 2] = (char)(root.val + '0');
writeArray(root, 0, arrayWidth / 2, res, treeDepth);
// 此时,已经将所有需要显示的元素储存到了二维数组中,将其拼接并打印即可
for (String[] line : res) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < line.length; i++) {
sb.append(line[i]);
if (line[i].length() > 1 && i <= line.length - 1) {
i += line[i].length() > 4 ? 2 : line[i].length() - 1;
}
}
System.out.println(sb);
}
}
}
最后写一个测试就结束了:
package RBTree;
import java.util.Scanner;
public class RBTreeTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
RBTree<String, Object> rbt= new RBTree<String,Object>();
while (true) {
System.out.println("输入key:");
String key = scanner.next();
System.out.println();
rbt.insert(key, null);
TreeOperation.show(rbt.getRoot());
}
}
}
后续我会补上红黑树的删除。