【C++入门到精通】C++入门 —— 红黑树(自平衡二叉搜索树)

简介: 【C++入门到精通】C++入门 —— 红黑树(自平衡二叉搜索树)

前言

前面我们讲了C语言的基础知识,也了解了一些初阶数据结构,并且讲了有关C++的命名空间的一些知识点以及关于C++的缺省参数、函数重载,引用 和 内联函数也认识了什么是类和对象以及怎么去new一个 ‘对象’ ,也了解了C++中的模版,以及学习了几个STL的结构,也了解了搜索二叉树的基本原理,以及学习了数据结构的大哥AVL树相信大家都掌握的不错,接下来博主将会带领大家继续学习有关C++比较重要的知识点—— 红黑树(自平衡二叉搜索树) 。下面话不多说坐稳扶好咱们要开车了😍

一、红黑树的概念

红黑树(Red-Black Tree)是一种自平衡的二叉查找树,红黑树之所以称为"红黑",是因为每个节点上都有一个颜色属性,可以是红色或黑色。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

二、红黑树的性质

  1. 每个节点都有一个颜色属性,要么是红色,要么是黑色。
  2. 根节点是黑色的。
  3. 所有叶子节点(NIL节点)都是黑色的。
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的。
  5. 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数量的黑色节点。

🚨🚨注意:NIL节点是一种特殊的节点,表示空节点或叶子节点。它被视为黑色节点,并且没有实际存储的值

三、红黑树节点的定义

// 颜色枚举类型,表示节点颜色属性
enum Colour
{
    RED, // 红色
    BLACK, // 黑色
};

// 红黑树节点结构体模板
template<class T>
struct RBTreeNode
{
    RBTreeNode<T>* _left; // 左子节点指针
    RBTreeNode<T>* _right; // 右子节点指针
    RBTreeNode<T>* _parent; // 父节点指针
    T _data; // 节点数据
    Colour _col; // 节点颜色属性,取值为 RED 或 BLACK

    // 构造函数,初始化节点的左右子节点、父节点和颜色属性
    RBTreeNode(const T& data)
        :_left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _data(data)
        , _col(RED) // 新插入节点的颜色总是红色(符合红黑树性质)
    {}
};

⭕它表示红黑树中的一个节点。该结构体包含以下成员:

  • _left:指向左子节点的指针。
  • _right:指向右子节点的指针。
  • _parent:指向父节点的指针。
  • _data:存储节点的数据。
  • _col:标识节点的颜色属性,可以是 RED(红色)或 BLACK(黑色)。

结构体模板还有一个构造函数,用于初始化节点的各个成员。构造函数接受一个参数 data,表示要存储在节点中的数据。构造函数会将左右子节点指针、父节点指针和节点颜色属性初始化为默认值,其中新插入的节点颜色总是红色。

红黑树的每个节点都是通过该结构体创建的实例,通过连接不同节点的指针,可以构建出红黑树的结构。

四、红黑树结构(带有头结点的红黑树)

带有头结点的红黑树是在普通红黑树的基础上添加了一个额外的头结点(也称为哨兵节点),用于简化红黑树的操作和边界情况的处理。**头结点通常被设置为黑色并且不存储任何实际数据。**具体来说,当红黑树为空时,根节点指针指向头结点,而不是空指针。这样,在对红黑树进行插入、删除等操作时,可以直接操作根节点,无需检查根节点是否为空。头结点的左子节点指向实际的根节点,而右子节点指向树中的最大节点(即最右侧的节点)这样,通过头结点可以方便地访问到根节点以及整个树的边界。总之,带有头结点的红黑树通过引入额外的头结点,简化了红黑树的操作和边界情况的处理,提高了代码的可读性和可维护性。

五、红黑树的插入操作

1. 按照二叉搜索的树规则插入新节点

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);
    // 进行变换操作使树满足红黑树性质
    // ... 缺少变换操作的代码 ...

    // 插入成功
    return true;
}

cur为新插入的节点,接下来针对这一个节点进行讨论变换(颜色)就可以了

2. 新节点插入后,红黑树的变色以及旋转

因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论。

🚨🚨约定:cur为新插入节点,p为父节点,g为祖父节点,u为叔叔节点

情况1: cur为红,p为红,g为黑,u存在且为红

如果g节点,调整完成后,需要将g改为黑色

如果g是子树,g一定有双亲,如果g的双亲是红色,需要继续向上调整

🍁解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整

情况2:cur为红,p为红,g为黑,u不存在或者u存在且为黑(单旋)

  1. 如果u节点不存在,则cur一定是新插入节点,因为如果cur不是新插入节点则cur和p一定有一个节点的颜色是黑色,就不满足性质4: 每条路径黑色节点个数相同。
  2. 如果u节点存在,则其一定是黑色的,那么cur节点原来的颜色一定是黑色的现在看到其是红色的原因是因为cur的子树在调整的过程中将cur节点的颜色由黑色改成红色。

🍁解决办法

  • p为g的左孩子,cur为p的左孩子,则进行右单旋转;
  • p为g的右孩子,cur为p的右孩子,则进行左单旋转
  • p、g变色–p变黑,g变红

⭕插入代码

  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);
    //cur为新插入的节点,接下来针对这一个节点进行讨论变换(颜色)就可以了

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

    while (parent && parent->_col == RED)
    {
      Node* grandfather = parent->_parent;
      if (grandfather->_left == parent)
      {
        Node* uncle = grandfather->_right;
        // 情况1:u存在且为红,变色处理,并继续往上处理
        if (uncle && uncle->_col == RED)
        {
          parent->_col = BLACK;
          uncle->_col = BLACK;
          grandfather->_col = RED;

          // 继续往上调整
          cur = grandfather;
          parent = cur->_parent;
        }
        else // 情况2+3:u不存在/u存在且为黑,旋转+变色
        {
          //     g
          //   p   u
          // c 
          if (cur == parent->_left)
          {
            RotateR(grandfather);
            parent->_col = BLACK;
            grandfather->_col = RED;
          }
          else
          {
            //     g
            //   p   u
            //     c
            RotateL(parent);
            RotateR(grandfather);
            cur->_col = BLACK;
            //parent->_col = RED;
            grandfather->_col = RED;
          }

          break;
        }
      }
      else // (grandfather->_right == parent)
      {
        //    g
        //  u   p
        //        c
        Node* uncle = grandfather->_left;
        // 情况1:u存在且为红,变色处理,并继续往上处理
        if (uncle && uncle->_col == RED)
        {
          parent->_col = BLACK;
          uncle->_col = BLACK;
          grandfather->_col = RED;

          // 继续往上调整
          cur = grandfather;
          parent = cur->_parent;
        }
        else // 情况2+3:u不存在/u存在且为黑,旋转+变色
        {
          //     g
          //  u     p
          //          c
          if (cur == parent->_right)
          {
            RotateL(grandfather);
            grandfather->_col = RED;
            parent->_col = BLACK;
          }
          else
          {
            //     g
            //  u     p
            //    c
            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;
    if (subRL)
      subRL->_parent = parent;

    Node* ppnode = parent->_parent;

    subR->_left = parent;
    parent->_parent = subR;

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

      subR->_parent = ppnode;
    }
  }

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

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

    Node* ppnode = parent->_parent;

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

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

六、红黑树的删除

1.被删除节点无子节点

  • 如果待删除节点是叶子节点,则直接删除该节点即可。
  • 如果待删除节点是根节点,则删除根节点并将根节点设置为 null。

2.被删除节点只有一个子节点

  • 如果待删除节点只有一个子节点,可以用其子节点来替代被删除的节点,并保持红黑树的性质。
  • 如果待删除节点是红色节点,则用其子节点来代替该节点,并不会影响树的性质;
  • 如果待删除节点是黑色节点,则需要根据其子节点和父节点的颜色组合,进行颜色变换并进行旋转操作,使树重新满足红黑树的性质。

3.被删除节点有两个子节点

如果待删除节点有两个子节点,需要先寻找其后继节点(即右子树中最小的节点或左子树中最大的节点),将其替换到待删除节点的位置,然后再删除该后继节点。

在实现过程中,需要注意以下几点:

  • 对于黑色节点的处理,需要进行额外的操作,以保证树的性质;
  • 在红黑树中,每个节点都带有颜色属性,因此需要在代码中对颜色进行判断和处理;
  • 删除节点或替换节点后,重新插入到树上的节点也需要进行变换操作,以保证树的性质。

下面是一个实现删除红黑树节点的代码示例:

bool Remove(const K& key)
{
    // 查找要删除的节点
    pair<Node*, bool> res = Find(key);
    if (!res.second)
    {
        return false;  // 没有找到要删除的节点
    }

    Node* cur = res.first;
    if (cur->_left && cur->_right)  // 待删除节点有两个子节点
    {
        Node* next = cur->_right;
        while (next->_left)  // 找到待删除节点的后继节点
        {
            next = next->_left;
        }
        cur->_kv = next->_kv;  // 将后继节点的值赋给待删除节点
        cur = next;  // 准备删除后继节点
    }

    Node* child = cur->_left ? cur->_left : cur->_right;  // 获取待删除节点的唯一子节点
    if (cur->_col == BLACK)  // 如果待删除节点为黑色节点,则需要进行额外的操作
    {
        cur->_col = child ? child->_col : RED;  // 将待删除节点的颜色赋给其子节点(若无子节点则为红色)
        FixUp(cur);  // 进行变换操作,维护树的性质
    }
    Replace(cur, child);  // 删除节点

    return true;
}


在该示例中,Find 函数用于查找要删除的节点,如果未找到则返回 false。如果待删除节点有两个子节点,则需要寻找其后继节点,并将后继节点的值赋给待删除节点。然后,判断待删除节点的颜色,如果为黑色节点,则需要进行额外的操作以维护树的性质。最后,调用 Replace 函数删除节点,并返回 true 表示删除成功。

七、红黑树与AVL树的比较

  1. 平衡性:
  • 红黑树:红黑树是一种弱平衡树,它通过满足一组红黑树性质来保证整个树的大致平衡。这些性质包括节点的颜色、路径上的黑色节点数量等。
  • AVL树:AVL树是一种严格平衡树,它要求任何节点的左右子树的高度差(平衡因子)不超过1。这种严格的平衡要求导致在插入和删除节点时需要更频繁地进行旋转操作。

性能:

  • 插入和删除操作:
  • 红黑树:由于红黑树是一种弱平衡树,它的调整操作相对较少,因此插入和删除节点的操作速度通常比较快。
  • AVL树:AVL树的平衡要求非常严格,插入和删除节点时可能需要进行多次旋转操作来维持平衡,因此这些操作相对较慢。
  • 查询操作:
  • 红黑树和AVL树在查询操作上具有相似的性能,都能在平均情况下以O(log n)的时间复杂度进行查找。

存储空间:

  • 红黑树:红黑树需要一个额外的位来存储节点颜色信息,所以相对于普通的二叉搜索树会占用更多的存储空间。
  • AVL树:AVL树不需要额外的存储空间来维护平衡,所以在存储空间上相对较高效。

🚨🚨注意:红黑树适用于插入和删除操作较频繁的场景,而AVL树适用于对平衡性要求更高的场景

八、红黑树的应用

红黑树是一种高效的平衡二叉搜索树,具有较好的查找、插入和删除性能。因此,它在许多方面都有广泛的应用。

  1. C++ STL中的map和set容器:STL库中的map和set容器都是基于红黑树实现的,它们提供了快速的键值查找和排序的功能。
  2. 数据库索引结构:许多数据库系统采用B+树或B树作为索引结构,但也有一些数据库系统采用红黑树作为索引结构来加速数据的查找。
  3. 操作系统调度:操作系统CPU调度算法中也使用了红黑树。例如,在Linux内核中,用红黑树来维护进程的优先级队列。
  4. 编译器标识符表:编译器在分析代码时需要维护一个标识符表来存放变量、函数等信息。红黑树可以作为标识符表的底层数据结构,提高符号查找的效率。
  5. 计算机网络路由表:路由表是计算机网络中进行数据包转发的重要组成部分之一。为了快速查找路由路径,许多路由器会使用红黑树等数据结构来维护路由表。

附:详细红黑树模拟代码

#pragma once
enum Colour
{
  RED,
  BLACK,
};

template<class T>
struct RBTreeNode
{
  RBTreeNode<T>* _left;
  RBTreeNode<T>* _right;
  RBTreeNode<T>* _parent;
  T _data;
  Colour _col;

  RBTreeNode(const T& data)
    :_left(nullptr)
    , _right(nullptr)
    , _parent(nullptr)
    , _data(data)
    , _col(RED)
  {}
};

template<class K, class T, class KeyOfT>
class RBTree
{
  typedef RBTreeNode<T> Node;
public:

  ~RBTree()
  {
    _Destroy(_root);
    _root = nullptr;
  }

  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;
  }

  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);
    //cur为新插入的节点,接下来针对这一个节点进行讨论变换(颜色)就可以了

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

    while (parent && parent->_col == RED)
    {
      Node* grandfather = parent->_parent;
      if (grandfather->_left == parent)
      {
        Node* uncle = grandfather->_right;
        // 情况1:u存在且为红,变色处理,并继续往上处理
        if (uncle && uncle->_col == RED)
        {
          parent->_col = BLACK;
          uncle->_col = BLACK;
          grandfather->_col = RED;

          // 继续往上调整
          cur = grandfather;
          parent = cur->_parent;
        }
        else // 情况2+3:u不存在/u存在且为黑,旋转+变色
        {
          //     g
          //   p   u
          // c 
          if (cur == parent->_left)
          {
            RotateR(grandfather);
            parent->_col = BLACK;
            grandfather->_col = RED;
          }
          else
          {
            //     g
            //   p   u
            //     c
            RotateL(parent);
            RotateR(grandfather);
            cur->_col = BLACK;
            //parent->_col = RED;
            grandfather->_col = RED;
          }

          break;
        }
      }
      else // (grandfather->_right == parent)
      {
        //    g
        //  u   p
        //        c
        Node* uncle = grandfather->_left;
        // 情况1:u存在且为红,变色处理,并继续往上处理
        if (uncle && uncle->_col == RED)
        {
          parent->_col = BLACK;
          uncle->_col = BLACK;
          grandfather->_col = RED;

          // 继续往上调整
          cur = grandfather;
          parent = cur->_parent;
        }
        else // 情况2+3:u不存在/u存在且为黑,旋转+变色
        {
          //     g
          //  u     p
          //          c
          if (cur == parent->_right)
          {
            RotateL(grandfather);
            grandfather->_col = RED;
            parent->_col = BLACK;
          }
          else
          {
            //     g
            //  u     p
            //    c
            RotateR(parent);
            RotateL(grandfather);
            cur->_col = BLACK;
            grandfather->_col = RED;
          }

          break;
        }
      }
    }
    _root->_col = BLACK;
    return true;
  }

  void InOrder()
  {
    _InOrder(_root);
  }

  bool IsBalance()
  {
    if (_root && _root->_col == RED)
    {
      cout << "根节点颜色是红色" << endl;
      return false;
    }

    int benchmark = 0;
    Node* cur = _root;
    while (cur)
    {
      if (cur->_col == BLACK)
        ++benchmark;
      cur = cur->_left;
    }

    // 连续红色节点
    return _Check(_root, 0, benchmark);
  }

  int Height()
  {
    return _Height(_root);
  }

private:
  void _Destroy(Node* root)
  {
    if (root == nullptr)
    {
      return;
    }

    _Destroy(root->_left);
    _Destroy(root->_right);
    delete root;
  }

  int _Height(Node* root)
  {
    if (root == NULL)
      return 0;

    int leftH = _Height(root->_left);
    int rightH = _Height(root->_right);

    return leftH > rightH ? leftH + 1 : rightH + 1;
  }

  bool _Check(Node* root, int blackNum, int benchmark)
  {
    if (root == nullptr)
    {
      if (benchmark != blackNum)
      {
        cout << "某条路径黑色节点的数量不相等" << endl;
        return false;
      }

      return true;
    }

    if (root->_col == BLACK)
    {
      ++blackNum;
    }

    if (root->_col == RED
      && root->_parent
      && root->_parent->_col == RED)
    {
      cout << "存在连续的红色节点" << endl;
      return false;
    }

    return _Check(root->_left, blackNum, benchmark)
      && _Check(root->_right, blackNum, benchmark);
  }

  void _InOrder(Node* root)
  {
    if (root == nullptr)
    {
      return;
    }

    _InOrder(root->_left);
    cout << root->_kv.first << " ";
    _InOrder(root->_right);
  }

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

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

    Node* ppnode = parent->_parent;

    subR->_left = parent;
    parent->_parent = subR;

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

      subR->_parent = ppnode;
    }
  }

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

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

    Node* ppnode = parent->_parent;

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

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

private:
  Node* _root = nullptr;
};

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!


目录
相关文章
|
4天前
|
C++ 存储 编译器
|
4天前
|
存储 算法 C语言
【C++入门到精通】C++的IO流(输入输出流) [ C++入门 ]
【C++入门到精通】C++的IO流(输入输出流) [ C++入门 ]
21 0
|
4天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
18 0
|
2天前
|
编译器 C++
C++入门(命名空间)
C++入门(命名空间)
|
4天前
|
C++ 编译器 程序员
C++ 从零基础到入门(3)—— 函数基础知识
C++ 从零基础到入门(3)—— 函数基础知识
|
4天前
|
C++ 存储
C++从零基础到入门(2)—— (if、switch、for、while语句)
C++从零基础到入门(2)—— (if、switch、for、while语句)
C++从零基础到入门(2)—— (if、switch、for、while语句)
|
4天前
|
编译器 C语言 C++
C++入门基础-2
C++入门基础
12 3
|
4天前
|
C语言 C++
C++入门基础-1
C++入门基础
18 1
|
4天前
|
存储 编译器 C++
C++:map&set 对红黑树的封装
C++:map&set 对红黑树的封装
11 1