STL源码剖析-RBTree

关联式容器

根据“数据在容器中的排列”特性,容器可分为序列式和关联式容器两种。标准的STL关联式容器可分为set(集合)和map(映射表)。这些容器的底层机制均以RB-tree完成,RB-tree也是一个独立容器,但并不对外开放。
此外,SGI STL还提供了一个不在标准规格之列的关联式容器:hash table(散列表)以及hash table为底层机制完成的hash_set(散列集合)、hash_map(散列映射表)、hash_multiset(散列多键集合)、hash_multimap(散列多键映射表)。
在这里插入图片描述
所谓关联式容器,观念上类似关联式数据库:每笔元素都有一个key和一个value。当元素被插入到关联式容器中时,容器内部结构便按照其键值大小,以某种特定规则将这个元素放置于适当位置,关联式容器没有所谓的头尾,只有最大元素和最小元素,所以不会有所谓的push_back(),push_front(),pop_back(),pop_front(),begin(),end()这样的操作行为。

关联式容器的内部结构是一个平衡二叉树,平衡二叉树有很多种类型,包裹AVL-tree、RB-tree、AA-tree。其中最被运用于STL的是RB-tree。

树的概述

树的一般概念如图所示
在这里插入图片描述
二叉搜索树
二叉树指任何节点最多只允许两个子节点,分别称为左子节点和右子节点,二叉树是可以为空的,编译器表达式树,哈夫曼编码树都是二叉树,如图
在这里插入图片描述
二叉搜索树
所谓二叉搜索树是可提供对数时间的元素插入和访问,其规则是任何节点的键值一定大于其左子树中的每个节点键值,并小于其右子树的每个节点的键值,因此最小元素就是往左一直走知道无路可走的元素,最大元素就是往右一直走,直到无路可走得到的元素。
如果要插入元素就从根节点出发,根节点大就往左走,小就往右走,直到走到叶子结点。
如果要删除某一结点,如果一般取右子树的最左结点或者左子树的最右结点来替换,然后删除。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
平衡二叉搜索树
为了防止二叉树输入值不够随机,导致二叉搜索树失去平衡。
AVL tree(Adelson-Velskii-Landis tree)
AVL tree是一个加上了额外平衡条件的二叉搜索树。为了确保整棵树的深度为O(logN),要求左右子树高度差不超过1,。
我们可以把平衡被破坏分成四种情况;

  1. 插入点位于X的左子节点的左子树-左左
  2. 插入点位于X的左子节点的右子树-左右
  3. 插入点位于X的右子节点的左子树-右左
  4. 插入点位于X的右子节点的右子树-右右
    情况1,4彼此对称,称为外侧插入,可以采用单旋转操作来调整,情况2,3彼此对称,称为内侧插入,可以采用双旋转操作来调整。
    在这里插入图片描述
    单旋转
    在这里插入图片描述
    这么做是因为二叉搜索树的规则使我们知道,k2>k1,所以k2必须称为新树形中k1的右子节点,B子树的所有结点的键值都位于k1和k2之间,所以新树形中B子树必须落在k2的左侧。
    双旋转
    在这里插入图片描述
    此时单旋转解决不了问题,我们必须以k2位新的根节点,这使得k1必须称为k2的左子节点,k3必须称为k2的右子节点,这种情况称为双旋转,因为可以由两次单旋转来完成。
    在这里插入图片描述
    以上所有调整都只需要将指针稍微做搬移,就可以迅速完成
    RB-tree
    RB-tree不仅是一颗二叉搜索树,而且必须满足以下规则:
  5. 每个结点不是红色就是黑色
  6. 根节点为黑色
  7. 如果结点为红,其子节点必须为黑
  8. 任一结点至NULL(树尾端)的任何路径,所含之黑结点数必须相同。
    根据规则4,新增结点必须为红,根据规则3,新增结点之父节点必须为黑。当新节点根据二叉搜索树的规则到达其插入点,却未能满足上述条件时,就必须调整颜色并旋转树形。
    在这里插入图片描述
    考虑四种破坏条件的情况
  9. 在这里插入图片描述
    此时,经过调整后也可能产生不平衡的状态(高度差超过1),假如图中A和B为null,D或E不为null。但这并无关系,因为RB-tree的平衡性比AVL-tree弱,然而RB-tree通常能够导致良好的平衡关系,RB-tree的搜索平均效率和AVL-tree几乎相等。
  10. 在这里插入图片描述
  11. 在这里插入图片描述
  12. 在这里插入图片描述
    为了避免状况4“父子皆为红色”的情况持续向上层发展,形成处理时效上的瓶颈,我们可以实行一个由上到下的程序:假设新增结点为A,那么就沿着A的路径,只要看到有某结点X的两个子节点都为红色,加把X改成红色,并把两个子结点改成黑色。在这里插入图片描述
    但是如果A的父节点P也是红色,就得像状况1一样做一次单旋转并改变颜色,或者像状况2一样地做一次双旋转并改变颜色。
    在这里插入图片描述

RB_tree的结点设计

采用双层结构

//定义颜色区分
    typedef bool rb_tree_color_type;
    const rb_tree_color_type rb_tree_red = false;//红色为0
    const rb_tree_color_type rb_tree_black = true;//黑色为1
    //双层红黑树节点结构
    struct rb_tree_node_base{
        typedef rb_tree_color_type color_type;
        typedef rb_tree_node_base* base_ptr;
        color_type color;
        base_ptr parent;
        base_ptr left;
        base_ptr right;
        //获取最小值
        static base_ptr minimun(base_ptr x){
            while(x->left)
                x = x->left;
            return x;
        }
        //获取最大值
        static base_ptr maximun(base_ptr x){
            while(x->right)
                x = x->right;
            return x;
        }
   };
    //红黑树结点第二层机构
    template <typename Value>
    struct rb_tree_node : public rb_tree_node_base{
        //定义出结点类型
        typedef rb_tree_node<Value>* link_type;
        Value value_field;//结点值
    };

RB_tree的迭代器

与结点对应,同样设计双层结构

    struct rb_tree_base_iterator{
        typedef rb_tree_node_base::base_ptr base_ptr;
        typedef bidirectional_iterator_tag iterator_category;
        typedef ptrdiff_t difference_type;

        base_ptr node;//用来与容器之间产生一个连接的关系
        //按照中序遍历的顺序,左子树的最右结点->根节点->右子树的最左结点,找出下一结点
        void increment(){
            if(node->right != 0){
                //状况1:右子结点存在,则当前可以看成是根节点,则找出右子树的最左结点
                node = node->right;
                while(node->left != 0)
                    node = node->left;
            }else{
                //状况2:没有右子节点,则当前可以看成是左子树的最右结点了,则找出根节点
                base_ptr y = node->parent;
                while(y->right == node){
                    node = y;
                    y = y->parent;
                }
                if(node->right != y)
                    //状况3:如果当前的node不是根节点,则y即为所求
                    node = y;
                //状况4:如果当前的node是根节点,则node为所求

            }
        }
        //按照中序遍历的顺序,左子树的最右结点->根节点->右子树的最左结点,
        // 找出上一结点,只需要处理后两者,后两者才有前驱结点
        void decrement(){
            if(node->color == rb_tree_red && node->parent->parent == node)
                //状况1:如果当前结点是header结点,那么其前驱应该是mostright结点,也就是其右子结点
                node = node->right;
            else if(node->left != 0){
                //状况2:也就是当前是根节点,那么其前驱应该是左子树中最右结点
                base_ptr y = node->left;
                while(y->right != 0)
                    y = y->right;
                node = y;
            }else{
                //状况3:如果当前不是根节点,也没有左子节点,那么当前处于右子树的最左结点,要去找根节点
                base_ptr y = node->parent;
                while(y->left == node){
                    node = y;
                    y = y->parent;
                }
                node = y;
                //如果当前处于root结点,y则为header结点,那么当前必定只有root一个结点
                //则可以得到node = header,y = root
                //最终得到node = y,不需要特殊处理
            }
        }

    };
    template <typename Value,typename Ref,typename Ptr>
    struct rb_tree_iterator:public rb_tree_base_iterator{
        typedef Value value_type;
        typedef Ref reference;
        typedef Ptr pointer;
        typedef rb_tree_iterator<Value,Value&,Value*> iterator;
        typedef rb_tree_iterator<Value,const Value&,const Vallue*> const_iterator;
        typedef rb_tree_iterator<Value,Ref,Ptr> self;
        typedef rb_tree_node<Value>* link_type;
        //构造函数
        rb_tree_iterator(){}
        rb_tree_iterator(link_type x){node = x};
        rb_tree_iterator(const iterator& it){node = it.node};
        //运算符重载
        reference operator*() const{
            return link_type(node)->value_field;
        }
        pointer operator->() const{
            return &(operator*());
        }
        self& operator++(){
            increment();
            return *this;
        }
        self operator++(int){
            self tmp = *this;
            increment();
            return tmp;
        }
        self& operator--(){
            decrement();
            return *this;
        }
        self operator--(){
            self tmp = *this;
            decrement();
            return tmp;
        }


    };

关键方法insert_unique/insert_equal

//将x插入到红黑树,并且保持key独一无二
        std::pair<iterator,bool> insert_unique(const value_type& v){
            link_type y = header;
            link_type x = root();
            bool comp = true;
            while(x != 0){
                y = x;
                comp = key_compare(KeyOfValue()(v),key(x));
                x = comp ? left(x):right(x);//小就往左,大或等就往右
            }
            iterator j = iterator(y);//父节点的迭代器
            if(comp)
                //如果comp为真表示遇到小
                if(j == begin())
                    //如果插入点的父节点是最左结点
                    return std::pair<iterator,bool>{_insert(x,y,v),true};
                else
                    //否则就找出比当前结点的前驱结点
                    --j;
                //判断前驱结点与插入值的大小
                //比前驱结点大说明可以插入
                //比前驱结点小或等于则说明一定是等于,不可能是小
                //因为小的话一定是先找到的这个前驱结点
                //所以返回false
            if(key_compare(key(j.node),KeyOfValue()(v)))
                return std::pair<iterator,bool>{_insert(x,y,v),true};
            return std::pair<iterator,bool>{j,false};
        }
        //允许key值重复
        iterator insert_equal(const value_type& v){
            link_type y = header;
            link_type x = root();
            //从根节点开始往下寻找
            while(x != 0){
                y = x;
                x = key_compare(KeyOfValue()(v),key(x)) ? left(x) : right(x);
            }
            return _insert(x,y,v);
        }

可以看到:
对于insert_equal,不管有没有相同结点,都是走一遍红黑树,找出一个空结点,然后插入进去.
对于insert_unique,则
走一遍红黑树,找到空结点
1.假如插入值比其父节点小,comp为真
1.1判断父节点是否为最左结点,是则直接插入不是则找到其前驱结点–>然后判断其前驱结点值是否比插入值小,小则说明不是相等结点,可以直接插入,否则说明已经有该结点了,插入失败
2.假如插入值比其父节点大,comp为假
直接判断其父节点与插入值的大小,这种情况判断父节点与插入点之间的大小,一定是插入点大,因此可以直接插入
那么为什么会这样呢?
因为我们在遍历红黑树的时候,如果比当前结点小就往左走,大于等于就往右走,那么如果当前是找到的相等的值,我们就会往与该结点相等的右子树走,然后继续往其左边走,最后会来到该结点的右子树的最左结点处,所以要判断与其前驱结点的关系(前驱结点自然就是与其相等的那个结点了),如果是前驱结点小,那就可以插入,那就是正常的小于的情况,如果是false,只能是相等的情况,因为在其右边一定是大于等于前驱结点的,那就说明找到相等结点,插入失败了。
而如果最后comp是false,也就是找到的是大于其父节点的值,这个是否是可以直接插入的,因为相等的情况会像上面那样走的!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值