数据结构之红黑树

其实红黑树就是一种自平衡的二叉搜索树,因为如果插入的数据时有序的时候,二叉树会变成链表,插入删除的效率非常的低。
在这里插入图片描述

规则

当插入(删除)一个节点时,必须遵循一定的规则,他们被称为红黑规则。遵循红黑规则,树就是平衡的:

  1. 每一个节点不是黑就是红;
  2. 根节点总是黑色的;
  3. 红节点的子节点必须是黑色的(红色节点不能连续),反之则不一定成立;
  4. 从根节点到叶子节点or空子节点的每条路径 上的黑色节点数目(黑色高度)是相同。

在这里插入图片描述

左旋和右旋

在进行插入和删除的时候,总会出现一些破坏了上述性质3/4的情况,所以涉及一些左旋和右旋操作。这里盗图说明:
左旋:
在这里插入图片描述
在这里插入图片描述
右旋:
在这里插入图片描述
在这里插入图片描述
需要注意的时,旋转的时候仍然要遵循搜索二叉树的规则。
无论是左旋还是右旋,比该节点小的值只能在该节点则左下方或者左上方;比该节点大的值只能在该节点的右下方或者右上方。这样才符合搜索二叉树的规则。

为什么插入的新节点要是红色?

因为插入一个红色节点违背红黑规则的可能性比插入一个黑色节点的要小。

插入一个红色节点,肯定不会改变树的黑色高度;另外,如果插入节点的父节点是黑色节点,不会违背父子节点同时为红色的规则,如果插入节点的父节点是红色节点才会违背这一规则,这个时候就需要对树进行变换来适应规则。还有一点就是,违背规则3(父子节点都是红色)比违背规则4(黑色高度不同)要容易修正,所以,我们把要插入的节点的颜色变成红色。

具体实现

好了,了解了红黑树的理论知识之后,我们来看一下具体实现。

1.红黑树的节点类 RBNode

首先,红黑树的节点类与二叉搜索树是差不多的,只不过是加了一个boolean型变量来表示节点的颜色。

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 == RED? "R" : "B");
	}
}
2.左旋的具体实现

二话不说,直接上代码

/*************对红黑树节点x进行左旋操作 ******************/
/*
 * 左旋示意图:对节点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
 */
private void leftRotate(RBNode<T> x) {
	//1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
	RBNode<T> y = x.right;
	x.right = y.left;
	
	if(y.left != null) 
		y.left.parent = x;
	
	//2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
	y.parent = x.parent;
	
	if(x.parent == null) {
		this.root = y; //如果x的父节点为空,则将y设为父节点
	} else {
		if(x == x.parent.left) //如果x是左子节点
			x.parent.left = y; //则也将y设为左子节点
		else
			x.parent.right = y;//否则将y设为右子节点
	}
	
	//3. 将y的左子节点设为x,将x的父节点设为y
	y.left = x;
	x.parent = y;		
}
3.右旋的具体实现
/*************对红黑树节点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
 */
private void rightRotate(RBNode<T> y) {
	//1. 将y的左子节点赋给x的右子节点,并将x赋给y左子节点的父节点(y左子节点非空时)
	RBNode<T> x = y.left;
	y.left = x.right;
	
	if(x.right != null) 
		x.right.parent = y;
	
	//2. 将x的父节点p(非空时)赋给y的父节点,同时更新p的子节点为y(左或右)
	x.parent = y.parent;
	
	if(y.parent == null) {
		this.root = x; //如果x的父节点为空,则将y设为父节点
	} else {
		if(y == y.parent.right) //如果x是左子节点
			y.parent.right = x; //则也将y设为左子节点
		else
			y.parent.left = x;//否则将y设为右子节点
	}
	
	//3. 将y的左子节点设为x,将x的父节点设为y
	x.right = y;
	y.parent = x;		
}
4. 插入操作

插入操作跟二叉搜索树的前半段是一样的,即先找到待插入的位置,再将节点插入。
先看前半段的代码:

/*********************** 向红黑树中插入节点 **********************/
public void insert(T key) {
	RBNode<T> node = new RBNode<T>(key, RED, null, null, null);
	if(node != null) 
		insert(node);
}
 
//将节点插入到红黑树中,这个过程与二叉搜索树是一样的
private void insert(RBNode<T> node) {
	RBNode<T> current = null; //表示最后node的父节点
	RBNode<T> x = this.root; //用来向下搜索用的
	
	//1. 找到插入的位置
	while(x != null) {
		current = x;
		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;
	}
	
	//3. 将它重新修整为一颗红黑树
	insertFixUp(node);
}

这与二叉搜索树中实现的思路一模一样,这里不再赘述,主要看看方法里面最后一步insertFixUp操作。因为插入后可能会导致树的不平衡,insertFixUp方法里主要是分情况讨论,分析何时变色,何时左旋,何时右旋。我们先从理论上分析具体的情况,然后再看insertFixUp方法的具体实现。

如果是第一次插入,由于原树为空,所以只会违反红-黑树的规则2,所以只要把根节点涂黑即可;如果插入节点的父节点是黑色的,那不会违背红-黑树的规则,什么也不需要做;但是遇到如下三种情况时,我们就要开始变色和旋转了:

1.插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的;

2.插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;

3.插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。

对于情况1:插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色的。此时,肯定存在祖父节点,但是不知道父节点是其左子节点还是右子节点,但是由于对称性,我们只要讨论出一边的情况,另一种情况自然也与之对应。这里考虑父节点是祖父节点的左子节点的情况,如下左图所示:
在这里插入图片描述
对于这种情况,我们要做的操作有:将当前节点(4)的父节点(5)和叔叔节点(8)涂黑,将祖父节点(7)涂红,变成上右图所示的情况。再将当前节点指向其祖父节点再次从新的当前节点开始算法(具体等下看下面的程序)。这样上右图就变成了情况2了。

对于情况2:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点。我们要做的操作有:将当前节点(7)的父节点(2)作为新的节点,以新的当前节点为支点做左旋操作。完成后如左下图所示,这样左下图就变成情况3了。
在这里插入图片描述
对于情况3:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。我们要做的操作有:将当前节点的父节点(7)涂黑,将祖父节点(11)涂红,在祖父节点为支点做右旋操作。最后把根节点涂黑,整个红-黑树重新恢复了平衡,如右上图所示。至此,插入操作完成!

我们可以看出,如果是从情况1开始发生的,必然会走完情况2和3,也就是说这是一整个流程,当然咯,实际中可能不一定会从情况1发生,如果从情况2开始发生,那再走个情况3即可完成调整,如果直接只要调整情况3,那么前两种情况均不需要调整了。故变色和旋转之间的先后关系可以表示为:变色->左旋->右旋

至此,我们完成了全部的插入操作。下面我们看看insertFixUp方法中的具体实现(可以结合上面的分析图,更加利与理解)

private void insertFixUp(RBNode<T> node) {  
    RBNode<T> parent, gparent; //定义父节点和祖父节点  
      
    //需要修整的条件:父节点存在,且父节点的颜色是红色  
    while(((parent = parentOf(node)) != null) && isRed(parent)) {  
        gparent = parentOf(parent);//获得祖父节点  
          
        //若父节点是祖父节点的左子节点,下面else与其相反  
        if(parent == gparent.left) {                  
            RBNode<T> uncle = gparent.right; //获得叔叔节点  
              
            //case1: 叔叔节点也是红色  
            if(uncle != null && isRed(uncle)) {  
                setBlack(parent); //把父节点和叔叔节点涂黑  
                setBlack(uncle);  
                setRed(gparent); //把祖父节点涂红  
                node = gparent; //将位置放到祖父节点处  
                continue; //继续while,重新判断  
            }  
              
            //case2: 叔叔节点是黑色,且当前节点是右子节点  
            if(node == parent.right) {  
                leftRotate(parent); //从父节点处左旋  
                RBNode<T> tmp = parent; //然后将父节点和自己调换一下,为下面右旋做准备  
                parent = node;  
                node = tmp;  
            }  
              
            //case3: 叔叔节点是黑色,且当前节点是左子节点  
            setBlack(parent);  
            setRed(gparent);  
            rightRotate(gparent);  
        } else { //若父节点是祖父节点的右子节点,与上面的完全相反,本质一样的  
            RBNode<T> uncle = gparent.left;  
              
            //case1: 叔叔节点也是红色  
            if(uncle != null & isRed(uncle)) {  
                setBlack(parent);  
                setBlack(uncle);  
                setRed(gparent);  
                node = gparent;  
                continue;  
            }  
              
            //case2: 叔叔节点是黑色的,且当前节点是左子节点  
            if(node == parent.left) {  
                rightRotate(parent);  
                RBNode<T> tmp = parent;  
                parent = node;  
                node = tmp;  
            }  
              
            //case3: 叔叔节点是黑色的,且当前节点是右子节点  
            setBlack(parent);  
            setRed(gparent);  
            leftRotate(gparent);  
        }  
    }  
      
    //将根节点设置为黑色  
    setBlack(this.root);  
}  

附上测试程序:

package test;
 
import tree.RBTree;
 
public class RBTreeTest {
	
	private static final int a[] = {10, 40, 30, 60, 90, 70, 20, 50, 80};
	private static final boolean mDebugInsert = true;    // "插入"动作的检测开关(false,关闭;true,打开)
        private static final boolean mDebugDelete = true;    // "删除"动作的检测开关(false,关闭;true,打开)
 
	public static void main(String[] args) {
		int i, ilen = a.length;
                RBTree<Integer> tree = new RBTree<Integer>();
 
                System.out.printf("== 原始数据: ");
                for(i=0; i<ilen; i++)
                    System.out.printf("%d ", a[i]);
                System.out.printf("\n");
 
                for(i=0; i<ilen; i++) {
                   tree.insert(a[i]);
                    // 设置mDebugInsert=true,测试"添加函数"
                    if (mDebugInsert) {
                        System.out.printf("== 添加节点: %d\n", a[i]);
                        System.out.printf("== 树的详细信息: \n");
                        tree.print();
                        System.out.printf("\n");
                    }
                }
 
                System.out.printf("== 前序遍历: ");
                tree.preOrder();
 
                System.out.printf("\n== 中序遍历: ");
                tree.inOrder();
 
                System.out.printf("\n== 后序遍历: ");
                tree.postOrder();
                System.out.printf("\n");
 
                System.out.printf("== 最小值: %s\n", tree.minValue());
                System.out.printf("== 最大值: %s\n", tree.maxValue());
                System.out.printf("== 树的详细信息: \n");
                tree.print();
                System.out.printf("\n");
        
                // 设置mDebugDelete=true,测试"删除函数"
                if (mDebugDelete) {
                    for(i=0; i<ilen; i++)
                    {
                        tree.remove(a[i]);
 
                        System.out.printf("== 删除节点: %d\n", a[i]);
                        System.out.printf("== 树的详细信息: \n");
                        tree.print();
                        System.out.printf("\n");
                    }
                }
        }
 
}

资料:https://blog.csdn.net/eson_15/article/details/51144079
https://blog.csdn.net/u012152619/article/details/46825969

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值