前言
还是一样,这里的红黑树重点讲述插入代码的实现 ,如果对红黑树的删除感兴趣,可以去翻阅其他资料。
在数据结构专栏中已经对 AVL 树的旋转调整做了分析和讲解,这里红黑树也会使用到旋转调整的代码,就不讲述旋转代码的实现,大家如果对旋转不熟悉,可以打开这个文章 JavaDS ------ AVL 树
概念
红黑树是一种二叉搜索树 ,每一个结点增加了一个存储位置来表示结点的颜色(RED 红色,BLACK 黑色) ,通过对任何一条从根结点到叶子的路径上各个结点的着色方式的限制,红黑树确保没有一条路径会比其他路劲长出两倍 ,这样就保证红黑树是接近平衡的。
性质
1.根节点是黑色的
2.每个结点不是红色就是黑色
3.如果一个节点是红色的,则它的两个孩子结点是黑色的【没有2个连续的红色节点】
注意是不会存在两个连续的红色结点,但是可以存在连续的黑色结点
4.对于每个结点,从该结点到其所有后代叶结点的简单路径 上,均包含相同数目的黑色结点
这个路径的终点条件是最后一个非空结点的左右孩子为空
5.每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
分析
为什么红黑树能保证最长路劲的结点个数一定不会超过最短路径的结点个数的两倍?
首先由于红黑树的性质三,我们可以知道不会存在两个连续的红色结点,但是黑色结点可以连续存在,并且由于性质四每条路劲上的黑色结点的数量是相同的,所以我们可以这样推断出,最短的路径上所有的结点都是黑色的,最长的路径上结点是红黑交替出现的。
例如上图中 root 到 A1 这条路径就是最短路径, root 到 B1 这条路径就是最长路径,最长路径确实最多是最短路劲的两倍
数学角度分析,因为最短路径的黑色结点都是连在一起的,加上最短路径一共有 x 个黑色结点,x 也是最短路径长度,最长路径就是每一个黑色结点接上一个红色结点,那么最长路径的长度为 2x其实现实情况中不太可能出现一条路径上全是黑色结点,或多或少都会带一些红色结点,这也就导致最短路径其实比 x 要大,但是最长路径最长也就是 2x ,所以红黑树能保证最长路劲的结点个数一定不会超过最短路径的结点个数的两倍
查找的时间复杂度为 O(log N)
假设一颗红黑树一共有N 个结点,红黑树最极端的情况就是全是黑色结点,设黑色结点个数为 x,即x = N,那么极端情况下查找的时间复杂度为O(log x) = O(log N),这也是最快的时间复杂度红黑树假设能实现红黑交替,也就是全部路径都处于最长形态,由于是二叉树,先除去根节点和根节点左右两个红色结点,剩下的结点总数为 2(x-1) ,总结点数为 2(x-1) + 3 = 2x +1,则查找的时间复杂度为O(log N) = O(log (2x + 1)) = O(log x)
结点定义
因为红黑树会涉及到旋转,所以要存储父亲结点(还有一个原因是判断颜色,下面会提到)
还有一个存储位置来存放颜色
这里要注意构造方法中颜色应该先定义为红色,因为红黑树的性质四中每条路径的黑色结点数量是相同的,如果插入的是黑色结点,那和它同一层的也要插入黑色结点依此来维持红黑树的性质,所以为了简化难题,我们就将插入的结点先设置成红色,这样就能保证红黑树的性质四不被破坏。
java
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode parent;
Color color;
public TreeNode(int val) {
this.val = val;
this.color = Color.RED;
}
}
颜色使用枚举来定义
java
public enum Color {
RED,BLACK
}
插入实现
开始时就和二叉搜索树的插入结点一样,要注意根节点的黑色是的,由于设置插入的结点都是红色,所以直接插入到根节点的时候需要调成黑色。
java
public TreeNode root;
public boolean insert(int val) {
TreeNode node = new TreeNode(val);
if(root == null) {
root = node;
root.color = Color.BLACK;
return true;
}
TreeNode cur = root;
TreeNode parent = null;
while(cur != null) {
if(cur.val == val) {
//数值相同的结点不能插入。
return false;
} else if(cur.val > val) {
parent = cur;
cur = cur.left;
} else {
parent = cur;
cur = cur.right;
}
}
if(parent.val > val) {
parent.left = node;
} else {
parent.right = node;
}
node.parent = parent;
cur = node;
调整颜色
我们在插入结点后,要验证一下结点是否会破坏红黑树的性质,那会破坏什么性质呢?
很简单,你插入的是红结点,就可能会出现两个连续的结点是红色的情况
如何判断?
也很简单,我们有一个父亲结点的引用域,我们只要查看这个结点的父亲结点是不是也是红色,如果是就需要进行调整,在此之前,还要避免出现空指针异常,所以要先判断父亲结点是否为空。
什么时候需要调整颜色?
答: 当插入的结点的父亲结点也是红色就需要调整.
那这时候会有多少种情况?
插入的结点有四种情况,可以类比AVL 树,左左型,右右型,左右型,右左型,此时 其父亲结点的兄弟结点有两种情况存在或者不存在,那么一共有 8 中情况。
但是有一点可以确定的是父亲结点的父亲结点(grandparent)一定是黑色 的,那我们可以这样子,先讨论简单的情况,就是兄弟结点存在,如果兄弟结点存在就一定是红色的,现在就分成了两大类 ,父亲结点是grandparent 的左孩子还是右孩子,我们先这样子分,然后在细分。
java
TreeNode grandparent = parent.parent; //一定存在
if(parent == grandparent.left) {
} else {
//parent == grandparent.right
大类一
假设父亲结点是grandparent 的左孩子,并且兄弟结点存在且为红色:
因为要调整颜色, 必须保证每条路径上的黑色结点数据是相同的 ,这时候无论cur 的位置是哪里,将 grandparent 变成红色,然后将 parent 变成黑色,uncle 变成黑色就可以,由于 grandparent 变成红色,我们不确定 grandparent 有没有 父亲结点,如果有并且也是红色那就需要继续调整,所以我们需要将 cur 置为 grandparent ,再将 parent 置为 现在的 cur 的父亲结点。
看到这里想必大家知道要使用循环了,这里可以类比AVL 树的向上调整。
java
while(parent != null && parent.color == Color.RED) {
TreeNode grandparent = parent.parent; //一定存在
}
如果经过循环,根节点会不会被调成红色?
答案是会的,所以循环的最后,我们要确保根节点调成黑色,这也就是一行代码的事情。
在这个大前提下:我们还有一种情况就是 兄弟结点不存在:这也有两种情况:
如果 cur 位于 parent 的 右孩子:我们需要先对parent 进行 左旋,然后是不是就会变成类似右边的情况,这时候我们交换 parent 和 cur ,这样完全就是右边的情况。然后我们在处理右边的情况,先对grandparent 进行右旋,然后将 grandparent 调成红色,将 parent 调成黑色,这样就不会破坏路径上的黑色结点数目相同这一个性质。
这时候红黑树调整完成,直接退出循环即可。
有没有可能 uncle 存在,并且 uncle 还是黑色的?
不要认为这违反了所有的路径上黑色结点的数目是相同的性质,你可能会认为这明显少一个黑色结点,其实没有少,因为你认为少的黑色结点其实在 cur 的下面,这里给你一个例图:
其实有可能的,在第一个大类的情况下的第一个小情况,我们是不是要准备使用循环 :将 cur 置为 grandparent ,再将 parent 置为 现在的 cur 的父亲结点,这时候就又可能会出现 uncle 存在且 uncle 是黑色的情况:
这时候也很好处理,左边的情况,先对 parent 进行左旋,然后交换 cur 和 parent 两个结点,最后就变成右边的情况。最后就是处理右边的情况:对 grandparent 进行右旋,将 grandparent 置为红色,再将 parent 置为黑色。
上面这些话是不是很耳熟,是不是和上面的 uncle 结点不存在的处理方法一模一样,这样我们就合并同类项,将它们合并成同一种情况 :uncle 结点不存在或者uncle 结点为黑色
这种合并之后的过程图如下:
java
else {
// uncle 不存在或者 uncle 是 黑色结点,那就需要进行旋转调整
if(cur == parent.right) {
//先对 parent 进行左旋,然后交换 cur 与 parent ,之后就会变成下面的代码的情况
rotateLeft(parent);
TreeNode tmp = parent;
parent = cur;
cur = parent;
}
// cur 是 parent 的右孩子,对grandparent 进行右旋
rotateRight(grandparent);
parent.color = Color.BLACK;
grandparent.color = Color.RED;
//红黑树已经调整完成,直接退出循环,不用继续调整颜色
break;
}
大类二
父亲结点是grandparent 的右孩子,并且 uncle 结点存在且为红色,还是一样的处理方法,将 uncle 结点 和 parent 结点调成黑色,再将 grandparent 调成红色,然后 cur 置为 grandparent ,parent 置为现在 cur 的父亲结点,向上调整颜色即可。
这其实就是大类一的相反情况,只需要修改一些代码即可,这里就给出例图,不进行详细的文字说明。
java
} else {
//parent == grandparent.right
TreeNode uncle = grandparent.left;
if(uncle != null && uncle.color == Color.RED) {
uncle.color = Color.BLACK;
parent.color = Color.BLACK;
grandparent.color = Color.RED;
//继续向上调整
cur = grandparent;
parent = cur.parent;
} else {
// uncle 不存在或者 uncle 是 黑色结点,那就需要进行旋转调整
if(cur == parent.left) {
//先对 parent 进行右旋,然后交换 cur 与 parent变成下面的情况
rotateRight(parent);
TreeNode tmp = parent;
parent = cur;
cur = tmp;
}
//此时 cur 是 parent 的 右孩子
rotateLeft(grandparent);
grandparent.color = Color.RED;
parent.color = Color.BLACK;
//红黑树已经调整完成,直接退出循环,不用继续调整颜色
break;
}
}
}
最后,大家记得循环结束后,将 root 的颜色置为黑色。
验证红黑树
首先我们要先验证这是不是一颗二叉搜索树,也就是中序遍历是否有序,我们可以直接打印出来,也可以直接写一个方法返回 true 或者 flase
java
private static void isSort(TreeNode root) {
if(root == null) {
return;
}
isSort(root.left);
System.out.print(root.val + " ");
isSort(root.right);
}
接着我们要判断有没有存在两个连续的结点,我们可以使用递归,当结点为红色就看看它的父亲结点是不是也是红色的
java
private static boolean isRedNodes(TreeNode root) {
if(root == null) {
return true;
}
if(root.color == Color.RED && root.parent.color == Color.RED) {
System.out.println("出现两个连续的红色结点!!!");
return false;
}
return isRedNodes(root.left) && isRedNodes(root.right);
}
还要判断每条路径上的黑色结点数目是不是相同的,我们可以使用递归,传递三个参数,一个是root ,一个是当前计算的黑色结点数目(pathBlack),另一个参数是一条路径上的已知黑色结点数目(blackNodes),当递归的结点是黑色的时候,就让pathBlack自增,如果递归的结点左右孩子都为空则判断二者是否相同。
分别递归左数 再 递归右树
java
private static boolean isSamePathBlackNodes(TreeNode root,int pathBlack, int blackNodes) {
if(root == null) {
return true;
}
if(root.color == Color.BLACK) {
pathBlack++;
}
if(root.left == null && root.right == null) {
if(pathBlack != blackNodes) {
System.out.println("路径上的黑色结点数目不相同!!!");
return false;
}
}
return isSamePathBlackNodes(root.left,pathBlack,blackNodes)
&& isSamePathBlackNodes(root.right,pathBlack,blackNodes);
}
最后我们还要判断根节点是不是黑色的,因为上面的第二个和第三个方法是判断有没有违反红黑树性质的方法,于是我们可以把判断根节点的方法和这两个方法结合在一起,形成一个新方法,正好在这里直接计算出一条路径的黑色结点传递给第三个方法:
java
private static boolean isRBTree (TreeNode root) {
if(root == null) {
return true;
}
if(root.color != Color.BLACK) {
System.out.println("根节点为红色!!!");
return false;
}
TreeNode cur = root;
int blackNodes = 0;
while(cur != null) {
if(cur.color == Color.BLACK) {
blackNodes++;
}
cur = cur.left;
}
return isRedNodes(root) && isSamePathBlackNodes(root,0,blackNodes);
}
最后就是在测试类里检测自己的红黑树有没有问题,这里我提供一个测试用例,这个测试用例涵盖了红黑树的 6 中情况:24,12,13,18,25,17,30,28
,大家可以根据答案去比对自己的红黑树有没有错误:
这颗红黑树的样子:
最终测试类
java
public class Test {
private static void isSort(TreeNode root) {
if(root == null) {
return;
}
isSort(root.left);
System.out.print(root.val + " ");
isSort(root.right);
}
private static boolean isRedNodes(TreeNode root) {
if(root == null) {
return true;
}
if(root.color == Color.RED && root.parent.color == Color.RED) {
System.out.println("出现两个连续的红色结点!!!");
return false;
}
return isRedNodes(root.left) && isRedNodes(root.right);
}
private static boolean isSamePathBlackNodes(TreeNode root,int pathBlack, int blackNodes) {
if(root == null) {
return true;
}
if(root.color == Color.BLACK) {
pathBlack++;
}
if(root.left == null && root.right == null) {
if(pathBlack != blackNodes) {
System.out.println("路径上的黑色结点数目不相同!!!");
return false;
}
}
return isSamePathBlackNodes(root.left,pathBlack,blackNodes)
&& isSamePathBlackNodes(root.right,pathBlack,blackNodes);
}
private static boolean isRBTree (TreeNode root) {
if(root == null) {
return true;
}
if(root.color != Color.BLACK) {
System.out.println("根节点为红色!!!");
return false;
}
TreeNode cur = root;
int blackNodes = 0;
while(cur != null) {
if(cur.color == Color.BLACK) {
blackNodes++;
}
cur = cur.left;
}
return isRedNodes(root) && isSamePathBlackNodes(root,0,blackNodes);
}
public static void main(String[] args) {
RedBlackTree redBlackTree = new RedBlackTree();
int[] arr = {24,12,13,18,25,17,30,28};
for (int i = 0; i < arr.length; i++) {
redBlackTree.insert(arr[i]);
}
isSort(redBlackTree.root);
System.out.println();
System.out.println(isRBTree(redBlackTree.root));
}
}
最终代码
java
public enum Color {
RED,BLACK
}
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode parent;
Color color;
public TreeNode(int val) {
this.val = val;
this.color = Color.RED;
}
}
public class RedBlackTree {
public TreeNode root;
public boolean insert(int val) {
TreeNode node = new TreeNode(val);
if(root == null) {
root = node;
root.color = Color.BLACK;
return true;
}
TreeNode cur = root;
TreeNode parent = null;
while(cur != null) {
if(cur.val == val) {
//数值相同的结点不能插入。
return false;
} else if(cur.val > val) {
parent = cur;
cur = cur.left;
} else {
parent = cur;
cur = cur.right;
}
}
if(parent.val > val) {
parent.left = node;
} else {
parent.right = node;
}
node.parent = parent;
cur = node;
while(parent != null && parent.color == Color.RED){
TreeNode grandparent = parent.parent; //一定存在
if(parent == grandparent.left) {
TreeNode uncle = grandparent.right;
if(uncle != null && uncle.color == Color.RED) {
uncle.color = Color.BLACK;
parent.color = Color.BLACK;
grandparent.color = Color.RED;
//继续向上调整
cur = grandparent;
parent = cur.parent;
} else {
// uncle 不存在或者 uncle 是 黑色结点,那就需要进行旋转调整
if(cur == parent.right) {
//先对 parent 进行左旋,然后交换 cur 与 parent ,之后就会变成下面的代码的情况
rotateLeft(parent);
TreeNode tmp = parent;
parent = cur;
cur = parent;
}
// cur 是 parent 的右孩子,对grandparent 进行右旋
rotateRight(grandparent);
parent.color = Color.BLACK;
grandparent.color = Color.RED;
//红黑树已经调整完成,直接退出循环,不用继续调整颜色
break;
}
} else {
//parent == grandparent.right
TreeNode uncle = grandparent.left;
if(uncle != null && uncle.color == Color.RED) {
uncle.color = Color.BLACK;
parent.color = Color.BLACK;
grandparent.color = Color.RED;
//继续向上调整
cur = grandparent;
parent = cur.parent;
} else {
// uncle 不存在或者 uncle 是 黑色结点,那就需要进行旋转调整
if(cur == parent.left) {
//先对 parent 进行右旋,然后交换 cur 与 parent变成下面的情况
rotateRight(parent);
TreeNode tmp = parent;
parent = cur;
cur = tmp;
}
//此时 cur 是 parent 的 右孩子
rotateLeft(grandparent);
grandparent.color = Color.RED;
parent.color = Color.BLACK;
//红黑树已经调整完成,直接退出循环,不用继续调整颜色
break;
}
}
}
root.color = Color.BLACK;
return true;
}
//右旋
private void rotateRight(TreeNode parent) {
TreeNode cur = parent.left;
TreeNode curR = cur.right;
TreeNode Pparent = parent.parent;
parent.left = curR;
if(curR != null) {
curR.parent = parent;
}
cur.right = parent;
parent.parent = cur;
cur.parent = Pparent;
if(root == parent) {
root = cur;
} else if(Pparent.left == parent) {
Pparent.left = cur;
} else {
Pparent.right = cur;
}
}
//左旋
private void rotateLeft(TreeNode parent) {
TreeNode cur = parent.right;
TreeNode curL = cur.left;
TreeNode Pparent = parent.parent;
parent.right = curL;
if(curL != null) {
curL.parent = parent;
}
cur.left = parent;
parent.parent = cur;
cur.parent = Pparent;
if(root == parent) {
root = cur;
} else if(Pparent.left == parent) {
Pparent.left = cur;
} else {
Pparent.right = cur;
}
}
}
小结
因为红黑树不追求绝对的高度平衡,而是相对平衡(只需要保证最长路径不超过最短路径的2倍),在上面的代码中,只使用了左旋和右旋代码,并且不是每次插入和删除都需要旋转,大部分还是调整颜色,这个时间和资源的消耗小于AVL 树,并且红黑树在查找的时候时间复杂度也能达到 O(log N)
红黑树在实际中有着大量的应用:例如在Java集合框架中的:TreeMap、TreeSet底层使用的就是红黑树, C++ STL库 -- map/set、mutil_map/mutil_set,linux内核:进程调度中使用红黑树管理进程控制块,epoll在内核中实现时使用红黑树管理事件块,其他一些库:比如nginx中用红黑树管理timer等