红黑树性质

红黑树是一颗自平衡二叉查找树,相较于普通的二叉查找树,红黑树左右子树更加平衡

  1. 每个节点是红色的或者黑色的
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个儿子都是黑色的。
  4. 对每个节点,从该节点到其子孙节点的所有路径上的包含相同数目的黑色节点。

对于红黑树来说,所有的叶子节点都是黑色并且在图中隐藏,下图就是一颗红黑树:

image.png
在上图中,我们发现所有的叶子节点都隐藏起来了,颜色为黑色,例如key为232的节点就有两个儿子,两个儿子都为叶子节点。

红黑树的应用

我们知道红黑树有很多应用,开源项目中同时也有很多红黑树的应用:

  1. Linux进程调度
  2. Nginx Timer事件管理
  3. Epoll事件块管理

红黑树定义

#define RED 1
#define BLACK 2

typedef int KEY_TYPE;

typedef struct _rbtree_node //红黑树节点
{
    unsigned char color;  //节点颜色
    _rbtree_node* left;      //左儿子
    _rbtree_node* right;  //右儿子
    _rbtree_node* parent; // 父亲
    void* value;          //节点所带的值
    KEY_TYPE key;          // 节点关键字
} rbtree_node;

typedef struct _rbtree //红黑树
{
    rbtree_node* root; // 红黑树树根
    rbtree_node* nil;  // 所有隐藏的黑色节点(叶子节点)所指向的节点 其实相当于一个空节点  写出来方便大家理解
} rbtree;

这里给出红黑树的代码定义片段

红黑树的插入

对于一颗普通的二叉查找树来说,树的结构取决于插入的顺序。例如按顺序插入值为1,2,3,4,5,6的节点,最后就会一颗深度为6的二叉树。这对于查找来说无疑是不理想的,红黑树的定义就解决了这个痛点。
由于一颗红黑树需要满足:

  1. 每个节点是红色的或者黑色的
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个儿子都是黑色的。
  4. 对每个节点,从该节点到其子孙节点的所有路径上的包含相同数目的黑色节点。

所以在插入的时候,我们需要不断的调整树的结构,以及变换树的颜色。

调整红黑树节点结构

对于红黑树节点的插入,我们有两种方式来调整树的结构。

  1. 左旋
  2. 右旋

image.png
对着上图,我们就来谈谈其中的左旋
左旋分为三个步骤:
首先,左旋一定是父节点的右儿子绕父节点进行左旋。右旋一定是父节点的左儿子绕父节点进行右旋。

  1. x节点的右儿子指向y的左儿子,y的左儿子的父亲指向x节点(但是要注意y的左儿子是不是叶子节点,如果是叶子节点,就统一指向二叉树的nil节点)
  2. x的父亲节点指向y节点,y的父亲节点指向x的父亲节点。

    1. 其中如果x为x的父亲节点的左儿子,那么x父亲节点左儿子指向y
    2. 其中如果x为x的父亲节点的右儿子,那么x父亲节点右儿子指向y
    3. 如果x为根节点,那么y为根节点。
  3. y的左儿子指向x,x的父亲节点指向y。

经过这三步,我们就可以把上图左边的树变成右边的树

附上左旋和右旋的相关代码

//只旋转,不改变颜色
void rbtree_left_rotate(rbtree* T, rbtree_node* x)
{
    rbtree_node* y = x->right;
    // x的父节点指向y y的父节点指向x的父节点
    // 先判断x是父节点的左儿子还是右儿子
    if (x->parent == T->nil)
    {
        T->root = y;
    }
    else if (x == x->parent->left)
    {
        x->parent->left = y;
    }
    else
    {
        x->parent->right = y;
    }
    y->parent = x->parent;

    //x的右儿子指向 y的左儿子
    //y的左儿子的父亲节点 指向x的右边的儿子
    x->right = y->left;
    if (y->left != T->nil)
    {
        y->left->parent = x;
    }

    //y的左儿子指向x x的父亲指向y
    y->left = x;
    x->parent = y;
}

void rbtree_right_rotate(rbtree* T, rbtree_node* x)
{
    rbtree_node* y = x->left;

    //x的左儿子指向y的右儿子
    //y右儿子的父亲指向x的左儿子
    x->left = y->right;
    if (y->right != T->nil) //T->nil 为空
    {
        y->right->parent = x->left;
    }

    // x的父亲指向y
    //y的父亲指向x的父亲
    if (x->parent == T->nil)
    {
        T->root = y;
    }
    else if (x->parent->left == x)
    {
        x->parent->left = y;
    }
    else
    {
        x->parent->right = y;
    }
    y->parent = x->parent;

    // y的右儿子指向x
    //x的父亲指向y
    y->right = x;
    x->parent = y;
}

调整红黑树节点颜色

红黑树插入,最重要的就是颜色的更改,因为颜色关乎红黑树的性质,性质3和性质4决定了我们如何调整红黑树的颜色和结构,调整颜色又分为4种情况,在介绍4种情况之前,我们要了解插入的节点应该是什么颜色?为了满足性质4,我们应该插入的节点的颜色是红色。并且我们假设再插入之前我们面对的是一颗红黑树,了解了这一点,我们将介绍4种不同的情况

父节点是黑色的情况

由于父节点是黑色,插入的节点又是红色,那么红黑树的性质没有被破坏,也就不需要调整

父节点是红色,同时是祖父节点左子树的情况

在这种情况下,我们面临三种情况
1.叔节点(父亲的兄弟)是红色的。

image.png
在这种情况下,我们只需要把祖父节点颜色变红,同时将父节点和叔节点变黑。注意需要循环执行,直到当前节点的父节点颜色是黑色,当前节点也就是z

2.叔节点是黑色,当前节点是右孩子
image.png
这种情况我们需要将z节点绕父节点左旋。将当前节点的父节点变成z(z为我们应该指向的节点),这样情况就变成了下面的情况。

3.叔节点是黑色,当前节点是右孩子
image.png
在这种情况下,我们将z节点的父亲节点绕z节点父亲节点的父亲节点进行右旋。然后将父节点变为黑色,旋转前的父亲节点的父亲节点变为红色。注意需要循环执行,直到当前节点的父节点颜色是黑色为止

父节点是红色,同时是祖父节点右子树的情况

和上面的情况相同,也分为三种小的情况,读者可以自行分析。

talk is cheap ,show me the code

void rbtree_insert_fixup(rbtree* T, rbtree_node* z)
{
    //递归处理当前节点为红色的情况
    while (z->parent->color == RED)
    {
        if (z->parent == z->parent->parent->left)
        {
            rbtree_node* uncle = z->parent->parent->right;
            if (uncle->color == RED)
            {
                z->parent->parent->color = RED;
                z->parent->color = BLACK;
                uncle->color = BLACK;

                z = z->parent->parent; // 改变了z->parent->parent的颜色为红色,并且没有改变路径上的黑色节点个数 接下来需要循环处理
            }
            else
            {
                if (z->parent->right == z)
                {
                    z = z->parent;
                    rbtree_left_rotate(T, z);
                }
                z->parent->color = BLACK;
                z->parent->parent->color = RED;
                rbtree_right_rotate(T, z->parent->parent);
            }
        }
        else
        {
            rbtree_node* uncle = z->parent->parent->left;
            if (uncle->color == RED)
            {
                uncle->color = BLACK;
                z->parent->color = BLACK;
                z->parent->parent->color = RED;

                z = z->parent->parent;
            }
            else
            {
                if (z == z->parent->left)
                {
                    z = z->parent;
                    rbtree_right_rotate(T, z);
                }
                z->parent->color = BLACK;
                z->parent->parent->color = RED;
                rbtree_left_rotate(T, z->parent->parent);
            }
        }
    }
    T->root->color = BLACK;
}

void rbtree_insert(rbtree* T, rbtree_node* z)
{
    rbtree_node* y = T->nil;
    rbtree_node* x = T->root;

    //判断插入的位置 循环结束后 x为空 y指向x的父亲
    while (x != T->nil)
    {
        y = x;
        if (x->key > z->key)
        {
            x = x->left;
        }
        else if (x->key < z->key)
        {
            x = x->right;
        }
        else
        {
            std::cout << "关键字重复" << std::endl;
            return; //节点存在 就不插入了 因为关键字唯一
        }
    }

    z->parent = y;
    if (y == T->nil)
    {
        T->root = z;
        z->color = BLACK;
    }
    else if (z->key > y->key)
    {
        y->right = z;
    }
    else
    {
        y->left = z;
    }

    // z的左儿子和右儿子成为了 新的叶子节点颜色为黑色
    z->left = T->nil;
    z->right = T->nil;
    z->color = RED;
    //插入的节点为红色不会改变红黑树的定义(树根到叶子节点的黑色节点不会改变)

    //但是由于插入红色节点位置,其父节点也可能为红色,违反了定义,接下来要改变节点的颜色

    rbtree_insert_fixup(T, z);
}

写到这里,红黑树的插入就大概差不多啦,完整代码如下附上:
https://paste.ubuntu.com/p/St...
是可以直接运行的,代码有些细节问题,读者需要注意一下,比如旋转的时候,如果有节点是叶子节点的时候,应该如何处理(叶子节点都是黑色且用nil代替表述,也就是说所有的叶子节点都对应着nil这一个节点,同时nil父亲节点为空,因为nil的父亲太多啦 就不需要指向特定的父亲节点)。


Noisyes
0 声望2 粉丝

努力学习东西中~