数据结构与算法----红黑树

1,568 阅读22分钟

简介

红黑树,为了优化排序二叉树极端情况(从小到大或从大到小插入,呈现链状)而衍生出来的另一种结构

基本性质

插入删除等操作基于排序二叉树,因此可以在此基础上,通过插入和删除,利用红黑树特有性质来维护红黑树

特有性质

性质1:每个节点要么是黑色,要么是红色。

性质2:根节点是黑色。

性质3:每个叶子节点(NULL)是黑色(由于数据结构基础特征,实际叶子节点下都会多出来一个NULL节点,当做黑色处理,或者当不存在)。

性质4:每个红色结点的两个子结点一定都是黑色。

性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

根据性质4也可以的:不会同时存在相连的两个红节点

红黑树与AVL树进行对比

1.AVL树由于严格平衡,查询的时间复杂度要略优于红黑树(查询次数实际很少)

2.红黑树的插入删除比AVL树更便于控制操作

3.红黑树整体性能略优于AVL树(红黑树旋转情况少于AVL树,由于红黑树不是严格平衡,只需局部达到条件)

红黑树节点基本数据结构

typedef struct TreeNode {
    int data;
    int color; //0红色 1黑色
    struct TreeNode *parentNode;
    struct TreeNode *l, *r;
}LSTreeNode;

红黑树节点称呼简介

后面会根据当前关系进行固定称呼

查询

所有二叉排序树的查询过程都是一致的,包括平衡二叉树和红黑树,都是通过二分法向下查询,可以参考二叉排序树的查询

旋转

红黑树每增加或者删除一个元素时,当变色已经不能满足红黑树性质时,只能通过旋转来解决问题

因此旋转是了解插入和删除之前,必须要知道的知识点,可参考上一章二叉树的旋转

红黑树旋转时,由于会涉及到变色问题,这里采用替换式方案进行旋转,后续的旋转,可参考此方案,不用专门处理变色问题

#pragma mark --替换式左旋,右节点作为临时变量,已经交接好父子关系,方便统一使用
void swapLeftRotation(LSTreeNode *parentNode, LSTreeNode *rightNode) {
    int data = parentNode->data;
    parentNode->data = rightNode->data;
    rightNode->data = data;
    parentNode->r = rightNode->r;
    rightNode->r->parentNode = parentNode;
    rightNode->r = rightNode->l;
    rightNode->l = parentNode->l;
    if (parentNode->l) parentNode->l->parentNode = rightNode;
    parentNode->l = rightNode;
}

#pragma mark --替换式右旋,通过左节点作为临时变量,已经交接好父子关系,方便统一使用
void swapRightRotation(LSTreeNode *parentNode, LSTreeNode *leftNode) {
    int data = parentNode->data;
    parentNode->data = leftNode->data;
    leftNode->data = data;
    parentNode->l = leftNode->l;
    leftNode->l->parentNode = parentNode;
    leftNode->l = leftNode->r;
    leftNode->r = parentNode->r;
    if (parentNode->r) parentNode->r->parentNode = leftNode;
    parentNode->r = leftNode;
}

红黑树的插入

根据红黑树的特性,根节点固定黑色暂时不考虑,当插入一个新元素时,若为黑色元素则一定不会满足红黑树的条件,会加大红黑树维护成本,因此插入新节点采用插入红色节点的方式来处理

因此当插入一个节点时节点时,会出现如下多种情况:

1.插入节点为根节点

直接插入黑色即可,无须额外处理

2.插入节点为非根节点

为减少额外场景,插入节点标记为红色,此时根据其所处的位置和周围节点情况,会出现不同的处理方案

2.1父节点为黑色

由于插入节点为红色,此时红黑树仍然满足所有性质,不需要额外平衡

如下图所示,插入节点为I

2.2父节点为红色

由于是红黑树,在插入之前节点关系一定是满足红黑树性质的,此时父节点为红色,所以祖父节点一定为黑色方可满足红黑树性质

此时插入了红色节点,其和父节点通通为红色,已经不满足红黑树性质4,且已知条件为:

1.祖父节点黑色

2.父节点红色

3.插入节点红色

因此可以根据叔叔节点和子节点情况进行分类平衡

2.2.1叔叔节点为红色

叔叔节点和父节点一样都为红色情况,为了维持黑节点总数不变,因此,经过一下步骤即可处理:

1.父节点和叔叔节点变成黑色

2.当祖父节点为根节点,满足红黑树性质,直接平衡结束;否则进入步骤3

3.祖父节点变成红色

4.处理祖父节点变成红色,但可能给上级带来的不平衡(出现两个红色节点情况),因此需要将父节点作为红色插入节点,继续向上平衡,重复整个大流程

处理过程发现,碰到祖父节点为根节点的情况,保持黑色不能改变,于此同时,只要两侧同时增加或减少一级黑色节点,仍然满足红黑树条件,因此直接执行步骤1即可

以I1为插入节点,平衡一轮后,将I作为插入节点继续向上平衡,如下图所示

2.2.2叔叔节点为黑色或者为空

叔叔节点为空的情况,说明是最下面一层子树插入,叔叔节点不为空,则为平衡一轮或者多轮(即自下而上平衡过程中存在的情况),部分参考2.2.1的图

如下图所示,分别以S1为插入节点(第三个)和I1为插入节点(第一个)为例,即为叔叔节点为黑色、为空的情况

平衡过程根据插入节点和其父节点位置进行如下平衡

2.2.2.1父节点为祖父左子节点

如果插入节点为父节点右子节点,则需要对插入节点和父节点先进行左旋(正常左旋),然后会形成插入节点在父节点左侧情况

如下图所示,图1中P1-I1左旋变成与图2一样的结构,因此形成父节点在祖父节点左侧情况,同时右节点为空,同时图3上部分以S1作为插入节点,一样的结构,除了叔叔节点变成了黑色节点

因此处理方案整体如下:

1.将父节点和祖父节点整体右旋(例如图2 S1-P1,图3 G-P,也可以采用前面介绍的替换式右旋方案,不用变色即可解决方案)

2.将新的祖父节点变成黑色,左右节点变成红色

处理后的结果如下图所示,以图2和图3为例:

2.2.2.2父节点为祖父右子节点

此情况与2.2.2.1为对称结构,如果了解了前面的此接对称处理即可

如果插入节点为父节点左子节点,则需要对插入节点和父节点先进行右旋(正常右旋),然后会形成插入节点在父节点右侧情况

如下图所示,图1中 P1-I1 右旋变成与图2一样的结构(只看颜色结构),因此形成父节点在祖父节点右侧情况,同时左节点为空,同时图3上部分以S1作为插入节点,一样的结构,除了叔叔节点变成了黑色节点

因此处理方案整体如下:

1.将父节点和祖父节点整体左旋旋(例如图2 S1-P1,图3 G-P,也可以采用前面介绍的替换式左旋方案,不用变色即可解决方案)

2.将新的祖父节点变成黑色,左右节点变成红色

处理后的结果如下图所示,以图2和图3为例:

至此,红黑树的插入完成

插入代码实现

#pragma mark --插入数据节点
//插入新数据,先插入后根据颜色节点关系平衡
LSTreeNode *insertRBTData(LSTreeNode *root, int data) {
    LSTreeNode *node = (LSTreeNode *)malloc(sizeof(LSTreeNode));
    node->parentNode = NULL;
    if (!root) {
        node->color = 1; //没有根节点插入根节点设置成黑色,不需要平衡
        return node;
    }
    LSTreeNode *p = root, *s = NULL;
    while (p) {
        s = p;
        if (p->data == data) {
            p->data = data; //赋予新的值即可,因为真实data可能不为数字,data可能只是一个key,也不需要平衡
            free(node);
            return root;
        }
        if (data < p->data) {
            p = p->l;
        }else {
            p = p->r;
        }
    }
    //除了根节点默认插入的是红色节点
    node->color = 0;
    node->parentNode = s; //设置父节点
    
    if (data < s->data) {
        s->l = node;
    }else {
        s->r = node;
    }
    return insertRBTNode(root, node); //到这里的插入节点需要进行后续的平衡操作
}

#pragma mark --平衡插入数据节点
//平衡插入的新数据,主要是处理本分支新加入的红节点,内部能消化就消化,不能消化的话,就直接拿着红节点向上接着消化
//主要通过变色旋转来平衡多出来的红色节点
LSTreeNode *insertRBTNode(LSTreeNode *root, LSTreeNode *insertNode) {
    LSTreeNode *parentNode = insertNode->parentNode; //父节点,到这里父节点肯定不为空
    if (parentNode->color == 1) return root; //父节点是黑色直接结束即可
    //祖父节点
    //如果父节点不是根节点,祖父节点也肯定存在,不会走到这里 ,因为无祖父节点的情况走到这个方法的情况只有:父节点为根节点,子节点为自下而上来的红节点不需要平衡,第一步就结束了
    LSTreeNode *grandNode = parentNode->parentNode;
    //到这里父节点一定是红色的,那么祖父节点一定是黑色的
    if (grandNode->l == parentNode) {
        //父节点是祖父节点的左节点
        if (!grandNode->r || grandNode->r->color == 1) {
            //父节点的兄弟节点不存在或者黑色
            if (parentNode->r == insertNode) {
                //当前插入节点是父节点的右节点,直接对父节点左旋, 形成和插入左子节点一样的情况
                swapLeftRotation(parentNode, insertNode);
            }
            //此时父节点和左子节点都为红色,直接右旋变色即可平衡两个连续红色节点问题,
            //这一层的旋转方案不要调用祖父节点的父节点,使用替换值的方式进行旋转,通用处理,避免出现根节点需要特殊处理的问题
            //交换父节点和祖父节点值,子节点和父节点变成了祖父节点的左右子节点
            swapRightRotation(grandNode, parentNode);
            //最后变色,本来就是值和关系交换行程的旋转,子节点和父节点变成了祖父节点的左右子节点,发现此过程节点颜色不需要变发现已经和平衡后一样
        }else {
            //叔叔节点为红色
            //父节点和叔叔节点设置成黑色,祖父节点设置成红色,不增加黑节点情况,通过变色进行平衡,但祖父会变成红色,需要自下而上平衡
            grandNode->l->color = 1;
            grandNode->r->color = 1;
            if (grandNode->parentNode) {
                grandNode->color = 0; //祖父节点不是是根节点变成红色
            }else {
                return root; //祖父节点是根节点就结束
            }
            return insertRBTNode(root, grandNode); //祖父节点变成插入节点自下而上平衡
        }
    }else {
        //父节点是祖父节点的右节点
        if (!grandNode->l || grandNode->l->color == 1) {
            //叔叔节点不存在或者黑色
            if (parentNode->l == insertNode) {
                //当前插入节点是父节点的右节点,直接对父节点右旋, 形成和插入左子节点一样的情况
                swapRightRotation(parentNode, insertNode);
            }
            //此时父节点和右子节点都为红色,直接左旋变色即可平衡两个连续红色节点问题,
            //这一层的旋转方案不要调用祖父节点的父节点,使用替换值的方式进行旋转,通用处理,避免出现根节点需要特殊处理的问题
            //交换父节点和祖父节点值,父节点和子节点变成了祖父节点的左右子节点
            swapLeftRotation(grandNode, parentNode);
            //最后变色,本来就是值和关系交换行程的旋转,父节点和子节点变成了祖父节点的左右子节点,发现此过程节点颜色不需要变发现已经和平衡后一样
        }else {
            //叔叔节点为红色
            //父节点和叔叔节点设置成黑色,祖父节点设置成红色,不增加黑节点情况,通过变色进行平衡,但祖父会变成红色,需要自下而上平衡
            grandNode->l->color = 1;
            grandNode->r->color = 1;
            if (grandNode->parentNode) {
                grandNode->color = 0; //祖父节点不是是根节点变成红色
            }else {
                return root; //祖父节点是根节点就结束
            }
            return insertRBTNode(root, grandNode); //祖父节点变成插入节点自下而上平衡
        }
    }
    return root;
}

红黑树的删除

根据红黑树的性质,适当改进二叉树删除过程,然后再平衡,如图所示,红黑树删除步骤经过如下步骤:

按照流程图所示,红黑树删除基本步骤如下:

1.左右节点均存在:按照排序二叉树的步骤,找到逻辑上下一个节点,替换之即可,那么该替换节点最多只能拥有一个右节点的情况,可以后续处理之

2.仅左节点存在:经过步骤1后,左节点仍然存在说明一开始就这样,根据红黑树性质其只有左侧一个红色节点,直接删除即可

3.仅右节点存在:根据红黑树性质,右节点一定为红色,直接使用右节点替换之即可

4.不存在子节点:此情况可能本来就是要删除的无子节点,也可能是替换删除节点为无子节点,且删除有三种情况:

(4.1)此时如果节点为根节点,直接删除即可,成为空树,不存在平衡问题;

(4.2)如果节点为红色,由于无子节点,也直接删除即可,且删除后不影响红黑树平衡;

(4.3)如果节点为黑色,删除后一侧会减少一个黑节点,不满足红黑树性质5,因此需要右侧节点和父节点配合平衡

红黑树平衡步骤:

此过程是经过基本步骤的4.3步骤之后的处理方案:即删除节点为黑色无子节点的平衡方案

由于一侧将缺少一个黑色节点,即将不满足性质5,为了保证黑色节点正常,有以下大体方案:

1.从右侧借出红色节点变黑处理到左侧

2.将父节点由红变黑,兄弟节点由黑变红

3.是左右分支整体减少一个黑色节点,交给上部消化,或者直接到根节点处理

参考删除流程,对如下情况进行处理:

1删除节点为左节点

此时删除节点在父节点左侧,且为黑色,因此需要手段来平衡少掉的那个黑色节点,只能靠父节点和兄弟节点情况来进行平衡

1.1删除节点兄弟节点为红色

根据红黑树性质,父节点为黑色,兄弟子节点均为黑色

此时经过直接旋转和变色已经无法直接处理掉多出的黑色子节点,但可以通过左旋和变色转化成1.2的情形处理

如下图所示,经过了如下步骤:

1.对父节点P和兄弟节点B进行左旋,并交换颜色

2.删除节点周边情况变成1.2的情况(即:兄弟节点变成黑色)

1.2删除节点兄弟节点为黑色

此时兄弟节点为黑色,无法直接推测出孩子和父节点的颜色,需要通过兄弟的孩子节点等情况来处理

1.2.1删除节点的兄弟左子节点为红色,右子节点黑色或者不存在

此场景无法通过旋转直接处理,可以通过旋转,交换颜色,形成和1.2.2一样的场景来处理

如下图所示,经过了如下步骤:

1.对兄弟节点B和其左子节点BL进行右旋,并交换颜色

2.删除节点周边情况变成1.2.2的情况(即:兄弟右子节点为红色)

1.2.2删除节点兄弟右子节点为红色

此时兄弟右子节点为红色,兄弟节点为黑色,直接左旋可以使得左侧多获取一个黑色节点,右侧红色节点标记黑色即可平衡

如下图所示,经过了如下步骤:

1.对父节点P和兄弟节点B进行左旋

2.将父节点P变为黑色,将原兄弟节点的右子节点BR变为黑色

注意:D节点是要删除的节点(或者作为删除节点自下而上平衡的可以忽略),BL节点可能为空也可能为其他颜色(自下而上平衡可能存在的场景),总而言之,BL和在B点处享受相同的场景

1.2.3删除节点兄弟子节点均不存在或者均为黑色

第一轮平衡时(如下图第一个)如果兄弟节点全为空,则符合红黑树情况;图二所示,则不符合红黑树场景,实际上,图二出现的兄弟子节点均为黑色场景,应当为图3所示的自下而上平衡过程所出现的情况,即:从D0开始无法平衡,无法平衡时将父节点D在作为删除节点向上平衡

平衡时根据父节点的情况对其进行如下平衡手段,如下图所示:

1.将兄弟节点变为黑色

2.父节点为红色(如下图第二排所示),父节点变为黑色即可平衡

3.父节点为黑点,已经无法通过变色来处理本机减少的黑色节点问题;但如果父节点已经是根节点,则整体减少一级黑色节点平衡结束;否则,将父节点作为删除节点,来平衡减少的一个黑色节点,自下而上重新开始整个平衡步骤(如下图第一排最后一个所示)

2删除节点为右节点(对称侧)

与1章节对称,此时删除节点在父节点右侧,且为黑色,因此需要手段来平衡少掉的那个黑色节点,只能靠父节点和兄弟节点情况来进行平衡

2.1删除节点兄弟节点为红色

根据红黑树性质,父节点为黑色,兄弟子节点均为黑色

此时经过直接旋转和变色已经无法直接处理掉多出的黑色子节点,但可以通过右旋和变色转化成2.2的情形处理

如下图所示,经过了如下步骤:

1.对父节点P和兄弟节点B进行右旋,并交换颜色

2.删除节点周边情况变成2.2的情况(即:兄弟节点变成黑色)

2.2删除节点兄弟节点为黑色

此时兄弟节点为黑色,无法直接推测出孩子和父节点的颜色,需要通过兄弟的孩子节点等情况来处理

2.2.1删除节点的兄弟右子节点为红色,左子节点黑色或者不存在

此场景无法通过旋转直接处理,可以通过旋转,交换颜色,形成和2.2.2一样的场景来处理

如下图所示,经过了如下步骤:

1.对兄弟节点B和其右子节点BR进行左旋,并交换颜色

2.删除节点周边情况变成2.2.2的情况(即:兄弟左子节点为红色)

2.2.2删除节点兄弟左子节点为红色

此时兄弟右子节点为红色,兄弟节点为黑色,直接右旋可以使得右侧多获取一个黑色节点,左侧红色节点标记黑色即可平衡

如下图所示,经过了如下步骤:

1.对父节点P和兄弟节点B进行右旋

2.将父节点P变为黑色,将原兄弟节点的左子节点BL变为黑色

2.2.3删除节点兄弟子节点均不存在或者均为黑色

第一轮平衡时(如下图第一个)如果兄弟节点全为空,则符合红黑树情况;图二所示,则不符合红黑树场景,实际上,图二出现的兄弟子节点均为黑色场景,应当为图3所示的自下而上平衡过程所出现的情况,即:从D0开始无法平衡,无法平衡时将父节点D在作为删除节点向上平衡

平衡时根据父节点的情况对其进行如下平衡手段,如下图所示:

1.将兄弟节点变为黑色

2.父节点为红色(如下图第二排所示),父节点变为黑色即可平衡

3.父节点为黑点(如下图第一排所示),已经无法通过变色来处理本机减少的黑色节点问题;但如果父节点已经是根节点,则整体减少一级黑色节点平衡结束;否则,将父节点作为删除节点,来平衡减少的一个黑色节点,自下而上重新开始整个平衡步骤(如下图第一排最后一个所示)

删除代码实现

#pragma mark --删除
//查找并删除节点
LSTreeNode *deleteRBTData(LSTreeNode *root, int data) {
    if (!root) return NULL;
    LSTreeNode *p = root, *s = NULL;
    while (p) {
        if (p->data == data) break; //找到了要删除的节点
        s = p; //否则s指向当前节点,p接着往下走
        if (p->data > data) {
            p = p->l;
        }else {
            p = p->r;
        }
    }
    if (!p) return root; //没找到要删除的节点
    //p为要删除的节点
    //下面开始,删除红黑树节点
    if (p->r && p->l) {
        //两个节点均存在,那么这次与排序二叉树和AVL树不同,从右分支找出删除节点的后继节点进行替换
        LSTreeNode *q = p; //保存被替换节点用于替换值
        s = p; //s仍然保存父节点
        p = p->r;
        while (p->l) {
            s = p;
            p = p->l;
        }
        //替换节点的值赋值到被替换节点,然后进行统一删除即可
        q->data = p->data;
        //紧接着删除节点p转化为了叶子节点或者是只有一个节点的叶子节点
    }
    //不存在两个子节点的情况了
    if (p->l) {
        //仍然有左节点,那么左节点一定是红节点,且无子节点,p节点为黑色,不然符合红黑树特征
        p->data = p->l->data;
        free(p->l);
        p->l = NULL;
    }else if (p->r) {
        //只存在右节点,那么右节点一定为红色,p是黑色,直接换色平衡即可
        p->data = p->r->data;
        free(p->r);
        p->r = NULL;
    }else {
        //没有子节点
        if (p == root) {
            //根节点
            free(p);
            root = NULL;
        }else {
            //删除节点p,并置空父节点
            if (s->l == p) s->l = NULL;
            else s->r = NULL;
            
            //由于子节点是黑色,删除后需要平衡
            if (p->color == 1) root = deleteRBTNode(root, s, NULL);
            
            free(p);
        }
    }
    return root;
}

#pragma mark --平衡删除数据节点
LSTreeNode *deleteRBTNode(LSTreeNode *root, LSTreeNode *parentNode, LSTreeNode *deleteNode) {
    //由于deleteNode少了个黑色的Node节点才需要平衡,所以brotherNode一定存在
    LSTreeNode *brotherNode = parentNode->l == deleteNode ? parentNode->r : parentNode->l;
    //父节点是黑色节点
    //由于deleteNode少了个黑色的Node节点才需要平衡,所以brotherNode的黑色节点至少得有一层
    if (parentNode->l == deleteNode) {
        //删除节点为左节点,兄弟节点是右节点
        if (brotherNode->color == 0) {
            //兄弟节点为红色节点,兄弟左右子节点一定都为黑色节点
            swapLeftRotation(parentNode, brotherNode); //左旋形成兄弟节点为黑节点的情况
            //更新原删除节点的最新父兄节点
            brotherNode = parentNode->l == deleteNode ? parentNode->r : parentNode->l;
            parentNode = deleteNode->parentNode;
        }
        //此时删除节点的兄弟节点为黑色
        if (brotherNode->l && brotherNode->l->color == 0 && (!brotherNode->r || brotherNode->r->color == 1)) {
            //兄弟左节点为红色,右节点为黑色或者不存在,直接右旋变成右节点为红色状态,由于替换式旋转这两个不会变色,则不需要变色
            swapRightRotation(brotherNode, brotherNode->l);
        }
        if (brotherNode->r && brotherNode->r->color == 0) {
            //兄弟节点右节点为红色
            swapLeftRotation(parentNode, brotherNode);
            parentNode->r->color = 1; //新形成的树,原父节点位置右节点变成黑色则平衡完毕
        }else {
            //根据红黑树性质,此时只剩下兄弟节点只剩下都为空或者都为黑色节点情况
            if (parentNode->color == 0) {
                //父节点为红色
                brotherNode->color = 0;
                parentNode->color = 1; //为了逻辑更清晰不提取了
            }else {
                //父节点为黑色
                brotherNode->color = 0; //兄弟节点设置红色
                if (parentNode->parentNode) {
                    //没到根节点向上消化
                    return deleteRBTNode(root, parentNode->parentNode, parentNode);
                }
                //到根节点已经平衡了结束
            }
        }
    }else {
        //删除节点为右节点,兄弟节点是左节点
        if (brotherNode->color == 0) {
            //兄弟节点为红色节点,兄弟左右子节点一定都为黑色节点
            swapRightRotation(parentNode, brotherNode); //右旋形成兄弟节点为黑节点的情况
            //更新原删除节点的最新父兄节点
            brotherNode = parentNode->l == deleteNode ? parentNode->r : parentNode->l;
            parentNode = deleteNode->parentNode;
        }
        //此时删除节点的兄弟节点为黑色
        if (brotherNode->r && brotherNode->r->color == 0 && (!brotherNode->l || brotherNode->l->color == 1)) {
            //兄弟右节点为红色,左节点为黑色或者不存在,直接左旋变成左节点为红色状态,由于替换式旋转这两个不会变色,则不需要变色
            swapLeftRotation(brotherNode, brotherNode->r);
        }
        if (brotherNode->l && brotherNode->l->color == 0) {
            //兄弟节点左节点为红色
            swapRightRotation(parentNode, brotherNode);
            parentNode->l->color = 1; //新形成的树,原父节点位置右节点变成黑色则平衡完毕
        }else {
            //根据红黑树性质,此时只剩下兄弟节点只剩下都为空或者都为黑色节点情况
            if (parentNode->color == 0) {
                //父节点为红色
                brotherNode->color = 0;
                parentNode->color = 1; //为了逻辑更清晰不提取了
            }else {
                //父节点为黑色
                brotherNode->color = 0; //兄弟节点设置红色
                if (parentNode->parentNode) {
                    //没到根节点向上消化
                    deleteRBTNode(root, parentNode->parentNode, parentNode);
                }
                //到根节点已经平衡了结束
            }
        }
    }
    return root;
}