首页 > 编程语言 >关联式数据结构_红黑树剖析 #C++

关联式数据结构_红黑树剖析 #C++

时间:2023-09-19 13:33:58浏览次数:42  
标签:node cur parent C++ 红黑树 数据结构 root 节点

红黑树的性质和定义

红黑树的性质

红黑树是一种平衡搜索二叉树。红黑树的每个节点存储了一个标记颜色的变量(红色或黑色),通过对任意一条从根到叶子结点的路径中节点着色方式的限制,使树的最长路径不超过最短路径的两倍,因而红黑树处于一种近似平衡的状态。与AVL树相比,红黑的平衡条件更宽松,旋转的次数更少,因此在频繁插入删除的情况下,红黑树的性能比AVL树好。

关联式数据结构_红黑树剖析 #C++_红黑树的判断

红黑树具有以下性质:

  1. 根(_root)节点是黑色的;
  2. 每个节点不是黑色就是红色;
  3. 如果一个节点是红色的,那么它的两个孩子结点和父亲节点一定是黑结点。即不存在两个连续的红节点
  4. 从每个节点到其所有叶子结点(nil节点)的路径中,黑色节点的数目相等;
  5. 每个叶子结点(nil)都是黑色的。

在红黑树中,叶子结点指的往往是空节点,逻辑上用nil进行标识,即认为空节点的颜色是黑色的

根据上面的几条性质,可以得知在一棵红黑树中,从某个节点出发的最短路径中一定全部都是黑色节点,而最长路径一定是一黑一红相间(性质c),再加上性质 d 的限制,可以证明红黑树的最长路径一定不超过最短路径的2倍

红黑树的节点定义

为了便于找到节点的父节点,红黑树使用三叉链的形式设计,每个节点存储了有效数据、左右孩子指针和父节点指针,除此之外,红黑树的节点还记录了颜色信息。

将节点的颜色初始化为红色,是为了在插入新节点后尽量保持红黑树的性质,这点将会在讨论插入操作时说明。

//用枚举记录颜色信息
enum Color
{
  RED,
  BLACK
};

template<typename T>
struct RB_tree_node
{
  typedef RB_tree_node<T> node;

  T _data; //数据信息
  node* _left;
  node* _right;
  node* _parent;
  Color _color; //颜色信息

  RB_tree_node(const T& data)
    :_data(data),
  _left(nullptr), _right(nullptr),
  _parent(nullptr),
  _color(RED) //将节点的颜色初始化为红色,是为了在插入新节点后尽量保持红黑树的性质
  { }
};

红黑树的定义

红黑树的结构定义与二叉搜索树类似。

/*为了同时兼容Key模型和Key-Value模型,设计红黑树时提供三个模板参数。
当T为pair<>类型时,红黑树存储的数据为Key-Value模型,
Key和KeyOfT仿函数针对Key-Value模型,
便于使用和拿取pair<>中的Key*/
template<typename Key, typename T, typename KeyOfT>
class RB_tree
{
private:
  typedef RB_tree_node<T> node;
  
  /*…………*/
  
private:
  node* _root;
};

KeyOfT是一个仿函数,针对Key模型或Key-Value模型分别拿取有效数据的Key值。

//针对Key模型(set)
struct SetKeyOfT
{
  const Key& operator()(const Key& key)
  {
    return key;
  }
};
//针对Key-Value模型(map)
struct MapKeyOfT
{
  //接受一个K-V键值对,返回kv.first
  const Key& operator()(const std::pair<const Key, Value>& kv)
  {
    return kv.first;
  }
};

红黑树的构造和析构

构造红黑树时,将_root初始化为nullptr,析构时进行后序递归遍历析构。

template<typename Key, typename T, typename KeyOfT>
class RB_tree
{
private:
    typedef RB_tree_node<T> node;

public:
    RB_tree()
        :_root(nullptr)
        { }

    /*…………*/
    
    ~RB_tree()
    {
        _destroy(_root);
    }

private:
    
    /*…………*/
    //后序遍历析构
    void _destroy(node* root)
    {
        if (root == nullptr) {
            return;
        }

        _destroy(root->_left);
        _destroy(root->_right);
        delete root;
        root = nullptr;
    }
    
    /*…………*/

private:
    node* _root;
};

红黑树的迭代器

涉及红黑树的迭代器时,需要考虑迭代器类型、前进和后退、解引用和成员访问操作。

红黑树的迭代器属于双向迭代器,不具备随机定位能力,与list类似。为了满足迭代器的使用需求,需要对节点指针进行封装。由于这里实现的红黑树没有头结点,所以简单以nullptr作为end迭代器。

//与list的迭代器类似,Ref与Ptr模板参数用来区分普通迭代器和const迭代器
template<typename T, typename Ref, typename Ptr>
class __RB_tree_iterator
{
private:
  typedef RB_tree_node<T> node;
  typedef __RB_tree_iterator<T, Ref, Ptr> Self;

public:
  __RB_tree_iterator(node* pnode)
    :_node(pnode)
    { }

  /*…………*/

  node* _node; //对节点指针进行封装
};

template<typename Key, typename T, typename KeyOfT>
class RB_tree
{
private:
  typedef RB_tree_node<T> node;

public:
  typedef __RB_tree_iterator<T, T&, T*> iterator;
  typedef __RB_tree_iterator<T, const T&, const T*> const_iterator;

public:
  RB_tree()
    :_root(nullptr)
    { }

  //begin迭代器指向树中的最小节点,即最左节点
  iterator begin()
  {
    node* leftMost = _root;
    while (leftMost && leftMost->_left) {
      leftMost = leftMost->_left;
    }
    return leftMost;
  }

  const_iterator begin() const
  {
    node* leftMost = _root;
    while (leftMost && leftMost->_left) {
      leftMost = leftMost->_left;
    }
    return leftMost;
  }
	//简单以nullptr作为end迭代器
  iterator end()
  {
    return nullptr;
  }

  const_iterator end() const
  {
    return nullptr;
  }
  /*…………*/

迭代器的解引用和元素访问接口直接返树节点的有效数据即可。

Ref operator*() const
{
  return _node->_data;
}

Ptr operator->() const
{
  return &(_node->_data);
}

红黑树迭代器的前进操作其实走的是一个非递归中序遍历二叉树的过程,以迭代器遍历红黑树,得到的是一个升序序列。在前进的过程中,根据中序遍历顺序 “左子树 根 右子树” 分情况对节点指针进行调整即可。

Self& operator++()
{
  node* cur = _node; //将当前节点视为“根”,优先向右子树走
  node* parent = cur->_parent;

  //当前节点的右子树不为空,寻找右子树的最小节点(最左节点)
  if (cur->_right)
  {
    node* subLeft = cur->_right;
    while (subLeft->_left) {
      subLeft = subLeft->_left;
    }
    _node = subLeft;
  }
  else //右子树为空,说明所在子树的左子树、根、右子树都被走完,向上层的根走
  {
    while (parent)
    {
      //此时cur处于parent的左子树,parent为“上层的根”,即_node的下一个位置
      if (cur == parent->_left) {
        break;
      }
      else //继续寻找上层的根
      {
        cur = cur->_parent;
        parent = parent->_parent;
      }
    }
    _node = parent; //当parent最终为空时,说明整棵树被走完,返回nullptr作为end迭代器
  }
  return *this;
}

红黑树的后退操作走的是中序遍历的逆过程,即“右子树 根 左子树”,其他细节与前进操作类似。

Self& operator--()
{
  node* cur = _node; //视当前节点为根,向左子树寻找
  node* parent = cur->_parent;
  if (cur->_left) //左子树不为空
  {
    //向左寻找最大的节点(最右节点)
    node* subRight = cur->_left;
    while (subRight->_right) {
      subRight = subRight->_right;
    }
    _node = subRight;
  }
  else //左子树为空,说明当前子树的右子树、根、左子树都被走完,向上层的根走
  {
    while (parent && cur == parent->_left)
    {
      //cur为parent的右子树,将_node直接调整为parent即可
      if (cur == parent->_right) {
        break;
      }
      else
      {
        cur = cur->_parent;
        parent = parent->_parent;
      }
    }
    _node = parent;
  }
  return *this;
}

比较迭代器时只需要比较对应的_node即可。

bool operator==(const Self& it) const
{
  return _node == it._node;
}

bool operator!=(const Self& it) const
{
  return _node != it._node;
}

红黑树的元素操作

元素查找

查找元素时,以Key按照二叉搜索树的性质查找即可。

/*…………*/
iterator find(const Key& k) const
{
  node* cur = _root;
  KeyOfT kot; //实例化出一个仿函数对象
  //按照二叉搜索树的性质进行遍历查找
  while (cur)
  {
    if (k < kot(cur->_data)) {
      cur = cur->_left;
    }
    else if (k > kot(cur->_data)) {
      cur = cur->_right;
    }
    else {
      return cur;
    }
  }
  return nullptr;
}
/*…………*/

插入操作

红黑树的插入操作分为两步:插入新节点,判断和调整平衡。插入新节点的过程与二叉搜索树的插入过程相同,按照二叉搜索树的性质找到合适的插入位置并插入即可。当插入节点后,如果树节点的着色情况破坏了红黑树的性质,则需要对树进行调整

由性质d,如果插入的新节点的颜色是黑色,那么插入后,从根节点到该新节点的黑色节点数量就比其他路径多 1,使整个红黑树被破坏,所以为了在插入后尽量保持红黑树的性质,将新节点的颜色设置为红色。

std::pair<iterator, bool> insert(const T& data)
{
  node* newNode = new node(data);
  if (_root == nullptr)
  {
    _root = newNode;
    _root->_color = BLACK;
    return std::make_pair(_root, true);
  }
  KeyOfT kot;
  node* cur = _root;
  node* parent = nullptr;
  //寻找合适的插入位置
  while (cur)
  {
    if ((kot(data)) < (kot((cur->_data))))
    {
      parent = cur;
      cur = cur->_left;
    }
    else if ((kot(data)) > (kot((cur->_data))))
    {
      parent = cur;
      cur = cur->_right;
    }
    else {
      return std::make_pair(cur, false);
    }
  }
  //进行比较和节点链接
  if (kot(data) < kot(parent->_data)) {
    parent->_left = newNode;
  }
  else {
    parent->_right = newNode;
  }
  newNode->_parent = parent;
  
  /*…………*/

红黑树的平衡调整

将新的红色节点插入到树中之后,如果新节点的父亲为红色,则需要对树进行调整。为了方便描述,假设新节点(当前调整的节点)为C,新节点的父亲节点为P,祖父节点为G,叔叔节点为U,下面大体分两种情况自下而上对树进行调整。

  • U节点存在(不为空)且为红色将 P 和 U 置为黑色,同时为了保证路径中黑色节点数目相等,将G置为红色。此时以G为根的子树已是一颗红黑树,继续向上判断和调整,不结束。

关联式数据结构_红黑树剖析 #C++_红黑树的调整_02

  • U节点存在且为黑色,或者U不存在(为空)。此时需要以G为根对树进行旋转和变色,具体分为以下几种情况:
  • P为G的左,且C为P的左,此时进行右单旋;

关联式数据结构_红黑树剖析 #C++_迭代器_03

  • P为G的左,且C为P的右,此时进行右双旋;

关联式数据结构_红黑树剖析 #C++_红黑树的判断_04

  • P为G的右,且C为P的右,此时进行左单旋;
  • P为G的右,且C为P的左,此时进行左双旋

完成旋转后,需要对树进行颜色调整,调整原则为,将旋转后的新根置为黑色,将新根的两个孩子节点置为红色,结束

/*…………*/

//调整平衡
cur = newNode;
parent = cur->_parent;
while (parent && parent->_color == RED)
{
  node* grandFather = parent->_parent;
  if (parent == grandFather->_left)
  {
    node* uncle = grandFather->_right;
    //U存在且为红色
    if (uncle && uncle->_color == RED)
    {
      parent->_color = uncle->_color = BLACK;
      grandFather->_color = RED;
      //继续向上检查调整,直到parent的颜色为红色
      cur = grandFather;
      parent = cur->_parent;
    }
    else //uncle存在且为黑 或者 uncle不存在
    {
      if (cur == parent->_left)
      {
        RotateRight(grandFather); //右单旋
        parent->_color = BLACK; //颜色调整
        grandFather->_color = RED;
      }
      else if (cur == parent->_right)
      {
        //右双旋
        RotateLeft(parent);
        RotateRight(grandFather);
        cur->_color = BLACK;
        grandFather->_color = RED;
      }
      else {
        assert(false);
      }
      break; //旋转后结束,停止调整
    }
  }
  else //parent == grandFather->_right
  {
    node* uncle = grandFather->_left;
    //U存在且为红色
    if (uncle && uncle->_color == RED)
    {
      parent->_color = uncle->_color = BLACK;
      grandFather->_color = RED;
      //继续向上调整
      cur = grandFather;
      parent = cur->_parent;
    }
    else //U存在且为黑色 或者 U不存在
    {
      if (cur == parent->_right)
      {
        RotateLeft(grandFather); //左单旋
        parent->_color = BLACK; //颜色调整
        grandFather->_color = RED;
      }
      else if (cur == parent->_left)
      {
        //左双旋
        RotateRight(parent); 
        RotateLeft(grandFather);
        cur->_color = BLACK;
        grandFather->_color = RED;
      }
      else {
        assert(false);
      }
      break; //旋转后结束判断
    }
  }
}
_root->_color = BLACK; //保持_root的颜色为黑色

/*…………*/

红黑树的合法性检验

为了方便测试,这里给出一个检验红黑树是否合法的接口,根据红黑树的性质对树进行检查。

/*…………*/  

	bool is_RB_tree() const
  {
    //_root是黑色的
    //一个红色节点的父亲和两个孩子一定是黑色节点
    //从任意节点开始的路径中黑色节点数量相等
    if (_root->_color == RED) {
      return false; //检查根节点的颜色
    }

    node* cur = _root;
    int controlValue = 0;
    //以最左路径中的黑色节点数量作为基准值,检查其他路径中的黑色节点数目
    //是否与该基准值相等
    while (cur)
    {
      if (cur->_color == BLACK) {
        ++controlValue;
      }
      cur = cur->_left;
    }
    int blackNum = 0;
    //递归检查树的着色情况
    return _is_RB_tree(_root, controlValue, blackNum);
  }

private:
  bool _is_RB_tree(node* root, int controlValue, int blackNum)
  {
    //认为空树平衡
    if (root == nullptr) {
      return true;
    }
		//存在两个连续的红色节点,false
    if (root->_color == RED && root->_parent->_color == RED) {
      return false;
    }
		//黑色节点数量 +1
    if (root->_color == BLACK) {
      ++blackNum;
    }
		//判断该条路径中的黑色节点数量
    if (root->_left == nullptr && root->_right == nullptr && blackNum != controlValue) {
      return false;
    }
		//递归判断左右子树
    return _is_RB_tree(root->_left, controlValue, blackNum) && 
      _is_RB_tree(root->_right, controlValue, blackNum);
  }

	/*…………*/

标签:node,cur,parent,C++,红黑树,数据结构,root,节点
From: https://blog.51cto.com/158SHI/7524374

相关文章

  • 线程劫持-进程注入C++示例和检测思考
    线程劫持:运行方法C:\Users\l00379637\source\repos\thread_hijack\x64\Release\thread_hijack.exe18132C:\Users\l00379637\source\repos\injected_dll\x64\Release\injected_dll.dllProcessID:18132Injected!劫持效果: 劫持代码如下:#include<iostream......
  • C++序列式容器
    需要注意的是,序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器:array<T,N>(数组容器):表示可以存储 N个T类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;vector<T>......
  • C++匿名对象生存期
    classSome{intn;public:Some(ints){n=s;}~Some(){cout<<"destroy\n";}intret(){returnn;}};intmain(intargc,char*argv[]){cout<<Some(111).ret()<<"\n";cout<<"wait......
  • Java并发Map的面试指南:线程安全数据结构的奥秘
    简介在计算机软件开发的世界里,多线程编程是一个重要且令人兴奋的领域。然而,与其引人入胜的潜力相伴而来的是复杂性和挑战,其中之一就是处理共享数据。当多个线程同时访问和修改共享数据时,很容易出现各种问题,如竞态条件和数据不一致性。本文将探讨如何在Java中有效地应对这些挑战,介......
  • Java并发Map的面试指南:线程安全数据结构的奥秘
    简介在计算机软件开发的世界里,多线程编程是一个重要且令人兴奋的领域。然而,与其引人入胜的潜力相伴而来的是复杂性和挑战,其中之一就是处理共享数据。当多个线程同时访问和修改共享数据时,很容易出现各种问题,如竞态条件和数据不一致性。本文将探讨如何在Java中有效地应对这些挑战,......
  • 个人项目——C++实现论文查重(简易版)
    本次项目GitHub地址:https://github.com/Focuspresent/Paper_Review这个作业属于哪个课程https://edu.cnblogs.com/campus/jmu/ComputerScience21这个作业的要求https://edu.cnblogs.com/campus/jmu/ComputerScience21/homework/13034这个作业的目标完成一次编程练......
  • C++系列十:日常学习-Lambda表达式
    目录前言必备理论知识:例子:前言有C#经验,使用起来,驾轻就熟。就是语法糖。但是也要熟悉用法,才好众享丝滑。内容参考:Chatjpt、文心一言必备理论知识:捕获列表:[]:默认不捕获任何变量;[=]:默认以值捕获所有变量;内部有一个相应的副本[&]:默认以引用捕获所有变量;[x]:仅以值捕获x,其它......
  • C++中的深拷贝和浅拷贝介绍
    对于基本类型的数据以及简单的对象,它们之间的拷贝非常简单,就是按位复制内存。例如:classBase{public:Base():m_a(0),m_b(0){}Base(inta,intb):m_a(a),m_b(b){}private:intm_a;intm_b;};intmain(){in......
  • C/C++中结构体占用内存大小的计算方法
    两个值:对齐系数:一般为8个字节。#pragmapack(8)设置对齐系数为8。有效对齐值:假设结构体中最长的类型的长度为len,则有效对齐值=min(len,对齐系数)。计算规则:计算存放的位置:第一个成员放在位置0,后面的成员A存放的时候,会先计算size=min(A大小,有效对齐值),A只放在size的整数倍......
  • C++ explicit
    C++explicitexplicit关键字有两个用途:指定构造函数或者转换函数(C++11起)为显示,即它不用用于隐式转换和赋值初始化。可以与常量表达式一同使用。当该表达式为true才为显示转换(C++20起)。1.将构造函数标记为显式C++中的explicit关键字通常用来将构造函数标记为显式类型转换,......