复习二叉查找树的一般性质:
1、root.left.key < root.key < root.right.key
- 不存在键值相同的结点。
2、查找、插入、删除等操作,时间复杂度为O(lgn)。因为,一棵由n个结点,随机构造的二叉查找树的高度为lgn,所以顺理成章,一般操作的执行时间为O(lgn)。
3、二叉查找树的特殊情况:一棵具有n个结点的线性链,则此些操作最坏情况运行时间为O(n)。
而红黑树,能保证在最坏情况下,基本的动态几何操作的时间均为O(lgn)。
复习红黑树的性质:
0)在红黑树中,叶子结点指空结点NULL。
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点,即空结点(NIL)是黑的。(首尾必黑)
4)如果一个结点是红的,那么它的俩个儿子都是黑的。(不存在连续2红)
5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。(黑色完美平衡)
5-1)右5知,如果一个结点存在黑子结点,那么该结点肯定有两个子结点
复习红黑树的旋转知识:
对红黑树进行插入、删除结点等操作时,难免破坏它的基本性质。通过旋转来恢复(二叉堆也有恢复操作)。
在旋转操作中只有指针的改变,其他属性都保持不变。
左旋:A树下沉,B树上浮;
右旋:A树上浮,B树下沉。
/**
* 左旋操作
* @param root 根结点引用
* @param node 旋转的节点
* @return 根节点
*/
public static RBTreeNode leftRotate(RBTreeNode root, RBTreeNode node) {
if (node.right == RBTreeNode.nullNode)
return root; // 左旋需要拥有右节点
RBTreeNode right = node.right;
// 旋转节点的右子树变为右节点的左子树
node.right = right.left;
if (node.right != RBTreeNode.nullNode)
node.right.p = node;
// 用右节点代替旋转节点位置
if (node.p != RBTreeNode.nullNode) {
right.p = node.p;
if (node.p.left == node)
node.p.left = right;
else
node.p.right = right;
} else {
root = right; // 没有父节点的节点为根结点
root.p = RBTreeNode.nullNode;
}
// 右节点的左子树变为旋转节点
right.left = node;
node.p = right;
return root;
}
/**
* 右旋操作
* @param root 根结点引用
* @param node 旋转节点
* @return 根节点
*/
public static RBTreeNode rightRotate(RBTreeNode root, RBTreeNode node) {
if (node.left == RBTreeNode.nullNode)
return root; // 右旋需要有左节点
RBTreeNode left = node.left;
// 旋转节点的左子树变为左节点的右子树
node.left = left.right;
if (node.left != RBTreeNode.nullNode)
node.left.p = node;
// 用左节点代替旋转节点
if (node.p != RBTreeNode.nullNode) {
left.p = node.p;
if (node.p.left == node)
node.p.left = left;
else
node.p.right = left;
} else {
root = left;
root.p = RBTreeNode.nullNode;
}
// 左节点的右子树变为旋转节点
left.right = node;
node.p = left;
return root;
}
复习红黑树插入、删除:
0、复习二叉搜索树的插入、删除操作
/* 插入操作 */
// 二叉搜索树插入节点
public static boolean insert(TreeNode root, TreeNode node) {
TreeNode tree = root;
TreeNode p = null;
// 寻找合适的插入位子 p节点即为插入节点的父节点
while (tree != null) {
p = tree;
if (tree.val > node.val) { // 小于比较节点 放入比较节点的左子树
tree = tree.left;
} else if (tree.val < node.val) { // 大于比较节点 放入比较节点的右子树
tree = tree.right;
} else {
return false; // 二叉搜索树中已有相同元素节点 插入失败
}
}
node.parent = p;
if (p == null) { // root 为null node为根节点
root = node;
} else if (p.val > node.val) {
p.left = node;
} else {
p.right = node;
}
return true;
}
二叉搜索树删除节点分为三种情况:
-
case1-删除节点没有子节点:直接把删除节点的位置置空即可
-
case2-删除节点有一个子节点:用该子节点顶替删除节点的位置
-
case3-删除节点有两个子节点:若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)替换删除结点,下文是在右子树中找(successor)。
-
结合图,思考树的变化:删除结点被替代后,在不考虑结点的键值的情况下,对于树来说,可以认为删除的是替代结点!
-
情景2:删除结点用其唯一的子结点替换,子结点替换为删除结点后,可以认为删除的是子结点,若子结点又有两个子结点,那么相当于转换为情景3,一直自顶向下转换,总是能转换为情景1。(对于红黑树来说,根据性质5.1,只存在一个子结点的结点肯定在树末了)
-
情景3:删除结点用后继结点(肯定不存在左结点),如果后继结点有右子结点,那么相当于转换为情景2,否则转为为情景1。
综上所述,删除的结点可以看作删除替代结点,而替代结点最后总是在树末。有了这结论,我们讨论的删除红黑树的情景就少了很多,因为我们只考虑删除树末结点的情景了。
// 二叉搜索树删除节点
/**
* 删除节点
* @param key
*/
public boolean delete(int key){
Node curr=root;//保存当前节点
Node parent=root;//保存当前节点父节点
boolean isLeft=true;//记录是否是左几点
//定位到将被删除的节点
while(key!=curr.key){
if(key<=curr.key){
isLeft=true;//经过左节点
if(curr.left!=null){
parent=curr;
curr=curr.left;
}
}else{
isLeft=false;//经过右节点
if(curr.right!=null){
parent=curr;
curr=curr.right;
}
}
if(curr==null){
return false;
}
}
//如果被删除节点是叶子节点
if(curr.left==null&&curr.right==null){
if(curr==root){
root=null;
}else if(isLeft){
parent.left=null;
}else{
parent.right=null;
}
//如果被删除节点只有左节点
}else if(curr.right==null){
if(curr==root){
root=root.left;
}else if(isLeft){
parent.left=curr.left;
}else{
parent.right=curr.left;
}
//如果被删除节点只有右节点
}else if(curr.left==null){
if(curr==root){
root=root.right;
}else if(isLeft){
parent.left=curr.right;
}else{
parent.right=curr.right;
}
}else{
Node successor=getSuccessor(curr);
//将后继节点与被删除的父节点进行连接,完成整个替换过程
if(curr==root){
root=successor;
}else if(curr==parent.left){
parent.left=successor;
}else{
parent.right=successor;
}
successor.left=curr.left;
}
return true;
}
public Node getSuccessor(Node delNode){
Node curr=delNode.right;
Node successor=curr;
Node sucParent=null;
while(curr!=null){
sucParent=successor;
successor=curr;
curr=curr.left;
}
if(successor!=delNode.right){
//将后继节点的子节点(只可能有右节点)替补到后继节点的位置上
sucParent.left=successor.right;
//将被删除的右孩子连接到后继节点的右节点上
successor.right=delNode.right;
//将被删除的左孩子连接到后继节点的右节点上(就是用后继节点替换掉被删除的节点)
}
return successor;
}
1、红黑树的插入操作
1.1、把红黑树看成一个普通的二叉搜索树,对其插入红色的结点。
为什么是插入红色结点?因为插入一个红色结点只会破坏性质2或性质4,可以尽量避免对树的调整。
1.2、恢复红黑树性质
性质2:若根节点变成了红色,此时我们把根节点染成黑色即可。
性质4:插入节点的父节点是红色时,那就出现红色节点有一个为红色的子结点。
- 情况1:插入节点z的父结点与叔节点同为红色
祖父结点由黑转红,可能也破坏了性质。指针跳转到祖父结点,继续恢复。
- 情况2:插入节点z的叔节点y是黑色的,且z是一个右孩子
- 情况3:插入节点z的叔节点y是黑色的,且z是一个左孩子
这两种情况可以放一起讨论,因为我们会把情况2转化为情况3,示意图如下:
- 左上角为情况2,此时叔节点w为黑色,且插入节点z为父节点x的右孩子,此时我们对父节点x进行一次左旋,然后交换x和z的引用,即可转换为右上角的情况3.
- 右上角为情况3,此时叔节点w为黑色,且插入节点z为父节点x的左孩子,此时我们进行如下操作即可恢复红黑树的性质:
- 交换父节点x和祖父节点w的颜色
- 对祖父节点w进行右旋
上面的操作既修正了对性质4的违反,也没有引起对其他红黑树性质的违反,因此我们此时可以结束对红黑树的性质修复工作。
/**
* 修復插入時违反的红黑树性质
* @param root 根节点引用
* @param node 修复节点
* @return 根节点
*/
public static RBTreeNode rbInsertFixup(RBTreeNode root, RBTreeNode node) {
// 修复节点不是根节点且为红色时
RBTreeNode parent = node.p, grandParent, parentBorther;
while(parent != RBTreeNode.nullNode && parent.color == RBColor.RED) {
grandParent = parent.p;
if (grandParent.left == parent) { // 父节点为左节点
parentBorther = grandParent.right; // 叔节点为右节点
if (parentBorther != RBTreeNode.nullNode && parentBorther.color == RBColor.RED) { // case 1
grandParent.color = RBColor.RED; // 祖父节点改为红色
parent.color = RBColor.BLACK; // 父节点和叔节点改为黑色
parentBorther.color = RBColor.BLACK;
node = grandParent; // 对祖父节点继续遍历
} else {
if (parent.right == node) { // case 2
root = leftRotate(root, parent); // 对父节点左旋
// 交换node和parent的引用
RBTreeNode temp = node;
node = parent;
parent = temp;
}
// case 3
grandParent.color = RBColor.RED; // 祖父染成红色
parent.color = RBColor.BLACK; // 父节点染成黑色
root = rightRotate(root, grandParent); // 对祖父右旋
node = root; // 把节点置为根节点退出修复
}
} else { // 父节点为右节点,镜像处理
parentBorther = grandParent.left;
if (parentBorther != RBTreeNode.nullNode && parentBorther.color == RBColor.RED) { // case 1
grandParent.color = RBColor.RED;
parent.color = RBColor.BLACK;
parentBorther.color = RBColor.BLACK;
node = grandParent;
} else {
if (parent.left == node) { // case 2
root = rightRotate(root, parent);
RBTreeNode temp = node;
node = parent;
parent = temp;
}
// case 3
grandParent.color = RBColor.RED;
parent.color = RBColor.BLACK;
root = leftRotate(root, grandParent);
node = root;
}
}
parent = node.p;
}
// 根节点染为黑色
root.color = RBColor.BLACK;
return root;
}
2、红黑树的删除操作
2.1、把红黑树看成一个普通的二叉搜索树,对其删除结点
由于删除节点会带走一种颜色,因此我们需要记录下被删除的颜色和删除颜色的位置,最后我们再考虑如何修复树的红黑性质。
思考:删除操作什么情况需要修复?
2.2、恢复红黑树性质
删除操作会影响红黑树的什么性质?
- 如果删除的节点颜色为红色,则不会影响任何红黑性质。
- 如果删除的颜色是黑色,则可能影响性质2(根节点是黑色的),也可能影响性质4(红色的节点的两个子结点均为黑色),也可能影响性质5(对于每个节点,从该节点到其所有后代的简单路径上,均包含相同数目的黑色节点)。
思考2种删除情况的恢复方式:
-
删除情景1:替换结点是红色结点
处理:颜色变为删除结点的颜色 -
删除情景2:替换结点是黑结点
处理:分4种情况讨论,此处假设修复位置节点为A(黑色,此处假设为父节点的左节点,右节点请镜像处理),其父节点为B,兄弟节点为C,兄弟节点的左子节点为D,兄弟节点的右子节点为E。
- 情况1:A的兄弟节点为红色
如上图所示,此时我们先交换父节点B和兄弟节点C的颜色,然后对父节点B进行左旋,以上操作并不会影响红黑树性质,而我们也把情况1转化为了别的情况。
- 情况2:A的兄弟节点为黑色,其子节点均为黑色(下图灰色代表未知颜色)
此时的处理方法很简单,因为A节点和其兄弟节点C均为黑色,且C的子节点也均为黑色,因此我们可以把A节点和C节点的黑色上移到父节点B上,再把修复位置换为父节点B,针对父节点B继续进行修复。(如果父节点B是红色或根节点就可以停止修复了~)
- 情况3:A的兄弟节点为黑色,兄弟节点的左子节点为红色,右子节点为黑色
此时我们首先交换兄弟节点C与其左子红色节点D的颜色,然后对兄弟节点C进行右旋,把情况3转化为情况4继续处理。
- 情况4:A的兄弟节点为黑色,兄弟节点的右子节点为红色
此时我们进行如下变换操作:
1、把父节点B和兄弟节点的右子节点E染成黑色,兄弟节点C染成父节点颜色
2、对父节点B进行左旋
删除修复过程的Java代码如下:
/**
* 红黑树删除操作
* @param root 根节点引用
* @param deleteNode 要删除的节点
* @return 根节点
*/
public static RBTreeNode rbDelete(RBTreeNode root, RBTreeNode deleteNode) {
RBTreeNode replaceNode, fixNode = RBTreeNode.nullNode; // 顶替删除节点的代替节点、需要修复颜色的节点位置
RBTreeNode fixNodeParent = deleteNode.p;
RBColor deleteColor = deleteNode.color; // 记录被删除节点的颜色
if (deleteNode.left == RBTreeNode.nullNode && deleteNode.right == RBTreeNode.nullNode) // 删除节点没有任何子结点
replaceNode = RBTreeNode.nullNode;
else if (deleteNode.right == RBTreeNode.nullNode) { // 处理只有左子节点的情况
replaceNode = deleteNode.left;
fixNode = replaceNode;
} else if (deleteNode.left == RBTreeNode.nullNode) { //处理只有右子节点的情况
replaceNode = deleteNode.right;
fixNode = replaceNode;
} else { // 处理有两个子节点的情况,寻找sucessor
replaceNode = deleteNode.right;
while (replaceNode.left != RBTreeNode.nullNode) // 找到右子树的最小节点
replaceNode = replaceNode.left;
fixNode = replaceNode.right; // 修复节点位置变为原顶替节点位置
if (replaceNode.p == deleteNode) { // 特殊情况,前面的while没有执行,表明右子树没有左节点。
if (fixNode != RBTreeNode.nullNode) // 修复节点不为空
fixNode.p = replaceNode;
fixNodeParent = replaceNode;
} else {
replaceNode.p.left = fixNode; // 修复节点顶替该节点的位置
if (fixNode != RBTreeNode.nullNode) // 修复节点不为空
fixNode.p = replaceNode.p;
fixNodeParent = replaceNode.p;
replaceNode.right = deleteNode.right;
}
// 用删除节点的颜色覆盖顶替节点的颜色
deleteColor = replaceNode.color;
replaceNode.color = deleteNode.color;
replaceNode.left = deleteNode.left;
}
if (replaceNode != RBTreeNode.nullNode) // 存在顶替节点
replaceNode.p = deleteNode.p;
if (deleteNode.p == RBTreeNode.nullNode) // 删除节点的父节点为空,是根节点
root = replaceNode;
else { // 删除节点不是根节点
if (deleteNode.p.left == deleteNode)
deleteNode.p.left = replaceNode;
else
deleteNode.p.right = replaceNode;
}
if (deleteColor == RBColor.BLACK) // 如果删除的颜色是黑色则需要进行修复
root = rbDeleteFixup(root, fixNode, fixNodeParent);
return root;
}
/**
* 修复删除时破坏的红黑树性质
* @param root 根引用
* @param fixNode 修复位置
* @param parent 修复位置的父节点(修复位置为叶结点时使用)
* @return 根
*/
public static RBTreeNode rbDeleteFixup(RBTreeNode root, RBTreeNode fixNode, RBTreeNode parent) {
RBTreeNode brother;
while (root != fixNode && fixNode.color == RBColor.BLACK) {
parent = fixNode.p == null ? parent : fixNode.p; // 处理fixNode为nullNode情况
if (fixNode == parent.left) { // 顶替位置在父节点左边
brother = parent.right;
if (brother.color == RBColor.RED) { // case 1
// 交换父节点和兄弟节点的颜色
RBColor temp = brother.color;
brother.color = parent.color;
parent.color = temp;
// 父节点进行左旋
root = leftRotate(root, parent);
} else if (brother == RBTreeNode.nullNode) { // case 2
// 兄弟节点为空,即为黑色,只需继续遍历父节点即可
fixNode = parent;
} else if (brother.left.color == RBColor.BLACK &&
brother.right.color == RBColor.BLACK) { // case 2
brother.color = RBColor.RED;
fixNode = parent; // 继续遍历父节点
} else { // case 3 and case 4
if (brother.left.color == RBColor.RED &&
brother.right.color == RBColor.BLACK) { // case 3
// 兄弟节点染成红色,左子节点染成黑色
brother.color = RBColor.RED;
brother.left.color = RBColor.BLACK;
// 兄弟节点右旋
root = rightRotate(root, brother);
brother = brother.p;
}
// case 4
// 变色
brother.color = parent.color;
parent.color = RBColor.BLACK;
brother.right.color = RBColor.BLACK;
// 父节点左旋
root = leftRotate(root, parent);
break;
}
} else {
brother = parent.left;
if (brother.color == RBColor.RED) { // case 1
// 交换父节点和兄弟节点的颜色
RBColor temp = brother.color;
brother.color = parent.color;
parent.color = temp;
// 父节点进行右旋
root = rightRotate(root, parent);
} else if (brother == RBTreeNode.nullNode) { // case 2
// 兄弟节点为空,即为黑色,只需继续遍历父节点即可
fixNode = parent;
} else if (brother.left.color == RBColor.BLACK &&
brother.right.color == RBColor.BLACK) { // case 2
brother.color = RBColor.RED;
fixNode = parent; // 继续遍历父节点
} else { // case 3 and case 4
if (brother.right.color == RBColor.RED &&
brother.left.color == RBColor.BLACK) { // case 3
// 兄弟节点染成红色,左子节点染成黑色
brother.color = RBColor.RED;
brother.right.color = RBColor.BLACK;
// 兄弟节点右旋
root = leftRotate(root, brother);
brother = brother.p;
}
// case 4
// 变色
brother.color = parent.color;
parent.color = RBColor.BLACK;
brother.left.color = RBColor.BLACK;
// 父节点左旋
root = rightRotate(root, parent);
break;
}
}
}
fixNode.color = RBColor.BLACK;
return root;
};