红黑二叉树优化哈希表


续哈希表那两篇博客

我们已经知道了哈希表有多么多么的牛逼,但是呢,有没有想过一个问题:对于用拉链法构造出来的哈希表,我们在查找一个元素时,还是要遍历后面的拉链。解决这个问题的一个比较好的方法就是红黑二叉树

这篇博客分析红黑二叉树的

  • 左旋
  • 右旋
  • 插入

参考文章:

一、红黑二叉树简介

红黑树(Red Black Tree) 是一种自平衡二叉查找树
红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。 ——百度百科

红黑二叉树的五条性质(五条全部满足就是红黑二叉树,只要有一条不符则不是

1. 每个结点的颜色只能是红色或黑色。
2. 根结点是黑色的。
3. 每个叶子结点都带有两个空的黑色结点(被称为黑哨兵),如果一个结点n的只有一个左孩子,那么n的右孩子是一个黑哨兵;如果结点n只有一个右孩子,那么n的左孩子是一个黑哨兵。
4. 如果一个结点是红的,则它的两个儿子都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5. 对于每个结点来说,从该结点到其子孙叶结点的所有路径上包含相同数目的黑结点。

二、定义结点、颜色等

public class RBNode<T extends Comparable<T>> {
		RBNode<T> left; // 左孩子
		RBNode<T> right; // 右孩子
		RBNode<T> parent; // 父节点
		boolean color; // 颜色
		T key; // 关键字

		public RBNode(T key, boolean color, RBNode<T> left, RBNode<T> right, RBNode<T> parent) {
			this.key = key;
			this.color = color;
			this.left = left;
			this.right = right;
			this.parent = parent;
		}
	}

左旋和右旋比较绕,需要仔细慢慢消化理解,我花了很长时间才搞明白。
一定要结合图去写代码,否则必出错(大神除外)

三、左旋

在这里插入图片描述
图片源:https://blog.csdn.net/sd09044901guic/article/details/84955116#commentBox

private void leftRotate(RBNode<T> x) {

		// 首先设置x的右孩子y
		RBNode<T> y = x.right;
		y.parent = x;

		// 判断x是否有父亲
		if (x.parent == null) { // 没有就把y变成父亲
			y = root;
		} else {
			if (x == x.parent.left) { // 如果x是父亲的左孩子,就把y变成父亲的左孩子
				x.parent.left = y;
			} else if (x == x.parent.right) { // 同上
				x.parent.right = y;
			}
		}
		x.parent = y;
		x.right = y.left; // y的左孩子变成x的右孩子
		y.left = x; // x变成y的左孩子
	}

四、右旋

在这里插入图片描述

private void rightRotate(RBNode<T> y) {
		// 右旋,y是x的父亲,y变成x的儿子,x右儿子变成y的左儿子

		// 设置y的左儿子x
		RBNode<T> x = y.left;
		x.parent = y;

		if (y.parent == null) { // y没爸,就把x变成根节点
			x = root;
		} else {
			if (y == y.parent.left) { // 如果y是它爸的左儿子,就把x变成y爸的左儿子
				x = y.parent.left;
			} else if (y == y.parent.right) { // 同上
				x = y.parent.right;
			}
		}
		y.parent = x; // 把x变成y的父亲
		y.left = x.right; // x右儿子给y做左儿子
		x.right = y; // y变成x的右儿子
	}

五、插入

首先要注意一个问题,插入的元素初始颜色都是红色,因为红色不会违背性质五。这样使需要讨论的情况更少,问题更简单,

插入要分情况讨论,每种情况都要去满足红黑二叉树的五条性质

  1. 父亲结点是黑色
    - 此时直接满足五条性质

  2. 父亲节点是红色(此时考虑叔叔结点)
    叔叔结点为红

    			1、父亲、叔叔变黑
    			2、新节点、祖父变红
    			3、由于祖父变红,两红不相邻,往上查找,循环使其满足性质四
    

    叔叔结点为黑

    		①父亲是祖父右孩子,新节点是父亲右孩子
    			1、父变黑
    			2、祖父变红
    			3、祖父右旋
    			
    		②父亲是祖父左孩子,新节点是父亲右孩子
    			1、父亲左旋
    			2、交换新节点与父亲的位置
    			3、经过上两步就变成了①,再用①操作即可
    			
    		③父亲是祖父右孩子,新节点是父亲右孩子
    			1、父变黑
    			2、祖父变红
    			3、祖父左旋
    			
    		④父亲是祖父右孩子,新节点是父亲左孩子 
    			1、父右旋
    			2、交换新节点与父亲位置
    			3、经过上两步就变成了③,再用③操作即可
    

在这里插入图片描述

//定义一些常用的方法
private RBNode<T> parentOf(RBNode<T> node) {
		return node != null ? node.parent : null;
	}

	private boolean isRed(RBNode<T> node) {
		return ((node != null) && (node.color == RED)) ? true : false;
	}

	private void setBlack(RBTree<T>.RBNode<T> node) {
		if (node != null) {
			node.color = BLACK;
		}
	}

	private void setRed(RBNode<T> node) {
		if (node != null) {
			node.color = RED;
		}
	}

新插入的结点都先插到叶子节点处

//先进行插入,再去满足性质
private void insert(RBNode<T> node) {
		int cmp;
		RBNode<T> y = null;
		RBNode<T> x = this.root;

		// 1、将红黑树当作一颗二叉查找树,将结点添加到二叉查找树中
		while (x != null) {
			y = x;
			cmp = node.key.compareTo(x.key);
			if (cmp < 0)
				x = x.left;
			else
				x = x.right;
		}

		node.parent = y;
		if (y != null) {
			cmp = node.key.compareTo(y.key);
			if (cmp < 0) {
				y.left = node;
			} else {
				y.right = node;
			}
		} else {
			this.root = node;
		}
		// 2、设置节点的颜色为红色
		node.color = RED;

		// 3、将他重新修正为一颗二叉查找树
		insertFixUp(node);
	}

下面对插入结点后的树进行修正,使其变成合法的红黑二叉树

private void insertFixUp(RBNode<T> node) {
		RBNode<T> parent, gparent;
		// 当父亲节点存在并且父亲节点是黑色,则不用考虑,不影响
		// 若父节点存在并且父节点是红色
		while (((parent = parentOf(node)) != null) && isRed(parent)) {
			/*
			 * 分几种情况考虑: 
			 * 1、叔叔红色 把叔叔、父亲变成黑,祖父变红(再往上判断祖父的父亲,循环) 
			 * 2、叔叔黑色
			 * (1.1)父亲是左孩子,新插入结点是左孩子 
			 * (1.2)父亲是左孩子,新节点是右孩子 
			 * (2.1)父亲是右孩子,新插入结点是右孩子
			 * (2.2)父亲是右孩子,新插入结点是左孩子
			 */
			gparent = parentOf(parent);
			//当父亲是左孩子
			if (parent == gparent.left) {
				RBNode<T> uncle = gparent.right;
				// case 1:叔叔结点是红色
				if ((uncle != null) && isRed(uncle)) {
					setBlack(uncle);
					setBlack(parent);
					setRed(gparent);
					node = gparent;
					continue;
				}

				//case2:叔叔是黑色,新节点是右孩子
				if(parent.right == node){
					RBNode<T> tmp;
					leftRotate(parent);
					tmp = parent;
					parent = node;
					node = tmp;
				}	//把这种情况变成case3
				
				// case3:叔叔是黑色,新节点是左孩子
				setBlack(parent);
				setRed(gparent);
				rightRotate(gparent);
			}else{
				//当父亲是右孩子
				RBNode<T> uncle = gparent.left;				
				//case1:叔叔结点是红色
				if((uncle!=null) && isRed(uncle)){
					setBlack(uncle);
					setBlack(parent);
					setRed(gparent);
					node = gparent;
					continue;
				}
				
				//case2:叔叔是黑色,当前结点是左孩子
				if(parent.left == node){
					RBNode<T> tmp;
					rightRotate(parent);
					tmp = parent;
					parent = node;
					node = tmp;
				}
				
				//case3:叔叔是黑色,当前节点是右孩子
				setBlack(parent);
				setRed(gparent);
				leftRotate(gparent);				
			}
		}

		//将根节点设置为黑色
		setBlack(this.root);
	}

插入操作相对于删除操作还要简单一点,这篇就到这,下篇学习删除操作

发布了45 篇原创文章 · 获赞 14 · 访问量 2487

猜你喜欢

转载自blog.csdn.net/qq_44357371/article/details/102888525