c++ 红黑树(自平衡二叉搜索树)(k,v型)

简介: 因为红黑树是一种特殊的AVL树(但少了平衡因子的存在),所以其结点的定义是在AVL树上加上新的成员变量,用于表示结点的颜色。RED,BLACK,//三叉链, _kv(kv){}首先我们在默认构造上,默认构造结点的颜色默认情况下为红色所以为什么构造结点时,默认将结点的颜色设置为红色?这是因为:当我们向红黑树插入结点时,若我们插入的是黑色结点,那么插入路径上黑色结点的数目就比其他路径上黑色结点的数目多了一个,即破坏了红黑树的性质4,此时我们就需要对红黑树进行调整。


目录

红黑树的概念
对于题目也可以大致猜出红黑树是一种什么类型的树,其是一种特殊的平衡二叉搜索树(AVL树)。了解到AVL树的性质,其实平衡二叉树最大的作用就是查找,AVL树的查找、插入和删除在平均和最坏情况下都是O(logn)。AVL树的效率就是高在这个地方。如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理, 那么创建一颗平衡二叉树的成本其实不小. 这个时候就有人开始思考,并且提出了红黑树的理论,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。那么红黑树到底比AVL树好在哪里

之所以叫红黑树是因为:在每个结点上增加了一个存储位用于表示结点的颜色,这个颜色可以是红色的,也可以是黑色的,因此我们称之为红黑树。

红黑树的由来
红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1978年发明,在当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。

红黑树的性质
红黑树有以下五点性质:

每个结点不是红色就是黑色。
根结点是黑色的。
如果一个结点是红色的,则它的两个孩子结点是黑色的。
对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点。
每个叶子结点都是黑色的(此处的叶子结点指定是空结点)。
由此还是可以引出一些疑惑的

其实从性质三我们可以得到一个信息:红黑树当中不会出现连续的红色结点,所以父子节点只有三种颜色情况:红--->黑,黑--->黑,黑--->红。而根据性质4又可以得出,从某一结点到其后代叶子结点的所有路径上包含的黑色结点的数目是相同的。

我们假设在红黑树中,从根到叶子的所有路径上包含的黑色结点的个数都是N个,那么最短路径就是全部由黑色结点构成的路径,即长度为N。

与之对应的最长可能路径就是由一黑一红结点构成的路径,该路径当中黑色结点与红色结点的数目相同,即长度为2N。

因此,红黑树从根到叶子的最长可能路径不会超过最短可能路径的两倍。

这里再解释一下性质5,这里的叶子结点不是传统意义上的叶子,不是下图中的25/74/78/86/90

实际上,在红黑树中真正被定义为叶子结点的,是那些空节点,如下图。

红黑树结点的定义
因为红黑树是一种特殊的AVL树(但少了平衡因子的存在),所以其结点的定义是在AVL树上加上新的成员变量,用于表示结点的颜色。

enum Colour
{
RED,
BLACK,
};

template
struct RBTreeNode
{
//三叉链
RBTreeNode _left;
RBTreeNode
_right;
RBTreeNode* _parent;
pair _kv;
Colour _col;

RBTreeNode(const pair<K,V>& kv)
    : _left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _kv(kv)
    , _col(RED)
{}

};

首先我们在默认构造上,默认构造结点的颜色默认情况下为红色

所以为什么构造结点时,默认将结点的颜色设置为红色?

这是因为:当我们向红黑树插入结点时,若我们插入的是黑色结点,那么插入路径上黑色结点的数目就比其他路径上黑色结点的数目多了一个,即破坏了红黑树的性质4,此时我们就需要对红黑树进行调整。

若我们插入红黑树的结点是红色的,此时如果其父结点也是红色的,那么表明出现了连续的红色结点,即破坏了红黑树的性质3,此时我们需要对红黑树进行调整;但如果其父结点是黑色的,那我们就无需对红黑树进行调整,插入后仍满足红黑树的要求。

总的来说:

插入黑色结点,一定破坏红黑树的性质4,必须对红黑树进行调整。
插入红色结点,可能破坏红黑树的性质3,可能对红黑树进行调整。
权衡利弊后,我们在构造结点进行插入时,默认将结点的颜色设置为红色。

红黑树的插入
红黑树插入结点的逻辑分为三步:

按二叉搜索树的插入方法,找到待插入位置。
将待插入结点插入到树中。
若插入结点的父结点是红色的,则需要对红黑树进行调整。
看起来,其实插入的步骤其实与AVL树是大差不差的,前两步都是按照二叉搜索树的方式进行插入,与之不同的就是第三步,AVL树对此的解决方法就是进行旋转,红黑树作为特殊的AVL树,那么也是要进行旋转的,同样也设计到了旋转。

实际上,在插入结点后并不是一定会对红黑树进行调整,若插入结点的父结点是黑色的,那么我们就不用对红黑树进行调整,因为本次结点的插入并没有破坏红黑树的五点性质。

所以要调整的大前提就是:只有当插入结点的父结点是红色时才需要对红黑树进行调整,因为我们默认插入的结点就是红色的,如果插入结点的父结点也是红色的,那么此时就出现了连续的红色结点,因此需要对红黑树进行调整。

因为插入结点的父结点是红色的,说明父结点不是根结点(根结点是黑色的),因此插入结点的祖父结点(父结点的父结点)就一定存在。

红黑树调整时具体应该如何调整,主要是看插入结点的叔叔(插入结点的父结点的兄弟结点),根据插入结点叔叔的不同,可将红黑树的调整总分为两种情况。

情况一:插入结点的叔叔存在,且叔叔的颜色是红色。
此时为了避免出现连续的红色结点,我们可以将父结点变黑,但为了保持每条路径黑色结点的数目不变,因此我们还需要将祖父结点变红,再将叔叔变黑。这样一来既保持了每条路径黑色结点的数目不变,也解决了连续红色结点的问题。

但是这次调整还没有完全结束

此时祖父结点变成了红色,如果祖父结点是根结点,那我们直接再将祖父结点变成黑色即可,此时相当于每条路径黑色结点的数目都增加了一个。
如果祖父不是根节点的话,因为祖父原本是黑色,所以其父亲可以是红色,也可以是黑色,但如果是红色的情况下,在本次的颜色调整过后,祖父变为了红,但其父亲又为红,这就会破坏性质三,这就还需要再看其祖父的叔叔的情况,再次进行选择调整。
因此,情况一的抽象图表示如下:

注意: 叔叔存在且为红时,cur结点是parent的左孩子还是右孩子,调整方法都是一样的。

情况二:插入结点的叔叔存在且颜色是黑色 / 叔叔不存在,
对于这种情况的错误,肯定不是因为cur作为新增结点造成的错误,即使删掉cur那也是需要调整的,那么其造成的原因一定是因为在情况一继续往上调整的过程中出现的,所以对于此cur结点一定不是新插入的结点,而是上一次情况一调整过程中的祖父结点。

所以对于此图需要进行修改

对于此的解决方案处理起来比较简单,但也细分为四种小情况,主要是cur与p,p与g的父子关系。

情况A:p为g的左孩子,cur为p的左孩子

情况B:p为g的右孩子,cur为p的右孩子

情况C:p为g的左孩子,cur为p的右孩子

情况D:p为g的右孩子,cur为p的左孩子

这里就拿情况A来说一些小点吧,我们从上面的分析,cur不是作为新插入的结点,而是因为情况一的调整后使得情况一下的祖父颜色变为了红,而导致情况二中的cur变为了红,所以我们得知a,b子树中至少是存在一个黑色结点的,所以再进行此次的调整后从抽象图中看貌似是不符合性质四,其实是符合的,然而更不必说c子树了,因为在本次的插入是原本就是一个完整的红黑树,c子树中必有一个黑结点,并且c中本就不会存在情况一,也不会导致情况二。

从思路上来讲是简便的,但是代码实现上还是非常复杂的

这里先直接展示代码,稍后说一下注意点

bool Insert(const pair<K, V>& kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);

        _root->_col = BLACK;
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;

    while (cur)
    {
        if (cur->_kv.first < kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_kv.first > kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            return false;
        }
    }

    cur = new Node(kv);
    //默认结点颜色为红色

    if (parent->_kv.first < kv.first)
    {
        parent->_right = cur;
        cur->_parent = parent;
    }
    else
    {
        parent->_left = cur;
        cur->_parent = parent;
    }

    while (parent && parent->_col == RED)
    {
        Node* grandfather = parent->_parent;
        //大前提
        //parent在左
        if (parent == grandfather->_left)
        {
            Node* uncle = grandfather->_right;
            //Node* uncle = parent->_right;//错误二
            if (uncle && uncle->_col == RED)
            {
            //情景一:cur->红,parent->红,grandfather->黑,uncle存在且为红
                //     g
                //   p   u
                // c
                // 
                //解决:p,u改为黑,g改为红,最后g为红所以,要继续向上调整
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            }
            else
            {                    
                if (cur == parent->_left)
                {
                //情景二:cur->红,parent->红,grandfather->黑,uncle不存在/为黑
                    //     g
                    //   p   u
                    // c
                    // 
                // 解决:对g右单旋,p改为黑,g改为红,最后g为黑所以,直接break
                    RotateR(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                else
                {
                //情景三:cur->红,parent->红,grandfather->黑,uncle不存在/为黑
                    //       g
                    //   p      u
                    //     c
                // 解决:对p左单旋,后变为情景二。
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }                
                break;
            }
        }
        else//情景大概反着来
        {
            //1  uncle
            Node* uncle = grandfather->_left;//错误一
            //Node* uncle = parent->_right;//错误一
            if (uncle && uncle->_col == RED)
            {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            }

            else
            {
                if (cur == parent->_right)//2
                {
                    RotateL(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                else//3
                {
                    RotateR(parent);
                    RotateL(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;
            }
        }            
    }
    //最后
    _root->_col = BLACK;

    return true;
}
void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    parent->_right = subRL;
    subR->_left = parent;

    Node* parentParent = parent->_parent;

    parent->_parent = subR;
    if (subRL)
        subRL->_parent = parent;

    if (_root == parent)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        if (parentParent->_left == parent)
        {
            parentParent->_left = subR;
        }
        else
        {
            parentParent->_right = subR;
        }

        subR->_parent = parentParent;
    }
}

void RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    if (subLR)
        subLR->_parent = parent;

    Node* parentParent = parent->_parent;

    subL->_right = parent;
    parent->_parent = subL;

    if (_root == parent)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        if (parentParent->_left == parent)
        {
            parentParent->_left = subL;
        }
        else
        {
            parentParent->_right = subL;
        }

        subL->_parent = parentParent;
    }
}

注意点:其实对于情况二中的A,B小情况其实进行的是单旋,而C,D小情况是进行的双旋,所以也可以将C,D单独归为情况三,就如上面的代码展示一样,然而对于每次的插入后,都需要进行检查是否破坏红黑树的结构,然后破坏的大前提就是其父亲结点也是红,然而对于情况一,我们在调整完后会将祖父颜色置为红,这就导致其可以继续向上进行查看是否导致发生情况二,三的存在,所以还需要继续向上,然而对于情况二,三都是调整完就已经保证了是完整的红黑树了,这就可以直接break跳出即可。

红黑树的验证
红黑树也是一种特殊的二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断该二叉树是否满足二叉搜索树的性质。

判断依据:

是否存在 红-红
每条路径黑色结点是否相同个数
最长的不超过最短的二倍
根,叶子为黑
void InOrder()
{
_InOrder(_root);
cout << endl;
}
void _InOrder(Node root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " ";
_InOrder(root->_right);
}
bool Check(Node
root, int blacknum, const int refVal)
{
if (root == nullptr)
{
if (refVal != blacknum)
{
cout << "存在黑色节点数量不相等的路径" << endl;
return false;
}
return true;
}
if (root->_col == RED)
{
if (root->_parent->_col == RED)
{
cout << "有连续的红色节点" << endl;
return false;
}
}
if (root->_col == BLACK)
{
++blacknum;
}
return Check(root->_left, blacknum, refVal)
&& Check(root->_right, blacknum, refVal);

}
bool IsBalance()
{
     //1:是否存在 红-红 
    //每条路径黑色结点是否相同个数
    //最长的不超过最短的二倍
    //根,叶子为黑
    if (_root == nullptr)
        return true;
    if (_root->_col == RED)
        return false;
    int refVal = 0;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_col == BLACK)
        {
            ++refVal;
        }
        cur = cur->_left;
    }
    int blacknum = 0;
    return Check(_root, blacknum, refVal);
}

剩下的内容也就是像二叉搜索树的平常函数:查找,看看有多少层,打印某一层......这里不在多多展示

同样红黑树作为特别的AVL树也是禁止进行修改的!!!
红黑树的删除代码复杂,并且不会就不多说了,但也推荐篇好的文章:

【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树-CSDN博客

目录
相关文章
|
4月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
115 2
|
1月前
|
存储 C++
c++ 红黑树(带头结点)(k,k型)
想必在看到这篇文章的时候,你一定是带着问题去搜索的,一定是对红黑树已经有了初步大致的认识,已经知道了红黑树的性质与普通红黑树的功能与如何代码实现,但是莫一天突然看到了带头结点的红黑树,肯定是对此有一些疑惑的,或者来说在代码的实现上自己存在着某些疑惑。那么话不多说,就先给出红黑树(带头结点)的完整实现代码。然后再给出相应的详细解释。
61 0
|
5月前
|
C++ 容器
c++中的二叉搜索树
c++中的二叉搜索树
|
7月前
|
存储 算法 安全
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
|
8月前
|
存储 C++
【C++】红黑树
红黑树是一种自平衡二叉搜索树,通过节点颜色(红或黑)及特定规则维持平衡,确保操作效率。其性质包括:每个节点非红即黑,根节点必黑,红节点的子节点必黑,从任一节点到其每个叶子的所有路径含相同数量的黑节点。实现上,通过节点结构定义、基本操作(插入、删除、旋转等)、维护平衡性质等步骤完成。代码示例展示了节点定义、插入操作及旋转调整方法。
95 2
【C++】红黑树
|
8月前
|
存储 C++
【C++】二叉搜索树(BST)
二叉搜索树(Binary Search Tree, BST)是一种特殊的二叉树,其每个节点的左子树所有节点值小于该节点值,右子树所有节点值大于该节点值,且左右子树也均为二叉搜索树。BST支持高效的数据查找、插入和删除操作,时间复杂度通常为O(log n)。本文档详细介绍了BST的基本概念、存储结构及其实现,包括迭代和递归两种方式的查找、插入、删除等操作的具体代码示例。
146 3
|
存储 C++
【C++】二叉树进阶之二叉搜索树(下)
【C++】二叉树进阶之二叉搜索树(下)
71 4
|
Java 编译器 C++
【C++】二叉树进阶之二叉搜索树(上)
【C++】二叉树进阶之二叉搜索树(上)
64 3
|
Java C++ Python
【C++】手撕红黑树
【C++】手撕红黑树
72 1
|
算法 测试技术 C++
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
【C++高阶】掌握AVL树:构建与维护平衡二叉搜索树的艺术
83 2