红黑树

目录

  • 目录
  • 前言
  • 二叉查找树
    • 插入
    • 删除
    • 小结
  • 红黑树
  • 树的旋转知识
  • 基础数据结构
    • 左旋
    • 右旋
  • 红黑树的插入
    • 情况一
    • 情况二
    • 情况三
      • 父节点p和叔叔节点u都是红色
      • 父节点p是红色但叔叔节点是黑色或者不存在
      • 父节点p是红色但叔叔节点是黑色或者不存在并且插入节点和父节点在一条直线上
  • 红黑树的删除
    • 情况一
    • 情况二
    • 情况三
    • 情况四
    • 情况五
    • 情况六

前言

学习Android Binder机制的时候,看到“宿主进程使用一个红黑树来维护它内部所有的Binder实体对象”这句话。本着对数据结构极大的热情,所以我决定学习一下Linux内核中红黑树的实现,增加一下自己的逼格。
学习过程中看了很多博客,基本上全是照抄算法导论的伪代码,我先献上自己深深的鄙视,这种贴伪代码的同学估计很难真正的搞懂红黑树。OK,鄙视过后,我们也按照介绍红黑树的基本思路,先从二叉查找树开始show代码,讲解原理。
show代码之前,我先定义一下接下来会用到的节点定义:

  1. n是新节点。
  2. r是根节点。
  3. x是某个节点。
  4. p是某个节点的父节点(x->parent)。
  5. g是某个节点的祖父节点(x->parent->parent)。
  6. b是某个节点的兄弟节点(x->parent->left或x->parent->right)。
  7. u是某个节点的叔叔节点(x->parent->parent->left或x->parent->parent->right)。

二叉查找树

由于红黑树本质上就是一棵二叉查找树,所以在了解红黑树之前,我们先来学习一下二叉查找树。二叉查找树(Binary Search Tree),是指一棵空树或者具有下列性质的二叉树:

  • 若任意节点的左子树不为空,则左子树上所有节点的值均小于它的根节点的值。
  • 若任意节点的右子树不为空,则右子树上所有节点的值均大于它的根节点的值。
  • 任意节点的左、右子树也分别为二叉查找树。
  • 没有键值相等的节点。

接下来,我们介绍一下二叉查找树的基本操作。

插入

插入的过程如下:

  • 如果是空树,则直接作为根(root)节点。
  • 如果不是空树,则从根搜寻,如果值小于当前节点(x),但是当前节点有左孩子,则继续找当当前节点的左孩子,直到当前节点没有左孩子,新节点作为当前节点的左孩子。
  • 如果不是空树,则从根搜寻,如果值小于当前节点(x),但是当前节点有右孩子,则继续找当当前节点的右孩子,直到当前节点没有右孩子,新节点作为当前节点的右孩子。

实现代码如下:

void insert(struct node *root, const int value)
{
    struct node *iNode = (struct node *)malloc(sizeof(struct node));
    iNode->left = iNode->right = iNode->parent = NULL;
    iNode->value = value;

    if (root == NULL) {
        root = iNode;
        return;
    }

    struct node *p = root;
    while (p) {
        // 不插入已有的节点
        if (value == p->value) {
            return;
        }

        if (value < p->value) {
            if (p->left == NULL) {
                iNode->parent = p;
                p->left = iNode;
                break;
            }
            p = p->left;
        } else {
            if (p->right == NULL) {
                iNode->parent = p;
                p->right = iNode;
            }
            p = p->right;
        }
    }
}

删除

删除二叉查找树中一个节点的方法如下:

  • 如果要删除的节点没有左右孩子,则直接删除。
  • 如果要删除的节点有孩子节点,则查找右子树的最小值或者左子树的最大值,与要删除的节点交换。

实现代码如下:

struct node* find_replace(struct node* node)
{
    struct node *p = node;

    if (p->left) {
        p = p->left;
        while (p->right) {
            p = p->right;
        }
    } else {
        p = p->right;
        while (p->left) {
            p = p->left;
        }
    }

    return p;
}

void delete(struct node *root, const int value)
{
    struct node *p = root;

    while (p) {
        if (p->value < value) {
            p = p->right;
        } else if (p->value > value) {
            p = p->left;
        } else {
            printf("Hit and Removed this value %d\n", value);
            if (p->left == NULL && p->right == NULL) {
                // p的左右孩子均为NULL
                if (p->value > p->parent->value) {
                    p->parent->right = NULL;
                } else {
                    p->parent->left = NULL;
                }
            } else {
                struct node *tmp = find_replace(p);
                p->value = tmp->value;
            }
        }
    }
}

小结

因为,一棵由n个节点,随机构造的二叉查找树的高度为logn,所以,一般二叉查找树的时间复杂度为O(logn)。但是,在最坏的情况下,例如二叉查找树只有左子树或者只有右子树的情况,这时,二叉查找树就退化为线性表,则操作的时间复杂度变为了O(n)。因此,红黑树是在二叉查找树的基础上,通过一些操作使得树相对平衡,不会出现最差时间复杂度的情况。

红黑树

红黑树通过在二叉查找树的基础上增加着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(logn)。但是,它是如何保证一棵n个节点的红黑树的高度始终保持在h=logn的呢?

这就引出了红黑树的5条性质:

  1. 节点是红色或者是黑色。
  2. 根节点是黑色。
  3. 每个叶节点(叶节点即指树尾端NIL指针或NULL节点)是黑的。
  4. 如果一个节点是红的,那么它的两个儿子节点都是黑色的(从每个叶子到根的所有路径上不能有两个连续的红色节点)。
  5. 对于任意一个节点而言,其到叶节点树尾端NIL指针的每一条路径都包含相同数目的黑色节点。

结合图示,解释一下上述性质。一棵参考的红黑树如下图所示:

通过图示,我们可以看到,节点只有红和黑两种颜色,并且根节点是黑色的。这就解释了性质1和2。
至于性质3,可以看到6这个节点的两个nil节点都是黑色的,其他节点的nil节点我为了省事,就不画了。
至于性质5,我们可以看到:13->8->11和13->17->25->27有相同的黑色节点数目,均为3。

树的旋转知识

当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。为了继续保持红黑树的性质,我们可以通过对结点进行重新着色,以及对树进行相关的旋转操作,即修改树中某些节点的颜色及指针结构,来达到对红黑树进行插入或删除节点等操作后,继续保持它的性质或平衡。

基础数据结构

参考的是Linux kernel中红黑树的实现,红黑树节点定义如下:

#define RB_RED 0
#define RB_BLACK 1
struct rb_node
{
    struct rb_node *left;
    struct rb_node *right;
    struct rb_node *parent;
    int value;
    int color;
};
struct rbtree {
    struct rb_node *root;
};

左旋

红黑树左旋的图示如下:

红黑树_第1张图片

当在某个节点pivot上,做左旋操作时,我们假设它的右孩子y不是NIL[T],pivot可以为任何不是NIL[T]的左孩子节点。左旋以pivot到y之间的链为”支轴”进行,它使y成为孩子树新的根,而y的左孩子b则成为pivot的右孩子。

左旋操作的参考代码如下所示:

void replace_node(struct rbtree *t, struct rb_node *o, struct rb_node *n)
{
    if (o->parent == NULL) {
        t->root = n;
    } else {
        if (o == o->parent->left) {
            o->parent->left = n;
        } else {
            o->parent->right = n;
        }
    }

    if (n != NULL) {
        n->parent = o->parent;
    }
}

void rotate_left(struct rbtree *t, struct rb_node *pnode)
{
    struct rb_node *rnode = pnode->right;
    replace_node(t, pnode, rnode);
    pnode->right = rnode->left;
    if (rnode->left != NULL) {
        rnode->left->parent = pnode;
    }

    rnode->left = pnode;
    pnode->parent = rnode;
}

右旋

红黑树的右旋图示如下:

红黑树_第2张图片

当在某个结点pivot上,做右旋操作时,我们假设它的左孩子y不是NIL[T],pivot可以为任何不为NIL[T]的右孩子节点。右旋以pivot到y之间链为“支轴”进行,使得y为孩子树的新根,y的右节点为pivot的左节点,并且将y的右节点重新赋值为pivot。

右旋操作的参考代码如下所示:

void replace_node(struct rbtree *t, strct rb_node *o, struct rb_node *n)
{
    if (o->parent == NULL) {
        t->root = n;
    } else {
        if (o == o->parent->left) {
            o->parent->left = n;
        } else {
            o->parent->right = n;
        }
    }

    if (n != NULL) {
        n->parent = o->parent;
    }
}

void rotate_right(struct rbtree *t, struct rb_node *pnode)
{
    struct rb_node *lnode = pnode->left;
    replace_node(t, pnode, lnode);
    pnode->left = lnode->right;
    if (lnode->right != NULL) {
        lnode->right->parent = pnode;
    }
    lnode->right = pnode;
    pnode->parent = lnode;
}

对于树的旋转,能保持不变的只有原树的搜索性质,而原树的红黑性质则不能保持,在红黑树的数据插入和删除后可利用旋转和颜色重涂来恢复树的红黑性质。

红黑树的插入

红黑树的插入和删除操作都非常复杂,我们先来学习一下红黑树的插入操作。红黑树插入操作的具体执行步骤为:

  1. 将红黑树当作一棵二叉查找树,将节点插入。
  2. 将节点着色为红色。
  3. 通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。

在插入后的平衡过程中,分为如下几种情况:

情况一

新节点位于树的根节点。这种情况比较简单,直接将根节点从红色变为黑色即可。如下图所示:

红黑树_第3张图片

实现代码如下:

void insert_case_1(struct rbtree *t, struct node *n)
{
    if (n->parent == NULL) {
        n->color = RB_BLACK;
    } else {
        insert_case_2(t, n);
    }
}

情况二

如果新插入节点的父节点是黑色,则新插入节点后没有违反红黑树的性质,仍是一棵合法的红黑树。因为新节点是红色的,在任何一条路径上都没有增加黑色节点的个数。如下图所示:
红黑树_第4张图片

实现代码如下:

void insert_case_2(struct rbtree *t, struct rb_node *n)
{
    if (n->parent->color == RB_BLACK) {
        return;
    } else {
        insert_case_3(struct rbtree *t, struct rb_node *n)
    }
}

情况三

如果新节点的父节点是红色,如上图中要插入一个4节点,并且4节点的父节点3也是红色,就违反了红黑树的性质,必须重新绘制(ps:违反的是红黑树的性质4:如果一个节点是红的,那么它的两个儿子节点都是黑色的)。重新绘制也分为下面几种情况。

父节点p和叔叔节点u都是红色

如果父节点p和叔叔节点u都是红色,则:

  1. 祖父节点变为红色。
  2. 把父节点变为黑色。
  3. 把叔叔节点变为黑色。

如下图:

红黑树_第5张图片

这个时候祖父节点g就变为红色了。祖父节点变为红色之后,可能也会遇到上述的各种问题,所以我们需要把祖父节点看成是新插入的节点,重新执行上述情况一、二、三。

实现代码如下:

void insert_case_3(struct rbtree *t, struct node *n)
{
    if (uncle(n)->color == RB_RED) {
        n->parent->color = RB_BLACK;
        uncle(n)->color = RB_BLACK;
        n->parent->parent->color = RB_RED;
        insert_case_1(t, n->parent->parent);
    } else {
        insert_case_4(t, n);
    }
}

父节点p是红色,但叔叔节点是黑色或者不存在

如果父节点p是红色的,但是叔叔节点是黑色或者没有,而且插入节点和父节点不在一条直线上,则:

  • 新节点n是父节点p的右孩子,而p是其父节点的左孩子,那么左旋p。
  • 新节点n是父节点p的左孩子,而p是其父节点的右孩子,那么右旋p。

如下图所示:

红黑树_第6张图片

实现代码如下:

void insert_case_4(struct rbtree *t, struct rb_node *n)
{
    if (n == n->parent->right && n->parent == n->parent->parent->left) {
        rotate_left(t, n->parent);
        n = n->left;
    } else if (n == n->parent->left && n->parent == n->parent->parent->right) {
        rotate_right(t, n->parent);
        n = n->right;
    } 
    insert_case_5(t, n);
}

上述操作之后,只是让新插入节点和父节点在一条直线上,并没有解决违反红黑树性质的问题,接下来,我们讨论新插入节点和父节点在同一条直线上,且均为红色的解决方案。

父节点p是红色,但叔叔节点是黑色或者不存在,并且插入节点和父节点在一条直线上

这种情况需要做如下操作:

  • 新节点n和父节点p都是左节点,则右旋祖父节点。
  • 新节点n和父节点p都是右节点,则左旋祖父节点。

旋转完毕后,需要切换父节点和祖父节点的颜色。

如下图所示:

实现代码如下:

void insert_case_5(struct rbtree *t, struct rb_node *n)
{
    n->parent->color = RB_BLACK;
    n->parent->parent->color = RB_RED;

    if (n == n->parent->left && n->parent == n->parent->parent->left) {
        rotate_right(t, n->parent->parent);
    } else {
        rotate_left(t, n->parent->parent);
    }
}

红黑树的删除

接下来,我们来学习红黑树的删除操作。在学习红黑树的删除操作之前,我们先来学习一个重要函数:replace_node,实现代码如下:

void replace_node(struct rbtree *t, struct rb_node *p, struct rb_node *n)
{
    if (p->parent == NULL) {
        t->root = n;
    } else {
        if (p == p->parent->left) {
            p->parent->left = n;
        } else {
            p->parent->right = n;
        }
    }

    if (n != NULL) {
        n->parent = p->parent;
    }
}

红黑树的删除也分为6种情况,我们分别来分析。(注意,这里的情况指的是节点n删除后,如何根据这六种情况进行调整,重新成为一棵合法的红黑树。节点删除的操作这里就不讲了)

情况一

如果要删除的节点是根节点,则什么也不做,否则执行删除情况2。

实现代码如下:

void delete_case_1(struct rbtree *t, strct rb_node *n)
{
    if (n->parent == NULL) {
        return;
    } else {
        delete_case_2(t, n);
    }
}

情况二

如果要删除节点的兄弟节点b的颜色为红色,那么把父节点p的颜色变为红色,同时把兄弟节点S的颜色变为黑色。

  • 如果要删除的节点是左孩子,那么左旋要删除节点的父节点p。
  • 如果要删除的节点是右孩子,那么右旋要删除节点的父节点p。

如下图所示:

红黑树_第7张图片

实现代码如下:

void delete_case_2(struct rbtree *t, struct rb_node *n)
{
    if (brother(n)->color == RB_RED) {
        n->parent->color = RB_RED;
        brother(n)->color = RB_BLACK;
        if (n == n->parent->left) {
            rotate_left(t, n->parent);
        } else {
            rotate_right(t, n->parent);
        }
    }
    delete_case_3(t, n);
}

情况三

n的父节点p、兄弟节点b和b的儿子节点均为黑色

这种情况下,只需要把兄弟节点的颜色变为红色,然后将指针指向父节点p,并进入第一种删除情况。因为兄弟节点变为红色之后,通过他的路径都少了一个黑色节点。

如下图所示:

红黑树_第8张图片

实现代码如下:

void delete_case_3(struct rbtree *t, struct rb_node *n)
{
    if (n->parent->color == RB_BLACK &&
        brother(n)->color == RB_BLACK &&
        brother(n)->left->color == RB_BLACK &&
        brother(n)->right->color == RB_BLACK)
    {
        brother(n)->color = RB_RED;
        delete_case_1(t, n->parent);
    } else {
        delete_case_4(t, n->parent);
    }
}

如果满足上述情况,则把n->parent作为新删除节点来重新进行判断。如果不满足,则直接进入情况四。

情况四

兄弟节点b和b的儿子都是黑色,但父节点是红色

这种情况我们只需要简单的交换父节点和兄弟节点的颜色即可。

如下图所示:

红黑树_第9张图片

实现代码如下:

void delete_case_4(struct rbtree *t, struct rb_node *n)
{
    if (n->parent->color == RB_RED &&
        brother(n)->color == RB_BLACK &&
        brother(n)->left->color == RB_BLACK &&
        brother(n)->right->color == RB_BLACK)
    {
        n->parent->color = RB_BLACK;
        brother(n)->color = RB_RED; 
    } else {
        delete_case_5(t, n);
    }
}

如果不满足上述情况,则进入情况五。

情况五

情况5又分为两种情况:

  • 如果兄弟节点b是黑色,b的右儿子是黑色,而删除节点n是父节点p的左孩子,则在叔叔节点b上做右旋。
  • 如果兄弟节点b是黑色,b的左儿子是黑色,而删除节点n是父节点p的右孩子,则在叔叔节点b上做左旋。

旋转结束之后,还有节点颜色的变化:

  • 如果删除的节点n是左孩子,把兄弟节点b的颜色变为红色,b的左孩子颜色变为黑色。
  • 如果删除的节点n是右孩子,把兄弟节点b的颜色变为红色,b的右孩子颜色变为黑色。

如下图所示:

实现代码如下:

void delete_case_5(struct rbtree *t, struct rb_node *n)
{
    if (n == n->parent->left &&
        brother(n)->color == RB_BLACK &&
        brother(n)->left->color == RB_RED &&
        brother(n)->right->color == RB_BLACK)
    {
        brother(n)->color = RB_RED;
        brother(n)->left->color == RB_BLACK;
        rotate_right(t, brother(n));
    } 
    else if ( n == n->parent->right &&
        brother(n)->color == RB_BLACK &&
        brother(n)->right->color == RB_RED &&
        brother(n)->left->color == RB_BLACK
    )
    {
        brother(n)->color = RB_RED;
        brother(n)->right->color = RB_BLACK;
        rotate_left(t, brother(n));
    } else 
    {
        delete_case_6(t, n);
    }
}

情况六

终于到了删除的最后一种情况,赶紧搞起。

  • 如果兄弟节点b是黑色,b的右儿子是红色,而删除节点n是父节点p的左孩子,则在父节点p上做左旋。
  • 如果兄弟节点b是黑色,b的左儿子是红色,而删除节点n是父节点p的右孩子,则在父节点p上做右旋。

旋转之前,还有节点颜色的变化:

  • 如果删除的节点n是左孩子,则把兄弟节点的右孩子变为黑色再旋转父节点。
  • 如果删除的节点n是右孩子,则把兄弟节点的左孩子变为黑色再旋转父节点。

如下图所示:

实现代码如下:

void delete_case_6(struct rbtree *t, struct rb_node *n)
{
    brother(n)->color = n->parent->color;
    n->parent->color = RB_BLACK;
    if (n == n->parent->left) {
        brother(n)->right->color = RB_BLACK;
        rotate_left(t, n->parent);
    } else {
        brother(n)->left->color = RB_BLACK;
        rotate_right(t, n->parent);
    }
}

好了,到这里红黑树的删除操作也结束了。接下去,我会带大家深入到Linux内核,看一下Linux内核中的红黑树实现。

你可能感兴趣的:(红黑树)