在大量数据中常用的查找数据的做法有四类:顺序查找,二分查找,二叉树查找(BST),红黑树查找(RBT)。
这四类查找方法分别对应着四种基本思想原理:
顺序查找 —— 无序简单查找
二分查找 —— 有序查找,每次折半搜索,插入数据费时
二叉树查找(BST) —— 不平衡二叉树有序查找,插入与搜索综合性能较优
红黑树查找(RBT) —— 平衡二叉树有序查找,插入与搜索综合性能最优
注:从上至下平均性能变优,算法难度增大,后三种查找为有序查找。
红黑树(RBT)性能分析
在编写红黑树实现前很有必要对其性能进行分析,通过与前三种算法的比较,分析出现RBT这种数据结构的原因。
算法 | 最坏查找 | 最坏插入 | 平均查找 | 平均插入 | 是否有序 |
---|---|---|---|---|---|
顺序查找 | N | N | N/2 | N | 否 |
二分查找 | lgN | N | lgN | N/2 | 是 |
二叉树查找(BST) | N | N | 1.39lgN | 1.39lgN | 是 |
红黑树查找(RBT) | 2lgN | 2lgN | 1.001lgN | 1.001lgN | 是 |
顺序查找
顺序查找是指对数据进行无序的从头至尾扫描,这种方法是最原始的最坏情况下查找目标数据需要遍历整个数组,平均查找需要遍历半个数组。而插入则需要完全遍历整个数组。
二分查找
二分查找是查找时间最短的方法,将数组每次以中间值判断,稳定查找lgN(注:lg = log2)时间。但二分查找的插入则需要花费N的复杂度级别的时间来完成。如果我们采用类似二分查找的方式进行插入,即:每次以中值判断最后找出要插入的点,这样看起来时间也是lgN,但对于数组而言插入意味着将之后的数字依次移位,这样更加得不偿失。如果你采用链表结构来实现二分查找,可以满足快速的插入,但是由于没有了索引,仍需遍历数组得到索引,所以二分查找的插入操作复杂度级别为N。
二叉树查找(BST)
二叉树查找是在二分基础上提出,为了解决二分查找的插入问题的一种新的查找模式,这种模式通过类似链表的树结构不断地插入数据达到有序,而其查找时间也在lgN复杂度左右,代码实现较为简单。在整个有序树中存在着固定的插入和查找的模式,最后树的形状受到插入数据的先后顺序影响。正因为树的形状受到随机因素影响,在最坏的情况下,这棵树只有左子树没有右子树(或只有右子树没有左子树)导致性能甚至不如二分查找。
红黑树查找(RBT)
红黑树查找是在二叉树查找的基础上,结合了二叉树中简洁高效的查找方法,又同时是一颗保证性能平衡树。在最坏的情况下,红黑树都远好于二叉树。在平均情况下的搜索,红黑树理论上只比二分查找差一点,这是因为红黑树数据结构的特性导致,我们可以认为红黑树的查找和二分查找是相同的。在插入的复杂度比较中比二分查找好太多,红黑树的算法思路是所有有序查找中最优的算法,用便于实现的平衡树高效地完成插入和搜索操作。
红黑树定义
满足下列条件的二叉树是红黑树:
- 红链接均为左链接
- 没有任何一个结点同时和两条红链接相连
- 该树完美黑色平衡,即任意叶结点到根结点的路径上的黑链接数量相同
红黑树(RBT)基本结点
package Tree;
public class RBT<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;
}
}
用boolean类型color变量表示该结点到其父节点的颜色。
RED结点左右旋转
private 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 = size(h.left) + size(h.right) + 1;
return x;
}
private 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 = h.left.N + h.right.N + 1;
return x;
}
private void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
新结点插入规则
在红黑树中插入一个新结点要遵循下面的规则:
- 任何插入的结点的颜色都设为红色(该结点到父接点的链接为红色)
- 整棵树的根节点始终设为黑色
- 如果一个结点的右结点链接为红色,则进行左旋转
- 如果一个结点与左右子结点都为红链接,则将其与左右两个子结点链接变为黑色,该结点到父结点颜色变为红色
- 如果一个结点的左结点为红,左结点的左结点也为红,将原结点进行一次右旋转,然后执行第3条
- 如果一个结点的左结点为红,左结点的右结点也为红,将原结点的左结点进行一次左旋转,然后执行第4条
第1、2条表示了在进行插入旋转调整时要遵循的规定,第3条表示在简单插入不满足红链接均为左链接的定义时进行的操作。第4、5、6条表示不满足一个结点同时和两条红链接相连的情况时进行的操作,这三种情况涵盖了所有可能发生的错误情况。
新结点代码中调整步骤
上述插入规则看起来非常复杂,但是只要我们在代码中依次进行这三步检查和调整,就可以避免所有错误情况的发生:
- 如果右子结点是红色的而左子结点是黑色的,左旋转。
- 如果左子结点是红色的且它的左子结点也是红色的,右旋转。
- 左右子结点均为红色,进行颜色转换。
复杂的逻辑通过这三步判断和检查会被处理的完全正确。分析一下这三步操作的核心思想,第一步为了将所有可以放在左边的红色右结点左移,这是最简单的操作,让尽量多的红结点左移可以让上面插入规则中第6条退化为第5条,第3条直接解决。完成第一步调整后只存在4和5两条的情况,第二步的两条连续左红调整是为了让第5条退化为第4条。第三步的颜色转化针对第4条情况。所以,3、4、5、6这四条错误情况在这三步过程中完全被解决。
红黑树(RBT)插入算法
public void put(Key key, Value val) {
root = put(root, key, val);
root.color = BLACK;
}
private Node put(Node root, Key key, Value val) {
if (root == null)
return new Node(key, val, 1, RED);
int comp = key.compareTo(root.key);
if (comp < 0)
root.left = put(root.left, key, val);
else if (comp > 0)
root.right = put(root.right, key, val);
else
root.val = val;
if (isRed(root.right) && !isRed(root.left))
root = rotateLeft(root);
if (isRed(root.left) && isRed(root.left.left))
root = rotateRight(root);
if (isRed(root.left) && isRed(root.right))
flipColors(root);
root.N = size(root.left) + size(root.right) + 1;
return root;
}
红黑树(RBT)删除算法
红黑树(RBT)删除算法策略:从根节点开始,在递归的过程中,通过结点的变换让每个当前结点都是一个非单独结点,即该结点上至少有一个红链接。这样可以保证在删除的时候不会直接删除一个单独的黑链接而导致树不平衡,这样确实会使得整棵树的红黑性变得混乱,可能出现一个结点上连接了多个红链接的情况,但是我们最后会通过balance方法递归的整理所有的结点颜色,确保正确性。
为了便于描述,我们将一个红链接连接的两个结点看成是一个包含两个结点的大结点,也称之为2结点,即包含2个结点的结点。因为删除2结点中的任意一个结点不会影响红黑树的平衡性,但如果我们删除了没有任何红链接的结点(也就是1结点)则肯定会影响平衡性。上段中说,在递归的过程中要通过结点的变换让每个当前结点至少有一个红链接(至少为2结点,也可以是3结点或更多),为了达到这个目的,我们有三种情况。
- 如果当前结点的左子结点不是1结点,完成。
- 如果当前结点的左子结点是1结点而它的亲兄弟节点不是1结点,将左子结点的兄弟结点中的一个结点移动到左子结点中。
- 如果当前结点的左子结点和它的亲兄弟结点都是1结点,将左子结点、父结点中最近的结点(因为我们保证了每个结点都不是1结点,所以父节点必不是1结点)、左子结点最近的兄弟结点合并为一个3结点,而父结点从2结点变为1结点或从3结点变为2结点。
这三种方法都可以达到我们的目的,但是更具体的,3的做法比2的做法对整棵树的影响更大,所以我们优先按照1、2、3的顺序来进行结点的变换。
删除算法所需的辅助函数
/**
* 此函数用于把父节点变为黑色,两个子结点变为红色,与flipColors函数功能正好相反
* 可以理解成是将三个结点(一个父结点和两个子结点)合并成含三个结点的大结点
*
* @param h
*/
private void reFlipColors(Node h) {
h.color = BLACK;
h.left.color = RED;
h.right.color = RED;
}
/**
* 获取以root为根的最小结点,就是整棵树最左边的结点
*
* @param root
* @return
*/
private Node min(Node root) {
if (root.left == null)
return root;
return min(root.left);
}
/**
* 获取相对应key的结点
*
* @param root
* @param key
* @return
*/
private Value get(Node root, Key key) {
if (root == null)
return null;
int comp = key.compareTo(root.key);
if (comp == 0)
return root.val;
else if (comp < 0)
return get(root.left, key);
else
return get(root.right, key);
}
/**
* 将h结点按照上面调整的三条步骤进行调整
*
* @param h
* @return
*/
private Node balance(Node h) {
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;
}
删除最小值算法——DeleteMin
删除最小值的算法思路是:一路向左判断,如果该结点的”左子结点”和”左子结点的左子结点”都是1结点则采用3种变换方式构建2结点或3结点。删除最小值算法给出了每一步的注释,删除最大值算法和删除算法具体的步骤含义留给大家自己思考。
private Node moveRedLeft(Node h) {
reFlipColors(h);
//让父结点为黑链接,两个子结点为红链接
//即将1结点变为3结点——上文中的第3步做法
if (isRed(h.right.left)) {
h.right = rotateRight(h.right);
h = rotateLeft(h);
}
//如果原结点的右结点的左结点是红色
//表示原结点的左子结点的兄弟结点是2结点或3结点
//采用上文第2步做法,借一个结点,将左子结点变化为2结点
//这里不需要将reFlipColors的颜色变动改回
//reFlipColors的颜色变动在最后的balance方法会被调整
return h;
}
public void deleteMin() {
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
//如果根结点的两个子结点都为黑色,则将根结点暂时设置为红色
//这样可使根结点不为1结点,便于递归
root = deleteMin(root);
if (root != null)
root.color = BLACK;
//将根结点颜色换回黑色,满足红黑树定义
}
private Node deleteMin(Node h) {
if (h.left == null)
return null;
//递归结束点,找到了要删除的左结点,置为null
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
//如果"左子结点"和"左子结点的左子结点"都为黑色则调用函数
//调用moveRedLeft,使得左子结点变成2结点或3结点
h.left = deleteMin(h.left);
return balance(h);
//调整整棵树的红黑颜色情况,使其满足红黑树定义
}
删除最大值算法——DeleteMax
删除最大值和最小值并不是left和right交换的简单情况,因为红黑树的红链接仅能出现在左边,所以删除最大值(删除最右边结点)的算法有些不同。但是还是按照上面的三条情况来处理结点。原理上是相同的。
private Node moveRedRight(Node h) {
reFlipColors(h);
if (isRed(h.left.left))
h = rotateRight(h);
return h;
}
public void deleteMax() {
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = deleteMax(root);
if (root != null)
root.color = BLACK;
}
private Node deleteMax(Node h) {
if (isRed(h.left))
h = rotateRight(h);
if (h.right == null)
return null;
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRight(h);
h.right = deleteMax(h.right);
return balance(h);
}
删除算法
删除算法非常复杂,但是我们前面做了足够的铺垫,所以总代码量看起来将不会太多。删除算法的策略是:按照key和当前结点的key比较,进行删除最小值的操作或删除最大值的操作,当相等的时候进行二叉查找树中替换后继结点的做法。最后用balance方法保持节点颜色正确。注意,删除的结点要在树中存在。
public void delete(Key key) {
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = delete(root, key);
if (root != null)
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);
}
红黑树其他操作算法
除去插入和删除算法,红黑树的其他算法与二叉查找树算法完全相同,因为红黑树本身就是一颗二叉树,只不过在黑色链接上具有平衡树的特性而更高效。可以发现,在插入操作中,查找位置替换和插入新结点部分和二叉树插入是完全相同的,在删除部分查找删除键和后继结点交换也是和二叉树相同的。