文章目录:
1. 红黑树
1.1:概念
红黑树:是一种二叉搜索树,每个节点添加了一个存储位表示当前节点的颜色,可以是红色(Red)或者黑色(Black);
红黑树通过对任何一条从根到叶子节点的路径上节点的着色方式进行限制,使得红黑树最长路径长度不会超过最短路径的二倍,所以红黑树是接近平衡的(相对平衡)
1.2:红黑树的性质
1. 每个节点不是红色就是黑色
2. 根节点是黑色
3. 如果一个节点是红色,则他的两个节点必须是黑色(不能出现连续的两个红色节点)
4. 对于每个节点,从该节点到其所有后代节点的简单路径上,均包含相同数目的黑色节点
5. 每个叶子节点都是黑色的(这里的叶子节点是指外部节点、空节点)
根据性质3和4,对于一棵红黑树:
最短路径:当前路径全是黑色节点;
最长路径:当前路径由红、黑节点交替排布;
又因为每条路径的黑色节点数量相同,所以最长路径长度不会超过最短路径的二倍
一般情况下,不会出现一条路径全是黑色节点的情况。
1.3:红黑树的时间复杂度
假设:一棵红黑树有N个节点,其中有X个黑色节点,那么N的范围为:[ X ,2X ]
最短路径查询的时间复杂度:O(logX)
最长路径查询的时间复杂度:O(log2X) = O(log 2 + log X )
= O(1 + log X) =O( log X)
所以红黑树查询的时间复杂度:O(logN)
2. 实现红黑树(Red-Black Tree)
2.1:红黑树节点的定义
public class RBTree {
public static class RBTreeNode {
public RBTreeNode left;
public RBTreeNode right;
public RBTreeNode parent;
public int val;
public Color color;
RBTreeNode(int val){
this.val=val;
//新增节点不能是黑色的,默认是红色的
//如果是黑色,要保证每条路径上黑色数量相同,就可能存在问题
//可能需要新增很多节点
this.color=Color.RED;
}
}
public RBTreeNode root;
}
注意:红黑树新增节点默认是红色节点
颜色用枚举类表示:
public enum Color {
RED,BLACK
}
2.2:红黑树插入节点操作
红黑树本质上是一棵相对平衡的二叉搜索树,每个节点多了一个属性表示颜色,所以插入一个节点的步骤:
1. 按照二叉搜索树的方式插入节点;
2. 调整节点的颜色;因为插入节点是红色节点,可能使插入节点后的二叉树不符合红黑树的性质。
插入节点后,可能的情况:
(约定:cur-当前节点 p-父节点 u-叔叔节点 g-祖父节点)
父节点在祖父节点的左侧:
情况一:cur红 p红 g黑 u存在且为红
调整方式:
将 p u 改成黑色,g改成红色,g赋值给cur 继续向上调整
RBTreeNode uncle = grandFather.right;
if(uncle != null && uncle.color==Color.RED){
parent.color=Color.BLACK;
uncle.color=Color.BLACK;
grandFather.color=Color.RED;
//继续向上调整
cur=grandFather;
parent=cur.parent;
情况二:cur红 p红 g红 u不存在/u为黑
1. p为g的左孩子,cur为p的左孩子,进行右单旋转
2. p为g的右孩子,cur为p的右孩子,进行左单旋转
情况三:cur红 p红 g红 u不存在/u为黑
1. p为g的左孩子,cur为p的右孩子,进行左单旋转
2. p为g的右孩子,cur为p的左孩子,进行右单旋转
于是转换成了情况二
//uncle不存在 或者颜色为黑
//情况三:cur是parent的右节点
if(cur==parent.right){
rotateLeft(parent);
RBTreeNode temp = parent;
parent=cur;
cur=temp;
//左旋并交换后 成为了情况二 继续往下走情况二步骤
}
//情况二:cur是parent的左节点
rotateRight(grandFather);
grandFather.color=Color.RED;
parent.color=Color.BLACK;
父节点在祖父节点的右侧:
和左侧情况的步骤大致相同,更改一下方向即可
完整代码:
public boolean insert(int val){
//按照二叉搜索树的方式插入节点
RBTreeNode node=new RBTreeNode(val);
if(root==null){
root=node;
root.color=Color.BLACK;
return true;
}
RBTreeNode parent = null;
RBTreeNode cur=root;
while(cur!=null){
if(val>cur.val){
parent=cur;
cur=cur.right;
} else if (val<cur.val) {
parent=cur;
cur=cur.left;
}else{
return false;
}
}
//cur==null
if(val> parent.val){
parent.right=node;
}else{
parent.left=node;
}
node.parent=parent;
cur=node;
//调整插入节点的颜色
while(parent != null && parent.color==Color.RED){
RBTreeNode grandFather = parent.parent;
if(parent == grandFather.left){
RBTreeNode uncle = grandFather.right;
if(uncle != null && uncle.color==Color.RED){
parent.color=Color.BLACK;
uncle.color=Color.BLACK;
grandFather.color=Color.RED;
//继续向上调整
cur=grandFather;
parent=cur.parent;
}else {
//uncle不存在 或者颜色为黑
//情况三:cur是parent的右节点
if(cur==parent.right){
rotateLeft(parent);
RBTreeNode temp = parent;
parent=cur;
cur=temp;
//左旋并交换后 成为了情况二 继续往下走情况二步骤
}
//情况二:cur是parent的左节点
rotateRight(grandFather);
grandFather.color=Color.RED;
parent.color=Color.BLACK;
}
}else { //parent = grandFather.right
//和以上情况相反 但步骤大致相同 直接复制粘贴 更改方向
RBTreeNode uncle = grandFather.left;
if(uncle != null && uncle.color==Color.RED){
parent.color=Color.BLACK;
uncle.color=Color.BLACK;
grandFather.color=Color.RED;
//继续向上调整
cur=grandFather;
parent=cur.parent;
}else {
//uncle不存在 或者颜色为黑
//情况三:cur是parent的右节点
if(cur==parent.left){
rotateRight(parent);
RBTreeNode temp = parent;
parent=cur;
cur=temp;
//左旋并交换后 成为了情况二 继续往下走情况二步骤
}
//情况二:cur是parent的左节点
rotateLeft(grandFather);
grandFather.color=Color.RED;
parent.color=Color.BLACK;
}
}
}
//将根节点颜色设置为黑色
root.color=Color.BLACK;
return true;
}
注意:插入节点后,一定要将根节点设置为黑色节点。
右旋和左旋的实现:
//右单旋
private void rotateRight(RBTreeNode parent){
RBTreeNode subL=parent.left;
RBTreeNode subLR=subL.right;
parent.left=subLR;
subL.right=parent;
if(subLR!=null){ //subLR可能不存在
subLR.parent=parent;
}
//先记录 parent.parent
RBTreeNode pParent = parent.parent;
parent.parent=subL;
if(pParent==root){ // pParent 为根节点
root=subL;
root.parent=null;
}else{ //不是根节点
// 看parent是pParent的左节点还是右节点
if(pParent.left==parent){
pParent.left=subL;
}else{ //pParent.right==parent
pParent.right=subL;
}
subL.parent=pParent;
}
}
//左单旋
private void rotateLeft(RBTreeNode parent){
RBTreeNode subR = parent.right;
RBTreeNode subRL = subR.left;
parent.right=subRL;
subR.left=parent;
if(subRL!=null){
subRL.parent=parent;
}
RBTreeNode pParent=parent.parent;
parent.parent=subR;
if(pParent==root){
root=subR;
root.parent=null;
}else{
if(pParent.left==parent){
pParent.left=subR;
}else{
pParent.right=subR;
}
subR.parent=pParent;
}
}
3. 红黑树的验证
红黑树本质上是一棵相对平衡的二叉搜索树,所以验证一棵红黑树的步骤:
1. 先验证是二叉搜索树:中序遍历得到的序列有序
//中序遍历 检测中序遍历得到的序列是否有序
public void inOrder(RBTreeNode root){
if(root==null){
return;
}
inOrder(root.left);
System.out.println(root.val+" ");
inOrder(root.right);
}
2. 检验是否符合红黑树的性质:
根节点是黑色节点:
//验证根节点是否是黑色的
if(root.color==Color.RED){
System.out.println("根节点不是黑色节点");
return false;
}
不存在连续的两个红色节点:遍历当前树的节点,如果是红色节点,检测其父节点是不是红色节点
//判断是否有两个连续的红色节点
//遍历当前树的节点,如果是红色节点,检测它父节点是不是红色
private boolean checkRedColor(RBTreeNode root){
if(root==null) return true;
if(root.color==Color.RED){
RBTreeNode parent=root.parent;
if(parent.color==Color.RED){
System.out.println("存在两个连续的红色节点");
return false;
}
}
return checkRedColor(root.left) && checkRedColor(root.right);
}
每条路径上的黑色节点数量相同:先计算红黑树某条路径的黑色节点数量,在与其他路径进行比较
//检测 是否每条路径黑色节点数量相同
//pathBlackNum : 每次递归时 计算黑色节点数量
//blackNum:事先计算好的某条路径的黑色节点数量
private boolean checkBlackNum(RBTreeNode root,int pathBlackNum,int blackNum){
if(root == null ) return true;
if(root.color==Color.BLACK){
pathBlackNum++;
}
if(root.left == null && root.right == null){
if(pathBlackNum != blackNum){
System.out.println("存在黑色节点数量不相等的路径");
return false;
}
}
return checkBlackNum(root.left,pathBlackNum,blackNum)
&& checkBlackNum(root.right,pathBlackNum,blackNum);
}
完整代码:
//红黑树的验证
public boolean isRBTree(){
if(root == null){
return true;
}
//验证根节点是否是黑色的
if(root.color==Color.RED){
System.out.println("根节点不是黑色节点");
return false;
}
//计算当前红黑树最左边路径的黑色节点树
int blackNum = 0;
RBTreeNode cur=root;
while(cur!=null){
if(cur.color==Color.BLACK){
blackNum++;
}
cur=cur.left;
}
return checkRedColor(root) //验证是否有两个连续的红色节点
&& checkBlackNum(root,0,blackNum);//验证每条路径的黑色节点书是否相同
}
//中序遍历 检测中序遍历得到的序列是否有序
public void inOrder(RBTreeNode root){
if(root==null){
return;
}
inOrder(root.left);
System.out.println(root.val+" ");
inOrder(root.right);
}
//判断是否有两个连续的红色节点
//遍历当前树的节点,如果是红色节点,检测它父节点是不是红色
private boolean checkRedColor(RBTreeNode root){
if(root==null) return true;
if(root.color==Color.RED){
RBTreeNode parent=root.parent;
if(parent.color==Color.RED){
System.out.println("存在两个连续的红色节点");
return false;
}
}
return checkRedColor(root.left) && checkRedColor(root.right);
}
//检测 是否每条路径黑色节点数量相同
//pathBlackNum : 每次递归时 计算黑色节点数量
//blackNum:事先计算好的某条路径的黑色节点数量
private boolean checkBlackNum(RBTreeNode root,int pathBlackNum,int blackNum){
if(root == null ) return true;
if(root.color==Color.BLACK){
pathBlackNum++;
}
if(root.left == null && root.right == null){
if(pathBlackNum != blackNum){
System.out.println("存在黑色节点数量不相等的路径");
return false;
}
}
return checkBlackNum(root.left,pathBlackNum,blackNum)
&& checkBlackNum(root.right,pathBlackNum,blackNum);
}
4. 红黑树和AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN);
红黑树不追求绝对平衡,是一种相对平衡的二叉搜索树,其只需保证最长路径不超过最短路径的二倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比 AVL树更优;
而且红黑树实现比较简单,所以实际运用中红黑树更多。