1、概念
- 红黑树是一种特定类型的二叉树,它是在计算机科学中用来组织数据比如数字的块的一种结构。若一棵二叉查找树是红黑树,则它的任一子树必为红黑树.
- 红黑树是一种平衡二叉查找树的变体,它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树(AVL),但 对之进行平衡的代价较低, 其平均统计性能要强于 AVL 。
- 由于每一颗红黑树都是一颗二叉排序树,因此,在对红黑树进行查找时,可以采用运用于普通二叉排序树上的查找算法,在查找过程中不需要颜色信息。
2、特征
- 节点是红色或黑色
- 根节点是黑色
- 所有叶子都是黑色。(叶子是NUIL节点)
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
3、红黑树变换规则
/**
* 插入之后修复红黑树颜色:
* 1、若红黑树为空树,则根结点为黑色
* 2、插入的key已经存在 不需要处理
* 3、插入的父结点为黑色,因为插入之后,黑色结点没有变化,所以依旧满足红黑树 不需要处理
* 4、插入的父结点为红色
* 4-1、叔叔结点存在,并且为红色(父-叔 双红)[处理方式:父+叔染黑,爷染红,再以爷结点为当前结点进行下一轮操作]
* 4-2、叔叔结点不存在或者为黑色,父结点为爷爷结点的左子树
* 4-2-1、插入结点为父结点的左子结点(LL)[处理方式:父染黑,爷染红,然后以爷结点右旋,结束]
* 4-2-2、插入结点为父结点的右子结点(LR)[处理方式:以父结点左旋,得到LL(4-2-1)情况,然后以父为当前结点进行下一轮操作]
* 4-3、叔叔结点不存在或者为黑色,父结点为爷爷结点的右子树
* 4-3-1、插入结点为父结点的左子结点(RL)处理方式:以父结点旋右旋,得到RR(4-3-1)情况,然后以父为当前结点进行下一轮操作]
* 4-3-2、插入结点为父结点的右子结点(RR)[处理方式:父染黑,爷染红,然后以爷结点左旋,结束]
*/
4、变色旋转总结
- 根为黑色
- 双红(父、叔均为红)
- 父、叔变色(变为黑色),爷变色(红色),以爷为结点递归
- LL
- 父变黑,爷变红,以爷右旋,结束
- LR
- 以父左旋,以父递归
- RR
- 父变黑,爷变红,以爷左旋,结束
- RL
- 以父右旋,以父递归
5、实例
public class Node {
private int data;
private int color;
private Node left;
private Node right;
private Node parent;
public Node() {
}
public Node(int data, int color,Node parent) {
this.data = data;
this.color = color;
this.parent = parent;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
@Override
public String toString() {
String s = "black";
if (color == 0){
s = "red";
}
return data + "->" +s;
}
}
红黑树核心方法
repairColor()变色方法为核心,除了变色、左旋、右旋、插入其他均为基础方法。
左旋、右旋、插入方法可以参考自平衡二叉树,和平衡二叉差别不大,主要差别在于此处红黑树node中加了一个parent结点,每次左旋、右旋、插入都需要考虑给parent赋值
public class RedBlackTree {
/**
* 红色
*/
private final int RED = 0;
/**
* 黑色
*/
private final int BLACK = 1;
/**
* 根结点
*/
private Node root;
/**
* 获取当前结点的父结点,node本身存在获取父结点方法,此处看似多写
* 此方法可以避免在获取父类时,当前对象为null,出现空指针,避免获取父类时到处加非空校验
* @param node
* @return
*/
private Node getParent(Node node){
if (node != null){
return node.getParent();
}
return null;
}
/**
* 结点是否为红色
* @param node
* @return true:是 false:否
*/
private boolean isRed(Node node){
if (node != null) {
return node.getColor() == this.RED;
}
return false;
}
/**
* 结点是否为黑色
* @param node
* @return true:是 false:否
*/
private boolean isBlack(Node node){
if (node != null) {
return node.getColor() == this.BLACK;
}
return false;
}
/**
* 设置结点为红色
* @param node
*/
private void setRed(Node node){
if (node != null){
node.setColor(this.RED);
}
}
/**
* 设置结点为黑色
* @param node
*/
private void setBlack(Node node){
if (node != null){
node.setColor(this.BLACK);
}
}
/**
* 中序遍历,方法公开,方便调用
*/
public void inOrderPrintln(){
show(this.root);
}
private void show(Node node){
if (node != null){
show(node.getLeft());
System.out.println(node);
show(node.getRight());
}
}
/**
* 左旋
* 注:下方方法备注中7或12:第一个数字指的是第一张图中的值,第二个数字指的是第二张图中的值
* 10
* / \
* 7 11
* / \
* 6 8
* \
* 9
*
*
* 10
* / \
* 7 12
* / \
* 11 14
* \
* 15
* @param node
*/
private void leftRotate(Node node){
// node代表上图中的7或12
// rightNode代表8或14
Node rightNode = node.getRight();
// ~~~~将8或14的左结点放在7或12的右子结点~~~~
node.setRight(rightNode.getLeft());
// 由于node存在一个父结点,所以还需要更新父结点
// 将8或14的父结点更新为7或12
if (rightNode.getLeft() != null){
rightNode.getLeft().setParent(node);
}
// ~~~~~将8或14放在(7或12)父结点下~~~~
// 判断当前结点(7或12)是否有父结点,没有则是root结点
if (node.getParent() != null){
// 将8或14放在(7或12)父结点下
rightNode.setParent(node.getParent());
// 需要考虑注释中第二种情况
// 判断是原node结点(7或12)是父结点(10)的左子节点还是右子结点
if (node == node.getParent().getLeft()){
node.getParent().setLeft(rightNode);
}else{
node.getParent().setRight(rightNode);
}
}else {
// 不存在父结点,则标示原结点为root结点
this.root = rightNode;
this.root.setParent(null);
}
// ~~~~将7或12放在8或14的左子结点~~~~
rightNode.setLeft(node);
node.setParent(rightNode);
}
/**
* 右旋
* 注:下方方法备注中7或14:第一个数字指的是第一张图中的值,第二个数字指的是第二张图中的值
* 10
* / \
* 7 11
* / \
* 6 8
* /
* 5
*
*
* 10
* / \
* 7 14
* / \
* 12 15
* /
* 11
*
* @param node
*/
private void rightRotate(Node node){
// node代表上图中的7或14
// 代表上图中的6或12
Node leftNode = node.getLeft();
// ~~~~6或12右子结点放在7或14的左边~~~~
node.setLeft(leftNode.getRight());
if (leftNode.getRight() != null){
leftNode.getRight().setParent(node);
}
// ~~~~~将6或12放在7或14父结点下~~~~
// 判断当前结点7或14是否有父结点,没有则是root结点
if (node.getParent() != null){
leftNode.setParent(node.getParent());
// 判断当前结点7或14是否为原父结点10的左结点
if (node == node.getParent().getLeft()){
node.getParent().setLeft(leftNode);
}else{
node.getParent().setRight(leftNode);
}
}else {
this.root = leftNode;
this.root.setParent(null);
}
// ~~~~~将7或14放在6或12右子结点下~~~~
leftNode.setRight(node);
node.setParent(leftNode);
}
/**
* 方便外间调用
* @param data
*/
public void insert(int data){
Node node = insert(this.root,data);
repairColor(node);
}
/**
* 插入
* @param node
* @param data
*/
private Node insert(Node node,int data){
// 如果不存在根结点时
// 跟结点必须为红色,但是后续有染色操作。此处红色黑色不影响
// 红黑树新加结点颜色必须为红色,所以按照红色处理,然后后续再做染色操作
if (this.root == null){
this.root = new Node(data,RED,node);
return this.root;
}
// 若当前值大于此结点的值,则放在右子节点
if (data > node.getData()){
// 右子节点不存在值则直接赋值
if (node.getRight() == null){
node.setRight(new Node(data,RED,node));
return node.getRight();
}else {
//否则进入递归
return insert(node.getRight(),data);
}
}else {
// 若当前值不大于此结点的值,则放在左子节点
// 左子节点不存在值则直接赋值
if (node.getLeft() == null){
node.setLeft(new Node(data,RED,node));
return node.getLeft();
}else {
//否则进入递归
return insert(node.getLeft(),data);
}
}
}
/**
* 插入之后修复红黑树颜色:
* 1、若红黑树为空树,则根结点为黑色
* 2、插入的key已经存在 不需要处理
* 3、插入的父结点为黑色,因为插入之后,黑色结点没有变化,所以依旧满足红黑树 不需要处理
* 4、插入的父结点为红色
* 4-1、叔叔结点存在,并且为红色(父-叔 双红)[处理方式:父+叔染黑,爷染红,再以爷结点为当前结点进行下一轮操作]
* 4-2、叔叔结点不存在或者为黑色,父结点为爷爷结点的左子树
* 4-2-1、插入结点为父结点的左子结点(LL)[处理方式:父染黑,爷染红,然后以爷结点右旋,结束]
* 4-2-2、插入结点为父结点的右子结点(LR)[处理方式:以父结点左旋,得到LL(4-2-1)情况,然后以父为当前结点进行下一轮操作]
* 4-3、叔叔结点不存在或者为黑色,父结点为爷爷结点的右子树
* 4-3-1、插入结点为父结点的左子结点(RL)处理方式:以父结点旋右旋,得到RR(4-3-1)情况,然后以父为当前结点进行下一轮操作]
* 4-3-2、插入结点为父结点的右子结点(RR)[处理方式:父染黑,爷染红,然后以爷结点左旋,结束]
*/
private void repairColor(Node node) {
// 根结点必须为黑色
this.root.setColor(BLACK);
// 父
Node parent = getParent(node);
// 爷
Node grandfather = getParent(parent);
// 4、插入的父结点为红色
if (parent != null && isRed(parent)) {
// 父结点不是null 且 为红色:说明肯定存在爷结点,最次爷结点也是根结点
// 叔
Node uncle = null;
// 若当前结点为左结点,叔结点则为爷结点的右子节点,相反则为左子节点
if (parent == grandfather.getLeft()) {
uncle = grandfather.getRight();
} else {
uncle = grandfather.getLeft();
}
//4-1、叔叔结点存在,并且为红色(父-叔 双红)[处理方式:父+叔染黑,爷染红,再以爷结点为当前结点进行下一轮操作]
if (uncle != null && isRed(uncle)) {
setBlack(parent);
setBlack(uncle);
setRed(grandfather);
repairColor(grandfather);
return;
} else {
// 4-2、叔叔结点不存在或者为黑色,父结点为爷爷结点的左子树
if (parent == grandfather.getLeft()) {
// 4-2-1、插入结点为父结点的左子结点(LL)[处理方式:父染黑,爷染红,然后以爷结点右旋,结束]
if (node == parent.getLeft()) {
setBlack(parent);
setRed(grandfather);
rightRotate(grandfather);
return;
}
// 4-2-2、插入结点为父结点的右子结点(LR)[处理方式:以父结点左旋,得到LL(4-2-1)情况,然后以父为当前结点进行下一轮操作]
if (node == parent.getRight()) {
{
leftRotate(parent);
repairColor(parent);
return;
}
}
}else {
// 4-3、叔叔结点不存在或者为黑色,父结点为爷爷结点的右子树
// 4-3-1、插入结点为父结点的左子结点(RL)处理方式:以父结点旋右旋,得到RR(4-3-1)情况,然后以父为当前结点进行下一轮操作]
if (node == parent.getLeft()) {
rightRotate(parent);
repairColor(parent);
return;
}
// 4-3-2、插入结点为父结点的右子结点(RR)[处理方式:父染黑,爷染红,然后以爷结点左旋,结束]
if (node == parent.getRight()) {
setBlack(parent);
setRed(grandfather);
leftRotate(grandfather);
return;
}
}
}
}
}
/**
* 打印树
*/
public void print_tree() {
print_node(this.root,0);
System.out.printf("-------------------------------------------\n");
}
private void print_node (Node root, int level ){
if ( root == null ){
padding ( "\t", level );
System.out.println( "NIL" );
} else {
print_node ( root.getRight(), level + 1 );
padding ( "\t", level );
if(root.getColor() == BLACK) {
System.out.printf(root + "\n");
} else {
System.out.printf(root + "\n");
}
print_node ( root.getLeft(), level + 1 );
}
}
private void padding ( String ch, int n ){
int i;
for ( i = 0; i < n; i++ )
System.out.printf(ch);
}
}
测试方法
public class test {
/**
* 1.节点是红色或黑色。
* 2.根节点是黑色。
* 3.所有叶子都是黑色。(叶子是NUIL节点)
* 4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
* 5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
* @param args
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
RedBlackTree tree = new RedBlackTree();
String key = "";
while (!key.equals("q")){
System.out.print("请输入:");
key = scanner.next();
if (!key.equals("q")){
tree.insert(Integer.valueOf(key));
// 中序遍历,看不出效果
// tree.inOrderPrintln();
// 使用树状
tree.print_tree();
}
}
scanner.close();
}
}