从零开始手写STL库:红黑树

从零开始手写STL库–红黑树的实现

Gihub链接:miniSTL



一、红黑树是什么?

红黑树为一种自平衡的二叉查找树。

其产生的原因是,传统的平衡二叉查找树在面对递增或递减数据时,操作效率会急剧下降,频繁地旋转会消耗很多时间。

具体地,红黑树在平衡二叉树的节点中附加了一个额外的属性:颜色,非红即黑

其具有的性质为:
在这里插入图片描述

黑高度相等代表的意思就是这个树的叶子节点之间的层高差<2,也就达到了近似平衡

红黑树自然是由Node构成,其专属Node一般包含:
在这里插入图片描述

二、红黑树要包含什么函数

红黑树的插入操作一般包含三个步骤:插入新节点,初始化为红色; 检查红黑树性质; 调整红黑树

(实际上插入的新节点必须是红色,进一步地以此为基础,调整树形来满足父子颜色不同、叶子&根为黑色的性质)

其中也需要两个平衡二叉树的固有操作:左旋和右旋

基础成员部分

首先定义好Node部分,并且适当地写好构造函数,虽然不写也能过

enum class Color { RED, BLACK };

template <typename Key, typename Value>
class myRedBlackTree{
    class Node
    {
    public:
        Key key;
        Value value;
        Color color;
        Node * left;
        Node * right;
        Node * parent;

        Node(const Key & k, const Value & v, Color c, Node * p = nullptr)
            : key(k), value(v), color(c), left(nullptr), right(nullptr), parent(p) {}
        
        Node()
            : color(Color::BLACK), left(nullptr), right(nullptr), parent(nullptr) {}
    };

};

有了节点,就可以定义整个树的元素了,包括头节点、长度和哨兵节点

哨兵节点是真正意义上的尾节点,所有的叶子节点都要指向该节点

从根节点沿着任意路径到哨兵节点,路径上的黑色节点数总数是相同的,这样才是红黑树

private:
    Node * root;
    size_t size;
    Node * Nil;

基础函数部分

这些函数依然是在private下的,不能暴露给用户使用

插入功能

首先是左右旋函数,这里建议看一下红黑树左右旋,动图很清晰明了

    void rightRotate(Node * node)
    {
        Node *l_son = node->left;

        node->left = l_son->right;
        if(l_son->right) l_son->right->parent = node;

        l_son->parent = node->parent;
        if(!node->parent) root = l_son;
        else if(node == node->parent->left) node->parent->left = l_son;
        else node->parent->right = l_son;

        l_son->right = node;
        node->parent = l_son;
    }

右旋函数包括以下步骤:
1、记录左子节点
2、把左子节点的右节点接到node上,双向的处理
3、再用左子节点代替node的位置,与node的parent双向连接
4、最后处理左子节点和node的双向连接

看着动图写,逻辑很清楚,左旋的逻辑基本一样

    void leftRotate(Node *node) 
    {
        Node *r_son = node->right;

        node->right = r_son->left;
        if (r_son->left) r_son->left->parent = node;
        
        r_son->parent = node->parent;
        if (!node->parent) root = r_son;
        else if (node == node->parent->left) node->parent->left = r_son;
        else node->parent->right = r_son;
        
        r_son->left = node;
        node->parent = r_son;
    }

当目标节点的父节点存在且父节点的颜色是红色时,需要修复(因为目标节点初始化为红色,冲突了)

所以在private中还存在一个修复函数,这里需要分类处理:

明确几个定义:

总共涉及四个节点:父节点、叔节点、爷节点、子节点,关系是:爷->[父->(子),叔]

情况一:父、叔都是红色
处理方式:父、叔都设为黑色,爷设为红色,并将爷设为新的“子”,迭代处理

情况二:父=红,叔=黑,父为左子树&子为右子树
处理方式:将父设为新的“子”,再左旋;新“子”的父设为黑,新“子”的爷设为红;再对新“子”的爷右旋

情况三:父=红,叔=黑,父为右子树&子为左子树
处理方式:将父设为新的“子”,再右旋;新“子”的父设为黑,新“子”的爷设为红;再对新“子”的爷左旋

情况二和三的操作是对称的

    void insertFixup(Node *target) 
    {
        while (target->parent && target->parent->color == Color::RED)           // father is red
        {
            if (target->parent == target->parent->parent->left)                 // father is left of grandfather
            {
                Node *uncle = target->parent->parent->right; 
                
                if (uncle && uncle->color == Color::RED)                        // father & uncle is red
                {
                    target->parent->color = Color::BLACK; 
                    uncle->color = Color::BLACK;          
                    target->parent->parent->color = Color::RED; 
                    target = target->parent->parent; 
                } 
                else                                                            // uncle is black or NULL
                {
                    if (target == target->parent->right) 
                    {
                        target = target->parent; 
                        leftRotate(target);      
                    }
                    
                    target->parent->color = Color::BLACK;
                    target->parent->parent->color = Color::RED;
                    rightRotate(target->parent->parent);
                }
            } 

            else                                                                // father is right of grandfather
            {
                Node *uncle = target->parent->parent->left; 
                
                if (uncle && uncle->color == Color::RED) 
                {
                    target->parent->color = Color::BLACK;
                    uncle->color = Color::BLACK;
                    target->parent->parent->color = Color::RED;
                    target = target->parent->parent;
                } 
                else 
                {
                    if (target == target->parent->left) 
                    {
                        target = target->parent; 
                        rightRotate(target);     
                    }

                    target->parent->color = Color::BLACK;
                    target->parent->parent->color = Color::RED;
                    leftRotate(target->parent->parent);
                }
            }
        }

        root->color = Color::BLACK;
    }

接着就可以考虑插入节点的处理了,正常地比较大小,顺序地找到最合适的位置,插入后处理父子节点关系

然后就可以进行树的检查了,是否还满足红黑树性质,也就是调用修复函数

    void insertNode(const Key &key, const Value &value) 
    {
        Node *newNode = new Node(key, value, Color::RED);
        Node *parent = nullptr; 
        Node *cmpNode = root;   

        while (cmpNode) 
        {
            parent = cmpNode; 
            if (newNode->key < cmpNode->key) cmpNode = cmpNode->left;               // smaller -> left
            else if (newNode->key > cmpNode->key) cmpNode = cmpNode->right;         // bigger -> right
            else                                                                    // already existed
            {
                delete newNode;
                return;
            }
        }

        size++;

        newNode->parent = parent;                                                   // align the parent to son
        if (!parent) root = newNode;
        else if (newNode->key < parent->key) parent->left = newNode;
        else parent->right = newNode;
        
        insertFixup(newNode);                                                       // check RedBlackTree
    }

删除功能

删除节点要考虑的事情就非常多了

先定义一个简单的查找函数,用来搜索到要删除的节点:

    Node *lookUp(Key key) 
    {
        Node *cmpNode = root;

        while (cmpNode) 
        {
            if (key < cmpNode->key) cmpNode = cmpNode->left;
            else if (key > cmpNode->key) cmpNode = cmpNode->right;
            else return cmpNode;
        }
        return cmpNode;
    }

以及一个替换函数,用新节点替换旧节点:

    void replaceNode(Node *targetNode, Node *newNode) 
    {
        if (!targetNode->parent) root = newNode;
        else if (targetNode == targetNode->parent->left) targetNode->parent->left = newNode;
        else targetNode->parent->right = newNode;
        
        if (newNode) newNode->parent = targetNode->parent;
    }

额外的定义一个查找左叶子节点函数,功能是返回某个子树下最小值所在的节点:

    Node *findMinimumNode(Node *node) 
    {
        while (node->left) node = node->left;
        return node;
    }

为了方便操作,对节点的颜色处理也定义好函数:

    Color getColor(Node *node) 
    {
        if (node == nullptr) return Color::BLACK;
        return node->color;
    }

    void setColor(Node *node, Color color) 
    {
        if (node == nullptr) return;
        node->color = color;
    }

接下来就可以考虑如何从删除节点后的树中恢复红黑树性质了,即针对删除功能的修复函数

首先判断是否为空,如果当前节点为哨兵节点,且父节点为空,那么明显满足红黑树,返回即可

接着从节点开始遍历,一直遍历到根节点,检查父子节点性质

明确几个定义:

总共涉及三个节点:父节点、兄节点、子节点,关系是:父->(子,兄)

情况一:兄节点为红色
处理方式:兄节点改为黑色,父节点改为红色,并且以父节点左旋(相当于交换父兄节点)

情况二:兄节点为黑色,兄节点的子节点全为黑色
处理方式:兄节点改为红色,并且递归点设置为父节点

情况三:兄节点为黑,其左子为红右子为黑
处理方式:子节点全改黑,并将兄改红,右旋

情况四:兄节点为黑,其左子为黑右子为红
处理方式:子节点全改黑,父节点改黑,并左选父节点

    void removeFixup(Node *node) 
    {
        if (node == Nil && node->parent == nullptr) return;
        
        while (node != root) 
        {
            if (node == node->parent->left)                                     // son is left
            {
                Node *sibling = node->parent->right;                            // bro is right
                if (getColor(sibling) == Color::RED)                            // bro is red
                {
                    setColor(sibling, Color::BLACK); 
                    setColor(node->parent, Color::RED);
                    leftRotate(node->parent);
                    
                    sibling = node->parent->right;
                }

                
                if (getColor(sibling->left) == Color::BLACK && 
                    getColor(sibling->right) == Color::BLACK)                   // bro and nephew are black
                {
                    setColor(sibling, Color::RED);
                    node = node->parent;
                
                    if (node->color == Color::RED) 
                    {
                        node->color = Color::BLACK;
                        node = root;
                    }
                } 
                else 
                {
                    if (getColor(sibling->right) == Color::BLACK) 
                    {
                        setColor(sibling->left, Color::BLACK); 
                        setColor(sibling, Color::RED);
                        rightRotate(sibling);
                        
                        sibling = node->parent->right;
                    }
                
                    setColor(sibling, getColor(node->parent));
                    setColor(node->parent, Color::BLACK);
                    setColor(sibling->right, Color::BLACK);
                    leftRotate(node->parent);
                    
                    node = root;
                }
            } 
            else 
            {
                Node *sibling = node->parent->left;

                if (getColor(sibling) == Color::RED) 
                {
                    setColor(sibling, Color::BLACK);
                    setColor(node->parent, Color::RED);
                    rightRotate(node->parent);
                    sibling = node->parent->left;
                }

                if (getColor(sibling->right) == Color::BLACK &&
                    getColor(sibling->left) == Color::BLACK) 
                {
                    setColor(sibling, Color::RED);
                    node = node->parent;
                    if (node->color == Color::RED) 
                    {
                        node->color = Color::BLACK;
                        node = root;
                    }
                } 
                else 
                {
                    if (getColor(sibling->left) == Color::BLACK) 
                    {
                        setColor(sibling->right, Color::BLACK);
                        setColor(sibling, Color::RED);
                        leftRotate(sibling);
                        sibling = node->parent->left;
                    }
                    setColor(sibling, getColor(node->parent));
                    setColor(node->parent, Color::BLACK);
                    setColor(sibling->left, Color::BLACK);
                    rightRotate(node->parent);
                    node = root;
                }
            }
        }
        setColor(node, Color::BLACK);
    }

有这些前置函数后,就可以进行删除节点操作了
首先定义几个需要用到的变量

    Node *rep = del; 			// rep(替代节点)初始指向要删除的节点
    Node *child = nullptr;      // 要删除节点的孩子节点
    Node *parentRP;             // 替代节点的父节点
    Color origCol = rep->color; // 保存要删除节点的原始颜色

分情况讨论:
若该节点无左或右孩子,那么用存在的孩子代替它即可

        if (!del->left) 
        {
            rep = del->right;        // 替代节点更换为右子
            parentRP = del->parent;  // 替代父节点更新
            origCol = getColor(rep); // 替代颜色更新
            replaceNode(del, rep);   // 替代操作,del节点从原树中脱离
        }
        else if (!del->right) 
        {
            rep = del->left;         
            parentRP = del->parent;  
            origCol = getColor(rep); 
            replaceNode(del, rep);   
        }

否则说明该节点左右子都存在,这里取删除节点的直接后继节点作为替代节点,将删除节点替代掉即可

注意分类操作,替代节点与删除节点是否为父子关系会影响替代过程的写法,只要实现了替代就行

后续再进行一些判断即可,调用删除恢复函数来保持红黑树性质

    void deleteNode(Node *del) 
    {
        Node *rep = del; 
        Node *child = nullptr;      
        Node *parentRP;             
        Color origCol = rep->color; 

        
        if (!del->left) 
        {
            rep = del->right;        
            parentRP = del->parent;  
            origCol = getColor(rep); 
            replaceNode(del, rep);   
        }
        else if (!del->right) 
        {
            rep = del->left;         
            parentRP = del->parent;  
            origCol = getColor(rep); 
            replaceNode(del, rep);   
        }
        else 
        {
            rep = findMinimumNode(del->right); 
            origCol = rep->color; 
            
            if (rep != del->right) 
            {
                parentRP = rep->parent; 
                child = rep->right; 
                parentRP->left = child; 
                if (child != nullptr) child->parent = parentRP; 
                
                
                del->left->parent = rep;
                del->right->parent = rep;
                rep->left = del->left;
                rep->right = del->right;
                
                if (del->parent != nullptr) 
                {
                    if (del == del->parent->left) 
                    {
                        del->parent->left = rep;
                        rep->parent = del->parent;
                    } 
                    else 
                    {
                        del->parent->right = rep;
                        rep->parent = del->parent;
                    }
                }
                
                else 
                {
                    root = rep;
                    root->parent = nullptr;
                }
            }
            else 
            {
                child = rep->right; 
                rep->left = del->left; 
                del->left->parent = rep; 
                
                if (del->parent != nullptr) 
                {
                    if (del == del->parent->left) 
                    {
                        del->parent->left = rep;
                        rep->parent = del->parent;
                    } 
                    else 
                    {
                        del->parent->right = rep;
                        rep->parent = del->parent;
                    }
                }
                else 
                {
                    root = rep;
                    root->parent = nullptr;
                }
                parentRP = rep; 
            }
        }

        if (rep != nullptr) rep->color = del->color;
        else origCol = del->color;
        
        if (origCol == Color::BLACK) 
        {
            if (child != nullptr) 
                removeFixup(child);
            
            else 
            {
                Nil->parent = parentRP;
                if (parentRP != nullptr) 
                {
                    if (parentRP->left == nullptr) parentRP->left = Nil;
                    else parentRP->right = Nil;
                }
                removeFixup(Nil);
                dieConnectNil();
            }
        }

        delete del;
    }
    void dieConnectNil() 
    {
        if (Nil == nullptr) return;
        
        if (Nil->parent != nullptr) 
        {
        if (Nil == Nil->parent->left) Nil->parent->left = nullptr;
        else Nil->parent->right = nullptr;
        }
    }

最后再将这些函数包装一下,输出给用户使用即可

public:
    myRedBlackTree() : root(nullptr), size(0), Nil(new Node()) {
        Nil->color = Color::BLACK;
    }

    void insert( const Key & key, const Value & value) { insertNode(key, value); }

    void remove( const Key & key)
    {
        Node * node = lookUp(key);
        if(node)
        {
            deleteNode(node);
            size --;
        }
    }

    Value *at(const Key & key)
    {
        auto ans = lookUp(key);
        if(ans) return &ans->value;
        return nullptr;
    }

    int getSize() { return size; } 
    bool empty() { return size == 0;}
    void clear() { deleteNode(root); size = 0;}

    ~myRedBlackTree()
    {
        deleteTree(root);
    }

private:
    void deleteTree(Node * node)
    {
        if(node)
        {
            deleteTree(node->left);
            deleteTree(node->right);
            delete node;
        }
    }

完整代码参考miniSTL

总结

红黑树的构建实际上是非常深入的一个工作,理论上讲应该按照:二叉搜索树-二叉平衡搜索树-红黑树 这样的顺序进行学习

而且内容多,知识杂,在408考试里也属于是边缘且难度极大的内容了,一篇博客属实无法全部讲解清楚

注意红黑树的五条性质即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值