算法笔记:红黑树

zhenghaoz
• 阅读 1273

红黑树,一种平衡二叉树,最为著名的应用就是C++ STL中的map,是有序集合最为理想的存储方式之一。除了二叉树所具有的属性之后,红黑树中每个节点多了一个“颜色”属性,可以是红色或者是黑色。一棵红黑树应该满足一下的性质:

  1. 每个节点是红色或者黑色的;
  2. 根节点是黑色的;
  3. 每个叶节点nil是黑色的(使用哨兵节点在删除调整时可以方便不少);
  4. 如果一个节点是红色的,那么它的两个子节点是黑色的;
  5. 对于每一个节点,到后代所有叶节点所经过的黑色节点数目相同。

根据定义,可以写出红黑树节点的数据结构:

struct Node {
    enum Color { RED, BLACK };
    Color _color;
    Key _key;
    Value _value;
    Node *_parent, *_left, *_right;
    Node(): _color(BLACK) {}
    Node(const Key &key, const Value &value, Node *left, Node *right, Node *parent):
        _key(key), _value(value), _color(RED), _left(left), _right(right), _parent(parent) {}
};

本文省略了拷贝、析构、赋值等操作,完整源代码放在Gist上。

旋转

想要红黑树维持着平衡,就需要在插入元素和删除元素的过程中不断对结构进行调整。其中,最基础的操作就是左旋右旋

算法笔记:红黑树

以左旋为例,操作可以分为三步:

  1. 将Q的左子节点变成P的右子节点;
  2. 将P变成Q的左子节点;
  3. 将Q变成当前子树的根节点。
void leftRotate(Node *x)
{
    Node *y = x->_right;
    // remove y->left to x->right
    x->_right = y->_left;
    if (x->_right != nil)
        x->_right->_parent = x;
    // remove y up
    y->_parent = x->_parent;
    if (x->_parent == nil)
        root = y;
    else if (x->_parent->_left == x)
        x->_parent->_left = y;
    else
        x->_parent->_right = y;
    // remove x down
    x->_parent = y;
    y->_left = x;
}

插入

先污染,后治理。首选按照二叉树的插入方法先将节点插入二叉树,然后再对红黑树进行调整。

void insert(Node *nptr)
{
    Node *it = root, *p = root;
    // find insert position
    while (it != nil) {
        p = it;
        if (nptr->_key < it->_key)
            it = it->_left;
        else if (nptr->_key > it->_key)
            it = it->_right;
        else {
            // find target key-value
            it->_value = nptr->_value;
            return;
        }
    }
    // insert
    nptr->_parent = p;
    if (p == nil)
        root = nptr;
    else if (nptr->_key < p->_key)
        p->_left = nptr;
    else
        p->_right = nptr;
    // fixup
    insertFixup(nptr);
}

在完成插入之后,将面临六种情况,由于存在左右对称的情况,实际上只需要考虑三种情况。

算法笔记:红黑树

情况1时,调整目标节点B的父节点和叔节点都是红节点,祖父节点为黑节点,我们需要将父节点和叔节点的颜色改成黑色,祖父节点设为红节点,并将调整目标设为祖父节点。

算法笔记:红黑树

情况2情况3中,新插入节点的父节点为红节点,祖父节点为黑节点,并且叔节点为黑节点。首选需要把情况2转换成情况3,让B成为黑节点,A和C为B的红色子节点,并将调整目标设为A。往往在处理完这两种情况后,红黑树完成了调整。

由于NIL也算是黑色节点,所以还需要定义一个获取节点颜色的宏。

调整过程是自下而上的,一个插入的新节点的颜色是红色的,每次调整的目标是使每个子树保持红黑树的性质。对于情况2和3来说,调整结束后子树树根为黑色,对整体的性质不会造成影响。而在情况1中,子树树根为红色,对整体的性质造成了影响,需要继续调整,这就是循环条件的意义

void insertFixup(Node *ptr)
{
    while (ptr->_parent->_color == Node::RED) {
        if (ptr->_parent == ptr->_parent->_parent->_left) {
            Node *y = ptr->_parent->_parent->_right;
            // case 1
            if (y->_color == Node::RED) {
                ptr->_parent->_color = Node::BLACK;
                y->_color = Node::BLACK;
                ptr->_parent->_parent->_color = Node::RED;
                ptr = ptr->_parent->_parent;
            } else {
                // case 2: switch case 2 to case 3
                if (ptr == ptr->_parent->_right) {
                    ptr = ptr->_parent;
                    leftRotate(ptr);
                }
                // case 3
                ptr->_parent->_color = Node::BLACK;
                ptr->_parent->_parent->_color = Node::RED;
                rightRotate(ptr->_parent->_parent);
            }
        } else {
            // with 'left' and 'right' exchanged
            Node *y = ptr->_parent->_parent->_left;
            if (y->_color == Node::RED) {
                ptr->_parent->_color = Node::BLACK;
                y->_color = Node::BLACK;
                ptr->_parent->_parent->_color = Node::RED;
                ptr = ptr->_parent->_parent;
            } else {
                if (ptr == ptr->_parent->_left) {
                    ptr = ptr->_parent;
                    rightRotate(ptr);
                }
                ptr->_parent->_color = Node::BLACK;
                ptr->_parent->_parent->_color = Node::RED;
                leftRotate(ptr->_parent->_parent);
            }
        }
    }
    root->_color = Node::BLACK;
}

删除

删除的过程比插入过程复杂很多。与二叉树一样,我们需要一个替换操作,将子树u替换成子树v。

void transplant(shared_ptr<Node> u, shared_ptr<Node> v)
{
    if (u->_parent == nil)
        root = v;
    else if (u == u->_parent->_left)
        u->_parent->_left = v;
    else
        u->_parent->_right = v;
    v->_parent = u->_parent;
}

删除的过程和二叉树类似,多了一些处理哨兵、记录颜色的过程。

    void remove(Node *ptr)
    {
        Node *y = ptr, *x;
        int y_original_color = y->_color;
        if (y->_left == nil) {
            x = ptr->_right;
            transplant(ptr, ptr->_right);
        } else if (y->_right == nil) {
            x = ptr->_left;
            transplant(ptr, ptr->_left);
        } else {
            y = min(ptr->_right);
            y_original_color = y->_color;
            x = y->_right;
            if (y->_parent == ptr)
                x->_parent = y; // change nil->_parent
            else {
                transplant(y, y->_right);
                y->_right = ptr->_right;
                y->_right->_parent = y;
            }
            transplant(ptr, y);
            y->_left = ptr->_left;
            y->_left->_parent = y;
            y->_color = ptr->_color;
        }
        if (y_original_color == Node::BLACK)
            deleteFixup(x);
    }

首先考虑一下删除节点的时候,我们对红黑树造成了什么样的影响。如果删除一个红色节点,那么红黑树的性质并不会收到任何影响;如果删除的是一个黑色节点,那么意味着黑高相等的性质将不复存在,删除一个黑色节点。那么和插入调整的思路类似,每次调整的目标是保持子树内的性质。

调整过程需要分左右对称的四种情况:

算法笔记:红黑树

  • 情况1:有一个红色的兄弟节点,通过旋转和颜色调换,使红色节点到调整目标节点一侧来,变成情况2、3、4中的一种;
  • 情况2:兄弟节点的两个子节点全为黑,于是把兄弟节点设为红节点,这样一来,子树中的黑高是相等了,但是删除的黑节点并没有被弥补,还需要继续往上调整;
  • 情况3:兄弟节点左红右黑,这个时候需要把红色节点调整到叔节点的右侧,变成情况4;
  • 情况4:兄弟节点右节点为红,这个时候需要把红节点调整到调整目标节点一侧来,用这个红色节点弥补删除的黑色节点。调整结束后,子树满足红黑树性质,结束调整过程。

而对于循环条件。应该怎么理解呢?如果调整过程到了根节点,那么就不存在某子树内黑高缺一的情况,可以结束循环。如果遇到了红节点,那么把这个红节点变成黑节点就可以解决黑高不等的情况。

void removeFixup(Node *ptr)
{
    while (ptr != root && ptr->_color == Node::BLACK) {
        if (ptr == ptr->_parent->_left) {
            Node *w = ptr->_parent->_right;
            // case 1
            if (w->_color == Node::RED) {
                w->_color = Node::BLACK;
                ptr->_parent->_color = Node::RED;
                leftRotate(ptr->_parent);
                w = ptr->_parent->_right;
            }
            // case 2
            if (w->_left->_color == Node::BLACK && w->_right->_color == Node::BLACK) {
                w->_color = Node::RED;
                ptr = ptr->_parent;
            } else {
                // case 3
                if (w->_right->_color == Node::BLACK) {
                    w->_left->_color = Node::BLACK;
                    w->_color = Node::RED;
                    rightRotate(w);
                    w = ptr->_parent->_right;
                }
                // case 4
                w->_color = ptr->_parent->_color;
                ptr->_parent->_color = Node::BLACK;
                w->_right->_color = Node::BLACK;
                leftRotate(ptr->_parent);
                ptr = root;
            }
        } else {
            // with 'left' and 'right' exchanged
            Node *w = ptr->_parent->_left;
            if (w->_color == Node::RED) {
                w->_color = ptr->_parent->_color;
                ptr->_parent->_color = Node::RED;
                rightRotate(ptr->_parent);
                w = ptr->_parent->_left;
            }
            if (w->_left->_color == Node::BLACK && w->_right->_color == Node::BLACK) {
                w->_color = Node::RED;
                ptr = ptr->_parent;
            } else {
                if (w->_left->_color == Node::BLACK) {
                    w->_color = Node::RED;
                    w->_right->_color = Node::BLACK;
                    leftRotate(w);
                    w = ptr->_parent->_left;
                }
                w->_color = ptr->_parent->_color;
                w->_left->_color = Node::BLACK;
                ptr->_parent->_color = Node::BLACK;
                rightRotate(ptr->_parent);
                ptr = root;
            }
        }
    }
    ptr->_color = Node::BLACK;
}

查找

节点颜色对于查找过程没有意义,红黑树查找过程和二叉树是一样的。

点赞
收藏
评论区
推荐文章
推荐程序员面试秘籍!2021年大厂Java岗面试必问
01JAVA基础1.1java知识点Hashmap源码级掌握,扩容,红黑树,最小树化容量,hash冲突解决,有些面试官会提出发自灵魂的审问,比如为什么是红黑树,别的树不可以吗;为什么8的时候树化,4不可以吗,等等concureentHashMap,段锁,如何分段,和hashmap在hash上的区别,性能,等等HashTable,同步锁,这块可
大厂Java初级开发工程师!!!面试必问项之Set实现类:TreeSet
一、TreeSet概述1、TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。2、TreeSet顾名思义他内部维护的是一个TreeMap,底层是红黑二叉树,他使得集合内都是有序的序列。  3、Tree可以按照添加对象的指定属性,进行排序,所以向TreeSet中添加的数据,要求是相同类的对象。4、两
Wesley13 Wesley13
2年前
java语言基础6
hashmap的数据结构,HashMap的数据结构是数组链表红黑树(红黑树sinceJDK1.8)。我们常把数组中的每一个节点称为一个桶。当向桶中添加一个键值对时,首先计算键值对中key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添
高级java面试题,附答案+考点
蚂蚁金服一面1.两分钟的自我介绍2.二叉搜索树和平衡二叉树有什么关系,强平衡二叉树(AVL树)和弱平衡二叉树(红黑树)有什么区别3.B树和B树的区别,为什么MySQL要使用B树4.HashMap如何解决Hash冲突5.epoll和poll的区别,及其应用场景6.简述线程池原理,FixedThreadPoo
Stella981 Stella981
2年前
B+树原理以及Java代码实现
最初查找二叉树,由于树的高度会随着有序序列输入而急剧增长,后来出现平衡二叉树,红黑树。B树可以海量数据的快速查询检索,B树主要分为B树(B树),B树,B\树等。B树(B树)M路搜索树,参数M定义节点的分支个数;对于根节点孩子数目为\2,M\,对于其余节点孩子数目为\M/2,M\;每个节点含有关键字属性,至少M/21
Wesley13 Wesley13
2年前
Java集合,TreeMap底层实现和原理
概述文章的内容基于JDK1.7进行分析,之所以选用这个版本,是因为1.8的有些类做了改动,增加了阅读的难度,虽然是1.7,但是对于1.8做了重大改动的内容,文章也会进行说明。TreeMap实现了SotredMap接口,它是有序的集合。而且是一个红黑树结构,每个keyvalue都作为一个红黑树的节点。如果在调用TreeMap的构造函数时没有指定
Wesley13 Wesley13
2年前
Java数据结构和算法(十五)——无权无向图
前面我们介绍了树这种数据结构,树是由n(n0)个有限节点通过连接它们的边组成一个具有层次关系的集合,把它叫做“树”是因为它看起来像一棵倒挂的树,包括二叉树、红黑树、234树、堆等各种不同的树,有对这几种树不了解的可以参考我前面几篇博客。而本篇博客我们将介绍另外一种数据结构——图,图也是计算机程序设计中最常用的数据结构之一,从数学意义上讲
Wesley13 Wesley13
2年前
2020面试题(答案中)
平衡二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(logn)。你可以按任意顺序位置插入/删除数据,或者使用AVL树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足对属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(logn)的性能。搜索在二叉树中搜索会很快,但是在堆中搜索会
Wesley13 Wesley13
2年前
B
BTreeBTree又叫做B树,和平衡二叉树不同的地方在于B树是多叉树(平衡多路查找树),Oracle和MongoDB的索引技术就是基于B树的数据结构,B树也可以看作是对23查找树的一种扩展。一个m阶的BTree有以下性质1.每个节点最多有m个子节点;2.每个非叶子节点(根节点除外)至少含有m/2个子节点;3.
京东云开发者 京东云开发者
3个月前
深入理解经典红黑树 | 京东物流技术团队
本篇我们讲红黑树的经典实现,Java中对红黑树的实现便采用的是经典红黑树。前一篇文章我们介绍过左倾红黑树,它相对来说比较简单,需要大家看完上篇再来看这一篇,因为旋转等基础知识不会再本篇文章中赘述。本篇的大部分内容参考《算法导论》和Java实现红黑树的源码,
zhenghaoz
zhenghaoz
Lv1
昔我往矣,杨柳依依;今我来思,雨雪霏霏。
文章
3
粉丝
1
获赞
2