C++之红黑树

简介: C++之红黑树

前言

前面一节我们介绍了平衡搜索二叉树AVL树,我们知道,AVL树虽然查找效率很高,但是不能过多的修改,因为它为了保持平衡要不断的进行旋转。我们今天介绍的红黑树也是一种平衡搜索树,不过它所要求的平衡没有AVL树那么严格,因此对它进行修改操作时所要进行的旋转比AVL树要进行的旋转少。


一、概念

红黑树,一种二叉搜索树,它额外在每一个结点上增加一个表示结点颜色的存储位,有Red和Black两种可能

通过对任意一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长处二倍,因此它是接近平衡的。

二、性质

  1. 每个结点不是黑色就是红色
  2. 根节点是黑色
  3. 如果一个结点是红色,那么它的两个孩子结点是黑色
  4. 对于每一个结点,从该节点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点
  5. 每个叶子节点都是黑色(此处值空结点,即NIL结点)。

三、结点的定义

//结点的颜色
  enum color
  {
    RED,
    BLACK
  };
//结点的定义
  template<typename T>
  struct RBnode//红黑树的结点(三叉链)
  {
    RBnode(T data)
    :_data(data)
    , _parent(nullptr)
    , _left(nullptr)
    , _right(nullptr)
    , col(RED)
    {}
    T _data;//数据
    RBnode* _parent;//父节点(为了方便实现旋转)
    RBnode* _left;//左孩子
    RBnode* _right;//右孩子
    color _col;//颜色
  };

看到这里,相信大家会有一个疑问:为什么默认新插入的结点颜色是红色?

答:如果我们将新结点的颜色设置为黑色,那么它一定会违背性质4(即,对于每一个结点,从该节点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点),这样我们就需要大幅度的在这棵树上进行调整(几乎需要所有路径进行调整),才能使它再次符合性质4;

如果我们将新结点颜色设置为红色,它有可能违背性质3(即,如果一个结点是红色,那么它的两个孩子结点是黑色),也有一定的可能不违背,即使违背性质3它所带来的后果比违背性质4严重性小很多,我们可以通过几次旋转(只对它所在的路径进行调整)使它重新符合性质3。

因为我们新插入一个结点,它不是黑色就是红色,因此一定会违反一条性质,我们选择违反后果较小的性质3。

四、红黑树的结构

五、插入操作

1.插入代码

bool insert(const pair<K, V>& kv)
    {
      Node* newnode = new RBnode<K,V>(kv);
      if (!_root)//如果根节点为空,则新插入的结直接就是根节点
      {
        _root = newnode;
      }
      else
      {
        Node* cur = _root;
        Node* parent = cur;
        while (cur)
        {
          parent = cur;
          if (cur->_kv.first > kv.first)
          {
            cur = cur->_left;
          }
          else if (cur->_kv.first < kv.first)
          {
            cur = cur->_right;
          }
          else//树中已经有这个结点,插入失败
          {
            return false;
          }
        }
        cur = newnode;
        if (parent->_kv.first > cur->_kv.first)
        {
          parent->_left = cur;
        }
        else
        {
          parent->_right = cur;
        }
        cur->_parent = parent;
        Node* Grandpa = parent->_parent;
        Node* uncle = nullptr;
        while(Grandpa && parent->_col == RED)//如果父节点不是黑色,且父节点不是根结点
        {
          if (parent == Grandpa->_left)//定义叔叔结点
          {
            uncle = Grandpa->_right;
          }
          else
          {
            uncle = Grandpa->_left;
          }
          if (uncle->_col == RED)//如果叔叔存在且为红
          {
            Grandpa->_col = RED;
            parent->_col = uncle->_col = BLACK;
          }
          else//如果叔叔不存在,或者存在且为黑
          {
            //p是g的右孩子,c是p的右孩子(左单旋)
            if (parent == Grandpa->_right && cur == parent->_right)
            {
              Rotetal(Grandpa);
              Grandpa->_col = RED;
              parent->_col = BLACK;
            }
            //p是g的左孩子,c是p的左孩子(右单旋)
            else if (parent == Grandpa->_left && cur == parent->_left)
            {
              Rotetal(Grandpa);
              Grandpa->_col = RED;
              parent->_col = BLACK;
            }
            //p是g的左孩子,c是p的右孩子(左右双旋)
            else if (parent == Grandpa->_left && cur == parent->_right)
            {
              //先以parent为轴进行左单旋
              Rotetal(parent);
              //再以Grandpa为轴进行右单旋
              Rotetar(Grandpa);
              //更新颜色
              cur->_col = BLACK;
              Grandpa->_col = parent->_col = RED;
            }
            //p是g的右孩子,c是p的左孩子(右左双旋)
            else if (parent == Grandpa->_right && cur == parent->_left)
            {
              //先以parent为轴进行右单旋
              Rotetar(parent);
              //再以Grandpa为轴进行左单旋
              Rotetal(Grandpa);
              //更新颜色
              cur->_col = BLACK;
              Grandpa->_col = parent->_col = RED;
            }
            //旋转之后就符合性质4了,因此不用再继续更新
            break;
          }
          cur = parent;
          parent = Grandpa;
          Grandpa = Grandpa->_parent;
        }
        if (_root->_col == RED)//如果到最后更新到根节点,导致根节点为红色,为了满足性质2(根节点是黑色),就要将根结点置为黑色
        {
          _root->_col = BLACK;
        }
      }
      return true;
    }

2.左单旋

//左单旋
    void Rotetal(Node* parent)
    {
      Node* SubR = parent->_right;
      Node* SubRL = SubR->_left;
      Node* Grandpa = parent->_parent;
      parent->_parent = SubR;
      parent->_right = SubRL;
      if (SubRL)
      {
        SubRL->_parent = parent;
      }
      SubR->_parent = Grandpa;
      if (!Grandpa)
      {
        _root = SubR;
        _root->_parent = nullptr;
      }
      else
      {
        if (parent == Grandpa->_left)
        {
          Grandpa->_left = SubR;
        }
        else
        {
          Grandpa->_right = SubR;
        }
      }
      SubR->_left = parent;
    }

3.右单旋

//右单旋
    void Rotetar(Node* parent)
    {
      Node* SubL = parent->_left;
      Node* SubLR = SubL->_right;
      Node* Grandpa = parent->_parent;
      parent->_parent = SubL;
      parent->_left = SubLR;
      if (SubLR)
      {
        SubLR->_parent = parent;
      }
      SubL->_parent = Grandpa;
      if (!Grandpa)
      {
        _root = SubL;
        _root->_parent = nullptr;
      }
      else
      {
        if (parent == Grandpa->_left)
        {
          Grandpa->_left = SubL;
        }
        else
        {
          Grandpa->_right = SubL;
        }
      }
      SubL->_right = parent;
    }

4.插入新结点的情况分析与总结

第一步、按照搜索二叉树的规则插入新结点

如果该树是空树,就让新结点作为它的根节点。

先找到要插入的位置,比当前结点小就向左子树寻找,比当前结点大就向右子树寻找。

第二步、分析插入结点后红黑树的性质是否被破坏

新结点默认为红色,

1.如果双亲节点的颜色是黑色,则没有违反红黑树性质,不需要调整;

2.如果双亲节点的颜色是红色,则违反性质4需要进行调整。

为了方便分析,我们约定当前结点为cur©,当前节点的父节点为parent§,当前节点的祖父结点为Grandpa(g),当前结点的叔叔结点为uncle(c).。

  • 情况一:c为红色,p为红,g为黑,u存在且为红

    只需要将p和u的颜色置为黑色,g的颜色置为红色。
    如果g是根节点,调整之后只需要将g改为黑色即可;
    如果g是子树,那么g一定有双亲结点,如果g的双亲结点为红色,就需要继续向上调整。
  • 情况二:cur为红,p为红,g为黑,u不存在/存在且为黑
  1. 如果u不存在,则说明cur是新增节点。因为如果cur不是新增节点,那么cur和p一定有一个是黑色,那么就不满足性质4(每条路径上的黑色结点的个数相同);
  2. 如果u存在且为黑,则说明cur原本就存在且为黑。现在是红色是因为cur所在子树新增节点导致向上调整颜色的过程中将cur置为红色。
    这种情况有四种可能:
  • p是g的左孩子,c是p的左孩子;(要进行右单旋)
    以g为轴进行右单旋:

    更新结点p为黑色,cur和g为红色。
  • p是g的右孩子,c是p的右孩子;(要进行左单旋)
    以g为轴进行左单旋:

    更新结点p为黑色,cur和g为红色。
  • p是g的左孩子,c是p的右孩子;(要进行左右双旋)
    先以p为轴进行左单旋,再以g为轴进行右单旋:

    更新结点cur为黑色,p和g为红色。
  • p是g的右孩子,c是p的左孩子。(要进行右左双旋)
    先以p为轴进行左单旋,再以g为轴进行右单旋:

    更新结点cur为黑色,p和g为红色。

动态演示:

升序:

降序:

随机插入构建红黑树:

右旋转:

左旋转:

六、验证红黑树

验证红黑树分为两步:

1.检测是否满足二叉搜索树

中序遍历是否为有序序列

2.检测是否满足红黑树性质

代码如下:

bool IsValidRBTree()//验证是否为红黑树
    {
      Node* pRoot = GetRoot();
      // 空树也是红黑树
      if (nullptr == pRoot)
        return true;
      // 检测根节点是否满足情况
      if (BLACK != pRoot->_col)
      {
        cout << "违反红黑树性质二:根节点必须为黑色" << endl;
        return false;
      }
      // 获取任意一条路径中黑色节点的个数
      size_t blackCount = 0;
      Node* pCur = pRoot;
      while (pCur)
      {
        if (BLACK == pCur->_col)
          blackCount++;
        pCur = pCur->_left;
      }
      // 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数
      size_t k = 0;
      return _IsValidRBTree(pRoot, k, blackCount);
    }
    bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount)
    {
      //走到null之后,判断k和black是否相等
      if (nullptr == pRoot)
      {
        if (k != blackCount)
        {
          cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
          return false;
        }
        return true;
      }
      // 统计黑色节点的个数
      if (BLACK == pRoot->_col)
        k++;
      // 检测当前节点与其双亲是否都为红色
      Node* pParent = pRoot->_parent;
      if (pParent && RED == pParent->_col && RED == pRoot->_col)
      {
        cout << "违反性质三:没有连在一起的红色节点" << endl;
        return false;
      }
      return _IsValidRBTree(pRoot->_left, k, blackCount) &&
        _IsValidRBTree(pRoot->_right, k, blackCount);
    }

七、红黑树与AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O(l o g 2 N log_2 Nlog2N),红黑树不追求绝对平衡,只要保证最长路径不超过最短路径的两倍。相对而言,插入和旋转的次数更少,在经常进行增删的结构中性能比AVL树更优,而且红黑树的实现比AVL树简单,因此更加常用。


总结

以上就是今天要讲的内容,本文介绍了C++中红黑树的相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。

最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

相关文章
|
3天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
69 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
51 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
53 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
41 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
48 4
|
1月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
34 3
|
3月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
90 2
|
3月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
156 5
|
3月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
170 4