二叉树虽然能解决查找的问题,但是在某些情况下,比如当任意相邻的两个元素之间是按照升序或者降序插入的话,那么得到的树将是一个链表的形状。当然这只是一种极端的情况,只是用来说明,树的形状对二叉查找的效率有很大的影响。
这里的平衡二叉树能够让树保持完美的平衡,不会出现极端的成为链表等,而且我们也会试图去保持树的完美平衡。
它实现这个平衡目的的方式是:对于 3-结点,如果再向其插入数据,就会使其变成 4-结点。4-结点会被分解成一棵子树,并将子树的根节点“进位”父结点。所以,如果进位并导致了根结点变成了 4-结点,那么根结点就会被分解成一棵子树。也就是说,实际上平衡查找树的根结点是会发生变化的,而且这种变化的趋势总是倾向于,将树的左子树中的高度与右子树的高度相差。
平衡查找树的定义:平衡查找树是一种二叉排序树,其中每一个节点的左子树和右子树的高度相差至多等于 1.
我们将树上结点的左子树深度减去右子树深度的值称为平衡因子,那么平衡二叉树上所有结点的平衡因子只可能是 -1, 0, 1.
1、2-3查找树
- 将一棵标准二叉查找树的结点称为 2-结点(含有一个键和两条链接),3-结点是指含有两个键和三条链接的结点;
- 一棵 2-3 查找树或为一棵空树,由 2-结点和 3-结点构成,并且左链接执行的键都小于该结点,右链接指向的键都大于该结点
如图所示是一课 2-3查找树,在它的内部插入新的结点的时候比平衡查找树略复杂。这里面包含一种类似“进位”的方式,即如果在某个结点插入了新的结点之后它变成了 4-结点(3 个键 4 个链接),那么需要将其分解并将分解后的中间结点传递到父结点中。如果父结点成了 4-结点,那么就继续讲父结点分解,并向上传递,一直打到最顶层的根结点为止。
上图就是关于 2-3 树插入结点的时候结点的分裂和融合的示意图,实际上我们可以借助上面的思考方式来考虑 2-3 树的一些操作的执行过程。即如果是以上面的 4-结点为例的话,从左到右四个链接分别代表的含义是:小于 A 的结点构成的树,A 和 C 之间的结点构成的树,C 和 E 之间的结点构成的树以及大于 E 的结点构成的树。
2、红黑二叉查找树
2.1 定义
如图所示,红黑二叉查找树只是将 2-3 查找树中的 3-结点转表示成由一条左斜的红色链接相连的两个 2-结点。这种表示方法的优点是我们可以无需修改就使用二叉查找树的 get() 方法。
有些图在表示红黑树的时候,没有使用红连接,而是使用红色的结点,比如这里的 a 就应该被表示成一个红色的结点。虽然表达方式不一样,但是实际意义是一样的—— a 和 b 构成了 2-3 树中的 3 结点。
红黑二叉查找树的等价定义:
- 红链接均为左链接;
- 树的根为黑色,叶子节点(即所谓的空节点)为黑色;(根必须为黑色的,但可以是 3-结点)
- 没有任何一个结点同时和两个红链接相连;
- 该树是完美黑色平衡,即任意空链接到根结点的路径上的黑链接数量相同。
实际上红黑二叉查找树将红链接画平时,一棵红黑树就是一棵 2-3 树。我们在二叉树的基础之上增加了一个红黑的条件,这使得红黑树的红链接既能够表示 2-3 树中的 3-结点的情形,又能具有二叉树的只具有两个分支的性能。即它既满足了我们对效率的要求,又能够比较方便地实现。
注意下面的关于红黑二叉树的操作中,除了完成基本的功能之外,是如何维护树的平衡的。
2.2 红黑树的表示
与二叉树类似,只是这里需要在二叉树的基础之上增加一个字段 color 用于表示红黑二叉查找树的结点的颜色:
public class RedBlackBST<Key extends Comparable<Key>, Value> {
private final static boolean RED = true;
private final static boolean BLACK = false;
private Node<Key, Value> root;
private static class Node<Key extends Comparable<Key>, Value> {
Key key;
Value value;
Node leftChild, rightChild;
int N;
boolean color; // 增加了一个颜色字段
public Node(Node leftChild, Key key, Value value, int n, boolean color, Node rightChild) {
this.key = key;
this.value = value;
this.leftChild = leftChild;
this.rightChild = rightChild;
N = n;
this.color = color;
}
}
}
2.3 旋转
2.3.1 原理
在实现某些操作的时候(比如插入、删除等),会出现红链接在右侧或者两条连续的红链接的状况,在继续操作之前需要对这些情况进行修复,这就需要对其进行旋转。将右侧的红链接转换为左侧的红链接的过程叫做左旋,相反的操作叫做右旋。
图 左旋的示意图
图 右旋的示意图
其实所谓的左旋和右旋是比较容易理解的,你可以将其想象成旋转就是将 E 和 S 中的那个“低”一些的“拎”起来的过程。比如左旋时,就像将S“拎”起来,拎起来之后,E 和 S 之间的那个结点,自然地从 S 滑落到了 E 上面。
旋转的时候有几个地方需要注意:旋转之前和之后根结点的颜色的变化,两个结点的颜色变化,两个结点的子结点的变化。
2.3.2 代码实现
下面是左旋和右旋的代码实现,以及容易出现错误的一些地方:
private Node<Key, Value> rotateLeft(Node<Key, Value> node) {
Node<Key, Value> right = node.rightChild;
// node.color = BLACK; // 错误:结点的颜色应该是链接到结点的链接的颜色,所以旋转之后node的结点的颜色变成红色才对
node.color = RED;
// right.color = RED; // 错误:旋转之之后right结点的颜色是node的颜色,本质上旋转只是这棵子树内部的变化,对外不变
right.color = node.color;
right.leftChild = node; // right的左结点变成了node
node.