【C++】红黑树的插入分析及验证

简介: 【C++】红黑树的插入分析及验证

1. 红黑树概念

红黑树 是一种二叉搜索树,但在每个节点上增加一个存储位表示节点的颜色,可以是red或black,

通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其他路径长处两倍,所以是接近平衡的

d32fbdaa73b64ab2bf99f6634714e968.png

2. 红黑树性质


1. 每个结点不是红色就是黑色

2. 根节点是黑色的

3. 如果一个节点是红色的,则它的两个孩子结点是黑色的

(不能出现连续的红色节点)

4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点

(每条路径上都有相同数量的黑色节点)

5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

(走到NULL才算一条路径)


3. 结构定义


8a5e357fce60453db91550d21d1060ae.png


使用枚举来记录红色与黑色,用_col表示当前节点颜色


但是在构造函数中为什么默认是红色呢?为什么不能是黑色呢?

关于默认节点为红/黑色的讨论



1e4ce54806414f67a188acdfc6cdc5e4.png

若在红框中插入黑色节点则违反规则4 即每条路径上都有相同数量的黑色节点,还需要再次将不同路径上都添加黑色节点,影响太大


8a25de096c1c4ea49df69e5badc63f10.png


若在红框中插入红色节点,则有可能违反规则3(存在两个连续的红色节点)

当前情况违反规则3


82d815c175cc4b7080041e5408a30d75.png


若插入红色节点后,父节点为黑色,则不违反规则3


所以默认节点为红色更利于去解决问题


4. insert

grandfather节点省略为g ,uncle节点省略为u ,parent节点省略为p,cur节点省略为c

情况1—— uncle节点存在且为红色(g p c左斜形成一条直线)

7c4f2e9576ab422b9553c30b595339e9.png


当插入红色节点后,与父节点形成连续的红色节点

把parent节点变成黑色,uncle节点置为黑色,并将grandfather节点置为红色

若grandfather节点的父节点为黑色,则不需要继续处理

若grandfather节点的父节点为红色,把当前的grandfather节点作为新增节点cur,继续向上调整


b3b14edf386a40cca6d649e5b2e0ae0a.png

情况2——uncle节点不存在/存在且为黑色(g p c 左斜形成直线 右单旋)

uncle节点不存在

9e7de8e5dc414026bcf2b414f7544942.png

当uncle节点不存在时,则cur作为新增节点,

因为红黑树也是一种二叉搜索树,因为左边高,所以进行右单旋

uncle节点存在并且为黑色

b77c7ab0e1734084aafd988fcf1a5e1d.png

首先进行变色,将新增节点的上面两个节点置为黑色,再将cur节点置为红色

同时需要进行右旋转


6edffabe61b346de8095c2973d741a1f.png

将c作为g的左子树,将g作为p的右子树

将g置为红色

将p置为黑色

f863cd05082343db8a18900411a15c26.png


RotateR/RotateL的实现,与AVL树的类似,只需把原来的代码的平衡因子去掉即可

不懂查看:AVL树的实现

情况3——uncle节点不存在/存在且为黑色(g p c 形成左折线 双旋)

因为 grandfather(g) parent( p) cur( c) 节点为一条折线,所以为双旋

uncle节点不存在

d1f4adfec8cd479da4e4575b62fe4ca3.png

作为这样的折线存在,所以要进行双旋,先对p进行右单旋,在对旋转后的根进行左单旋

uncle节点存在并且为黑色

74cff0f4756c48af8118275666b528fd.png

首先进行变色,将新增节点上面的两个节点由红色置为黑色

再将cur节点由黑色置为红色

7f628a218ad443349ef8b777a43138b6.png

在进行左单旋,将cur的左子树节点 作为p的右子树,将p作为cur的左子树

1c477486e5604ee3ab7530ab53a18305.png


进行右单旋,将cur的右子树节点作为g的左子树,将g作为cur的右子树

最终cur变为黑色,g变为红色


833325dc2cf24740b280853fe600265b.png

情况1—— uncle节点存在且为红色(g p c右斜形成一条直线)

0da36eb90a0049fd8dc28897e7565d78.png

当插入红色节点后,与父节点形成连续的红色节点

把parent节点变成黑色,uncle节点置为黑色,并将grandfather节点置为红色


83072e1b74ff476fa8a283f589791976.png


若grandfather节点的父节点为红色,把当前的grandfather节点作为新增节点cur,继续处理


与上述左斜形成直线的写法相同


情况2——uncle节点不存在/存在且为黑色(g p c 右斜形成直线 左单旋)

538b9e67be4a482e9c0a197b3d942f73.png

这里以节点不存在举例


此时的uncle节点处于NULL

将parent节点置为黑色,将grandfather节点置为红色

并进行旋转,将1作为6的左子树,将6作为8的左子树

相当于进行左单旋

4343d5d92667475fb9aa3830ee1d6211.png

情况3——uncle节点不存在/存在且为黑色(g p c 形成右折线 双旋)

acdac1f73ce945219b0263afa4f020ca.png

首先进行变色,将新增节点上面的两个节点由红色置为黑色

再将cur节点由黑色置为红色


555f5ef6c5924e52ac9cded8f884bf3f.png


在进行右单旋,将cur的右子树节点 作为p的左子树,将p作为cur的右子树


40102a4318b44cc992d8d04e46021110.png


进行左单旋,将cur的左子树节点作为g的右子树,将g作为cur的左子树

最终cur变为黑色,g变为红色

658b6969d6b14fc383fa63d112d345db.png

5.判断是否为红黑树

092d19326fe645fb90e565e4e85695a1.png

规则中要求根节点为黑色,所以当根为红色时就返回false


连续的红色节点

若当前节点为红时,检查儿子是否为红,但是儿子节点有可能为空

所以采用当前节点为红时,若父节点也为红,则返回false


使用blacknum用于记录每条路径的黑色节点个数

blacknum作为一个形参传值调用,下一层的++不会影响上一层

如果当前节点的颜色为黑色,则blacknum++


6. 整体代码

#pragma once
#include<iostream>
#include<cassert> 
using namespace std;
enum colour
{
  RED,//红色 默认为0
  BLACK,//黑色 默认为1
};
template<class K, class V>
struct  RBTreeNode
{
  RBTreeNode<K, V>* _left;
  RBTreeNode<K, V>* _right;
  RBTreeNode<K, V>* _parent;
  pair<K, V> _kv;//当前节点值
  colour _col;//表示颜色
  RBTreeNode(const pair<K, V>& kv)
    :_left(nullptr),
    _right(nullptr),
    _parent(nullptr),
    _kv(kv),
    _col(RED)
  {
  }
};
template<class K,class V>
class RBTree
{
  typedef RBTreeNode<K,V>  Node;
   public:
     bool insert(const pair<K, V>& kv)
     {
       if (_root == nullptr)
       {
         _root = new Node(kv);
         _root->_col = BLACK;//若刚插入一个节点,则该节点颜色是黑色
         return true;
       }
       Node* parent = nullptr;//用于记录cur的前一个节点
       Node* cur = _root;
       while (cur)
       {
         //若插入的值比当前树的值小 插入左边
         if (cur->_kv.first > kv.first)
         {
           parent = cur;
           cur = cur->_left;
         }
         //若插入的值比当前树的值大 插入右边
         else if (cur->_kv.first < kv.first)
         {
           parent = cur;
           cur = cur->_right;
         }
         else
         {
           //若插入的值,在树中有相同的值 ,则插入失败
           return false;
         }
       }
       cur = new Node(kv);
       //再次判断parent当前节点值与插入值大小
       if (parent->_kv.first > kv.first)
       {
         parent->_left = cur;
       }
       else
       {
         parent->_right = cur;
       }
       //cur的上一个节点即为 刚才的记录cur前一个节点的parent
       cur->_parent = parent;
           //当父节点不为NULL并且父节点为红色
       while (parent != nullptr && parent->_col == RED)
       {
         Node* grandfather = parent->_parent;//爷爷节点
         //若父节点为爷爷节点的左子树,则unlce为爷爷节点的右子树
         if (grandfather->_left == parent)
         {
           Node* uncle = grandfather->_right;
           //       g
           //    p     u
           // c
           //情况1:u存在并且为红,直接变色即可,并继续往上处理
           if (uncle && uncle->_col == RED)
             //若uncle节点为红色,将parent与uncle都置为黑色,爷爷节点置为红色
           {
             parent->_col = BLACK;
             uncle->_col = BLACK;
             grandfather->_col = RED;
             //继续往上调整
             cur=grandfather;
             parent = cur->_parent;
           }
           //情况2+3:u不存在或者u存在且为黑,旋转+变色
           else
           {   
             //情况2
             //g p c 作为一条直线 所以为单旋
             //    g
             //  p   u
             //c 
             if (cur == parent->_left)
             {
               //右旋转
               RotateR(grandfather);
               //最终p作为最终的根 为黑  g为红 
               parent->_col = BLACK;
               grandfather->_col = RED;
             }
             //情况3
             //g p c 作为一条折线 所以为双旋
             //    g
            //   p   u
            //     c 
             else
             {
               //左单旋
               RotateL(parent);
               //右单旋
               RotateR(grandfather);
               //最终cur作为最终的根 为黑  g为红 
               cur->_col = BLACK;
               grandfather->_col = RED;
               //父节点继续保持红色
               parent->_col = RED;
             }
             break; 
           }
         }
         else//grandfather->_right == parent
           若父节点为爷爷节点的右子树,则unlce为爷爷节点的左子树
         {
           //   g
           // u   p
           //       c
           Node* uncle = grandfather->_left;
           //情况1:u存在并且为红,直接变色即可,并继续往上处理
           if (uncle && uncle->_col == RED)
             //若uncle节点为红色,将parent与uncle都置为黑色,爷爷节点置为红色
           {
             parent->_col = BLACK;
             uncle->_col = BLACK;
             grandfather->_col = RED;
             //继续往上调整
             cur = grandfather;
             parent = cur->_parent;
           }
           //情况2+3:u不存在或者u存在且为黑,旋转+变色
           else
           {
             //情况2
             //g p c 作为一条直线 所以为单旋
             //    g
             //  u   p
             //        c  
             if (cur == parent->_right)
             {
               //左旋转 
               RotateL(grandfather);
               //最终p作为最终的根 为黑  g为红 
               parent->_col = BLACK;
               grandfather->_col = RED;
             }
             //情况3
             //g p c 作为一条折线 所以为双旋
             //   g
            //  u   p
            //    c 
             else
             {
               //右单旋
               RotateR(parent);
               //左单旋
               RotateL(grandfather);
               //最终cur作为最终的根 为黑  g为红 
               cur->_col = BLACK;
               grandfather->_col = RED;
             }
             break;
           }
         }
       }
       //为了避免grandfather节点正好为根时,会被更新成红色的情况
       _root->_col = BLACK;
       return true;
     }
     void inorder()//中序遍历
     {
       _inorder(_root);
       cout << endl;
     }
     //判断一颗二叉树是否为红黑树
     bool isbalance()
     {
       //检查根是否为黑
       if (_root && _root->_col == RED)
       {
         cout << "根节点颜色是红色" << endl;
         return false;
       }
       //连续的红色节点
       return _check(_root,0);
     }
  private:
    bool _check(Node* root,int blacknum)
    {
      if (root == nullptr)
      {
        //为空时,blacknum代表一条路径的黑色节点个数
        cout << blacknum << " ";
        return true;
        }
      //blacknum代表黑色节点的个数
      if (root->_col == BLACK)
      {
        blacknum++;
      }
      //若当前节点为红 父节点也为红
      if (root->_col == RED
        &&root->_parent 
        &&root->_parent->_col==RED)
      {
        cout << "存在连续的红色节点" << endl;
        return false;
      }
      //遍历整棵树
      return  _check(root->_left,blacknum) && _check(root->_right,blacknum);
    }
    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 != nullptr)
         {
           subRL->_parent = parent;
         }
         Node* ppnode = parent->_parent;//记录parent的前一个节点
         subR->_left = parent;
         parent->_parent = subR;
         if (ppnode == nullptr)//说明parent是根即代表整棵树
         {
           _root = subR;//subR作为新的根
           _root->_parent = nullptr;//subR的父节点指向原来的parent,所以要置nullptr
         }
         else//说明旋转的是一部分,所以要跟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;
         Node* subLR = subL->_right;
         parent->_left = subLR;
         if (subLR != nullptr)
         {
           subLR->_parent = parent;
         }
         Node* ppnode = parent->_parent;//记录parent的父节点
         subL->_right = parent;
         parent->_parent = subL;
         if (ppnode == nullptr)//若旋转整棵树
         {
           _root = subL;
           _root->_parent = nullptr;
         }
         else//若旋转整棵树的部分子树
         {
           if (ppnode->_left == parent)
           {
             ppnode->_left = subL;
           }
           else
           {
             ppnode->_right = subL;
           }
           subL->_parent = ppnode;//使subL的父节点为ppnode
         }
       }
  private:
    Node* _root = nullptr;
};
void test_RBTree1()
{
  int a[]={16, 3, 7, 11, 9, 26, 18, 14, 15};
  //int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16,14 };
  RBTree<int, int>v1;
  for (auto e : a)
  {
    v1.insert(make_pair(e, e));
  }
  v1.inorder();
  cout << v1.isbalance() << endl;
}

相关文章
|
2月前
|
程序员 编译器 C++
【C++核心】C++内存分区模型分析
这篇文章详细解释了C++程序执行时内存的四个区域:代码区、全局区、栈区和堆区,以及如何在这些区域中分配和释放内存。
51 2
|
9天前
|
Ubuntu Linux Shell
C++ 之 perf+火焰图分析与调试
【11月更文挑战第6天】在遇到一些内存异常的时候,经常这部分的代码是很难去进行分析的,最近了解到Perf这个神器,这里也展开介绍一下如何使用Perf以及如何去画火焰图。
|
1月前
|
存储 算法 搜索推荐
对二叉堆的简单分析,c和c++的简单实现
这篇文章提供了对二叉堆数据结构的简单分析,并展示了如何在C和C++中实现最小堆,包括初始化、插入元素、删除最小元素和打印堆的函数,以及一个示例程序来演示这些操作。
34 19
|
1月前
|
Ubuntu Linux Shell
C++ 之 perf+火焰图分析与调试
【10月更文挑战第8天】在遇到一些内存异常的时候,经常这部分的代码是很难去进行分析的,最近了解到Perf这个神器,这里也展开介绍一下如何使用Perf以及如何去画火焰图。
|
5月前
|
存储 程序员 编译器
C/C++堆栈详细分析,新老程序员必会
C/C++堆栈详细分析,新老程序员必会
176 1
|
5月前
|
存储 自然语言处理 安全
C++ STL标准库 《string原理与实战分析》
C++ STL标准库 《string原理与实战分析》
83 0
|
6月前
|
C++ 容器
【C++】红黑树模拟实现STL中的map与set
【C++】红黑树模拟实现STL中的map与set
|
2月前
|
Ubuntu Linux Shell
C++ 之 perf+火焰图分析与调试
简介 在遇到一些内存异常的时候,经常这部分的代码是很难去进行分析的,最近了解到Perf这个神器,这里也展开介绍一下如何使用Perf以及如何去画火焰图。 1. Perf 基础 1.1 Perf 简介 perf是Linux下的一款性能分析工具,能够进行函数级与指令级的热点查找。利用perf剖析程序性能时,需要指定当前测试的性能时间。性能事件是指在处理器或操作系统中发生的,可能影响到程序性能的硬件事件或软件事件 1.2 Perf的安装 ubuntu 18.04: sudo apt install linux-tools-common linux-tools-4.15.0-106-gen
|
3月前
|
关系型数据库 C++ 容器
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——红黑树
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——红黑树
37 0
|
4月前
|
Java C++ Python
【C++】手撕红黑树
【C++】手撕红黑树
26 1