【C++】红黑树(下)

简介: 【C++】红黑树(下)

【C++】红黑树(上)https://developer.aliyun.com/article/1515242?spm=a2c6h.13148508.setting.27.11104f0e63xoTy

首先记录 cur 的父亲 p 和祖父 g 的位置,然后判断父亲 p 的位置

(1)如果父亲 p 是祖父 g 的左孩子

说明叔叔 u 是祖父 g 的右孩子,先判断叔叔的状态:

  • 如果叔叔 u 存在且为红,说明是情况 1,直接先变色处理,然后再往上调整。

1、先调整颜色:父亲 p 和叔叔 u 变黑,祖父 g 变红。

2、再往上调整:原先祖父 g 当成新的 cur,判断新 cur 的父亲 p:

  • 若父亲 p 不存在,说明调整到头了,停止调整,然后将根节点变黑。
  • 若父亲 p 存在且为黑,没有破坏性质,停止调整
  • 若父亲 p 存在且为红继续调整,并判断是否出现了情况 2 / 3,要一直调整到根节点或者父亲 p 存在且为黑时,才停止调整。

  • 如果叔叔 u 不存在 / 叔叔 u 存在且为黑,说明是情况 2 / 3,先判断 cur 的位置:

1、如果 cur 是父亲 p 的左孩子(此时 cur、p、g 是一条直线,说明是情况 2)

  • 进行右单旋 + 变色处理(父亲 p 变黑,祖父 g 变红)

2、如果 cur 是父亲 p 的右孩子(此时 cur、p、g 是一条折线,说明是情况 3)

  • 进行左右双旋 + 变色处理(cur 变黑,祖父 g 变红)

3、上述情况 2 / 3 处理完成后,当前子树的根节点为黑 (p / cur),没有连续红节点了,则停止调整。


(2)如果父亲 p 是祖父 g 的右孩子

说明叔叔 u 是祖父 g 的左孩子,先判断叔叔的状态:

  • 如果叔叔 u 存在且为红,说明是情况 1,先变色处理(p 和 u 变黑,g 变红),然后再往上调整。

去判断新的父亲 p 的状态,检测新的子树是否平衡,情况 1 处理方式类似于上面,此处不再详细介绍。


  • 如果叔叔 u 不存在或者叔叔 u 存在且为黑,说明是情况 2 / 3,先判断 cur 的位置:

1、如果 cur 是父亲 p 的右孩子(此时 cur、p、g是一条直线,说明是情况二)

  • 进行左单旋 + 变色处理(父亲 p 变黑,祖父 g 变红)

2、如果 cur 是父亲 p 的左孩子(此时 cur、p、g是一条折线,说明是情况三)

  • 进行右左单旋 + 变色处理(cur 变黑,祖父 g 变红)

3、上述情况 2 / 3 处理完成后,当前子树的根节点为黑 (p / cur),没有连续红节点了,则停止调整。

注意:上面几个停止调整,是循环的出口,否则就要一直调整到根节点或者父亲 p 存在且为黑时。

// 插入节点
bool Insert(const pair<K, V>& kv)
{
    // 1、查找到适合插入的空位置
 
    // 判断树是否为空
    if (_root == nullptr)
  {
    _root = new Node(kv); // 插入新节点
    _root->_col = BLACK;  // 根节点为黑色
    return true;
  }
 
    // 记录当前节点和它的父节点
  Node* parent = nullptr;
  Node* cur = _root;
 
    while (cur) // cur为空时,说明找到插入位置了
  {
    if (kv.first > cur->_kv.first) // 键值大于当前节点
    {
      parent = cur;
      cur = cur->_right;
    }
    else if (kv.first < cur->_kv.first) // 键值小于当前节点
    {
      parent = cur;
      cur = cur->_left;
    }
    else // 键值等于当前节点
    {
      return false;  // 不允许数据冗余,返回false
    }
  }
 
    // 插入新节点,颜色为红色(可能会破坏性质3,产生两个连续红色节点)
  cur = new Node(kv);
  cur->_col = RED;
 
    if (parent->_kv.first < kv.first) // 判断新节点是其父亲的左孩子还是右孩子
  {
    parent->_right = cur;
  }
  else
  {
    parent->_left = cur;
  }
  cur->_parent = parent; // 更新cur的双亲指针
 
 
    // 2、检测红黑树性质有没有被破坏,并控制树的平衡
 
    // 如果cur的父亲p存在且为红,则树不平衡,就需要一直往上调整
    while (parent && parent->_col == RED)
  {
    Node* grandfater = parent->_parent; // 记录cur的祖父grandfather
 
    assert(grandfater);
    assert(grandfater->_col == BLACK);
 
    // 关键看叔叔
        // 1、如果parent是grandfather的左孩子
    if (parent == grandfater->_left)
    {
      Node* uncle = grandfather->_right; // uncle是grandfather的右孩子
      // 情况(1):uncle存在且为红,变色+继续往上处理
      if (uncle && uncle->_col == RED)
      {
        parent->_col = uncle->_col = BLACK; // parent和uncle变黑
        grandfather->_col = RED; // grandfather变红
 
        // 继续往上处理
        cur = grandfater;
        parent = cur->_parent;
      }
            // 情况(2)+(3):uncle不存在+存在且为黑
      else
      {
        // 情况(2):右单旋+变色
        //     g 
        //   p   u
        // c
        if (cur == parent->_left) // 如果cur是parent的左孩子,说明是情况2
        {
                    // 单旋 + 调整颜色
          RotateR(grandfater);    // 右单旋
          parent->_col = BLACK;   // parent变黑
          grandfater->_col = RED; // grandfather变红
        }
        else
        {
          // 情况3:左右单旋+变色
          //     g 
          //   p   u
          //     c
                    // 双旋 + 调整颜色
          RotateL(parent);        // 左单旋
          RotateR(grandfater);    // 右单旋
          cur->_col = BLACK;      // cur变黑
          grandfater->_col = RED; // grandfather变红
        }
        break; // 情况2或3处理完成后,当前子树的根节点为黑,没有连续红节点了,则停止调整
      }
    }
        // 2、如果parent是grandfather的右孩子
    else // (parent == grandfater->_right)
    {
      Node* uncle = grandfater->_left; // uncle是grandfather的左孩子
 
      // (1) uncle存在且为红,说明是情况1
      if (uncle && uncle->_col == RED)
      {
        parent->_col = uncle->_col = BLACK; // parent和uncle变黑
        grandfater->_col = RED; // grandfather变红
 
        // 继续往上处理
        cur = grandfater;
        parent = cur->_parent;
      }
            // (2) uncle不存在/存在且为黑,说明是情况2或3
      else
      {
        // 情况2:左单旋+变色
        //     g 
        //   u   p
        //         c
                // 如果cur是parent的右孩子,说明是情况2
        if (cur == parent->_right)
        {
                    // 单旋 + 调整颜色
          RotateL(grandfater);    // 左单旋
          parent->_col = BLACK;   // parent变黑
          grandfater->_col = RED; // grandfather变红
        }
                // 如果cur是parent的左孩子,说明是情况3
        else
        {
          // 情况3:右左单旋+变色
          //     g 
          //   u   p
          //     c
                    // 双旋 + 调整颜色
          RotateR(parent);        // 右单旋
          RotateL(grandfater);    // 左单旋
          cur->_col = BLACK;      // cur变黑
          grandfater->_col = RED; // grandfather变红
        }
        break; // 情况2或3处理完成后,当前子树的根节点为黑,没有连续红节点了,则停止调整
      }
    }
  }
  _root->_col = BLACK; // 根节点变黑
  return true;
}
 
 
// 左单旋
void RotateL(Node* parent)
{
  Node* subR = parent->_right; // 记录parent的右孩子
  Node* subRL = subR->_left;   // 记录parent右孩子的左孩子
 
  parent->_right = subRL;
  if (subRL)
  {
    subRL->_parent = parent;
  } 
 
  Node* ppNode = parent->_parent; // 先记录下parent的父节点
  subR->_left = parent;
  parent->_parent = subR;
 
    // root为空,说明parent原先是根节点
  if (_root == parent)
  {
    _root = subR;            // subR为新的根节点
    subR->_parent = nullptr; // subR的双亲指针指向空
  }
    // root不为空,说明parent原先是一个普通子树
  else
  {
        // 判断parent原先是父亲ppNode的左孩子还是右孩子
    if (ppNode->_left == parent)
    {
      ppNode->_left = subR;
    }
    else
    {
      ppNode->_right = subR;
    }
    subR->_parent = ppNode; // subR的双亲指针指向ppNode
  }
}
 
 
// 右单旋
void RotateR(Node* parent)
{
  Node* subL = parent->_left; // 记录parent的左孩子
  Node* subLR = subL->_right; // 记录parent左孩子的右孩子
 
  parent->_left = subLR;
  if (subLR)
  {
    subLR->_parent = parent;
  }
 
  Node* ppNode = parent->_parent; // 先记录下parent的父节点
  subL->_right = parent;
  parent->_parent = subL;
 
    // root为空,说明parent原先是根节点
  if (_root == parent)
  {
    _root = subL;            // subL为新的根节点
    subL->_parent = nullptr; // subL的双亲指向指向空
  }
    // root不为空,说明parent原先是一个普通子树
  else
  {
        // 判断parent原先是ppNode的左孩子还是右孩子
    if (ppNode->_left == parent)
    {
      ppNode->_left = subL;
    }
    else
    {
      ppNode->_right = subL;
    }
      subL->_parent = ppNode; // subL的双亲指针指向ppNode
  }
}

动态效果演示:

  • 升序插入构建红黑树。


  • 降序插入构建红黑树。


  • 随机插入构建红黑树


6、红黑树的验证

红黑树的检测分为两步:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)。
  2. 检测其是否满足红黑树的性质。
  • 根节点是否为黑色。
  • 是否存在连续红节点。
  • 统计每条路径上的黑节点数是否相等。

(1)检测是否存在连续红节点

// 检测红黑树是否有连续红节点
bool CheckRedRed(Node* root)
{
    if (root == nullptr)
        return true;
 
    // 思路1:如果当前节点为红色,检测它的孩子是否为红色,但孩子可能为空,每次还得判断孩子是否为空,太麻烦了
    // 思路2:如果当前节点为红色,我们去检测它的父亲是否为红色。因为根节点没有父亲,且根节点为黑色,是不会被判断的
 
    if (root->_col == RED && root->_parent->_col == RED)
    {
        cout << "存在连续的红色节点" << endl;
        return false;
    }
 
    // 继续判断当前节点的左右孩子
    return CheckRedRed(root->_left)
        && CheckRedRed(root->_right);
}

(2)检测每条路径上的黑节点数是否相等

首先计算出红黑树其中一条路径的黑节点数,作为一个 baseValue 基准值(参考值),然后再求出红黑树每条路径的黑节点数,与基准值比较,如果不相等,说明违反性质了。

  • blackNum:表示从根节点到当前节点的黑节点数。
  • benchmark:基准值(最左侧路径的黑节点数)。
// 计算红黑树最左侧这条路径的黑色节点数量基准值(参考值)
int CountBaseValue()
{
  int benchmark = 0;
  Node* cur = _root;
  while (cur) // 遇到NIL时,统计结束
  {
    if (cur->_col == BLACK)
        benchmark++;
 
    cur = cur->_left;
  }
    return benchmark;
}
 
bool PrevCheck(Node* root, int blackNum, int& benchmark)
{
    // 当前节点为空,说明遇到了NIL,判断该路径的黑节点数是否等于基准值
  if (root == nullptr)
  {
    if (benchmark == 0)
    {
      benchmark = blackNum;
      return true;
    }
 
    if (blackNum != benchmark)
    {
      cout << "某条黑色节点的数量不相等" << endl;
      return false;
    }
    else
    {
      return true;
    }
  }
 
    // 当前节点为黑色,则从根节点到当前节点的黑节点数加1
  if (root->_col == BLACK)
  {
    blackNum++;
  }
 
  if (root->_col == RED && root->_parent->_col == RED)
  {
    cout << "存在连续的红色节点" << endl;
    return false;
  }
  return PrevCheck(root->_left, blackNum, benchmark)
    && PrevCheck(root->_right, blackNum, benchmark);
}

注意

这里计算每条路径的黑节点数 blackNum 时,使用的是传值,因为这样就可以在递归的过程中计算到每条路径的黑节点数,因为每个函数栈帧中的 blackNum 变量都是独立存在的

下一层的 blackNum 是上一层的拷贝,下一层中++,不会影响上一层。

比如在黑节点 1 中对 blackNum++,变成 2,但红节点 8 中的 blackNum 值还是1,所以就不会影响到计算右孩子即黑节点 11 所在路径的黑节点数。

补:求一棵树的叶子节点数和总的节点数,就可以用引用。

// 检测红黑树是否平衡
bool IsBalance()
{
    if (_root == nullptr)
        return true;
 
    // 1、检测红黑树的根节点是否为红色
    if (_root->_col == RED)
    {
        cout << "根节点不是黑色" << endl;
        return false;
    }
 
    // 2、CheckRedRed:检测红黑树是否有连续红节点
    // 3、CheckBlackNums:检测红黑树每条路径黑节点数是否相等
 
    return CheckRedRed(_root) && PrevCheck(_root, 0, CountBaseValue());
}

7、红黑树的删除

红黑树的删除这里不做讲解,感兴趣可以参考:《算法导论》或者《STL源码剖析》。


8、 红黑树与AVL树的比较

红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是 O(logN),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的 2 倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。


9、红黑树的应用

  1. C++ STL库 —— map/set、mutil_map/mutil_set。
  2. Java 库。
  3. linux内核。
  4. 其他一些库。


相关文章
|
7月前
|
C++
c++的学习之路:27、红黑树
c++的学习之路:27、红黑树
56 4
|
7月前
|
测试技术 C++
C++进阶--红黑树
C++进阶--红黑树
|
7月前
|
编译器 C++ 容器
【C++学习手札】基于红黑树封装模拟实现map和set
【C++学习手札】基于红黑树封装模拟实现map和set
|
28天前
|
存储 C++
【C++】红黑树
红黑树是一种自平衡二叉搜索树,通过节点颜色(红或黑)及特定规则维持平衡,确保操作效率。其性质包括:每个节点非红即黑,根节点必黑,红节点的子节点必黑,从任一节点到其每个叶子的所有路径含相同数量的黑节点。实现上,通过节点结构定义、基本操作(插入、删除、旋转等)、维护平衡性质等步骤完成。代码示例展示了节点定义、插入操作及旋转调整方法。
25 2
【C++】红黑树
|
7月前
|
C++ 容器
【C++】红黑树模拟实现STL中的map与set
【C++】红黑树模拟实现STL中的map与set
|
4月前
|
关系型数据库 C++ 容器
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——红黑树
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——红黑树
40 0
|
5月前
|
Java C++ Python
【C++】手撕红黑树
【C++】手撕红黑树
30 1
|
7月前
|
Java C语言 C++
从C语言到C++_28(红黑树RedBlackTree)概念+插入接口实现(上)
从C语言到C++_28(红黑树RedBlackTree)概念+插入接口实现
60 4
|
7月前
|
C语言
从C语言到C++_29(红黑树封装set和map)红黑树迭代器的实现(下)
从C语言到C++_29(红黑树封装set和map)红黑树迭代器的实现
54 3
|
6月前
|
关系型数据库 C++
【c++】红黑树
【c++】红黑树
27 0