数据结构之三-红黑树
概述
搜索二叉树在插入的数据是有序的时候会非常不平衡,几乎变成了线性结构,如插入数据顺序为10,20,30,40,50,那么该二叉树的结构会如下图所示,那么这样就和链表没啥区别,查找的时间复杂度就为O(n),而不是O(logN),为了以较快的时间搜索一颗树,我们就要保证这颗树的平衡性,也就是树的左右子树节点个数趋近相等,红黑树就是这样一颗平衡树,红黑树对插入的数据项会进行检查,如果插入项破坏了树的平衡,树会自动修复。
1.1红黑树特征
1 树中节点非黑及红
2 插入和删除节点必须遵循红黑规则:
1.每个节点不是红色就是黑色的;
2.根节点总是黑色的;
3.如果节点是红色的,则它的子节点必须是黑色的(反之不一定),(也就是从每个叶子到根的所有路径上不能有两个连续的红色节点);
4.从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
注意:新插入的节点颜色总是红色的,这是因为插入一个红色节点比插入一个黑色节点违背红-黑规则的可能性更小,原因是插入黑色节点总会改变黑色高度(违背规则4),但是插入红色节点只有一半的机会会违背规则3(因为父节点是黑色的没事,父节点是红色的就违背规则3)另外违背规则3比违背规则4要更容易修正。当插入一个新的节点时,可能会破坏这种平衡性,那么红-黑树是如何修正的呢?
1.2 红黑树自我修正方法
红黑树节点实体类
public class RBNode<T extends Comparable<T>> { boolean color;//颜色 T key;//关键值 RBNode<T> left;//左子节点 RBNode<T> right;//右子节点 RBNode<T> parent;//父节点 public RBNode(boolean color,T key,RBNode<T> parent,RBNode<T> left,RBNode<T> right){ this.color = color; this.key = key; this.parent = parent; this.left = left; this.right = right; } //获得节点的关键值 public T getKey(){ return key; } }
1.2.1改变节点颜色
新插入的节点为15,一般新插入颜色都为红色,那么我们发现直接插入会违反规则3,改为黑色却发现违反规则4。这时候我们将其父节点颜色改为黑色,父节点的兄弟节点颜色也改为黑色。通常其祖父节点50颜色会由黑色变为红色,但是由于50是根节点,所以我们这里不能改变根节点颜色。
1.2.2 右旋
首先要说明的是节点本身是不会旋转的,旋转改变的是节点之间的关系,选择一个节点作为旋转的顶端,如果做一次右旋,这个顶端节点会向下和向右移动到它右子节点的位置,它的左子节点会上移到它原来的位置。右旋的顶端节点必须要有左子节点。
1.2.3 左旋
左旋代码实现(右旋正好相反,原理一样):
/** * 以C为轴做左旋 * 左旋操作做了三件事 * 1 将的B的左子节点赋给C的右子节点,并将C赋予B的左子节点 * 2 将C的父节点A赋给B的父节点,同时更新A的子节点为B(区分左右) * 3 将B的左子节点设置为C,将C父节点设置为B * @param C */ private void leftRotate(RBNode<T> C){ RBNode<T> root; RBNode<T> B = C.right; /* 1 将的B的左子节点赋给C的右子节点, 并将B的左子节点父节点指向C(B的左子节点不为空) */ C.right = B.left; if(B.left != null){ B.left.parent=C; } /* 2 将C的父节点A赋给B的父节点,同时更新A的子节点为B(区分左右) */ B.parent = C.parent; if(C.parent == null){ root=B;//如果C为根节点则将B设置为根 }else{ if(C == C.parent.left){ C.parent.left = B; }else{ C.parent.right = B; } } //3 将B的左子节点设置为C,将C父节点设置为B B.left = C; C.parent = B; }
2 插入操作
和搜索二叉树一样,都是都是得先找到插入的位置,然后再将节点插入,最后利用旋转操作修正红黑树;修正的过程要分情况讨论。
1:如果是第一次插入,由于原树为空,所以只会违反红-黑树的规则2,所以只要把根节点涂黑即可;
2:如果插入节点的父节点是黑色的,那不会违背红-黑树的规则,什么也不需要做;
但是遇到如下三种情况,我们就要开始变色和旋转了:
①、插入节点的父节点和其叔叔节点(祖父节点的另一个子节点)均为红色。
②、插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的右子节点。
③、插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的左子节点。
下面我们挨个分析这三种情况都需要如何操作,然后给出实现代码。
在下面的讨论中,使用N,P,G,U表示关联的节点。N(now)表示当前节点,P(parent)表示N的父节点,
U(uncle)表示N的叔叔节点,G(grandfather)表示N的祖父节点,也就是P和U的父节点。
①、插入节点的父节点和其叔叔节点均为红色。此时,肯定存在祖父节点,但是不知道父节点是其左子节点还是右子节点,但是由于对称性,我们只要讨论出一边的情况,另一种情况自然也与之对应。这里考虑父节点是其祖父节点的左子节点的情况,如下左图所示:
情况1
对于这种情况,我们要做的操作有:将当前节点(4) 的父节点(5) 和叔叔节点(8) 涂黑,将祖父节点(7)涂红,变成了上有图所示的情况。再将当前节点指向其祖父节点,再次从新的当前节点开始算法(具体看下面的步骤)。这样上右图就变成情况2了。
情况2
对于情况2:插入节点的父节点是红色的,叔叔节点是黑色的,且插入节点是其父节点的右子节点。我们要做的操作有:将当前节点(7)的父节点(2)作为新的节点,以新的当前节点为支点做左旋操作。完成后如左下图所示,这样左下图就变成情况3了。
情况3
对于情况3:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。我们要做的操作有:将当前节点的父节点(7)涂黑,将祖父节点(11)涂红,在祖父节点为支点做右旋操作。最后把根节点涂黑,整个红-黑树重新恢复了平衡,如右上图所示。至此,插入操作完成!
修复完的红黑树
红黑树修复代码:
private void insertFixUp(RBNode<T> node){ RBNode<T> parent,gparent;//定义父节点和祖父节点 //需要修正的条件:父节点存在,且父节点的颜色是红色 while(((parent = parentOf(node)) != null) && isRed(parent)){ gparent = parentOf(parent);//获得祖父节点 //若父节点是祖父节点的左子节点,下面的else相反 if(parent == gparent.left){ RBNode<T> uncle = gparent.right;//获得叔叔节点 //case1:叔叔节点也是红色 if(uncle != null && isRed(uncle)){ setBlack(parent);//把父节点和叔叔节点涂黑 setBlack(uncle); setRed(gparent);//把祖父节点涂红 node = gparent;//把位置放到祖父节点处 continue;//继续while循环,重新判断 } //case2:叔叔节点是黑色,且当前节点是右子节点 if(node == parent.right){ leftRotate(parent);//从父节点出左旋 RBNode<T> tmp = parent;//然后将父节点和自己调换一下,为下面右旋做准备 parent = node; node = tmp; } //case3:叔叔节点是黑色,且当前节点是左子节点 setBlack(parent); setRed(gparent); rightRotate(gparent); }else{//若父节点是祖父节点的右子节点,与上面的情况完全相反,本质是一样的 RBNode<T> uncle = gparent.left; //case1:叔叔节点也是红色的 if(uncle != null && isRed(uncle)){ setBlack(parent); setBlack(uncle); setRed(gparent); node = gparent; continue; } //case2:叔叔节点是黑色的,且当前节点是左子节点 if(node == parent.left){ rightRotate(parent); RBNode<T> tmp = parent; parent = node; node = tmp; } //case3:叔叔节点是黑色的,且当前节点是右子节点 setBlack(parent); setRed(gparent); leftRotate(gparent); } } setBlack(root);//将根节点设置为黑色 }