数据结构——平衡查找树与红黑树(2)

在前面我们已经粗略的说过了平衡二叉树和2-3树的基本概念,但是我们并没有进行代码的实现,原因是二叉平衡树的完美平衡要求过高,实现困难并且不利于维护,而2-3树中则需要维护2种结点(如果算上临时4-结点就是三种),同时还要将这几种节点进行相互转化,实现成本高,代码量巨大,而且其结构非常脆弱,很容易在疏忽的情况下其查找性能退化为一棵普通的二叉查找树甚至更糟。

我们需要一种既能简单实现,同时又像2-3树那样易于理解的数据结构来实现平衡性,那么红黑树就是最佳选择。


红黑二叉查找树实际上是由一棵标准二叉查找树和一些额外信息构成,虽然从结构上来看,其形状类似二叉查找树,但实际上其思维方式以及处理情况都和2-3树近乎相同。这样我们就既能结合了二叉树代码的简洁和2-3树思路上的清晰来轻松构建一棵完美平衡的查找树。

在红黑树中,两个节点之间同样用链接相连,但是这里链接我们分为2中颜色,红色和黑色

黑色链接即为我们2-3树中的普通链接,而红色链接,则将其链接的两个结点视为一个3-节点,右图中我们可以通过对比看出,虽然在代码实习上a节点是b结点的子节点,但因为红色链接,我们可以将它们视为同一层的结点。通过这种方式,我们就将3-结点的思维模式用2-结点和红色链接进行了适当表示。而且这种方法还适用于二叉查找树的大部分操作,例如get()方法,我们同样可以通过三种顺序遍历后命中元素返回。这样的结构也让我们代码实现变的更加简单。

对于任意的2-3树,只要进行结点变换,我们都可以理解派生出一棵对应的二叉查找树,我们将这种方式表示的2-3树称之为红黑二叉查找树。   《算法》

红黑树 另一种定义是含有红黑链接并满足下列条件的二叉查找树

  • 红链接均为左链接
  • 没有任何一个结点同时由两条红链接相连
  • 该树是完美黑色平衡,即任意空链接到根节点的路径上的黑色链接数量相同

这三条定义对于我们之后处理红黑变换非常重要,基本上,所有的变换我们都会根据这三条定义来完成。

如果我们将红黑树中的红色链接画平后,我们将的得到一棵黑色链接平衡的二叉树,同时也是一棵2-3树。


Node类

每个结点都会有且只有一条指向自己的链接(由父结点指向),我们将其颜色用布尔值来表示保存在类中。用true来表示红色,false来表示黑色。同时我们默认空链接为黑色链接。

public class TreeNode {
	

	int Key;//键
	int Value;//值
	TreeNode leftNode;//左子节点
	TreeNode rightNode;//右子节点
	int N;// 子树中的节点数
	boolean color;// 链接颜色

}

同时我们还应实现一个方便确认该节点链接颜色的方法。

private static final boolean RED=true;
private static final boolean BLACK=false;
private boolean isRed(TreeNode node) {
        
	if (node == null)
		return false;
	return node.color == RED;
}

好了,以上一个结点类基本就构成完成了,之后还有一点小细节我们留在后面说。现在我们来总结下红黑树中非常重要的操作——旋转


旋转

因为就像2-3树,我们会将数据在处理中不断进行节点的改变,所有可能会出现红链接在右侧或者同一节点有两条红链接的状况,这种情况需要我们用选择的方式改变红链接的指向从而调整树达到平衡。

假如有一条指向右侧的红链接需要我们进行调整,我们需要给出一个方法,该方法的参数为这个需要调整的结点,而方法的返回值将是调整后的一个新节点,这是非常关键的。我们只需要将两个根节点中较小者作为根节点变为较大者作为根节点,然后改变其颜色即可。同理,右旋操作只需要改变方向即可。

 

这两幅图即为左右旋转的代码实现,其中size()方法之后会给出。

我们通过左右旋转可以改变该节点链接的颜色,但是我们无法确定是否会出现父节点指向该节点链接颜色同样为红色,所有在这里我们只保留原来颜色,如果出现两条红色链接的情况,我们会之后定义一个方法来解决这种情况。

在我们插入新键时,有可能破坏掉树的平衡,此时我们就应该使用旋转来调整子树高度来维护整棵树的平衡。

旋转操作能够有效的保持红黑树的两个重要性质:有序性和完美平衡性

 

 


插入操作

向树底部的2-结点插入新键

我们会在底部新增一个节点,但是总用红色的链接将新增节点和父节点进行链接。如果其父节点是一个2-节点,那么我们只需要结合刚才的旋转方法,将该节点变为一个3-节点。

如果指向新节点的是父节点的左链接,那么父节点就直接成为了一个3-结点,如果指向新节点的是父节点的右链接,这就是一个错误的3-节点,但我们进行一次左旋即可修正。

 

 

 

 

 

 

向3-节点中插入新键

这种情况分为三种子情况:新键大于树中的两个键,在二者之间或者小于两个键

  • 大于原树中的两个键  

这种是最简单的情况,我们知道,插入最大时,应该插入到3-节点的最右侧,此时这个3-结点将由两条红色多的链接相连,变成了一棵平衡的二叉树,根节点是中间键,左边最小健,右边最大键。这时我们只需要将两条红色的链接都变为黑色,就能得到一棵高位2的平衡二叉树。

  • 新键小于两个键

如果插入的键为最小键,其将被链接到最左侧的空链接,这样将产生连续两条向左的键,此时我们只需要将上层的空链接向右旋转即可得到第一种情况,再将两条红链接置黑即可。

  • 新键在二者之间

当我们要插入的键在二者之间时,我们同样会获得两条连续的红链接,去之前结果不同的,新增这条红色链接是右链接,我们首先需要将其修正为左链接,我们通过左旋即可修正。这时我们所面对的情况就是第二种情况,我们只需要按照第二种情况处理即可。

我们能够发现,这三种情况实际上复杂度是一次递增的,所以我们只需要按照顺序,先判断是否是最复杂的,依次判断,就能够将三种情况都通过简单的代码解决。

这里我们需要定义一个方法,能够改变第一种情况的颜色。

        /*
	 * 当我们旋转完成后,可能出现一个节点的两个子节点连接都为红色
	 * 我们需要一个方法来处理这种情况,将两个子节点的连接都变为黑色
	 * 同时将该节点与其父节点之间的连接变为红色
	 */
	private void flipColors(TreeNode h) {
		h.setColor(true);
		h.getLeftNode().setColor(false);
		h.getRightNode().setColor(false);
	}

根节点总应该是黑色的,我们在每次插入完成后,根节点都应该重新改为黑色。每当根节点由红色变为黑色时,树的高度都应该增加1.


下面我们总结一下这些操作

  • 如果右子节点是红色的而左子节点是黑色的,进行左旋转
  • 如果左子结点是红色的且它的左子结点也是红色的,进行右旋转
  • 如果左右子节点都为红色,则进行颜色变化

下面来看红黑树的插入代码实现

package 红黑树;

public class Red_BlackTree {
	// 静态常量,红链接为true,黑色链接为false
	private static final boolean RED = true;
	private static final boolean BLACK = false;
	
	//创建一个根节点
	private TreeNode root;
	
	//这里我们先需要实现几个基本操作,如左旋,右旋等
	/*
	 * 左右旋转操作返回值为一个旋转后的节点引用、
	 * 左右旋转操作并不能够保证其红黑链接的颜色依旧正确
	 * 需要通过其他方法来修改链接颜色来保证整体树的完整有序
	 */
	private TreeNode rotateLeft(TreeNode h) {
		//首先我们找到旋转节点的右子树
		TreeNode x = h.getRightNode();
		//将右子树的左子树移动到旋转节点的右子树上
		h.setRightNode(x.getLeftNode());
		//再将x的左子树设置为h
		x.setLeftNode(h);
		x.setN(h.getN());
		//旋转完成后,更新子节点值
		h.setN(1+size(h.getLeftNode())+size(h.getRightNode()));
		return x;
	}
	//右旋操作
	private TreeNode rotateRight(TreeNode h) {
		TreeNode x = h.getLeftNode();
		h.setLeftNode(x.getRightNode());
		x.setRightNode(h);
		x.setN(h.getN());
		h.setN(1+size(h.getLeftNode())+size(h.getRightNode()));
		return x;
	}
	//获得子树中节点总数操作
	/*
	 * 通过遍历子树,获取其中所有节点的个数
	 */
	private int size(TreeNode h) {
		if(h == null) {
			return 0;
		}
		h.setN(0);
		//通过递归遍历每一个节点
		if(h != null) {
			size(h.getLeftNode());
			h.setN(h.getN()+1);
			size(h.getRightNode());
			h.setN(h.getN()+1);
		}
		return h.getN();
	}
	/*
	 * 当我们旋转完成后,可能出现一个节点的两个子节点连接都为红色
	 * 我们需要一个方法来处理这种情况,将两个子节点的连接都变为黑色
	 * 同时将该节点与其父节点之间的连接变为红色
	 */
	private void flipColors(TreeNode h) {
		h.setColor(true);
		h.getLeftNode().setColor(false);
		h.getRightNode().setColor(false);
	}
	/*
	 * 判断一个节点的链接颜色
	 */
	private boolean isRed(TreeNode node) {
		if (node == null)
			return false;
		return node.color == RED;
	}
	/*
	 * 插入算法实现,我们在这里进行一次封装,将无返回值的方法向外暴露
	 */
	public void put(int Key,int Value) {
		//对外暴露方法
                //自顶向下递归,查找其值,如果没有则建立一个新的节点
                root = put(root,Key,Value);
                //最后完成后把根节点颜色变为黑色
                root.color = false;
	}
	//私有方法
	private TreeNode put(TreeNode h,int Key,int Value) {
		/*
		 * 标准插入操作,和父节点由红色链接相连
		 */
		if(h == null) {
			return new TreeNode(Key, Value, 1, true);
		}
		//与插入节点比较 
		if(Key<h.Key) {
			//小于则继续向左递归
			h.leftNode = put(h.leftNode, Key, Value);
		}
		if(Key > h.Key) {
			//大于则向右递归
			h.rightNode = put(h.rightNode, Key, Value);
		}
		else {
			//等于,覆盖该值
			h.Value = Value;
		}
		//当查找到最深处空节点时,将调用第一个判空if,新建节点
		
		/*
		 * 当我们插入完成后,我们需要对树的红黑两色进行调整
		 * 可能出现几种情况,插入节点左右都为红色,
		 * 左红右黑,左黑右红。
		 */
		//先判断最复杂的左黑右红
		if(isRed(h.rightNode) && !isRed(h.leftNode)) {
			h = rotateLeft(h);
		}
		//然后判断左侧都是红色
		if(isRed(h.leftNode) && !isRed(h.rightNode)) {
			h = rotateRight(h);
		}
		//然后判断左右红,直接进行颜色改变即可
		if(isRed(h.leftNode) && isRed(h.rightNode)) {
			flipColors(h);
		}
		
		h.N = size(h.leftNode) + size(h.rightNode) + 1;
		
		return h;
	}
	
	
}

一棵红黑二叉树构造轨迹

猜你喜欢

转载自blog.csdn.net/weixin_41582192/article/details/81626062