这里主要解决关于大名鼎鼎的红黑树的下面的问题
(1)理解什么是红黑树
(2)实现红黑树操作(添加和删除)
(3)性能测试
一、理解什么是红黑树
1、理解什么是2-3树
2、红黑树和2-3树的等价性
1、首先,理解红黑树之前,我们先来解决第一个问题,什么是2-3树?
这样的一颗树就是2-3树
(1)每个节点可以有1个或者2个元素
(2)满足二叉搜索树的性质
(3)是一种绝对平衡树
2-3树是怎么维持绝对平衡的过程(这里只讲插入过程,删除过程类似)
根据二叉搜索树的特点,进行数据的判断,找到插入的位置,假设在根节点40中插入30,二叉搜索树的插入如下
但是这样会破坏2-3树的平衡性,所以应该这样形成一个节点
如果再插入一个25呢
这样破坏了绝对平衡性,所以应该这样
但是这样一个节点就有3个元素了,所以这个时候就会分解
这样即具有二叉搜索树的性质,又维持了平衡性
如果再插入26 22呢
插入26如下
插入22
很明显,需要进行分解,分解后如下
但是这样就不在满足绝对的平衡性了,所以需要合并
这样不断在插入元素,然后根据情况进行分解合并可以使2-3树是一个绝对平衡的数
2、红黑树和2-3树的等价性
可以这么说,红黑树是2-3树的一种二叉表示的数,实现的原理就是如果2-3数中一个节点具有2个元素,那在红黑树的表现如下
也就是红色的节点表示与其父节点在2-3树中是一个节点,这里并且要保证红色节点的左倾性,如下
这个时候需要进行左旋转,变成这样
现在我把开始的那张2-3树的图转换为红黑树,如下
上的数看起来很奇怪,但是如果认真看这个就是一棵二叉树,不过我为了表示红黑树和2-3树的等价性故意画成这样,所以说红黑树等价于2-3树,那么红黑树就具有如下的特点
(1)红黑树不是很平衡的数,准确的说,红黑树不能保证这个二叉树是平衡二叉树,但是红黑树是“黑平衡”的二叉树,也就是每一个叶子节点到根的路径经过的黑色节点数目是一样的(这个由与2-3树等价可以知道,因为2-3树是绝对平衡的)
(2)由于红黑树不是一棵树不是一棵平衡二叉树,那么如果只论查询数据的效率而已,红黑树并不比AVL树的性能更优。红黑树存在的意义在于,他在统计意义上一般是最优的(添加,删除,查询)
二、实现红黑树的操作
1、添加
(1)在一个左右子树都为空的节点上添加
添加在左边:
添加在右边:保证红色节点的左倾性,需要左旋转
(2)在左子节点的左边添加:右旋转后颜色翻转(1-3-4-5)
(3)在左子节点的右边添加:左旋转后右旋转,最后颜色翻转(1-2-3-4-5)
(4)左节点为红色节点,然后在右节点添加(1-4-5)
由上得知,上面的步骤无非只有3个子步骤:颜色翻转,左旋转,右旋转
代码如下
/**
* 颜色翻转,这里不舍临界条件,比如node是否为空,node左右子树是否为空是因为使用这个方法前就已经知道node和node的左右子树不为空
* @param node
*/
private void turnColor(Node<K, V> node)
{
node.color=Color.Red;
node.left.color=Color.Black;
node.right.color=Color.Black;
}
/**
* 左旋转
* @return
*/
private Node<K, V> leftRotate(Node<K, V> node)
{
//正常的旋转过程
Node<K, V> node1=node.right;
Node<K, V> node2=node1.left;
node1.left=node;
node.right=node2;
//维护颜色
node1.color=node.color;
node.color=Color.Red;
return node1;
}
/**
* 右旋转
* @param node
* @return
*/
private Node<K, V> rightRotate(Node<K, V> node)
{
//正常的选择过程
Node<K, V> node1=node.left;
Node<K, V> node2=node1.right;
node1.right=node;
node.left=node2;
//维护颜色
node1.color=node.color;
node.color=Color.Red;
return null;
}
添加操作:
/**
* 向AVL树中插入新的节点,插入之后进行平衡使这个数一直是平衡二叉树
* @param node
*/
public void add(K key)
{
add(key, null);
}
public void add(K key, V value)
{
//使用递归来实现
root=add(root, key, value);
}
private Node<K, V> add(Node<K, V> node, K key, V value)
{
//如果这个节点为空,那么新建一个节点返回给它的父节点
if(node==null)
{
size++;
return new Node<K, V>(key, value);
}
//如果key小于目前节点,进入其左子树
if(key.compareTo(node.key)<0)
node.left=add(node.left, key, value);
//如果key大于目前节点,进入其右子树
else if(key.compareTo(node.key)>0)
node.right=add(node.right, key, value);
else
node.value=value;
//颜色维护过程
//如果node的右节点是红色,同时左节点不是红色,那么发生左旋转
if(isRed(node.right)&&!isRed(node.left))
node=leftRotate(node);
//如果node的左节点是红色,左节点的左节点也是红色,那么右旋转
if(isRed(node.left)&&isRed(node.left.left))
node=rightRotate(node);
//如果左节点是红色,右节点也是红色,那么颜色翻转
if(isRed(node.left)&&isRed(node.right))
turnColor(node);
return node;
}
当然,需要在Node中添加一个项纪录节点颜色
//枚举颜色
enum Color{
Red,Black;
}
//新节点开始都是叶子节点,而叶子节点一开始都是红色,所以默认是红色
private Color color=Color.Red;
2、删除(由于删除操作和添加操作时基本一样的,所以这里就不再说明)
/**
* 删除节点
* @param key
*/
public void remove(K key)
{
root=remove(root,key);
}
private Node<K, V> remove(Node<K, V> node,K key)
{
//如果node为空,表示找不到
if(node==null)
return null;
Node<K, V> retNode;
//key小于node的key,那么在其左子树上删除
if(key.compareTo(node.key)<0)
{
node.left=remove(node.left,key);
retNode=node;
}
//key大于node的key,那么在其右子树上删除
else if(key.compareTo(node.key)>0)
{
node.right=remove(node.right, key);
retNode=node;
}
//key等于node的key,那么删除node,然后维护这个新树
else
{
size--;
//表示删除node
//node左子树如果是null,那么返回其右子树即可
if(node.left==null)
retNode=node.right;
//node左子树不为null,右子树是null,那么返回其左子树即可
else if(node.right==null)
retNode=node.left;
//node的左右子树都不为null,那么返回node节点中序遍历的后继节点,同时维护这棵新树
else {
//node的右子节点
Node<K, V> nodeMidNext=node.right;
//如果nodeRight的左子树为空,那nodeRight就是node节点中序遍历的后继节点
if(nodeMidNext.left==null)
{
//维护新数
nodeMidNext.left=node.left;
retNode=nodeMidNext;
}
//如果其左子树不为null,那么nodeRight的最左子树就是node节点中序遍历的后继节点
else
{
//这个后继节点的父节点
Node<K, V> nodeMidNextParent=nodeMidNext;
nodeMidNext=nodeMidNext.left;
//找到这个后继节点
while(nodeMidNext.left!=null)
{
nodeMidNextParent=nodeMidNext;
nodeMidNext=nodeMidNext.left;
}
//维护新数
nodeMidNextParent.left=nodeMidNext.right;
nodeMidNext.left=node.left;
nodeMidNext.right=node.right;
retNode=nodeMidNext;
}
}
}
//颜色维护过程
//如果node的右节点是红色,同时左节点不是红色,那么发生左旋转
if(isRed(retNode.right)&&!isRed(retNode.left))
retNode=leftRotate(retNode);
//如果node的左节点是红色,左节点的左节点也是红色,那么右旋转
if(isRed(retNode.left)&&isRed(retNode.left.left))
node=rightRotate(retNode);
//如果左节点是红色,右节点也是红色,那么颜色翻转
if(isRed(retNode.left)&&isRed(retNode.right))
turnColor(retNode);
return retNode;
}
三、性能测试
测试代码:
public static void main(String[] args) {
BST<Integer, Object> bst=new BST<>();
AVLTree<Integer, Object> avl=new AVLTree<>();
RBTree<Integer, Object> rbt=new RBTree<>();
//BST
long startTime = System.nanoTime();
for(int i=1;i<=10000;i++)
{
bst.add(i);
}
for(int i=1;i<=10000;i++)
{
bst.get(i);
}
long endTime = System.nanoTime();
System.out.println("BST: "+(endTime-startTime)/(100000000-0.0)+"s");
//AVL
startTime = System.nanoTime();
for(int i=1;i<=10000;i++)
{
avl.add(i);
}
for(int i=1;i<=10000;i++)
{
avl.get(i);
}
endTime = System.nanoTime();
System.out.println("AVL: "+(endTime-startTime)/(100000000-0.0)+"s");
//RBTree
startTime = System.nanoTime();
for(int i=1;i<=10000;i++)
{
rbt.add(i);
}
for(int i=1;i<=10000;i++)
{
rbt.get(i);
}
endTime = System.nanoTime();
System.out.println("RBTree: "+(endTime-startTime)/(100000000-0.0)+"s");
}
测试结果:
上面并不是说明红黑树在任何情况下效率都比AVL树的效率高,而是说明在整体的统计意义上红黑树的效率好一点(毕竟红黑树不是平衡二叉树,如果单论查找效率要比AVL树差一点)