学习视频链接:红黑树学习
注:本篇文档仅是个人学习的简单记录(一刷上述链接的视频),代码是无法运行的,照着视频上随便写的!!!
(注意!!!,这里面是笔者一刷的学习记录,不一定是正确的理解)
本篇文档的后续:红黑树学习笔记02
2023.4.10述:学得不行,建议重学
一. 二叉搜索树BST
定义:左节点都比父节点小,右节点都比父节点大;
常用操作:
- 查找/插入:这两种操作基本上步骤都是重合的,都是从根节点出发开始比较,小于根节点就与左子树比较,大于根节点就与右子树比较,直到左子树为空或者右子树为空;
- 遍历:先、中、后序遍历,这个很好理解的,主要会写递归方式和非递归(栈)方式实现就行;
- 查找最小值:二叉树的最左边节点
- 查找最大值:二叉树的最右边节点
- 查找前驱节点:小于当前节点的最大值,即左子树的最右节点
- 查找后继节点:大于当前节点的最小值,即右子树的最左节点
- 删除:本质上就是找前驱节点或者后继节点来替代删除节点的位置
二. AVI树
定义:任何节点的两个子树的高度最大差别为1,相当于自带平衡功能的二叉搜索树;
- 插入:前面步骤和二叉搜索树一样,然后从根节点开始判断平不平衡,并从距离叶节点最近侧的不平衡节点开始右旋
三. 2-3-4树
定义:四阶B树,属于多路查找树,有以下几种特点:
1.所有叶节点都有相同的深度
2-节点:包含1个元素的节点,有2个子节点;3-节点:包含2个元素的节点,有3个子节点;4-节点:包含3个元素的节点,有4个子节点;
3.节点有多个元素时,每个元素必须大于它左边的和他的左子树中元素
2-3-4树的生成:从叶子节点出发,优先拼接成4-节点,如果拼接前节点已经包含3个元素了,就将中间的元素往上层抬一级(旁边的源节点成为子节点),抬上去之后也尽量凑4-节点
为什么要牵扯到2-3-4树呢?
2-3-4树与红黑树之间有等价关系,一颗234树可以对应多棵红黑树,一棵红黑树只对应一棵234树。红黑树的本质就是2-3-4树。
- 2-3-4树中的2-节点对应黑节色点
- 3-节点的左边节点是黑色,右边是红色,并且拉成上下节点
- 4-节点的左边节点和右边节点是红色,中间是黑色,并且拉成上下节点
- 2-3-4树的裂变状态:红黑树的新增节点一定是红色,而裂变上去的节点变成红色(如果裂变上去的是根节点,要再转成黑色),下面的原节点变成黑色,新增节点会有四种情况:
由2-3-4树转变成红黑树:
- 先找2-节点,将其转变成黑色节点
- 再找3-节点,转换成左倾(上黑下红)
- 再找4-节点转变
- 当然也能转换成左倾
四. 红黑树
上述的性质基本上都可以有2-3-4树推导出来,第一条性质和第二条性质就是由2-3-4树到红黑树转换的性质可以得到,第三条性质是配合第四条性质的,第五条性质就是2-3-4树的叶节点高度相等,然后每个节点都会带有一个黑色节点
旋转操作
左旋:以某个节点为旋转点,其右子节点变成旋转节点的父节点,右子节点的左子节点变成旋转节点的右子节点,左子节点保持不变;
右旋:以某个节点为旋转点,其左子节点变成旋转节点的父节点,左子节点的右子节点变成旋转节点的左子节点,右子节点保持不变;
右旋操作形象一点理解就是,以4为轴,将6往下拉,6拉到下面后占了5的位置,5往右边调,调到6的左边
具体代码实现逻辑(这里用左旋,具体步骤看代码,不想写了):
步骤一:判断待旋转节点p是否为空,如果不为空,就将要调开的左子节点(rl)先调到待旋转节点p的右子节点的位置
步骤二:判断pr的左节点rl是否为空,如果不为空,就说明前一步的操作是有实际意义的,然后就将rl指向节点p(实际上是有双向指针的)
//谁要动参数就是谁,不要错写成轴了
private void leftRotate(RBNode p){
if(p!=null){
RBNode r = p.right;//先取出待旋转节点的右子节点(备份一下?)
//步骤一:处理待旋转节点的右子节点部分
p.right = r.left;//然后按照规定的逻辑对右子节点进行修改,此时也不清楚是不是空,注意这是一个由上指下的指针
if(r.left!=null){//说明上一步实现的结果是待旋转节点的右子节点不为空
r.left.parent = p;//将右子节点的父节点指向带旋转节点,注意这是一个由下指向上的指针
//至此待旋转节点的子节点部分完成
}
//步骤二:处理新节点和待旋转节点的父节点部分
r.parent = p.parent;//将r的父节点指向p的父节点,注意这是一个由下指向上的指针
//注意p有两种情况:
if(p.parent = null) {//表明p是根节点
root = r;
}
//如果上述不满足,表明p不是根节点,那么就要判断p相对于其父节点是左节点还是右节点
else if(p == p.parent.left){
p.parent.left = r;//注意这是一个由上指下的指针
}
else {
p.parent.right = r;//注意这是一个由上指下的指针
}
//步骤三:处理新节点的左子节点部分
r.left = p;//注意这是一个由上指下的指针
p.parent = r;//注意这是一个由上指下的指针
//至此新节点的子节点部分完成
}
}
新增操作
由2-3-4树到红黑树的等价关系
特殊的左三:
//注意代码是不能运行的,只是按逻辑随便写的伪代码
public void put(K key, V value){
RBNode t = this.root;
if(t == null){
//如果是第一个节点,就是根节点
root = new RBNode<>(key, value == ? key : value, null)
return;
}
//如果不符合上诉条件,就说明不是第一个节点了,那么就要和普通二叉树一样从根节点开始找到需要插入的位置
//注意这里比较的是key,而节点是带有两参数的,如果插入节点的key和二叉树内的某个节点的key相同,就用插入节点的value值覆盖原value值就行
RBNode parent;//定义一个双亲指针
if(key==null) throw error;//插入节点的数值为空,抛出异常
//沿着根节点找
do{
parent = t;//将当前节点给到parent,便于后面操作
if(key > t.key) {
t=t.right;
}//进来的值和二叉树本身的值比较
else if(key < t.key) {
t=t.left;
}
else{
t.setValue(value==null?key:value);
return;
}
}while(t != null);
//此时移动到空节点,生成一个新的节点
RBNode<K, Object> new_point = new RBNode<>(key,value==null?key:value, parent);
if(new_point.key>parent.key){
parent.right = new_point;//插入值大于其父节点的值,成为右子树
}
else if(new_point.key<parent.key){
parent.left= new_point;//插入值小于其父节点的值,成为左子树
}
fixAfterPut(new_point);//调整
}
private void fixAfterPut(RBNode x){
x.color = RED;//插入进来的节点一定是红色的
//只有父节点是红色节点才需要调整,对应2-3-4树的插入3-节点的情况和插入4-节点的情况,具体是哪种里面还会判断
while(x!=null && x!=root && x.parent.color==RED){
//1.x的父节点是祖父节点的左孩子(左3)
if(getParent(x) == getLeftSon(getParent(getParent(x))){
//这里包含图1、图2和图3的情况
//这里注意有一个技巧点,就是如果一个节点是空节点,就默认颜色是黑色,也就是说只要节点颜色为红色,就一定不为空
RBNode y = getRightSon(getParent(getParent(x)));//这是叔叔节点
if(getColor(y) == RED) {
//这里是图2的情况
//通过判断父节点的兄弟节点来区分是插入3-节点还是插入4-节点,因为3-节点是只有一个子节点的,另一边的空子节点颜色是黑色
//如果进入了这个程序段,就说明是插入4-节点的情况,这种情况不需要进行旋转操作,只需要变色
setColor(getParent(x),BLACK);
setColor(y,BLACK);
setColor(getParent(getParent(x)),RED);
//这里有递归意思:在完成上述操作后,所调整的只是一个局部的情况,而我们需要将整棵红黑树进行调整,因此将x指针指向祖父节点继续调整
x=getParent(getParent(x));//祖父节点继续往上调整,
//因为插入4-节点的裂变状态,上升节点对应到红黑树一定是红色的,就有可能发生冲突;
//而单边的情况涉及到旋转,会将父节点设置为黑色,就不存在双红连接的情况,就不需要往上调整
}
else {
//这里是图1和图3的情况,注意不要遗漏那个特殊的“左三”(图3)
if(x==getRightSon(getParent(x))){
//这里是图3的情况
x=getParent(x);
leftRotate(x);
}
//后续是图1需要的操作
setColor(getParent(x),BLACK);//父节点变色
setColor(getParent(getParent(x) ) , RED);//祖父节点变色
rightRotate(getParent(getParent(x));//以祖父节点为对象进行右旋
x = getParent(getParent(x));//继续往上调整
}
}
//2.x的父节点是祖父节点的右孩子(右3)
else {
//这里包含图4、图5和图6的情况
//这里注意有一个技巧点,就是如果一个节点是空节点,就默认颜色是黑色,也就是说只要节点颜色为红色,就一定不为空
RBNode y = getLeftSon(getParent(getParent(x)));//这是叔叔节点
if(getColor(y) == RED) {
//这里是图2的情况
//通过判断父节点的兄弟节点来区分是插入3-节点还是插入4-节点,因为3-节点是只有一个子节点的,另一边的空子节点颜色是黑色
//如果进入了这个程序段,就说明是插入4-节点的情况,这种情况不需要进行旋转操作,只需要变色
setColor(getParent(x),BLACK);
setColor(y,BLACK);
setColor(getParent(getParent(x)),RED);
x=getParent(getParent(x));//祖父节点继续往上调整
} else {
//这里是图4和图6的情况,注意不要遗漏那个特殊的“右三”(图6)
if(x==getLeftSon(getParent(x))){
//这里是图6的情况
x=getParent(x);
rightRotate(x);
}
//后续是图4需要的操作
setColor(getParent(x),BLACK);//父节点变色
setColor(getParent(getParent(x)),RED);//祖父节点变色
leftRotate(getParent(getParent(x));//以祖父节点为对象进行右旋
}
}
}
//在完成上述调整后,根节点的颜色一定是红色,要转成黑色
root.color = BLACK;
}
删除操作
首先看普通二叉树的删除操作:
- 删除叶子结点,直接删除;
- 删除的节点只有一个子节点,用子节点替代;
- 删除的节点有两个子节点,此时需要用前驱或者后继节点替代;
具体的删除方案:如果删除的是二叉树的叶子结点,那么就直接删除对应的指针,如果不是叶子结点,那么待删除节点的指针不变,用其前驱节点或者后继节点的值来覆盖待删除节点的值,然后删除其前驱或者后继节点即可。
能出现的状况只有两种:待删除节点是叶子结点和待删除节点只有一个子节点(例如要删除4,用后继节点替代的话就要用6,就将6的值赋给4,再将6删除,其子节点6.5就替代6的位置挂到7下面)
那么接下来就先看一下获取前驱节点的代码实现逻辑
private void predecessor(RBNode node){
if (node==null){
return null;
}
else if(node.left != null){
RBNode p = node.left;
while(p.right!=null){
p=p.right;
}
return p;
}
else{//没有左子树的情况,沿着父节点往回找到根节点
RBNode p = node.parent;
RBNode ch = node;
while(p!=null && ch !=p.right){
ch = p;
p= p.parent;
}
return p;
}
}
//找后继节点
private void sucessor(RBNode node){
if (node==null){
return null;
}
else if(node.right != null){
RBNode p = node.left;
while(p.left!=null){
p=p.left;
}
return p;
}
else{//没有左子树的情况,沿着父节点往回找到根节点
RBNode p = node.parent;
RBNode ch = node;
while(p!=null && ch !=p.left){
ch = p;
p= p.parent;
}
return p;
}
}
其次就是删除节点
public V remove(K key){
RBNode node=getNode(key);//先找到待删除节点
if(node==null){
return null;
}
V oldValue = node.value;
deteleNode(node);
return oldValue;
}
private RBNode getNode(K key){
RBNode node = this.root;
while(node!=null){
if(key > node.key){
node = node.right;
}
else if(key < node.key){
node = node.left;
}
else{
return node;
}
}
return null;
}
//删除操作有三种情况:1.删除叶子结点,直接删除;2.删除的节点有一个子节点,用子节点替代;3.删除的节点有两个子节点,需要找到前驱或者后继来替代;
//注:这个删除节点函数不仅是用于删除叶子结点,应该是删除任意节点
private void deteleNode(RBNode node){
//情况3.node节点有两个孩子
if(node.left!=null && node.right!=null){
RBNode successor = successor(node);//找到后继节点
node.key = successor.key;//赋值
node.value = successor.value;
node = successor;//将node指针指向successor
//到这一步就只会出现两种情况,要么找到的右子树的最左边是叶子节点,要么就是右子树的最左边节点后带有一个右子节点
}
//如果符合if条件,上面的if程序段执行完后,节点指针就会是图7,图8两种情况,是一定会来到有子树的最左边;
//注意这里的情况还包括原来的待删除节点只有一棵子树的情况,也就是不走if的程序段,其实这里的情况就包括了图7、8,
//三目运算符就表示如果有子节点就取出子节点,没有子节点就取出空节点,接下来就要区分是否为根节点
RBNode replacement = node.left!=null?node.right:node.right;
//替代节点不为空
if(replacement != null){
replacement.parent = node.parent;
//注意在待删除节点不是叶子节点的情况下,又有两种情况了,一是待删除节点的父节点是根节点,二是正常情况
if(node.parent = null){
//说明待删除节点是根节点
root = replacement;
}
else if(node == node.parent.left) {
//待删除节点属于是一个左子节点的性质
node.parent.left = replacement;
}
//如果上面两个if都不满足,说明待删除节点属于是一个右子节点的性质
node.parent.right = replacement;
//将待删除节点删除后,接下来就要考虑颜色调整的问题
if(node.color == BLACK){
//如果待删除节点的颜色是黑色就一定要调整,因为黑色节点被删除了,就会出现两个红色节点相邻的情况,红黑树的平衡就是黑色平衡
//这种情况替代节点是红色的,此时只需要变色
fixAfterRemove(replacement);
}
}
//接下来这一部分才涉及真正的删除操作,前面都是用其他节点替代待删除节点的值,然后将删除这个操作的对象转移到其他节点上
//如果没有执行上面的if(replacement != null)里的程序,说明待删除节点是叶子结点,这时也有两种情况,要么待删除节点是根节点,要么不是根节点
else if(node.parent == null){
root = null;
}
else{
//这时就是真正的叶节点删除
//先调整颜色
if(node.color == BLACK){
//如果待删除节点的颜色是黑色就一定要调整,因为黑色节点被删除了,就会出现两个红色节点相邻的情况,红黑树的平衡就是黑色平衡
fixAfterRemove(replacement);
}
//再进行删除
if(node.parent != null){
if(node == node.parent.left){
node.parent.left=null;
}
else if(node == node.parent.right){
node.parent.right=null;
}
node.parent=null;
}
}
}
private void fixAfterRemove(RBNode x){
while(x!=root&&getColor(x)==BLACK){
//x是左孩子的情况
if(x==getLeftSon(getParent(x))){
RBNode rnode = getRightSon(getParent(x));
//判断此时兄弟节点是否是真正的兄弟节点(即是不是2-3-4树对应的那个兄弟),代码下面的文字有 代码解释----兄弟节点的情况:
if(getColor(rnode)==RED){
setColor(rnode,BLACK);
setColoe(getParent(x),RED);
leftRotate(getParent(x));
rnode = getRightSon(getParent(x));//找到真正的兄弟节点
}
if(getColor(getLeftSon(rnode))==BLACK && getColor(getRightSon(rnode))==BLACK){
//注意这里的代码判断的不是黑色,而是判断是否为空,因为getColor定义时是默认空节点为黑色
//情况三,找兄弟借,兄弟没得借(兄弟没有子节点,2节点),一直往上递归调整黑色
setColor(rnode,RED);
x=getParent(x);
}
else{
//情况二,找兄弟借,兄弟有得借(这里也分为两种情况,兄弟节点为3节点或者4节点)
if(getColor(getLeftSon(rnode))==BLACK){
//右孩子为空,兄弟节点为三节点
setColor(getLeftSon(rnode),BLACK);
setColor(rnode,RED);
rightRotate(rnode);
rnode=getRightSon(getParent(x));
}
//兄弟节点为4节点,此时可以有两种做法,一是父亲下来,从兄弟节点借一个节点上去,这种情况需要旋转两次;二是父亲下来,从兄弟节点借两节点,一个上去,一个移到父亲旁边,这种情况只需要旋转一次,因此下面的代码是借两
setColor(rnode,getColor(getParent(x));
setColor(getParent(x),BLACK);
setColor(getRightSon(rnode),BLACK);
leftRotate(getParent(x));
x=root;//结束
}
}
else{
RBNode rnode = getLeftSon(getParent(x));
//判断此时兄弟节点是否是真正的兄弟节点(即是不是2-3-4树对应的那个兄弟),代码下面的文字有 代码解释----兄弟节点的情况:
if(getColor(rnode)==RED){
setColor(rnode,BLACK);
setColoe(getParent(x),RED);
RightRotate(getParent(x));
rnode = getLeftSon(getParent(x));//找到真正的兄弟节点
}
if(getColor(getRightSon(rnode))==BLACK && getColor(getLeftSon(rnode))==BLACK){
//注意这里的代码判断的不是黑色,而是判断是否为空,因为getColor定义时是默认空节点为黑色
//情况三,找兄弟借,兄弟没得借(兄弟没有子节点,2节点),一直往上递归调整黑色
setColor(rnode,RED);
x=getParent(x);
}
else{
//情况二,找兄弟借,兄弟有得借(这里也分为两种情况,兄弟节点为3节点或者4节点)
if(getColor(getRightSon(rnode))==BLACK){
//右孩子为空,兄弟节点为三节点
setColor(getRightSon(rnode),BLACK);
setColor(rnode,RED);
leftRotate(rnode);
rnode=getLeftSon(getParent(x));
}
//兄弟节点为4节点,此时可以有两种做法,一是父亲下来,从兄弟节点借一个节点上去,这种情况需要旋转两次;二是父亲下来,从兄弟节点借两节点,一个上去,一个移到父亲旁边,这种情况只需要旋转一次,因此下面的代码是借两
setColor(rnode,getColor(getParent(x));
setColor(getParent(x),BLACK);
setColor(getLeftSon(rnode),BLACK);
rightRotate(getParent(x));
x=root;//结束
}
}
}
//情况一:如果替代节点是红色节点,直接染黑
setColor(x,BLACK);
}
红黑树的删除操作对应到2-3-4数的删除操作,对于红黑树来说,在前面介绍到的可以用其他节点的值覆盖待删除节点的值来节省删除这个操作的前提下,真正删除操作就只会发生在红黑树的倒数第一层和倒数第二层内,而红黑树的倒数两层对应到2-3-4树就是叶子结点
既然可以转换到2-3-4树进行删除操作,接下来就看2-3-4树的删除情况:
- 首先对于3节点和4节点来说,都是可以依靠自己节点完成的,例如下图
删除0就直接删除,删除1就用0顶替位置后变黑,删除9就直接删除,删除11也是直接删除,删除10就用9或者10顶替位置后变黑 - 删除2节点 ,自己节点是搞不定的,需要跟兄弟借,但是兄弟不借,找父亲借,然后兄弟找一个节点代替父亲位置
(注意2-3-4树本身是没有颜色的,图9只是用红黑树的颜色在2-3-4树的结构表示)
代码解释----兄弟节点的情况:
在2-3-4树上,节点5,7是兄弟节点,而在红黑树上5和8是兄弟节点,将6和8互换颜色后即可(下面两图的区别)