[数据结构]:红黑树(一)
红黑树是二叉查找树的一种。二叉查找树的存在就是为了加快查找速度,时间复杂度为O(logn);为了防止二叉查找树在较坏的情况下退化为链表使时间复杂度变为O(n),引入了平衡二叉树(AVL树)和红黑树。
AVL平衡二叉树对二叉查找树的平衡要求较高,左右子树的高度差不能超过1,这会使得在插入/删除操作中频繁进行树的旋转,使得效率反而降低。红黑树对树的平衡度没有AVL树严格,但也大致保持了树的平衡,在实际中应用得更广泛,包括HashMap中冲突链表,TreeMap中的排序,优先级队列PriorityQueue中的排序等等,都广泛使用红黑树。
红黑树的性质&定义
- 每个结点不是红色就是黑色;
- 根节点是黑色;
- 叶结点都是黑色;(叶结点都为NIL)
- 如果一个结点是红色,那么它的两个子结点都是黑色(即不可能有两个相连的红色结点);
- 对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
如下图所示,即为一棵红黑树
满足上面的5个性质&定义
红黑树的插入操作
C 新增的当前结点;
P 父亲结点;
U 叔叔结点;
G 祖父结点
Case 1: C == root
- 新增当前结点,默认为红色;
- 修改C当前结点颜色为黑色。
Case 2: C.parent.color == Black
新增当前结点C,颜色为红色。
Case 3: C.parent.color == Red && C.uncle.color == Red
父结点和叔叔结点都为红色。
- 新增当前结点C,默认为红色;
- P父结点变成黑色;
- U 叔叔结点变成黑色;
- G 祖父结点变成红色;
- 【如果祖父结点变红导致红黑树不满足性质】
将祖父结点作为新增结点C地柜处理【1, 2, 3, 4情况处理】
Case 4: C.parent.color == Red && (C.uncle.color == Black or C.uncle == nil)
Case 4.1 CPG三点一线
- 以P父亲结点为圆心,旋转G祖父结点;
- 变色父亲结点P、祖父结点G两个结点
Case 4.1.1
PCG三点一线,且在左边:以当前结点的父结点为支点,右旋;
Case 4.1.2
PCG三点一线,且在右边:以当前结点的父结点为支点,左旋;
Case 4.2 CPG三角关系
- 以C当前新增结点为圆心,旋转P点;
- 按照情况4.1处理
Case 4.2.1
C在右边,PG在左边:以当前结点为支点,左旋,变为Case 4.1.1,再处理;
Case 4.2.2
C在左边,PG在右边:以当前结点为支点,右旋,变为Case 4.1.2,再处理;
采用以下示例详细演示
原红黑树(以下所有红黑树省略NIL结点)
1. 插入一个新元素6
2. Case 3
将当前结点的父结点和叔叔结点变为黑色;祖父结点变为红色;
将指针定义到祖父结点
继续调整
3. Case 4.2
以当前结点为支点,旋转
当前结点设为原来的父结点,继续调整
4. Case 4.1
将父结点变为黑色,祖父结点变为红色
以当前结点的祖父结点作为根结点右旋旋
5. 结束调整
检查当前红黑树不满足上面的3种调整规则,调整结束。满足红黑树的5个性质。
代码
public class RedBlackTree {
private final int R = 0;
private final int B = 1;
private Node root = null;
class Node {
int data; // 存具体的数字
int color;
Node left;
Node right;
Node parent;
public Node(int data) {
this.data = data;
this.color = R; // 新插入的结点默认为红色的
}
}
public void insert(int data) {
if (root == null) { // 插入第一个结点,新建根结点
root = new Node(data);
root.color = B;
} else {
insert(root, data);
}
}
public int find(int data) {
Node res = find(root, data);
if (res == null) return -1;
else return res.data;
}
private void insert(Node root, int data) {
if (root.data < data) {
if (root.right == null) {
Node node = new Node(data);
root.right = node;
node.parent = root;
adjust(node); // 从新插入的结点开始,向上调整,直到重新满足红黑树的性质
} else {
insert(root.right, data);
}
} else {
if (root.left == null) {
Node node = new Node(data);
root.left = node;
node.parent = root;
adjust(node);
} else {
insert(root.left, data);
}
}
}
private int validate(Node node) {
// node为空结点或则为根结点 或则
// 最多只有两层 或则
// 当前结点是黑结点(说明该结点之上都是满足红黑树性质的)
// 则一定是合法的红黑树
if (node == null || node.parent == null || node.parent.parent == null || node.color == B) {
return 0;
}
Node grandParent = node.parent.parent;
Node parent = node.parent;
Node uncle; //叔叔结点
uncle = (parent == grandParent.left)? grandParent.right: grandParent.left;
// Case 3: 父结点是红色 && 叔叔结点是红色
if (parent.color == R && uncle != null && uncle.color == R) {
return 1;
}
// Case 4: 父结点是红色 && 叔叔结点不存在(视为NIL结点,黑色)或者叔叔结点存在且为黑色
if (parent.color == R && (uncle == null || uncle.color == B)) {
// Case 4.1.1: CPG在同一条直线上,且在左边
if (node == parent.left && parent == grandParent.left) return 2; // CPG同在左边
// Case 4.1.2: CPG在同一条直线上,且在右边
else if (node == parent.right && parent == grandParent.right) return 3; // CPG同在右边
// Case 4.2: CPG成三角形
else return 4; // CPG成三角形
}
// 其他情况满足红黑树的性质,无需调整
return 0;
}
private void adjust(Node node) {
int condition;
while ((condition = validate(node)) != 0) {
Node grandParent = node.parent.parent;
Node uncle = (node.parent == grandParent.left)? grandParent.right: grandParent.left;
// Case 3
if (condition == 1) {
node.parent.color = B; //父结点变为黑色
uncle.color = B; //叔叔结点变为黑色
grandParent.color = R; //祖父结点变为红色
node = grandParent; // 当前结点设为祖父结点,继续调整
if (node.parent == null) node.color = B; // 到达了根结点,设置根结点的颜色为黑色
continue;
}
// Case 4.1.1
if (condition == 2) {
rotate1(node);
}
// Case 4.1.2
if (condition == 3) {
rotate2(node);
}
// Case 4.2
if (condition == 4) {
rotate(node);
}
node = node.parent; // 设置当前结点为原来的父结点,继续向上调整
if (node.parent == null) { // node到达了根结点,则重新设置根结点及设根结点为黑色
node.color = B;
root = node;
}
}
}
/***
* CPG在同一条直线上,左边,进行调整
* @param node
*/
private void rotate1(Node node) {
Node grandParent = node.parent.parent;
Node ancestor = grandParent.parent; // 祖父结点之上的结点
Node parent = node.parent;
grandParent.left = parent.right; // 父结点的右子树变为祖父结点的左子树
if (parent.right != null) parent.right.parent = grandParent.left;
parent.right = grandParent; // 祖父结点变为父结点的右子树
grandParent.parent = parent;
parent.color = B; // 父结点修改为黑色
grandParent.color = R; // 祖父结点修改为红色
parent.parent = ancestor; // 将旋转过后的子树连接到上一级结点上
if (ancestor != null && grandParent == ancestor.left) ancestor.left = parent;
if (ancestor != null && grandParent == ancestor.right) ancestor.right = parent;
}
/***
* CPG在同一条直线上,右边,进行调整
* @param node
*/
private void rotate2(Node node) {
Node grandParent = node.parent.parent;
Node ancestor = grandParent.parent; // 祖父结点之上的结点
Node parent = node.parent;
grandParent.right = parent.left; // 父结点的左子树变为祖父结点的右子树
if (parent.left != null) parent.left.parent = grandParent;
parent.left = grandParent; // 祖父结点变为父结点的左子树
grandParent.parent = parent;
parent.color = B; // 父结点修改为黑色
grandParent.color = R; // 祖父结点修改为红色
parent.parent = ancestor; // 将旋转后的子树连接到上一级结点上
if (ancestor != null && grandParent == ancestor.left) ancestor.left = parent;
if (ancestor != null && grandParent == ancestor.right) ancestor.right = parent;
}
/***
* CPG成三角形
* @param node
*/
private void rotate(Node node) {
Node grandParent = node.parent.parent;
Node parent = node.parent;
// Case 4.2.1: C在右边,PG在左边,以当前结点为支点左旋
if (parent == grandParent.left && node == parent.right) {
parent.right = node.left; // node的左子树变为parent的右子树
if (node.left != null) node.left.parent = parent;
parent.parent = node; // parent变为node的左子树
node.left = parent;
grandParent.left = node; // node变为grandParent的左子树
node.parent = grandParent;
rotate1(parent); // 旋转过后,就是Case 4.1.1的情况,以原结点的父结点为当前结点旋转
}
// Case 4.2.2: C在左边,PG在右边,以当前结点为支点右旋
if (parent == grandParent.right && node == parent.left) {
parent.left = node.right; // node的右子树变为parent的左子树
if (node.right != null) node.right.parent = parent;
parent.parent = node; // parent变为node的右子树
node.right = parent;
grandParent.right = node; // node变为grandParent的右子树
node.parent = grandParent;
rotate2(parent); // 旋转过后,就是Case 4.1.2的情况,以原结点的父结点为当前结点旋转
}
}
private Node find(Node root, int data) {
if (root == null) return null;
if (root.data == data) return root;
else if (data < root.data) {
return find(root.left, data);
}
else {
return find(root.right, data);
}
}
public void preOrder(Node root) {
if (root != null) {
System.out.print(root.data);
if(root.color == B) System.out.print("B");
else System.out.print("R");
System.out.print(" ");
preOrder(root.left);
preOrder(root.right);
}
}
public static void main(String[] args) {
RedBlackTree redBlackTree = new RedBlackTree();
for (int i = 1; i <= 10; i++) {
redBlackTree.insert(i);
}
redBlackTree.preOrder(redBlackTree.root);
int data = 8;
int res = redBlackTree.find(data);
System.out.println();
System.out.println("查找 data = " + data + " 的结果为: " + res);
data = 11;
res = redBlackTree.find(data);
System.out.println();
System.out.println("查找 data = " + data + " 的结果为: " + res);
}
}
下面以插入1 ~ 10的每一个步骤,图示说明:
① 插入1
② 插入2
③ 插入3
符合 Case 4.1.2
④ 插入4
符合 Case 3
⑤ 插入5
符合 Case 4.1.2
⑥ 插入6
符合 Case 3
⑦ 插入7
符合 Case 4.1.2
⑧ 插入8
符合 Case 3
将当前结点的祖父结点设为新的当前结点,符合 Case 4.1.2
⑨ 插入9
符合 Case 4.1.2
⑩ 插入10
符合 Case 3
将当前结点的祖父结点设为新的当前结点,符合 Case 3
将根结点设为黑色
最终,依次插入1 ~ 10的数字后得到的红黑树如下图所示:
由一颗二叉树的前序遍历序列和中序遍历序列可以唯一确定一颗二叉树。
红黑树是一种特殊的二叉查找树,因此这棵树的中序遍历序列是:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
对得到的这棵树进行前序遍历,并输出结点对应的颜色(R为红色,B为黑色):
由此可以确定得到的红黑树的结构,与上面一步一步分析得到的是相同结构的红黑树。可知,本文给出的红黑树的插入操作的代码是正确的。
红黑树的查找操作
跟普通的二叉查找树一样。
在红黑树累中增加如下代码:
public int find(int data) {
Node res = find(root, data);
if (res == null) return -1;
else return res.data;
}
private Node find(Node root, int data) {
if (root == null) return null;
if (root.data == data) return root;
else if (data < root.data) {
return find(root.left, data);
}
else {
return find(root.right, data);
}
}
测试代码如下:
public static void main(String[] args) {
RedBlackTree redBlackTree = new RedBlackTree();
for (int i = 1; i <= 10; i++) {
redBlackTree.insert(i);
}
redBlackTree.preOrder(redBlackTree.root);
int data = 8;
int res = redBlackTree.find(data);
System.out.println();
System.out.println("查找 data = " + data + " 的结果为: " + res);
data = 11;
res = redBlackTree.find(data);
System.out.println();
System.out.println("查找 data = " + data + " 的结果为: " + res);
}
找到输出,找不到输出-1.
测试结果如下: