红黑树定义:
- 性质1:结点有两种颜色,黑色和红色;
- 性质2:根结点是黑色,所有空结点为黑色;
- 性质3:任意空结点到根结点路径上黑色结点数量相同(黑色平衡条件);
- 性质4:每个红结点的两个子结点一定都是黑色,即父子结点不能同时为红色。
满足以上条件的二叉树就可以称为红黑树;
红黑树的性质
红黑树可以理解为一种特殊平衡二叉树;放宽了对平衡条件的限制(保证性质3即可);但树的最大高度也不会超过 2lg(N)。
平衡二叉树要求任意节点的左子树和右子树的高度差不超过1,因此平衡二叉树的高度不超过 lg(N)。
如何构造红黑树
红黑树的性质很简单,性能也非常好,但是实现起来的难度却很大,这也是这篇文章的意义。
在实现红黑树之前我们先来学习一种新的查找树,2-3 查找树;这将有助于简化对红黑树的理解。
1、2-3 查找树
2-3 查找树中包含二结点和三结点:
二结点:一个结点包含两个子结点;
三结点:一个结点包含三个子结点;
2-3 查找树也是一颗平衡树,即所有的空结点到根结点的路径长度相等;
构建2-3查找树(插入)
构建一颗有序的2-3查找树需要考虑以下几种情况:
- 1、向2-结点插入新结点;
- 2、向3-结点插入新结点;
- 3、向父节点为2-结点的3-结点插入新结点;
- 4、向父结点为3-结点的3-结点插入新结点;
(1)向2-结点插入新结点
我们只要把2-结点替换为3-结点,即可保证树的平衡性;
(2) 向3-结点插入新结点
向3-结点插入新结点方法是构造一个临时的4-结点,然后将4-结点分解为三个2-结点,不会破坏平衡性;
(3)向父节点为2-结点的3-结点插入新结点
(4)向父结点为3-结点的3-结点插入新结点
同理,仍然是先构造临时4-结点,然后将4-结点分解;分解后父节点会形成一个新的4-结点,继续分解。
小结:对于2-3查找树保持平衡性的原理就是,对于会破坏树的平衡性的新结点,我们通过局部变换将其与父节点构造成3-结点或4-结点以保证新结点不会改变树的高度,然后在通过分解4-结点使左右子树的整体高度+1,而且不会破坏平衡性。
2、构造红黑树
在理解了2-3查找树后,我们再来构造红黑树;
首先,之所以2-3查找树对红黑树的理解有帮助是因为红黑树可以看成是特殊的2-3查找树;
黑结点 对应于 2-结点;
红结点与其父结点 对应于 3-结点;
为了使红黑树构造更加简单,我们增加两条限制:
- 红结点只能存在于左子结点中;
- 插入结点均为红色,然后对结点颜色进行调整;
插入
对于构造红黑树时的插入操作,我们依旧可以分两种情况:
- 向黑结点(2-结点)插入;
- 向红结点或红结点的父节点(3-结点)插入;
注意:插入操作中会涉及到结点颜色转换的过程,如果无法理解可以将结点颜色看成指向该节点的链的颜色,然后自己重新画一下就很好理解了
(1)向黑结点左侧插入
类似于2-3查找树,向2-结点插入后构成一个3-结点以保证平衡性;
(2)向黑结点右侧插入
通过旋转操作来保证我们新增的限制条件,红结点只能在左子结点处;
如果对于旋转操作中结点颜色交换无法理解,我们可以将结点的颜色理解为指向结点的链的颜色,这样就容易理解结点颜色的变换了;
左旋操作代码:
// 左旋
private Node leftRotate(Node node){
Node temp = node.right;
node.right = temp.left;
temp.left = node;
temp.color = node.color;
node.color = RED;
return temp;
}
(3)向红结点左侧插入
向红结点插入新结点类似于向3-结点插入新结点临时构造一个4-结点再分解的过程,颜色转换就相当于分解4-结点过程(因为我们将红结点和其父节点类比于2-3查找树中3-结点,那么两个红结点和其父节点相当于一个4-结点);
/**
* 右旋
*/
private Node rightRotate(Node node){
Node temp = node.left;
node.left = temp.right;
temp.right = node;
temp.color = node.color;
node.color = RED;
return temp;
}
/**
* 转换节点颜色
*/
private void flipColor(Node node){
node.color = !node.color;
node.left.color = !node.left.color;
node.right.color = !node.right.color;
}
(4)向红结点右侧插入
先对结点b进行左旋操作,然后在对结点d进行右旋操作,最后进行颜色转换;
(5)向红结点父结点插入新结点
以上就是构造一颗红黑树的插入操作了,因为红黑树的平衡条件限制和额外条件限制(红结点只能在左子节点出现),每次插入新结点都会进行平衡操作(左旋,右旋和颜色转换称为平衡操作),保证了不会出现其他情况。 平衡操作会随着插入操作递归向上最终保证所有树的平衡性,在插入完成后,将根节点颜色设置为黑色即可。
插入操作完整代码
public void put(Key key, Value value){
if(key == null)
throw new IllegalArgumentException("key不能为空!");
root = put(root,key,value);
// 将根节点颜色设置为黑色
root.color = BLACK;
}
private Node put(Node node, Key key, Value value){
if(node == null)
return new Node(key,value,1,RED);
// 插入新节点
int cmp = key.compareTo(node.key);
if(cmp < 0)
node.left = put(node.left,key,value);
else if(cmp > 0)
node.right = put(node.right,key,value);
else
node.val = value;
// 处理节点颜色,保证平衡性
if(isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
if(isRed(node.left) && isRed(node.left.left))
node = rightRotate(node);
if(isRed(node.left) && isRed(node.right))
flipColor(node);
node.size = size(node.left)+size(node.right)+1;
return node;
}
/**
* 判断节点颜色, 空节点默认黑色
* @return
*/
private boolean isRed(Node node){
if(node == null)
return false;
return node.color;
}
查找
红黑树也是一颗标准二叉树,因此查找操作和二叉树查找操作一样;
删除最小结点
对红黑树的删除操作相对插入操作会比较麻烦,因为删除操作可能会破坏树的平衡性;因此会有一些额外的平衡操作;同时为了便于理解,我们先来介绍删除最小结点的方法;
删除最小结点就是删除最左侧的结点(树的有序性保证这一点);
在删除最小结点的过程中,同样可能会破坏树的平衡性,而2-3查找树同样为我们提供了避免平衡性被破坏的方法,对于一个3-结点或临时4-结点来说,删除其中一个结点并不会影响树的高度,树的平衡性也不会被破坏;对于红黑树如果将红结点和父节点看做3-结点,那么只要保证被删除的结点是红结点或红结点的父节点(可以通过旋转操作将父节点转换为红结点)就不会破坏平衡性。
因此在删除最小结点的操作中,我们首先自顶向下构造3-结点或临时4-结点,核心就是保证当前结点及其左子结点不是2-节点,因为如果查找命中被删除的就是当前结点,未命中被删除的就是左子结点(没有红结点与其构成3-节点或4-结点)。删除后在通过上面的平衡操作自底向上对树进行平衡。
在结点转换过程中会遇到以下3种情况:
在保证当前结点不是2-结点情况下:
-
1、当前结点左子结点不是2-结点,继续;
-
2、当前结点左子结点是2-结点,而其兄弟结点不是2-结点,可以通过旋转操作向兄弟结点借一个结点过来;
-
3、左子结点和右子结点都是2-结点,那么就令其与父节点共同组成一个临时4-结点;
当查找命中后找到最小结点,这时之前的操作保证了当前最小结点是一个红结点(3-结点),直接删除(使用空连接代替),然后在自底向上对树重新进行平衡操作。因为所有操作至于左侧结点及左节点的兄弟结点有关,因此操作的时间复杂度可以接受。
/**
* 删除最小节点
* 核心:在保证当前节点不是二节点的前提下,
* 对三种情况讨论:
* 1、左子节点不是二节点;
* 2、左子节点是二节点,右子节点不是二节点;
* 3、左右子节点都是二节点
*/
private Node deleteMin(Node node){
// 红黑树的性质决定左子节点为空的时候,右子节点一定为空
if(node.left == null)
return null;
//左子节点为二节点(黑节点)
if(!isRed(node.left) && !isRed(node.left.left)){
flipColor(node);
// 右子节点为三节点(红节点)
if(isRed(node.right.left)){
node.right = rightRotate(node.right);
node = leftRotate(node);
flipColor(node);
}
}
node.left = deleteMin(node.left);
// 处理节点颜色,保证平衡性
if(isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
if(isRed(node.left) && isRed(node.left.left))
node = rightRotate(node);
if(isRed(node.left) && isRed(node.right))
flipColor(node);
return node;
}
删除
在删除最小结点的基础上,对任意节点的删除有两种情况:叶子结点和非叶子结点;
对于非叶子结点,查找命中后可以直接用右子树的最小结点代替当前节点,然后删除右子树的最小结点;
对于叶子结点直接删除即可;
但是在删除操作中依旧要保证当前节点及下一次递归的结点不是2-结点;
- 如果key小于当前结点key,则下一次递归结点为当前结点左子结点;
- 如果key=当前结点key,则命中;
- 如果key大于当前结点key,则下一次递归结点为当前结点右子结点;
- 这里可以用删除最小结点同样方法保证前节点及下一次递归的结点不是2-结点,但是注意当处理右子节点时要先对左子结点进行一次右旋判断
if(isRed(node.left)) node = rightRotate(node);
,保证左子节点和父节点为独立的两个节点,否则将对左子树的平衡性造成破坏,在删除最小结点时由于红结点只出现在左子结点的条件限制,所以不必对右子节点判断
/**
* 删除任意节点
* 核心:保证当前节点不是二节点,其他操作和删除最小节点相同
* @param key
*/
public void delete(Key key){
if (key == null) throw new IllegalArgumentException("argument to delete is null");
if (!contains(key)) return;
if(!isRed(root.right) && !isRed(root.left))
root.color = RED;
root = delete(root,key);
if(root != null)
root.color = BLACK;
}
private Node delete(Node node, Key key){
if(key.compareTo(node.key) < 0){
//左子节点为二节点(黑节点)
if(!isRed(node.left) && !isRed(node.left.left)){
flipColor(node);
// 右子节点为三节点(红节点)
if(isRed(node.right.left)){
node.right = rightRotate(node.right);
node = leftRotate(node);
flipColor(node);
}
}
node.left = delete(node.left,key);
} else {
// 保证左子节点和父节点为独立的两个节点
if(isRed(node.left))
node = rightRotate(node);
//命中且为叶子结点
if(key.compareTo(node.key)==0 && node.right==null)
return null;
// 右子节点是2-节点
if(!isRed(node.right) && !isRed(node.right.left)){
flipColor(node);
// 左子节点不是2-节点,从左子节点借一个节点保证右子节点不是2-节点
if(isRed(node.left.left)){
node = rightRotate(node);
flipColor(node);
}
}
if(key.compareTo(node.key)==0){
Node min = findMin(node.right);
node.key = min.key;
node.val = min.val;
node.right = deleteMin(node.right);
} else {
node.right = delete(node.right,key);
}
}
// 处理节点颜色,保证平衡性
if(isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
if(isRed(node.left) && isRed(node.left.left))
node = rightRotate(node);
if(isRed(node.left) && isRed(node.right))
flipColor(node);
return node;
}
总结
以上就是对红黑树的插入、查找、和删除操作,红黑树的结构保证了高度不超过(2*lgN),同时避免了平衡查找树的过多的平衡操作,但是插入和删除操作相对复杂,本文是学习算法(第四版)这本书中的红黑树的学习笔记,如果有不理解的地方可以回到书中系统的学习一下