红黑树概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。如下图所示:
注意:这里的 NIL 是空节点的意思,但是在红黑树中空节点是我们平常所说的叶子节点,并且这个节点的颜色为黑色。
红黑树的性质
1、每个结点不是红色就是黑色;
2、根节点是黑色的;
3、如果一个节点是红色的,则它的两个孩子结点是黑色的,也就是说 没有2个连续的红色节点;
4、对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点数;
5、每个叶子结点都是黑色的(此处的叶子结点指的是空结点);
由上述性质,便可推出:其中最长路径中节点个数不会超过最短路径节点个数的两倍。如下验证:
红黑树节点的定义
static class TreeNode {
public int val;
public COLOR color; // 颜色
public TreeNode left; // 左孩子
public TreeNode right; // 右孩子
public TreeNode parent; // 父亲
public TreeNode(int val) {
this.val = val;
this.color = COLOR.RED; // 默认是红色的节点
}
}
之所以把红黑树的节点默认定义为红色,是因为在插入元素时,如果节点为黑色,就会改变从根结点不同路径到叶子结点之间的黑色节点的个数,修改十分困难,而节点为红色,只是可能会违反不能出现两个连续是红色节点的情况,修改起来比较容易。
红黑树的插入
枚举颜色:
public enum COLOR {
RED,BLACK;
}
根结点:
public TreeNode root;
思路:和二叉搜索树一样,先得找到合适的位置进行插入节点的操作。
// 判断根结点是不是为 null
if (root == null) {
root = new TreeNode(val);
root.color = COLOR.BLACK; // 根结点要改为黑色
return true;
}
// 开始寻找要插入节点的位置
TreeNode cur = root;
TreeNode parent = null;
while (cur!= null) {
parent = cur;
if (cur.val < val) {
cur = cur.right;
} else if (cur.val > val) {
cur = cur.left;
} else {
return false; // 不能插入相同的元素
}
}
// parent 是要插入的前一个位置(判断是哪棵树)
TreeNode node = new TreeNode(val);
if (val < parent.val) {
parent.left = node;
}else {
parent.right = node;
}
node.parent = parent;
cur = node;
插入操作完成之后,就得判断当前红黑树是否违反了其性质:不能出现连续的红色节点。
// 判断是否需要调整
if (parent.color == COLOR.BLACK) {
// 父亲节点为黑色,则没有改变性质,无需调整
return true;
}
接下来就是需要调整的, 根据叔叔节点是否存在和颜色划分了大概两种情况:
因为 grandfather节点原先的颜色是黑色,当调整为红色之后,可能会出现:有连续的红色节点的情况,因此还得继续往上判断,直至parent为null即可。
从这里我们可以发现:其实叔叔节点为黑色和叔叔节点不存在的处理方式是一样的。
当然,上面两种只是主要的情况,还有 parent 和 uncle 所处的位置不同,以及cur 所处的位置不同步,会细分出很多种情况。
检查修改代码实现:
// 判断是否需要调整
if (parent.color == COLOR.BLACK) {
// 父亲节点为黑色,则没有改变性质,无需调整
return true;
}
// 父亲节点为红色,则与性质发生冲突,需要调整
while (parent!= null && parent.color == COLOR.RED) {
// 判断叔叔节点的颜色是啥,根据这个来判断调整方式
TreeNode grandfather = parent.parent;
// 根据叔叔节点和父亲节点位置来划分不同的情况
if (parent == grandfather.left) {
// 再判断叔叔节点是否存在,根据叔叔节点的情况来处理
TreeNode uncle = grandfather.right;
if (uncle!= null && uncle.color == COLOR.RED) {
// 修改叔叔节点、父亲节点和祖父节点的颜色,然后继续判断
grandfather.color = COLOR.RED;
uncle.color = COLOR.BLACK;
parent.color = COLOR.BLACK;
cur = grandfather;
parent = cur.parent;
} else { // uncle 为 null 或者 uncle 为黑色
// 判断 cur 的位置在 parent 的哪边
if (cur == parent.left) {
// uncle 为 null 和不为 null 直接右旋祖父节点,然后再修改颜色
rotateRight(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
} else { // cur == parent.right
// 左旋parent,再交换
rotateLeft(parent);
TreeNode tmp = parent;
parent = cur;
cur = tmp;
// 经过上面的处理,变成了cur为parent的left的情况
rotateRight(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
}
}
} else { // parent == grandfather.right
// 再判断叔叔节点是否存在,根据叔叔节点的情况来处理
TreeNode uncle = grandfather.left;
if (uncle!= null && uncle.color == COLOR.RED) {
// 修改叔叔节点、父亲节点和祖父节点的颜色,然后继续判断
grandfather.color = COLOR.RED;
uncle.color = COLOR.BLACK;
parent.color = COLOR.BLACK;
cur = grandfather;
parent = cur.parent;
} else { // uncle 为 null 或者 uncle 为黑色
// 判断 cur 的位置在 parent 的哪边
if (cur == parent.right) {
rotateLeft(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
} else { // cur == parent.left
// 先变为cur == parent.right的情况
rotateRight(parent);
TreeNode tmp = parent;
parent = cur;
cur = tmp;
// 再进行上面的处理
rotateLeft(grandfather);
grandfather.color = COLOR.RED;
parent.color = COLOR.BLACK;
}
}
}
}
root.color = COLOR.BLACK; // 把根结点修改为黑色
return true;
左单旋代码实现:(思路可见AVL树的博客文章)
private void rotateLeft(TreeNode parent) {
TreeNode subR = parent.right;
TreeNode subRL = subR.left;
TreeNode parentP = parent.parent;
// 修改四个指针的指向
parent.parent = subR;
parent.right = subRL;
if (subRL != null) {
subRL.parent = parent;
}
subR.left = parent;
// 修改本级根结点和上级树的指向
if (root == parent) {
root = subR;
root.parent = null;
} else {
subR.parent = parentP;
if (parentP.left == parent) {
parentP.left = subR;
} else {
parentP.right = subR;
}
}
}
右单旋代码实现:(思路可见AVL树博客文章)
private void rotateRight(TreeNode parent) {
TreeNode subL = parent.left;
TreeNode subLR = subL.right;
TreeNode parentP = parent.parent;
// 修改四个指针的指向
parent.parent = subL;
parent.left = subLR;
if (subLR != null) {
subLR.parent = parent;
}
subL.right = parent;
// 修改本级根结点和上级树的指向
if (parent == root) {
root = subL;
root.parent = null;
} else {
subL.parent = parentP;
if (parentP.left == parent) {
parentP.left = subL;
} else {
parentP.right = subL;
}
}
}
处理的思路:
1、先判断父亲节点和叔叔节点在哪边;
2、再根据叔叔节点是否存在以及颜色来分别处理:
2.1、叔叔节点存在且为红色,就是修改颜色再继续检查;
2.2、叔叔节点不存在(NIL也是黑色)或者叔叔节点为红色,就是进行旋转处理(旋转需要判断cur具体是在哪边,从而进行不同的旋转);
红黑树构建完成之后,就得开始验证这棵二叉树是不是一棵红黑树。
红黑树的验证
我们是利用红黑树的性质来进行验证的。
1、是一棵二叉搜索树。因此采取中序遍历的方式输出,看是不是一棵二叉搜索树;
代码实现:
// 中序遍历:检查是否为有序序列
public void inorder(TreeNode root) {
if(root == null) {
return;
}
inorder(root.left);
System.out.print(root.val+" ");
inorder(root.right);
}
2、根结点一定得是黑色的;
3、不能出现两个连续的红色节点;
思路:遍历树的所有结点,判断这个节点的颜色是不是和其父亲节点一样都是红色。如果是,则返回false,否则一直判断下去,直至全部遍历完成,返回true。
代码实现:
private boolean isContinuousRedNodes(TreeNode root) {
if (root == null) {
return true;
}
// 该节点为红色
if (root.color == COLOR.RED) {
// 其父亲节点也为红色
if (root.parent != null && root.parent.color == COLOR.RED) {
System.out.println("违反了性质:连续出现了两个红色的节点");
// return false; 这个可有可无
}
}
// 递归判断左子树和右子树
return isContinuousRedNodes(root.left) && isContinuousRedNodes(root.right);
}
4、从根结点到不同路径上的叶子节点之间的黑色节点个数要相同。
思路:先记录任意一条从根结点到叶子结点之间的黑色节点的个数。然后再遍历树的路径判断是否一致。如果不一致,就返回false,一致就一直判断,直至返回true。
代码实现:
// 求某条路径上的所有黑色节点个数
private int getBlckCount(TreeNode root) {
TreeNode cur = root;
int count = 0;
while (cur != null) {
if (cur.color == COLOR.BLACK) {
count++;
}
cur = cur.left;
}
return count;
}
private boolean sameBlackNodes(TreeNode root, int pathBlack, int sameBlackCount) {
if (root == null) {
return true;
}
if (root.color == COLOR.BLACK) {
pathBlack++;
}
// 到叶子节点了,就得进行比较
if (root.left == null && root.right == null) {
if (pathBlack != sameBlackCount) {
System.out.println("违反了性质:每条路径上黑色的节点个数是不一样的!");
// return false; 可有可无
}
}
return sameBlackNodes(root.left, pathBlack, sameBlackCount)
&& sameBlackNodes(root.right, pathBlack, sameBlackCount);
}
剩余代码:
public boolean isRBTree(TreeNode root) {
// 就是看是否满足红黑树的性质
// 1、根结点是不是黑色
if (root.color != COLOR.BLACK) {
System.out.println("违反了性质:根节点必须是黑色的!");
}
int sameBlackCount = getBlckCount(root);
// 是不是存在两个连续的红色节点 && 是不是不同;路径上有相同的黑色节点个数
return isContinuousRedNodes(root) && sameBlackNodes(root, 0, sameBlackCount);
}
测试代码:
public class Test {
public static void main(String[] args) {
//int[] array = {16, 3, 7, 11, 9, 26, 18, 14, 15};
int[] array = {4, 2, 6, 1, 3, 5, 15, 7, 16,14};
RBTree rbTree = new RBTree();
for (int i = 0; i < array.length; i++) {
rbTree.insert(array[i]);
}
System.out.println(rbTree.isRBTree(rbTree.root));
rbTree.inorder(rbTree.root);
}
}
AVL树和红黑树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比 AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。 就像在 Java集合框架中的:TreeMap、TreeSet底层使用的就是红黑树。
好啦!本期 数据结构之红黑树 的学习之旅就到此结束啦!我们下一期再一起学习吧!