平衡二叉搜索树
平衡二叉搜索树(Balanced Binary Search Tree)的每个节点的左右子树高度差不超过 1,它可以在 O(logn) 时间复杂度内完成插入、查找和删除操作,最早被提出的自平衡二叉搜索树是 AVL 树。
AVL 树在执行插入或删除操作后,会根据节点的平衡因子来判断是否平衡,若非平衡则执行旋转操作来维持树的平衡,本文主要是对红黑树相关的讲解,如果大家感兴趣可以去了解一下 AVL 树相关的知识,在这里不做赘述。
2-3 搜索树
标准二叉树中的节点称为 2-节点(含有一个键和两个指针),为了保证二叉搜索树的平衡性,需要增加一些灵活性,增加 3-节点(含有两个键和三个指针)。2-节点的指针和 3-节点的指针对应的区间大小关系如下:
-
2-节点:left 指针指向的左子树中所有的键值均小于当前节点键值,right 指针指向的右子树中所有的键值均大于当前节点键值
-
3-节点:left 指针指向的左子树中所有的键值均小于当前节点键值,mid 指针指向的中子树中所有的键值在当前节点的两个键值之间,right 指针指向的右子树中所有的键值均大于当前节点的键值
我们先来看一下一棵2-3搜索树的样例:
这里需要注意:2-3搜索树无论如何增删节点,它始终都能维持完美的平衡性,看下文时要牢记这一点。
3-节点的引入是如何保证树平衡的?
我们以插入键值 11 的节点为例,它会查找到该键对应的位置为 12 节点的左子树,如果我们未引入3-节点,那么树高会增加,如下图中(1)所示,而我们引入3-节点后,我们只需将2-节点替换为3-节点,而不会导致树高的增加,2-3搜索树依然完美平衡,如下图中(2)所示:
我们接着看,如果我们插入的键值为 26,它会查找到该键的位置为 19 和 24 这个3-节点,因为这个节点中已经没有多余的键的位置了,所以26键只能放在右子树的位置,使得树高加一。不过我们可以通过巧妙的方法,先将该节点转换为4-节点,一个4-节点又能转换成 3 个 2-节点, 我们将这 3 个2-节点的根节点(4-节点的中键值)插入到原来的父节点中,那么树高仍能保持不变(完美平衡),如下图所示:
虽然我们临时借助了4-节点,但是最终变换完毕后,整棵树还是依然是由2-节点和3-节点组成的。
我们再插入键值为4的节点会如何呢?它会查找到1和3这个3-节点的位置,还是采用相同的办法,将其转换成4-节点,但是转换完后准备将它插入到原来的父节点时,会发现父节点也是3-节点,这就导致我们不得不再次使用同样的方法,这样推广到一般情况,需要不断地分解临时的4-节点并将其中键插入到更高的父节点中,直到遇到一个2-节点并将其转换成不需要分解的3-节点或者到达为2-节点的根节点,如下图所示:
那如果处理到根节点时,根节点仍然为3-节点呢?其实原理是一样的,只不过根节点没有父节点,不需要再将4-节点的中键向上合并,转换成3个2-节点即可,相应地树高会加一。
在以上所述的情况中,2-3树的插入算法使树本身结构发生的改变是局部的,除了相关的节点和引用之外,不必修改和检查树的其他部分,这些局部变换不会影响到树的全局有序性和平衡性,在插入的过程中,2-3树始终是完美平衡二叉树。
左倾红黑树(LLRBT)
学会了 2-3 树我们先来学习一种简单的红黑树,左倾红黑树,它是经典红黑树的变体,相对更容易实现。它的基本思想是用标准的二叉搜索树和“颜色信息”来表示2-3树:其中的链接分为两种,红链接将两个2-节点连接起来构成一个3-节点,黑链接是2-3树中的普通链接,如下图所示:
我们规定被红色链接指向的节点为红色节点,被黑色链接指向的节点为黑色节点,从上图中我们也能够发现:如果我们将红链接“横过来”,它和3-节点的表示是一样的,这就是在左倾红黑树中用颜色信息表示3-节点的方式,确切地说,3-节点是由两个2-节点组成,并且其中一个2-节点被另一个2-节点用红链接左引用。
可以说左倾红黑树就是一棵2-3搜索树,它满足如下条件:
-
根节点是黑色的
-
红链接均为左引用
-
没有任何一个节点同时和两条红链接相连
-
任意叶子节点到根节点路径上的黑色节点数量相同,即该树是黑色平衡的
2-3搜索树始终能保持完美平衡,那么任意叶子节点到根节点的距离是相等的。左倾红黑树又是一棵2-3搜索树,其中的黑链接是2-3搜索树中的普通链接,那么左倾红黑树中被黑色链接引用的黑色节点也必然是完美平衡的,所以任意叶子节点到根节点路径上的黑色节点数量必然相同。
性质我们已经理解了,接下来我们看看左倾红黑树的代码实现:
节点定义
上文我们已经规定,被红链接引用的节点为红色,被黑色链接引用的节点为黑色,我们在节点中添加color
字段来标记颜色:True
表示红色,False
表示黑色,定义如下:
public class LeftLeaningRedBlackTree {
private static final boolean RED = true;
private static final boolean BLACK = false;
static class Node {
int key;
int value;
boolean color;
Node left;
Node right;
public Node(int key, int value, boolean color) {
this.key = key;
this.value = value;
this.color = color;
}
}
/**
* 判断节点是否为红色
*/
private boolean isRed(Node node) {
if (node == null) {
return false;
} else {
return node.color == RED;
}
}
}
旋转
插入和删除操作可能会使左倾红黑树出现红链接右引用或者左右引用都是红链接的情况,造成黑色不平衡,为了始终满足左倾红黑树的性质,我们需要通过旋转操作进行修复。
左旋
如果有红链接右引用,我们可以通过左旋操作将其调整为红链接左引用,过程如下图所示:
代码如下:
/**
* 左旋
*/
private Node rotateLeft(Node node) {
Node newRoot = node.right;
node.right = newRoot.left;
newRoot.left = node;
newRoot.color = node.color;
node.color = RED;
return newRoot;
}
旋转完成后可以发现,左旋是将两个键中较小的作为根节点转变为较大的作为根节点。
右旋