《数据结构与算法》之红黑树

二叉查找树对于某个节点而言,其左子树的节点关键值都小于该节点关键值,右子树的所有节点关键值都大于该节点关键值。二叉查找树作为一种数据结构,其查找、插入和删除操作的时间复杂度都为 O(logN) ,底数为 2。但是我们说这个时间复杂度是在平衡的二叉查找树上体现的,也就是如果插入的数据是随机的,则效率很高,但是如果插入的数据是有序的,这种情况下查找的时间复杂度为 O(N),而不是 O(logN) 。当然这是在最不平衡的条件下,实际上,二叉查找树的效率应该在 O(N) 和 O(logN) 之间,这取决于树的不平衡程度。

  那么为了能够以较快的时间 O(logN) 来搜索一棵树,我们需要保证树总是平衡的(或者大部分是平衡的),也就是说每个节点的左子树节点个数和右子树节点个数尽量相等。红黑树就是这样的一棵平衡树,对一个要插入的数据项(删除也是),插入例程要检查会不会破坏树的特征,如果破坏了,程序就会进行纠正,根据需要改变树的结构,从而保持树的平衡。红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。

1、红黑树的特征

①、节点都有颜色;

②、在插入和删除的过程中,要遵循保持这些颜色的不同排列规则。

第①点很好理解,在红黑树中,每个节点的颜色或者是黑色或者是红色的。当然也可以是任意别的两种颜色,这里的颜色用于标记,我们可以在节点类Node中增加一个boolean型变量,以此来表示颜色的信息。

第②点,在插入或者删除一个节点时,必须要遵守的规则称为红黑规则:

(1)每个节点不是红色就是黑色的;

(2)根节点总是黑色的;

(3)如果节点是红色的,则它的子节点必须是黑色的(反之不一定),也就是从每个叶子到根的所有路径上不能有两个连续的红色节点;

(4)从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。

从根节点到叶节点的路径上的黑色节点的数目称为黑色高度,规则 4 另一种表示就是从根到叶节点路径上的黑色高度必须相同。

注意:新插入的节点颜色总是红色的,这是因为插入一个红色节点比插入一个黑色节点违背红黑规则的可能性更小,原因是插入黑色节点总会改变黑色高度(违背规则4),但是插入红色节点只有一半的机会会违背规则3(因为父节点是黑色的没事,父节点是红色的就违背规则3)。另外违背规则3比违背规则4要更容易修正。

2、红黑树的自我修正

(1)节点颜色转换

新插入的节点颜色都为红色,直接插入会违反规则3,改为黑色却发现违反规则4。这时候我们将其父节点颜色改为黑色,父节点的兄弟节点颜色也改为黑色。通常祖父节点颜色会由黑色变为红色。注意:当节点为根节点时,始终为黑色。这项操作最重要的性质在于它是局部变换,不会影响整棵树的黑色平衡性。

(2)左旋转

首先要说明的是节点本身是不会旋转的,旋转改变的是节点之间的关系,选择一个节点作为旋转的顶端,如果做一次左旋,这个顶端节点会向下和向左移动到它左子节点的位置,它的右子节点会上移到它原来的位置。左旋的顶端节点必须要有右子节点。

(3)右旋转

首先要说明的是节点本身是不会旋转的,旋转改变的是节点之间的关系,选择一个节点作为旋转的顶端,如果做一次右旋,这个顶端节点会向下和向右移动到它右子节点的位置,它的左子节点会上移到它原来的位置。右旋的顶端节点必须要有左子节点。

3、红黑树的实现

3.1 红黑树的节点

public class RBNode<T extends Comparable<T>> {
	private static final boolean RED = false; // 定义红黑树标志
	private static final boolean BLACK = true;
	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");
	}
}

3.2 红黑树左旋的实现

	/************* 对红黑树节点x进行左旋操作 ******************/
	/*
	 * 左旋示意图:对节点x进行左旋 
	 * 左旋做了三件事: 
	 * 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的父节点为空(即原来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.3 红黑树右旋的实现

/************* 对红黑树节点y进行右旋操作 ******************/
	/*
	 * 左旋示意图:对节点y进行右旋 
	 * 右旋做了三件事: 
	 * 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;
	}

 3.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);
	}

在插入过程中,只要谨慎的使用左旋转、右旋转和颜色转换这三种简单的操作,就能够保证插入操作后红黑树和2-3树的一一对应关系。在沿着插入点到根节点的路径向上移动时在所经过的每个节点中顺序完成以下操作,就可以完成插入操作了。

	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);
	}

 

参考:https://blog.csdn.net/eson_15/article/details/51144079

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值