【数据结构】红黑树

二叉查找树

一棵二叉查找树(BST)是一棵二叉树,其中每个结点都含有一个Comparable的键(以及相关的值)且每个结点的键都大于其左子树中任意结点的键而小于右子树中任意结点的键
请添加图片描述

查找

根据二叉查找树的特点我们可以得到:如果树是空的,则查找未命中;如果被查找的键和根结点的键相等,查找命中,否则我们就(递归地)在适当的子树中继续查找。如果被查找的键较小就选择左子树,较大则选择右子树。对于未命中的查找,返回null。

插入

如果树是空的,就返回一个含有该键值对的新结点;如果被查找的键小于根结点的键,我们就继续在左子树中插入该键,否则在右子树中插入该键,同时要增加路径上每个结点中的计数器的值
请添加图片描述

删除

deleteMin()和deleteMax()

对于deleteMin(),我们不断深入根节点的左子树中直至遇见一个空链接,然后将其父节点指向该结点a的链接指向结点a的右子树,指向结点a的链接解除后,结点a会被垃圾收集器回收掉。deleteMax()方法和deleteMin()方法类似。

删除操作
  1. 将指向即将被删除的结点的链接保存为t;
  2. 将x指向它的后继结点min(t.right)
  3. 将x的右链接指向deleteMin(t.right)
  4. 将x的左链接(本为空)设为t.left
    请添加图片描述

通过上述操作,替换后仍然能够保证树的有序性。

分析

使用二叉查找树算法的效率取决于树的形状,当树的形状退化为链表,则相应的查找和插入效率会退化为N。相对于使用二分查找,二叉查找树的查找的效率不如二分查找,但是二叉查找树的插入效率为对数级别,这点高于二分查找。同时二叉查找树能保持键的有序性。

代码实现

public class BST<Key extends Comparable<Key>, Value> {
	private Node root;
	private class Node {
		private Key key;
		private Value value;
		private Node left, right;
		private int N;
		public Node(Key key, Value value, int N) {
			this.key = key;
			this.value = value;
			this.N = N;
		}
	}
	
	public int size() {
		return size(root);	
	}
	public int size(Node x) {
		if (x == null) return 0;
		else return x.N; 
	}

	public Value get(Key key) {
		return get(root, key);
	}
	
	private Value get(Node x, Key key) {
		if (x == null) return null;
		int cmp = key.compareTo(x.key);
		if (cmp < 0) return get(x.left, key);
		else if (cmp > 0) return get(x.right, key);
		else return x.value;  
	}

	public void put(Key key, Value val) {
		root = put(root, key, val);
	}
	
	private Node put(Node x, Key key, Value val) {
		if (x == null) return new Node(key, val, 1);
		int cmp = key.compareTo(x.key);
		if (cmp < 0) x.left = put(x.left, key, val);
		else if (cmp > 0) x.right = get(x.right, key, val);
		else x.val = x.val;
		x.N = size(x.left) + size(x.right) + 1;
		return x;
	}

	public Key min() {
		return min(root).key;
	}
	
	private Node min(Node x) {
		if (x.left == null) return x;
		return min(x.left);
	}

	public Key floor(Key key) {
		Node x = floor(root, key);
		if (x == null) return null;
		return x.key;
	}
	
	private Node floor(Node x, Key key) {
		if (x == null) return null;
		int cmp = key.compareTo(x.key);
		if (cmp == 0) return x;
		if (cmp < 0) return floor(x.left, key);
		Node t = floor(x.right, key);
		if (t != null) return t;
		else return x;
	}
	
	public Key select(int k) {
		return select(root, k).key;
	}

	public Node select(Node x, int k) {
		if (x == null) return null;
		int t = size(x.left);
		if (t > k) return select(x.left, k); 
		else if (t < k) return select(x.right, k - t - 1);
		return x;
	}

	public int rank(Key key) {
		return rank(key, root);
	}
	
	private int rank(Key key, Node x) {
		if (x == null) return 0;
		int cmp = key.compareTo(x.key);
		if (cmp < 0) return rank(key, x.left);
		else if (cmp > 0) return 1 + size(x.left) + rank(key, x.right);
		else return size(x.left); 
	}

	public void deleteMin() {
		root = deleteMin(root);
	}
	
	private Node deleteMin(Node x) {
		if (x.left == null) return x.right;
		x.left = deleteMin(x.left); 
		x.N = size(x.left) + size(x.right) + 1;
		return x;
	}

	public void delete(Key key) {
		root = delete(root, key);
	}

	private Node delete(Node x, Key key) {
		if (x == null) return null;
		int cmp = key.compareTo(x.key);
		if (cmp < 0) x.left = delete(x.left, key);
		else if (cmp > 0) x.right = delete(x.right, key);
		else {
			if (x.right == null) return x.left;
			if (x.left == null) return x.right;
			Node t = x;
			x = min(x.right);
			x.right = deleteMin(t.right);
			x.left = t.left;
		}
		x.N = size(x.left) + size(x.right) + 1;
		return x;
	}

	public Iterable<Key> keys(Key lo, Key hi) {
		Queue<Key> queue = new Queue<>();
		keys(root, queue, lo, hi);
		return queue;
	}
	
	private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
		if (x == null) return;
		int cmplo = lo.compareTo(x.key);
		int cmphi = hi.compareTo(x.key);
		if (cmplo < 0) keys(x.left, queue, lo, hi);
		if (cmplo <= 0 && cmphi >= 0) queue.enque(x.key);
		if (cmphi > 0) keys(x.right, queue, lo, hi);
	}

	public Iterable<Key> keys() {
		return keys(min(), max());
	}
}

红黑树

平衡查找树

理想情况下,我们希望能够保持二分查找树的平衡性。在一棵含有N个结点的树中,我们希望树高logN,这样我们就能保证所有查找都能在logN次内比较。

2-3查找树

为了保证树的平衡性,我们允许树的一个结点保存多个键。通常一棵标准的二叉查找树中的结点称为2-结点(含有一个键和两条链接),而现在我们引入3-结点(含有两个键和三条链接),它含有两个键和三个结点。

定义一棵2-3查找树或为一棵空树,或由以下结点组成:

  • 2-结点,左连接指向2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
  • 3-结点,左连接指向2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。

请添加图片描述

查找

查找算法和一般的二叉查找树算法一致,先将要查找的键和根结点中的键比较,如果它和其中任意一个相等,查找命中,否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这是个空链接,查找未命中。

向2-结点中插入新键

先进行一次未命中的查找,把新结点挂在树的底部。如果查找结束于一个2-结点,我们只要把这个结点替换为3-结点,将要插入的键保存在其中即可。
请添加图片描述

向一棵只含有一个3-结点的树中插入新键

先临时将新键存入该结点中,使之成为一个4-结点。它很自然地拓展了以前的结点并含有3个键和4条链接。然后将其转换为一棵由3个2-结点组成的2-3树,其中一个结点(根)含有中键,一个结点含有3个键中的最小者(和根结点的左链接相连),一个结点含有3个键的最大者(和根结点的右链接相连)。这棵树既是一棵含有3个结点的二叉查找树,同时也是一棵完美平衡的2-3树。

请添加图片描述

向一个父结点为2-结点的3-结点中插入新键

和向3-结点插入新键类似,不过此时我们不会为中键创建一个新结点,而是将其移入原来的父结点中,指向原3-结点的一条链接替换为新父结点中的原中键左右两边的两条链接,并分别指向两个新的2-结点。
请添加图片描述

向一个父结点为3-结点的3-结点中插入新键

和上述情况类似,不断向上分裂,直到遇到一个2-结点并将其替换为一个不需要分裂的3-结点,或者是到达3-结点的根。如果从插入结点到根结点的路径上全都是3-结点,将该根结点分解为3个2-结点,中键作为根结点,并使树高加1。

请添加图片描述

局部变换

将一棵4-结点分解为一棵2-3树可能由6种情况。这个4-结点可能是根结点,可能是一个2-结点的左子结点或者右子节点,也可能是一个3-结点的左子结点、中子结点或者右子结点。2-3树插入算法的根本在于这些变换都是局部的:除了相关的结点和链接之外不必修改或者检查树的其他部分。
请添加图片描述

全局性质

局部变换不会影响树的全局有序性和平衡性:任意空链接到根结点的路径长度都是相等的。与标准二叉查找树由上向下生长不同,2-3树的生长是由下向上的。

红黑树

红黑树的思想是使用标准的二叉查找树实现,同时使用一些额外信息表示2-3树,红链接将两个2-结点连接起来构成一个3-结点,黑链接则是2-3树中的普通链接。确切地说,我们将3-结点表示为由一条左斜的红色链接(两个2-结点其中之一是另一个的左子结点)相连的两个2-结点。

定义

红黑树是含有红黑链接并满足下列条件的二叉查找树:

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

如果将红链接画平,那么所有的空链接到根结点的距离都将是相同的。

请添加图片描述

旋转

我们某些操作可能会出现红色右链接或者两条连续的红链接,但在操作完成前我们可以通过旋转修复。

  1. 向单个2-结点插入新键,如果新键小于老键,我们只需要新增一个红色的结点即可,新的红黑树和单个3-结点完全等价,如果新键大于老键,那么新增的红色结点将会产生一条红色的右链接,我们需要使用root=rotateLeft(root)将其旋转为红色左链接并修正根结点的链接。
  2. 向树底部的2-结点插入新键,用和二叉查找树相同的方式向一棵红黑树中插入一个新键会在树底部新增一个结点,但总是用红链接将新结点被它的父结点相连,如果它的父结点是一个2-结点,那么刚才讨论的两种处理方法仍然适用。
  3. 向一棵双键树(一个3-结点)中插入新键。1)如果新键大于原树中的两个键,因此它被连接到3-结点的右链接,此时树是平衡的,根节点为中间大小的键,它有两条红链接分别和较大和较小的结点相连。如果我们将两条链接的颜色都由红变黑,那么我们就得到了一棵由三个结点组成、高为2的平衡树。2)如果新键小于原树中的两个键,它会被连接到最左边的空连接,这样就产生了两条连续的红链接,此时我们只需要将上层的红链接右旋转即可得到第一种情况。3)如果新键介于原树中的两个键之间,这又会产生两条连续的红链接,一条红色左链接接一条红色右链接,此时我们只需要将下层的红链接左旋转即可得到第二种情况。
颜色转换

如果一个结点的两个子节点的颜色为红色,我们需要把这两个子节点的颜色变为黑色,同时将它自己的颜色变为红色,这个过程需要向上传递,并将高度+1。

删除操作

删除最小键
从树底部的3-结点中删除键是很简单的,但2-结点不然。从2-结点中删除一个键会留下一个空结点,如果替换为一个空链接,会破坏树的完美平衡性。为了保证我们不会删除一个2-结点,我们沿着左链接向下进行变换,确保当前结点不是2-结点(可能是3-结点,也可能是临时的4-结点)。如果根是2-结点且它的两个子结点都是2-结点,我们可以直接将这三个结点变为一个4-结点;否则我们需要保证根结点的左子结点不是2-结点,如有必要我们可以从它右侧的兄弟结点借一个键来。在沿着左链接向下的过程中,保证以下情况之一成立:

  1. 如果当前结点的左子结点不是2-结点,完成;
  2. 如果当前结点的左子结点是2-结点,而它的亲兄弟不是2-结点,将左子结点的兄弟结点的一个键移动到左子结点中
  3. 如果当前结点的左子结点和它的亲兄弟结点都是2-结点,将左子结点、父结点中的最小键和左子结点最近的兄弟结点合并为一个4-结点,使父结点由3-结点变为2-结点或者由4-结点变为3-结点。

在查找路径上进行和删除最小键相同的变换同样可以保证在查找过程中任意当前结点均不是2-结点。如果被查找的键在树的底部,我们可以直接删除它,如果不在,我们需要将它和它的后继结点交换。

public class RedBlackBST<Key extends Comparable<Key>, Value> {
	private static final boolean RED = true;
	private static final boolean BLACK = FALSE;

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

	private boolean isRed(Node x) {
		if (x == null) return false;
		return x.color == RED;
	}

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

	Node rotateRight(Node h) {
		Node x = h.left;
		h.left = x.right;
		x.right = h;
		x.color = h.color;
		h.color = RED;
		x.N = h.N;
		h.N = 1 + size(h.left) + size(h.right);
		return x;
	}
	
	private void flipColors(Node h) {
		h.color = RED;
		h.left.color = BLACK;
		h.right.color = BLACK;
	}
	
	public void put(Key key, Value val) {
		root = put(root, key, val);
		root.color = BLACK;
	}
	
	private Node put(Node x, Key key, Value val){
		if (h == null) return new Node(key, val, 1, RED);
		int cmp = key.compareTo(h.key);
		if (cmp < 0) h.left = put(h.left, key, val);
		else if (cmp > 0) h.right = put(h.right, key, val);
		else h.val = 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;
	}

	private Node moveRedLeft(Node h) {
		flipColors2(h); // 将h.left或者h.left的子结点之一变红
		if (isRed(h.right.left)) {
			h.right = rotateRight(h.right);
			h = rotateLeft(h);
		}
		return h;
	}
	private Node moveRedRight(Node h) {
		flipColors3(h); // 将h.right或者h.right的子结点之一变红
		if (isRed(h.left.left)) {
			h = rotateRight(h);
		}
		return h;
	}

	public void deleteMin(){
		if (!isRed(root.left) && !isRed(root.right)) root.colot = RED;
		root = deleteMin(root);
		if (!isEmpty()) root.colot = BLACK;
	}

	private Node deleteMin(Node h) {
		if (h.left == null) return null;
		if (!isRed(h.left) && !isRed(h.left.left)) h.moveRedLeft(h);
		h.left = deleteMin(h.left);
		return balance(h); 
	}

	private Node balance(Node x){
		if (isRed(h.right)) h = rotateLeft(h);
		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 void delete(Key key) {
		if (!isRed(root.left) && !isRed(root.right)) root.color = RED;
		root = delete(root, key);
		if (!isEmpty()) root.color = BLACK;
	}

	private Node delete(Node h, Key key) {
		if (key.compareTo(h.key) < 0) {
			if (!isRed(h.left) && !isRed(h.left.left)) {
				h.moveRedLeft(h);
			}
			h.left = delete(h.left, key);
		} else {
			if (!isRed(h.left)) h = rotateRight(h);
			if (key.compareTo(h.key) == 0 && (h.right == null))
				return null;
			if (!isRed(h.right) && !isRed(h.right.left)) {
				h = moveRedRight(h);
			} 
			if (key.compareTo(h.key) == 0) {
				h.val = get(h.right, min(h.right).key);
				h.key = min(h.right).key;
				h.right = deleteMin(h.right);
			} else {
				h.right = delete(h.right, key);
			}
		}
		return balance(h);
	}
}

以上内容取自《算法4》,感兴趣的可以购原书阅读

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值