[C++] 红黑树的实现:原理与底层解析

news2025/1/19 20:27:28


文章目录

    • @[toc]
  • 红黑树的概念
    • 红黑树的规则
    • 红黑树如何确保最长路径不超过最短路径的2倍
      • 红黑树规则
      • 最短路径与最长路径的分析
        • 最短路径:全黑路径
        • 最长路径:红黑交替路径
      • 结论:红黑树的平衡性如何保障操作效率
  • 红黑树的实现
    • 红黑树的节点结构
    • 红黑树的插入操作
      • 插入基本步骤
      • 插入后调整
        • 情况1:`p`为红,`u`为红,只着色
          • 变色过程代码讲解
        • 情况2:`p`为红,`u`为空或不存在,单旋+变色
          • 步骤1:判断父节点 `p` 是否为红色
          • 步骤2:找到祖父节点 `g` 和叔叔节点 `u`
          • 步骤3:判断叔叔节点 `u` 是否为黑色或不存在
          • 步骤4:根据 `c` 和 `p` 的位置选择旋转方式
          • 步骤5:变色
          • 步骤6:确保根节点为黑
        • 情况3:双旋+变色
          • 步骤1:判断父节点 `p` 是否为红色
          • 步骤3:判断 `p` 和 `c` 的位置关系
          • 步骤4:执行双旋操作
          • 步骤5:变色
          • 步骤6:确保根节点为黑色
      • 红黑树的查找
      • 红黑树的验证
        • `Check` 函数
          • `Check` 函数的实现
        • `IsBalance` 函数的完整实现:

红黑树的概念

红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它通过在节点上附加额外的颜色属性(红色或黑色),并遵循一定的规则来确保树的高度尽可能小,确保没有一条路径会比其他路径长出2倍,从而在最坏的情况下保证 (O(log N)) 的操作效率。

红黑树的规则

红黑树的每个节点除了键值外,还存储一个颜色属性。要保持树的平衡性,必须满足以下规则:

  1. 每个节点要么是红色,要么是黑色
  2. 根节点必须是黑色
  3. 红色节点的两个子节点必须是黑色,即没有两个连续的红色节点。
  4. 从任意节点到其叶子节点的所有路径上,必须包含相同数量的黑色节点(这被称为黑高,Black Height,bh)。

这些规则确保了红黑树的近似平衡。通过规则4,可以推导出红黑树的最长路径不会超过最短路径的两倍,这就意味着红黑树的高度维持在 (O(log N)),从而保证了查找、插入和删除的效率。

红黑树如何确保最长路径不超过最短路径的2倍

红黑树的一个重要特性是保持相对平衡,从而使得查找、插入和删除操作的时间复杂度都能保持在 (O(log N)) 的范围内。具体而言,红黑树的设计确保了最长路径的长度不会超过最短路径的2倍。这是通过红黑树的四条规则,尤其是规则2、3和4来实现的。

红黑树规则

为了更好地理解红黑树的平衡性,让我们复习一下红黑树的四条基本规则:

  1. 每个节点要么是红色,要么是黑色
  2. 根节点必须是黑色
  3. 红色节点的两个子节点必须是黑色,即没有连续的红色节点。
  4. 从任意节点到其叶子节点的所有路径上,必须包含相同数量的黑色节点。

其中,规则4 是红黑树平衡的核心,它直接限制了路径上的黑色节点数量,进而影响树的高度和路径长度。

最短路径与最长路径的分析

最短路径:全黑路径

根据规则4,从根节点到每一个叶子节点的路径上,黑色节点的数量是相同的。因此,最短路径就是只有黑色节点的路径。这种路径上没有任何红色节点,只有黑色节点。

我们用 bh(Black Height,黑高)来表示从根节点到叶子节点的黑色节点数量(不包括红色节点)。因此,最短路径的长度等于 bh

最长路径:红黑交替路径

根据规则3,红色节点的子节点必须是黑色,意味着不可能有连续的红色节点。因此,在极端情况下,最长的路径是红黑交替的路径。

如果路径上的黑色节点数量为 bh,那么在最坏的情况下,每个黑色节点下面都有一个红色节点。因此,最长的路径将是交替排列的红黑节点的路径,其长度为 2 * bh

结论:红黑树的平衡性如何保障操作效率

红黑树通过严格的颜色规则和旋转操作,确保了树的路径长度差异有限。通过保证黑色节点数目相等没有连续的红色节点,红黑树确保了每一条从根到叶子的路径上的黑色节点数量相同,同时限制了红色节点的数量。

具体来说,红黑树的平衡性源于以下几个因素:

  1. 红色节点不能连续:这减少了极端情况下过多红色节点导致的树的不平衡。
  2. 路径上的黑色节点数相同:这意味着所有路径的黑色节点数量一致,保证了从根到叶子的路径差距不会太大。
  3. 旋转操作和变色:在插入或删除节点时,红黑树通过旋转和变色操作,动态维护这几个规则。

由于红黑树的这些规则,红黑树的最短路径和最长路径之间的长度差异不会超过2倍。也正是因为这一点,红黑树能够保证插入、删除和查找操作的时间复杂度为 (O(log N)),即便在最坏的情况下,红黑树的效率仍然能够得到保证。

红黑树的实现

红黑树的节点结构

红黑树的每个节点包含一个键值对(key-value pair),并且除了普通二叉树的左子节点、右子节点外,还需要一个父节点指针来维护树的结构。此外,每个节点还需要一个颜色标记(红或黑),以便在插入和删除操作时维护平衡。

红黑树的结构与普通二叉搜索树类似,但它为每个节点额外引入了颜色属性。每个节点需要存储:

  • 键值对(_kv)。
  • 左右子节点(_left_right)。
  • 父节点(_parent),用于追踪节点的父亲,以便于修复平衡。
  • 颜色(_col),表示节点是红色还是黑色。
template<class K, class V>
struct RBTreeNode
{
    pair<K, V> _kv;
    RBTreeNode<K, V>* _left;
    RBTreeNode<K, V>* _right;
    RBTreeNode<K, V>* _parent;
    Colour _col;

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

在红黑树的实现中,根节点初始化为黑色,其余新增节点在插入时先标记为红色。

红黑树的插入操作

插入基本步骤

红黑树的插入操作类似于二叉搜索树(BST)的插入,但在插入后需要通过一系列调整来维护红黑树的平衡性。插入的核心思想是:插入一个新节点后,如果不违反红黑树的规则,则插入结束;如果违反了某些规则(特别是红-红冲突),则通过颜色变换旋转来修复。

  1. 按二叉搜索树规则进行插入,红黑树首先是一个二叉搜索树(BST),因此,插入一个新值的第一步就是根据BST的规则查找插入位置:
    1. 如果插入的值小于当前节点的值,则向左子树移动。如果插入的值大于当前节点的值,则向右子树移动。
      这一步的目的是在红黑树中找到合适的插入位置。此时仅仅是按照二叉搜索树的规则,不涉及颜色和平衡性问题。
  2. 新增节点颜色的选择当我们找到插入位置后,分为两种情况:
    1. 空树插入:如果红黑树是空树,则插入的节点是根节点,根据红黑树的第二条规则,根节点必须是黑色,因此将根节点设为黑色。插入结束,不需要进一步调整。
    2. 非空树插入:如果树非空,则新插入的节点必须是红色。这是因为如果插入一个黑色节点,会违反红黑树的规则4(从根到叶子的路径中黑色节点数必须相同)。红色节点的插入不会改变任何路径上的黑色节点数量,因此能减少违规的可能性。
  3. 根据第二点情况说明,在非空子树插入黑节点会导致第四条规则很难维护,所以统一新加节点为红色节点,在插入到root位置的时候变为黑色即可。

插入后调整

情况1:p为红,u为红,只着色
  • c 是当前新插入的节点(红色),
  • pc 的父节点(红色),
  • gp 的父节点(黑色),即 p 的祖父节点(规则限制)
  • up 的兄弟节点(即 g 的另一侧子节点,p 的叔叔节点)。

如果 pu 都是红色,而 g 是黑色,这种情况下,我们需要通过重新着色来调整红黑树的平衡,而不是旋转。

红黑树的规则3规定:不能有两个连续的红色节点,但新插入的节点 c 和它的父节点 p 都是红色,违反了这条规则。而 p 的叔叔节点 u 也是红色时,子树中的红色节点过多。此时,我们通过变色解决问题:

  • pu 变成黑色:这会增加这条路径上黑色节点的数量,确保黑高的平衡。
  • g 变为红色:由于 pu 都变为黑色,g 变红可以保持树的总平衡,即从 g 的根节点到叶子节点的黑色节点数量保持不变
  • **循环处理 **g:由于 g 现在是红色,它可能会导致祖父节点进一步违反红黑树规则,因此我们需要将 g 当作新的 c,向上继续检查和修复,如果新的p为黑节点,即完成。

变色过程代码讲解

结合代码,具体分析:

// 如果父节点是红色,则修复红黑树属性
while (parent && parent->_col == RED)
{
    Node* grandfather = parent->_parent;  // 找到祖父节点 g
    if (parent == grandfather->_left) // parent 在左  uncle在右
    {
        Node* uncle = grandfather->_right;  // 找到叔叔节点 u

        // 情况 1: 叔叔节点是红色,需要重新着色
        if (uncle && uncle->_col == RED)  // u 为红色,进行变色
        {
            parent->_col = uncle->_col = BLACK;  // p 和 u 变为黑色
            grandfather->_col = RED;  // g 变为红色
            cur = grandfather;  // 继续向上修复,将 g 当作新的 cur
            parent = cur->_parent;  // 更新父节点
        }
        else
        {
            // 情况 2: 叔叔节点是黑色或不存在,需要旋转
            // 此部分是旋转处理,当前情况为变色,不涉及此部分
        }
    }
    else // parent 在右  uncle在左
    {
        Node* uncle = grandfather->_left;  // 找到叔叔节点 u

        // 情况 1: 叔叔节点是红色,需要重新着色
        if (uncle && uncle->_col == RED)  // u 为红色,进行变色
        {
            parent->_col = uncle->_col = BLACK;  // p 和 u 变为黑色
            grandfather->_col = RED;  // g 变为红色
            cur = grandfather;  // 继续向上修复,将 g 当作新的 cur
            parent = cur->_parent;  // 更新父节点
        }
        else
        {
            // 情况 2: 叔叔节点是黑色或不存在,需要旋转
            // 此部分是旋转处理,当前情况为变色,不涉及此部分
        }
    }
}

// 确保根节点始终为黑色
_root->_col = BLACK;

步骤1:判断父节点 p 是否为红色

这段代码的前提是父节点 p 是红色。因为 c(当前插入的节点)也是红色,连续的红色节点 违反了红黑树的规则,因此需要进行调整。

步骤2:找到祖父节点 g 和叔叔节点 u

我们首先通过 p 找到它的父节点 g,以及 g 的另一个子节点(p 的兄弟节点)u,即叔叔节点。根据 pg 的左侧还是右侧,分别处理左叔叔和右叔叔的情况。

步骤3:判断叔叔节点 u 是否为红色

如果 u 是红色,意味着子树中有过多的红色节点,此时我们需要进行变色

  1. p** 和 u 变为黑色**:通过变色,我们让这两个子树的黑色节点数量增加,这样所有从 g 到叶子的路径上的黑色节点数量保持一致。
  2. g** 变为红色**:我们将 g 变为红色,保持整体黑色节点数平衡。

步骤4:递归处理祖父节点 g

此时,g 被变为红色,可能会导致它与其父节点形成连续的红色节点。因此,我们将 g 作为新的 c,继续向上修复,重复这一过程。

步骤5:确保根节点为黑色

无论经过了多少次变色和旋转调整,红黑树的根节点必须始终为黑色。因此在代码的最后,我们将根节点重新着色为黑色,确保红黑树的规则始终得以遵守。

抽象图

情况2:p为红,u为空或不存在,单旋+变色

在红黑树的插入过程中,情况2 涉及的是当新插入的节点 c 和它的父节点 p 都是红色,而 p 的兄弟节点 u(叔叔节点)不存在或是黑色的情况。此时,简单的变色操作不足以解决问题,必须进行旋转加变色来恢复平衡。

  • c 是新插入的节点,颜色为红色;
  • pc 的父节点,颜色也是红色;
  • gp 的父节点,颜色为黑色(p 的祖父节点);
  • up 的兄弟节点,可能不存在,或者存在但颜色为黑色。

在这种情况下,红黑树的规则3 规定,不能有两个连续的红色节点,而当前 cp 都是红色,违反了这一规则。由于 u 不存在或是黑色,简单的颜色变换不足以恢复树的平衡,因此需要通过旋转加变色的方式来解决。

旋转和变色的分析:

为了消除连续的红色节点并恢复平衡,我们需要进行以下操作:

  1. 旋转:根据 cpg 的相对位置,进行左旋右旋。旋转的目的是让 p 上移,成为新子树的根,并将 g 下移。
  2. 变色:将 p 变为黑色,将 g 变为红色。这样可以确保没有连续的红色节点,同时保证黑色节点数量保持不变。

旋转和变色的具体情况:

旋转的具体操作取决于 pcg 的位置关系,分为两种情况:

  • p** 是 g 的左子节点**:
    • 如果 cp 的左子节点(LL 失衡),进行右旋
    • 如果 cp 的右子节点(LR 失衡),先**左旋 p,再右旋 **g
  • p** 是 g 的右子节点**:
    • 如果 cp 的右子节点(RR 失衡),进行左旋
    • 如果 cp 的左子节点(RL 失衡),先**右旋 p,再左旋 **g

代码过程解讲解

以下是红黑树插入过程中处理情况2:旋转+变色的代码段:

// 如果父节点是红色,则修复红黑树属性
while (parent && parent->_col == RED)
{
    Node* grandfather = parent->_parent;
    if (parent == grandfather->_left) // parent 在左  uncle在右
    {
        Node* uncle = grandfather->_right;

        // 情况 1: 叔叔节点是红色,需要重新着色
        if (uncle && uncle->_col == RED)
        {
            parent->_col = uncle->_col = BLACK;
            grandfather->_col = RED;
            cur = grandfather;  // 继续向上修复
            parent = cur->_parent;
        }
        else
        {
            // 情况 2: 叔叔节点是黑色或不存在,需要旋转
            if (cur == parent->_left)
            {
                RotateR(grandfather);  // 右旋转
                parent->_col = BLACK;
                grandfather->_col = RED;
            }
            else // 情况3
            {
                RotateL(parent);  // 先左旋父节点
                RotateR(grandfather);  // 再右旋祖父节点
                cur->_col = BLACK;
                grandfather->_col = RED;
            }
            // 叔叔节点是黑色或不存在时,在旋转处理后当前最上面的是黑色节点,所以已经满足红黑树要求,可以直接break
            break;  
        }
    }
    else // parent 在右  uncle在左
    {
        Node* uncle = grandfather->_left;

        // 情况 1: 叔叔节点是红色,需要重新着色
        if (uncle && uncle->_col == RED)
        {
            parent->_col = uncle->_col = BLACK;
            grandfather->_col = RED;
            cur = grandfather;  // 继续向上修复
            parent = cur->_parent;
        }
        else
        {
            // 情况 2: 叔叔节点是黑色或不存在,需要旋转
            if (cur == parent->_right)
            {
                RotateL(grandfather);  // 左旋转
                parent->_col = BLACK;
                grandfather->_col = RED;
            }
            else // 情况3
            {
                RotateR(parent);  // 先右旋父节点
                RotateL(grandfather);  // 再左旋祖父节点
                cur->_col = BLACK;
                grandfather->_col = RED;
            }
            // 叔叔节点是黑色或不存在时,在旋转处理后当前最上面的是黑色节点,所以已经满足红黑树要求,可以直接break
            break;
        }
    }
}

    // 确保根节点始终为黑色
    _root->_col = BLACK;
    return true;
}
步骤1:判断父节点 p 是否为红色

p 为红色时,由于 c 也是红色,因此违反了红黑树的规则3,需要进行调整。

步骤2:找到祖父节点 g 和叔叔节点 u

首先找到 p 的父节点 g(祖父节点)和 p 的兄弟节点 u(叔叔节点)。根据 pg 的左侧还是右侧,分为两种处理情况。

步骤3:判断叔叔节点 u 是否为黑色或不存在

如果 u 是黑色或不存在,表示我们不能通过简单的变色来修复树的平衡。此时需要进行旋转操作。

步骤4:根据 cp 的位置选择旋转方式
  • LL 失衡pg 的左子节点,cp 的左子节点。这种情况下需要对 g 进行右旋
  • LR 失衡pg 的左子节点,cp 的右子节点。这种情况下先对 p 进行左旋,再对 g 进行右旋
  • RR 失衡pg 的右子节点,cp 的右子节点。这种情况下需要对 g 进行左旋
  • RL 失衡pg 的右子节点,cp 的左子节点。这种情况下先对 p 进行右旋,再对 g 进行左旋
步骤5:变色

无论是 LL、LR、RR 还是 RL 失衡,旋转完成后都需要进行变色操作。具体做法是:

  1. pc 变为黑色,确保消除连续的红色节点。
  2. g 变为红色,保持子树的黑色节点数量不变。
步骤6:确保根节点为黑

无论经过了多少次变色和旋转调整,红黑树的根节点必须始终为黑色。因此在代码的最后,我们将根节点重新着色为黑色,确保红黑树的规则始终得以遵守。

抽象图

情况3:双旋+变色

在红黑树的插入过程中,当新插入的节点 c 和其父节点 p 都是红色,而 p 的兄弟节点 u(叔叔节点)不存在或是黑色时,若 cp 处于特定位置关系,无法通过单次旋转解决平衡问题,就需要进行双旋+变色。这一过程的核心是通过两次旋转调整树的结构,并通过颜色变换来恢复红黑树的规则。

  • c 是当前新插入的节点,颜色为红色。
  • pc 的父节点,颜色也是红色。
  • gp 的父节点,颜色为黑色(即 p 的祖父节点)。
  • up 的兄弟节点(叔叔节点),可能不存在或是黑色。

此时,红黑树的规则3 规定,不能有两个连续的红色节点,而 cp 都是红色,违反了这一规则。由于 u 不存在或是黑色,变色不能解决问题,必须通过双旋+变色来调整树的平衡。

双旋和变色的分析:

在双旋中,我们首先会对 p 进行一次旋转,使 c 成为新的父节点;然后对 g 进行一次旋转,让 pc 成为新的子树根节点。经过这两次旋转后,c 将上移,gp 位置调整,确保没有连续的红色节点。

  • LL 和 RR 失衡:直接通过一次旋转即可恢复平衡(属于单旋情况)。
  • LR 和 RL 失衡:需要先对 p 进行一次旋转,使树局部结构转换为 LL 或 RR 失衡,接着对 g 进行第二次旋转来恢复平衡。这就是所谓的双旋
  1. pg 的左子节点,cp 的右子节点(LR 失衡)

在这种情况下,pcg 的左侧,但是 cp 的右子节点:

1. **左旋**:首先对 `p` 进行**左旋**,使 `c` 上移,`p` 成为 `c` 的左子节点。
2. **右旋**:接着对 `g` 进行**右旋**,使 `c` 上移,`g` 成为 `c` 的右子节点。
3. **变色**:旋转完成后,将 `c` 变为黑色,将 `g` 变为红色。
  1. pg 的右子节点,cp 的左子节点(RL 失衡)

与 LR 失衡类似,只不过这次 pcg 的右侧,cp 的左子节点:

1. **右旋**:首先对 `p` 进行**右旋**,使 `c` 上移,`p` 成为 `c` 的右子节点。
2. **左旋**:接着对 `g` 进行**左旋**,使 `c` 上移,`g` 成为 `c` 的左子节点。
3. **变色**:旋转完成后,将 `c` 变为黑色,将 `g` 变为红色。

根据红黑树的插入调整规则,以下代码展示了在双旋+变色情况下的处理:

// 如果父节点是红色,则修复红黑树属性
while (parent && parent->_col == RED)
{
    Node* grandfather = parent->_parent;
    if (parent == grandfather->_left) // parent 在左  uncle在右
    {
        Node* uncle = grandfather->_right;

        // 情况 1: 叔叔节点是红色,需要重新着色
        if (uncle && uncle->_col == RED)
        {
            parent->_col = uncle->_col = BLACK;
            grandfather->_col = RED;
            cur = grandfather;  // 继续向上修复
            parent = cur->_parent;
        }
        else
        {
            // 情况 2: 叔叔节点是黑色或不存在,需要旋转
            if (cur == parent->_left)
            {
                RotateR(grandfather);  // 右旋转
                parent->_col = BLACK;
                grandfather->_col = RED;
            }
            else // 单旋无法解决问题 
            {
                RotateL(parent);  // 先左旋父节点
                RotateR(grandfather);  // 再右旋祖父节点
                cur->_col = BLACK;
                grandfather->_col = RED;
            }
            // 叔叔节点是黑色或不存在时,在旋转处理后当前最上面的是黑色节点,所以已经满足红黑树要求,可以直接break
            break;  
        }
    }
    else // parent 在右  uncle在左
    {
        Node* uncle = grandfather->_left;

        // 情况 1: 叔叔节点是红色,需要重新着色
        if (uncle && uncle->_col == RED)
        {
            parent->_col = uncle->_col = BLACK;
            grandfather->_col = RED;
            cur = grandfather;  // 继续向上修复
            parent = cur->_parent;
        }
        else
        {
            // 情况 2: 叔叔节点是黑色或不存在,需要旋转
            if (cur == parent->_right)
            {
                RotateL(grandfather);  // 左旋转
                parent->_col = BLACK;
                grandfather->_col = RED;
            }
            else
            {
                RotateR(parent);  // 先右旋父节点
                RotateL(grandfather);  // 再左旋祖父节点
                cur->_col = BLACK;
                grandfather->_col = RED;
            }
            // 叔叔节点是黑色或不存在时,在旋转处理后当前最上面的是黑色节点,所以已经满足红黑树要求,可以直接break
            break;
        }
    }
}

    // 确保根节点始终为黑色
    _root->_col = BLACK;
    return true;
}
步骤1:判断父节点 p 是否为红色

p 为红色时,且 u 为黑色或不存在,表示我们无法通过简单的变色来解决问题。此时必须通过旋转和变色来恢复平衡。

步骤2:找到祖父节点 g 和叔叔节点 u

首先通过 p 找到其父节点 g,并找到 p 的兄弟节点 u,即 p 的叔叔节点。由于 u 为黑色或不存在,需要进行旋转和变色。

步骤3:判断 pc 的位置关系

根据 pcg 中的位置,可以确定是需要单旋还是双旋:

  • 如果 cp 的右子节点,而 pg 的左子节点,则发生LR 失衡,需要进行双旋
  • 如果 cp 的左子节点,而 pg 的右子节点,则发生RL 失衡,也需要进行双旋
步骤4:执行双旋操作
  • 对于 LR 失衡,先对 p 进行左旋,再对 g 进行右旋,让 c 成为新的根节点。
  • 对于 RL 失衡,先对 p 进行右旋,再对 g 进行左旋,让 c 成为新的根节点。
步骤5:变色

旋转完成后,通过变色来确保红黑树的平衡:

  • c 变为黑色,确保没有连续的红色节点。
  • g 变为红色,保持子树的黑色节点数量不变。
步骤6:确保根节点为黑色

无论旋转多少次,红黑树的根节点必须始终保持为黑色。这是红黑树的第二条规则,确保红黑树的整体平衡性。

抽象图

红黑树的查找

红黑树的查找过程与二叉搜索树相同,时间复杂度为 (O(log N))。通过比较节点的键值,沿着树的一条路径进行查找。

Node* Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < key)
            cur = cur->_right;
        else if (cur->_kv.first > key)
            cur = cur->_left;
        else
            return cur;  // 找到目标节点
    }
    return nullptr;  // 未找到
}

红黑树的验证

红黑树的验证是确保其平衡性和四条规则不被违反的关键步骤。红黑树插入或删除节点后,为了保持树的平衡性,必须通过递归检查整棵树,确保遵守红黑树的性质。这包括检查没有连续的红色节点、每条路径的黑色节点数量相同等。

Check 函数

Check 函数的主要任务是递归遍历整棵红黑树,检查以下几点:

  1. 确保没有连续的红色节点(红色节点的子节点不能是红色,规则3)。
  2. 计算每条路径的黑色节点数量,并确保所有路径的黑色节点数量相同(规则4)。

通过递归遍历树的左右子树,我们能够从根节点开始,验证每一个节点是否满足红黑树的规则。如果遇到违反红黑树规则的情况,Check 函数会返回 false

Check 函数的实现
// 递归检查树是否满足红黑树的属性
bool Check(Node* root, int blackNum, const int refNum)
{
    // 基本情况:如果到达叶子节点(空节点)
    if (root == nullptr)
    {
        // 到达空节点,说明这是一条从根到叶子的路径
        // 检查路径上黑色节点的数量是否与参考值相等
        if (refNum != blackNum)
        {
            cout << "路径中的黑色节点数量不相等" << endl;
            return false;
        }
        return true;
    }

    // 检查规则3:确保没有连续的红色节点
    if (root->_col == RED && root->_parent && root->_parent->_col == RED)
    {
        cout << root->_kv.first << "存在连续的红色节点" << endl;
        return false;
    }

    // 计算黑色节点数量:如果当前节点是黑色,增加计数
    if (root->_col == BLACK)
    {
        blackNum++;
    }

    // 递归检查左右子树
    return Check(root->_left, blackNum, refNum)  // 检查左子树
        && Check(root->_right, blackNum, refNum);  // 检查右子树
}

步骤1:检查是否到达叶子节点(root == nullptr

  • 当遍历到叶子节点(空节点)时,说明这一条从根到叶子的路径走完了。
  • 检查规则4:红黑树的规则4要求每一条从根节点到叶子节点的路径上必须包含相同数量的黑色节点。因此,在到达空节点时,blackNum 记录了当前路径上的黑色节点数量。如果 blackNum 与参考路径的黑色节点数量 refNum 不相等,则违反了规则4,返回 false

步骤2:检查红色节点是否连续

  • 根据红黑树的规则3,红色节点的子节点必须是黑色,即不能有连续的红色节点。如果当前节点 root 是红色,并且它的父节点也是红色,说明出现了连续的红色节点,违反规则3。
  • 在这种情况下,打印错误信息并返回 false,表示红黑树的结构被破坏了。

步骤3:计算路径中的黑色节点数量

  • 如果当前节点是黑色,则增加 blackNum 计数。因为每经过一个黑色节点,路径上的黑色节点数量会增加。
  • 这个计数器会在递归遍历时传递给下一层的左右子树,确保能够统计每条路径上的黑色节点数量。

步骤4:递归检查左右子树

  • 使用递归的方式分别检查左子树右子树,通过递归传递当前路径上的黑色节点计数器 blackNum
  • 如果左子树和右子树都满足红黑树的规则,递归返回 true;否则,返回 false,表示树的结构被破坏了。
IsBalance 函数的完整实现:
bool IsBalance()
{
    // 如果树为空,认为是平衡的
    if (_root == nullptr)
        return true;

    // 检查根节点是否为红色(根节点必须是黑色,规则2)
    if (_root->_col == RED)
        return false;

    // 计算参考的黑色节点数量(从根节点到最左侧的叶子节点)
    int refNum = 0;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_col == BLACK)
        {
            
            ++refNum;
        }
        cur = cur->_left;  // 走到最左侧,获取参考路径的黑色节点数量
    }

    // 递归检查整棵树,确保所有路径的黑色节点数量与参考值相同
    return Check(_root, 0, refNum);
}
  1. 根节点检查:首先检查根节点的颜色,确保根节点是黑色。如果根节点为红色,则返回 false,因为违反了红黑树的规则2。
  2. 计算参考黑高:从根节点沿着左子树路径走到最左侧的叶子节点,统计路径上黑色节点的数量 refNum。这条路径的黑色节点数作为参考值,后续将与其他路径的黑色节点数进行比较。
  3. 递归验证:使用辅助函数 Check,从根节点开始递归遍历整棵树,检查:
    • 每一条从根节点到叶子节点的路径上是否有相同数量的黑色节点;
    • 是否存在连续的红色节点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2213975.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【性能测试】jmeter通过定时器来模拟接口的动态请求时机

在 Apache JMeter 中使用定时器组件来模拟真实用户行为是非常重要的&#xff0c;因为这有助于确保你的性能测试结果更加贴近实际情况。 1. 固定定时器&#xff08;Constant Timer&#xff09; 这是最基础的定时器&#xff0c;用于在每个样本之间增加固定的时间延迟。然而&…

OpenCV-物体跟踪

文章目录 一、物体跟踪的定义二、OpenCV中的物体跟踪算法三、OpenCV物体跟踪的实现步骤四、代码实现五、注意事项 OpenCV是一个开源的计算机视觉和机器学习软件库&#xff0c;它提供了丰富的功能来实现物体跟踪。以下是对OpenCV中物体跟踪的详细解释&#xff1a; 一、物体跟踪的…

微服务架构是如何运作的?

在当今的软件架构领域&#xff0c;微服务架构以其灵活性、可扩展性和高可用性等优势&#xff0c;受到了越来越多企业和开发者的青睐。那么&#xff0c;微服务架构究竟是如何运作的呢&#xff1f;让我们一起来深入了解。 一、微服务架构的基本概念 微服务架构是一种将单个应用…

QT开发--网络编程

第十七章 QT网络编程 Qt Network模块用于TCP/IP编程&#xff0c;提供HTTP请求、cookies、DNS等功能的C类。 使用需在pro文件中添加“QT network”。 tcp通信流程图 17.1 QHostInfo QHostInfo类用于查找主机名与IP地址的关联。它提供两种查找方式&#xff1a; 1、异步查找&…

windows10创建系统账户

方式一: .a.点击 windows 图标->设置 .b.点击账户 .c.点击其他账户->将其他人添加到这台电脑->弹出对话框 .d.选中用户->右键->新用户->输入用户名和密码->创建 如下创建完成 方式二&#xff1a; windows R 输入lusrmgr.msc 后续操作步骤同上。 修改用…

YOLO11改进|SPPF篇|引入FocalModulation特征金字塔

目录 一、【FocalModulation】特征金字塔1.1【FocalModulation】特征金字塔介绍1.2【FocalModulation】核心代码二、添加【FocalModulation】特征金字塔2.1STEP12.2STEP22.3STEP32.4STEP4三、yaml文件与运行3.1yaml文件3.2运行成功截图一、【FocalModulation】特征金字塔 1.1【…

Linux网络命令:用于配置防火墙规则的一个用户友好的工具ufw详解

目录 一、概述 二、安装 UFW 三、启动、重启和关闭 UFW 1、启动 2、关闭UFW 3、 重启 UFW 四、查看 UFW 状态 五、UFW 基本命令 1. 允许端口 &#xff08;1&#xff09;单个 TCP 端口 &#xff08;2&#xff09;允许单个 UDP 端口 &#xff08;3&#xff0…

MySQL增删改进阶

目录 1.数据库约束 1.1约束类型 1.2 not null约束 1.3 unique&#xff1a;唯一约束 1.4 default&#xff1a;默认约束 1.5 primary key&#xff1a;主键约束 1.6 foreign key:外键约束 1.7 check约束&#xff08;了解&#xff09; 2.表的设计 3.新增&#xff08;进阶&…

CUDA 运行时GPU信息查询

cuda 官网文档名&#xff1a;CUDA_Runtime_API 运行时API查询GPU信息 调用 cudaDeviceProp prop; cudaGetDeviceProperties(&prop, device_id) 定义 由此可见&#xff0c;只能在主机上调用。 #include <cuda_runtime.h>#include <iostream> #include <…

Apache Kafka消息传递策略

kafka消息传递策略 微信公众号&#xff1a;阿俊的学习记录空间小红书&#xff1a;ArnoZhangwordpress&#xff1a;arnozhang1994博客园&#xff1a;arnozhangCSDN&#xff1a;ArnoZhang1994 现在我们了解了一些关于生产者和消费者的工作原理&#xff0c;接下来讨论Kafka在生产…

Java:玩家打怪小游戏

今天&#xff0c;我们尝试用Java来做一个“打怪小游戏”&#xff0c;听名字就知道&#xff0c;我们是应该创建几个成员和怪物&#xff0c;还有知道知道成员和怪物的血量&#xff0c;一次攻击的伤害等等。。当然我们的游戏攻击模式是“回合制”&#xff08;其实是别的方法&#…

SpringCloud-OpenFeign-服务接口调用

是什么 把需要暴露的api使用接口来暴露&#xff0c;客户端需要调用的时候&#xff0c;直接查看这个接口中有没有就可以了 通用步骤 架构说明 common模块 common 引入 openfeign 新建服务接口类 FeignClient(value "cloud-payment-service") // 服务名 public i…

【数据采集工具】Flume从入门到面试学习总结

国科大学习生活&#xff08;期末复习资料、课程大作业解析、大厂实习经验心得等&#xff09;: 文章专栏&#xff08;点击跳转&#xff09; 大数据开发学习文档&#xff08;分布式文件系统的实现&#xff0c;大数据生态圈学习文档等&#xff09;: 文章专栏&#xff08;点击跳转&…

# linux从入门到精通-从基础学起,逐步提升,探索linux奥秘(十三)--权限设置注意事项和属主属组设置sudo操作

linux从入门到精通-从基础学起&#xff0c;逐步提升&#xff0c;探索linux奥秘&#xff08;十三&#xff09;–权限设置注意事项和属主属组设置sudo操作 一、linux 权限设置 特殊注意事项 1、使用root用户创建一个文件夹&#xff08;/oo&#xff09;&#xff0c;权限默认&…

前端知识点总和

目录 一、canvas&#xff1a; &#xff08;1&#xff09;创建canvas标签&#xff1a; &#xff08;2&#xff09;使用JS获得这个canvas标签的DOM对象&#xff1a; &#xff08;3&#xff09;决定是画二维还是三维的画&#xff1a; &#xff08;4&#xff09;API&#xff1…

企业防止信息泄露的措施有哪些?10个防止信息泄露小技巧分享给你

在数字化时代&#xff0c;企业面临的安全挑战日益严峻&#xff0c;尤其是信息泄露问题。一旦企业内部或外部的敏感信息遭到泄露&#xff0c;不仅会造成巨大的经济损失&#xff0c;还可能影响企业声誉、客户信任&#xff0c;甚至可能引发法律纠纷。为了有效防止信息泄露&#xf…

使用 SQLmap 自动化检测 SQL 注入

使用 SQLmap 自动化检测 SQL 注入是一种常见的渗透测试技术。SQLmap 是一个强大的开源工具&#xff0c;可以自动检测和利用 SQL 注入漏洞&#xff0c;提取数据库信息&#xff0c;并接管目标数据库服务器。下面是如何使用 SQLmap 进行自动化检测 SQL 注入的基本步骤。 准备环境…

RabbitMQ 入门(七)SpringAMQP五种消息类型

一、Topic Exchange&#xff08;消息模式&#xff09; TopicExchange 与DirectExchange类似&#xff0c;区别在于routingKey可以是多个单词的列表&#xff0c;并且以.分割。 Topic类型的Exchange与Direct相比&#xff0c;都是可以根据RoutingKey把消息路由到不同的队列。只不过…

Spring WebFlux 核心原理(2-1)

1、Spring 响应式编程 1.1、早期响应式解决方案 响应式编程是构建响应式系统的主要候选方案。Spring 4.x 引入了 ListenableFuture 类&#xff0c;它扩展了 Java Future&#xff0c;并且可以基于 HTTP 请求实现异步执行操作。但是只有少数 Spring 4.x 组件支持新的 Java 8 Com…

Nginx(Linux):服务器版本升级和新增模块

目录 1、概述2、使用Nginx服务信号完成Nginx升级2.1 备份当前版本的Nginx2.2 向服务器导入新的Nginx2.3 向服务器导入新的Nginx2.4 停止老版本Nginx 3、使用Nginx安装目录的make命令完成升级3.1 备份当前版本的Nginx3.2 向服务器导入新的Nginx3.3 执行更新命令 1、概述 如果想…