数据结构和算法—红黑树

学习数据结构和算法的里程是艰辛的,没有捷径,就是看书和看视频,再结合不断的敲代码去记忆,这里记录下红黑树的相关性质和Java代码实现。
红黑树也是一种二叉查找树,只不过它由2-结点和3-结点组成,通过2-结点和3-结点来保证平衡,结点的链接分为两种类型:红链接将两个2-结点连接起来构成一个3-结点,黑链接就是普通的链接。
什么是2-结点,3-结点?如下图(第一个为2-结点图,第二个为3-结点图)
在这里插入图片描述
在这里插入图片描述
其实就是由父结点指出的链接数量有多少个而定。
在红黑树中,我们将3-结点转为红链接表示:
在这里插入图片描述

一颗红黑树的结构:
在这里插入图片描述
它具有如下特点:
1.红链接均为左链接;
2.没有任何一个结点同时和两条红链接相连(会构成4-结点);
3.该树是完美黑色平衡的,即任意空链接到根结点的路径上黑链接的数量相同。

结点定义:

private class Node{
		Key key;
		Value value;
		Node left,right;
		int N;
		boolean color;
		public Node(Key key,Value value,int N,boolean color) {
			this.key=key;
			this.value=value;
			this.N=N;
			this.color=color;
		}		
	}

成员变量:

	private Node root;
	private static final boolean RED=true;
	private static final boolean BLACK=false;

它相比于普通二叉查找树来讲能实现更高效的平衡插入算法(维护树的平衡),原因在于当树出现不平衡时,可以进行左旋转、右旋转和颜色转换操作进行修复:
在这里插入图片描述
1.如果右子结点是红色的而左子结点是黑色的,进行坐旋转;
2.如果左子结点是红色的而且左子结点的左子结点也是红色的,进行右旋转;
3.如果做右子结点都为红色,进行颜色转换。

左旋转:

private Node rotateLeft(Node h) {
		Node e=h.right;
		h.right=e.left;
		e.left=h;
		e.color=h.color;
		h.color=RED;
		e.N=h.N;
		h.N=size(h.left)+size(h.right)+1;
		return e;
	}

右旋转:

private Node rotateRight(Node h) {
		Node e=h.left;
		h.left=e.right;
		e.right=h;
		e.color=h.color;
		h.color=RED;
		e.N=h.N;
		h.N=size(h.left)+size(h.right)+1;
		return e;		
	}

颜色转换:

private void flipColors(Node h) {
		h.color=RED;
		h.left.color=BLACK;
		h.right.color=BLACK;
	}

最坏情况下的树的高度:一颗大小为N的红黑树的高度不会超过2lgN,最坏情况下一颗红黑树所对应的2-3树中最左边的结点全部是3-结点而其余为2-结点。最左边的路径长度是只包含2-结点的路径长度(~lgN)的两倍。
平均情况下树的高度:一颗大小为N的红黑树,根结点到任意结点的平均路径长度为~lgN。
由此可得最坏情况下红黑树的查找和插入运行时间的增长数量级为2lgN,平均情况下为lgN。

红黑树的插入算法:
新加入元素默认是红结点,加入完后再进行旋转或者改变颜色操作来维持平衡。

public void put(Key key,Value val) {
		root=put(root,key,val);
		root.color=BLACK;
	}
	private Node put(Node h,Key key,Value val) {
		if(h==null)  return new Node(key,val,1,true);
		int v=key.compareTo(h.key);
		if(v<0)  h.left=put(h.left,key,val);
		else if(v>0)  h.right=put(h.right,key,val);
		else h.value=val;
		if(isRed(h.right)&&!isRed(h.left)) h=rotateLeft(h);
		if(isRed(h.left)&&isRed(h.left.left)) h=rotateRight(h);
		if(isRed(h.left)&&isRed(h.right)) flipColors(h);
		h.N=size(h.left)+size(h.right)+1;
		return h;		
	}

红黑树的查找算法:

public Value get(Key key) {
		return get(root,key);
	}
	private Value get(Node x,Key key) {
		if(x==null)  return null;
		int v=key.compareTo(x.key);
		if(v<0)  return get(x.left,key);
		else if(v>0)  return get(x.right,key);
		else return x.value;
	}
	public Key min() {
		return min(root).key;
	}
	private Node min(Node x) {
		if(x.left==null)  return x;
		return min(x.left);
	}
	public Key max() {
		return max(root).key;
	}
	private Node max(Node x) {
		if(x.right==null)  return x;
		return max(x.right);
	}
	public Key select(int k) {
		return select(root,k).key;
	}
	private Node select(Node x,int k) {
		if(x==null)  return null;
		int i=size(x.left);
		if(i>k)  return select(x.left,k);
		else if(i<k)  return select(x.right,k-i-1);
		else return x;
	}
	public int rank(Key k) {
		return rank(root,k);
	}
	private int rank(Node x,Key k) {
		if(x==null) return 0;
		int i=k.compareTo(x.key);
		if(i<0)  return rank(x.left,k);
		else if(i>0)  return size(x.left)+1+rank(x.right,k);
		else return size(x.left);
	}

红黑树的删除算法:
难点!!!
为了能在删除元素后维持红黑树的平衡特性,需要有非常大的开(dai)销(ma)去维护删除后的结构。
在删除元素时,最好的情况是当前结点为3-结点,那直接删除就好了,不会影响树的高度,如果是2-结点,需要从父结点或者左右子树中借结点变为3-结点再进行删除操作:
和父结点借结点变为红节点:

	private void moveflipColors(Node e) {//和父节点借节点给子节点,以及还回去
		e.color=!e.color;
		e.left.color=!e.left.color;
		e.right.color=!e.right.color;
	}

在删除左子结点的操作中向右子结点借结点:

	private Node moveRedLeft(Node e) {//把左子节点变成4-、3-节点
		moveflipColors(e);// 先从父节点中借一个
		if(isRed(e.right.left)) {//判断兄弟节点,如果是红节点,也从兄弟节点中借一个
			e.right=rotateRight(e.right);//各种借的操作
			e=rotateLeft(e);//....
			moveflipColors(e);//借完左子节点就不需要父节点的了 反转还回去
		}
		return e;
	}

删除红黑树中最小值:

	public void deleteMin() {
		if(!isRed(root.left)&&!isRed(root.right))
			root.color=RED;
		root=deleteMin(root);
		root.color=BLACK;
	}
	private Node deleteMin(Node x) {
		if(x.left==null)  return null;//判断一个节点是否可以删除,就是看该节点的左子节点是否为null
		if(!isRed(x.left)&&!isRed(x.left.left))// 判断x的左节点有没2-节点
			x=moveRedLeft(x);//都是2-就需要转移生成非2-节点,借红节点,删除时保证树高不变
		x.left=deleteMin(x.left);//最后会返回null上来删掉最小节点
		return balance(x);//   解除临时组成的4-节点
	}

删除元素后为保证平衡,需要检查结点:

	private Node balance(Node x) {
		if(isRed(x.right))  x=rotateLeft(x);
		if(!isRed(x.left)&&isRed(x.right))  x=rotateLeft(x);
		if(isRed(x.left)&&isRed(x.left.left))  x=rotateRight(x);
		if(isRed(x.left)&&isRed(x.right))  flipColors(x);
		x.N=size(x.left)+size(x.right)+1;
		return x;
	}

在删除右子结点的操作中向左子结点借结点:

	private Node moveRedRight(Node e) {
		moveflipColors(e);
		if(isRed(e.left.left)) {// 在这里对于兄弟节点的判断都是.left,
                                // 因为红色节点只会出现在左边
			e=rotateRight(e);
			moveflipColors(e);
		}
		return e;
	}

删除红黑树中最大值:

	public void deleteMax() {
		if(!isRed(root.left)&&!isRed(root.right))
		root.color=RED;
		root=deleteMax(root);
		root.color=BLACK;
	}
	private Node deleteMax(Node x) {
		if(isRed(x.left)) {
			/* 这里比deleteMin多了一步操作,因为右子节点从父节点中获得节点的时候,
	         * 我们需要将左边节点给于到右边节点,如果我们不移动的话,会破坏树的平衡
	         *          5,6
	         *      1,2     9       对于所展示的这个红黑树,如果不把5从左边移到右边的话,
	         *      我们会直接删除9,这样会导致树的不平衡,因为红节点总是在左边的,我们进行删除操作
	         *      的时候,直接将结点给予,只需要改变颜色即可,不需要移动
	         *                      对于红黑树而言,6是黑结点,再删除的时候,是不需要移动的,
	         *                      我们移动的是5这样的红结点
	         */
			x=rotateRight(x);
		}
		if(x.right==null)  return null;
		if(!isRed(x.right)&&!isRed(x.right.left))
			x=moveRedRight(x);
		x.right=deleteMax(x.right);
		return balance(x);
	}

删除操作(结合删除最小值和最大值的代码):

	public void delete(Key key) {
		if(!isRed(root.left)&&!isRed(root.right))
			root.color=RED;
		root=delete(root,key);
		root.color=BLACK;
	}
	private Node delete(Node e,Key key) {
		if(key.compareTo(e.key)<0) {
			if(!isRed(e.left)&&!isRed(e.left.left))  e=moveRedLeft(e);
			e.left= delete(e.left,key);
		}else {
			if(isRed(e.left))  e=rotateRight(e);
			if(key.compareTo(e.key)==0&&e.right==null)  return null;//
			if(!isRed(e.right)&&!isRed(e.right.left))  e=moveRedRight(e);
			if(key.compareTo(e.key)==0) {
				Node min=min(e.right);
				e.key=min.key;
				e.value=min.value;
				e.right=deleteMin(e.right);
			}
			else e.right=delete(e.right,key);
		}
		return balance(e);
	}

可以看到删除代码的操作及其复杂和难以理解,主要原因是为了维持红黑树的数据结构。可能会有人不懂从左/右子树中借结点的代码和从父结点中借结点的代码,这里放三张网上找的图个人觉得很生动,结合代码看下就能明白了。
从父结点中借:
在这里插入图片描述
从右子结点中借:
在这里插入图片描述
从左子结点中借:
在这里插入图片描述

判断是否平衡:

    // do all paths from root to leaf have same number of black edges?
    private boolean isBalanced() { 
        int black = 0;     // number of black links on path from root to min
        Node x = root;
        while (x != null) {
            if (!isRed(x)) black++;
            x = x.left;
        }
        return isBalanced(root, black);
    }

    // does every path from the root to a leaf have the given number of black links?
    private boolean isBalanced(Node x, int black) {
        if (x == null) return black == 0;
        if (!isRed(x)) black--;
        return isBalanced(x.left, black) && isBalanced(x.right, black);
    }
}

返回存入元素数量:

	public int size() {
		return size(root);
	}
	private int size(Node n) {
		if(n==null) return 0;
		return n.N;
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值