【高阶数据结构】红黑树 {概念及性质;红黑树节点的定义;红黑树插入操作详细解释;红黑树的验证}

news2024/11/27 16:37:34

红黑树

一、红黑树的概念

红黑树(Red Black Tree) 是一种自平衡二叉查找树,在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

在这里插入图片描述

AVL树 VS 红黑树

  • 红黑树是一种特化的AVL树,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。

  • AVL树要求每棵子树的左右高度差不超过1,是严格平衡;而红黑树要求最长路径不超过最短路径的2倍,是接近平衡。

  • 而红黑树是一种AVL树的变体,它要求最长路径不超过最短路径的2倍,左右子树高差有可能大于 1。所以红黑树不是严格意义上的平衡二叉树(AVL),但对之进行平衡的代价较低, 其平均统计性能要强于 AVL

  • 相对而言,插入或删除同样的数据,AVL树旋转的更多,而红黑树则旋转的更少效率相对较高


二、红黑树的性质

红黑树是每个结点都带有颜色属性的二叉查找树,颜色或红色或黑色。 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  • 性质1. 结点是红色或黑色。

  • 性质2. 根结点是黑色。

  • 性质3. 每个红色结点的两个子结点都是黑色。(每条路径上不能有两个连续的红色结点)

  • 性质4. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。 (每条路径上的黑色节点数量相同)

  • 性质5. 所有NIL结点都是黑色的。(NIL节点即空结点)

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

是性质3导致路径上不能有两个连续的红色结点确保了这个结果。最短的可能路径都是黑色结点,最长的可能路径有交替的红色和黑色结点。因为根据性质4所有路径都有相同数目的黑色结点,这就表明了没有路径能多于任何其他路径的两倍长。

思考:新插入的节点应该设为黑色还是红色?

  • 如果将新插入的节点设为黑色,不管插到那条路径都必然违反性质4。

  • 如果将新插入的节点设为红色:如果父节点是红色则违反性质3,需要进行调整;如果父节点是黑色就正常插入,无需调整。

  • 对比两种情况,最终选择将新插入的节点设为红色。


三、红黑树节点的定义

enum Color{
  RED,
  BLACK
};

template <class K, class V>
struct RBTreeNode{
  RBTreeNode<K,V> *_left;
  RBTreeNode<K,V> *_right;
  RBTreeNode<K,V> *_parent;

  pair<K,V> _kv;
  Color _color; //颜色属性,红或黑
  RBTreeNode(const pair<K,V> &kv=pair<K,V>(), Color color = RED)
    :_left(nullptr),
    _right(nullptr),
    _parent(nullptr),
    _kv(kv),
    _color(color)
  {}
};

四、红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

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

  2. 检测新节点插入后,红黑树的性质是否造到破坏。因为新节点的默认颜色是红色,因此:

    • 如果新插入的节点是根节点,需要将节点变为黑色以满足性质2。
    • 如果父节点是黑色的,没有违反红黑树的任何性质,则不需要调整;
    • 但如果父节点颜色为红色时,就违反了性质3:路径上不能有两个连续的红色结点。此时需要对红黑树分情况来讨论:

在讲解情况三、四、五之前,先说明一下:

  • cur为当前节点(关注节点),p(parent)为父节点,g(grandparent)为祖父节点,u(uncle)为叔叔节点;
  • cur不一定就是新插入的节点,也有可能是因为 cur 的子树在调整的过程中将 cur 节点的颜色由黑色改成红色。

4.1 情况一:u存在且为红

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

抽象分析:

在这里插入图片描述

  1. 因为cur和p都为红色违反性质3,所以一定要把p变为黑色。
  2. 但只变p又违反性质4各路径上黑色节点的数量不同,所以要把u也变为黑色。
  3. 但原来所有路径上只有1个黑色节点(可见的)而现在变为2个。如果g树是子树,又会使整棵树违反性质4。所以要把g变为红色。
  4. g的父节点也可能是红色,所以要继续向上调整。

解决方式:变色并继续向上调整

  1. 将p,u都改为黑色,g改为红色;
  2. 如果g不为根,就把g当成cur继续向上调整;
  3. 如果g为根,就把g变为黑色。性质2:根节点是黑色的。

具体分析:

cur就是新插入的节点:

在这里插入图片描述

cur节点原来是黑色之后又被调整为红色:

在这里插入图片描述

注意:a,b,c,d,e可能是连续的几层黑色节点(要求每条路径的黑色节点数量相同),然后才出现上述情况。因为情况太多,过于复杂故作省略。


4.2 情况二:u不存在/u存在且为黑(单旋)

情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑(单旋)

抽象分析:
在这里插入图片描述

  1. 因为cur和p都为红色违反性质3,所以一定要把p变为黑色。
  2. 但只变p使左路黑节点+1违反性质4,因此还要以g为轴点右单旋,使左路黑节点-1。
  3. 但此时由于右单旋使右路黑节点+1,所以要将g变为红色,右路黑节点-1。最终满足性质4。

解决方式:单旋+变色

  1. 如果p为g的左孩子,cur为p的左孩子(左左),则对g进行右单旋;
  2. 如果p为g的右孩子,cur为p的右孩子(右右),则对g进行左单旋;
  3. p、g变色–p变黑色,g变红色。
  4. 完成旋转变色后每条路径的黑节点数量相同且与插入前也相同,并且根节点为黑色不需要继续往上处理。

具体分析:u 的情况有两种

uncle节点不存在:

如果 u 节点不存在,则 cur 一定是新插入节点,因为如果 cur 不是新插入节点,则 cur 和 p 一定有一个节点的颜色是黑色,就不满足性质4:每条路径黑色节点个数相同。

在这里插入图片描述

uncle节点存在且为黑色:

如果 u 节点存在且为黑色,那么 cur 节点原来的颜色也一定是黑色的,现在看到其是红色的原因是因为 cur 的子树在调整的过程中将 cur 节点的颜色由黑色改成红色。

在这里插入图片描述

注意:a,b,c,d,e可能是连续的几层黑色节点(要求每条路径的黑色节点数量相同),然后才出现上述情况。因为情况太多,过于复杂故作省略。


4.3 情况三:u不存在/u存在且为黑(双旋)

情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑(双旋)

抽象图:
在这里插入图片描述
情况三先以p为轴点左单旋,转换为情况二。

解决方式:双旋+变色

  1. p为g的左孩子,cur为p的右孩子(左右),则先对p做左单旋,再对g做右单旋;
  2. p为g的右孩子,cur为p的左孩子(右左),则先对p做右单旋,再对g做左单旋;
  3. cur、g变色–cur变黑色,g变红色。
  4. 完成旋转变色后每条路径的黑节点数量相同且与插入前也相同,并且根节点为黑色不需要继续往上处理。

具体分析:

uncle节点不存在

在这里插入图片描述

uncle节点存在且为黑色:

在这里插入图片描述

注意:a,b,c,d,e可能是连续的几层黑色节点(要求每条路径的黑色节点数量相同),然后才出现上述情况。因为情况太多,过于复杂故作省略。

总结:

  • 二叉树插入操作的难点在于通过变色和旋转操作恢复红黑树的性质,性质得到满足红黑树就能做到近似平衡:最长路径不超过最短路径的两倍。
  • 恢复的最终目的:1.关注子树满足红黑树的所有性质 2.插入前后关注子树每条路径的黑节点数量不变(保证整棵树的性质4)

4.4 实现代码

template <class K, class V>
bool RBTree<K,V>::Insert(const pair<K,V> &kv)
{
  //1.按照二叉搜索的树规则插入新节点
  if(_root == nullptr)
  {
    _root = new Node(kv, BLACK); //性质2:根节点是黑色的
    return true;
  }

  Node *cur = _root;
  Node *parent = nullptr;
  while(cur != nullptr)
  {
    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;
    }
  }
    
  cur = new Node(kv,RED); //新插入的节点是红色的
  if(kv.first > parent->_kv.first)
  {
    parent->_right = cur;
  }
  else{
    parent->_left = cur;
  }
  cur->_parent = parent;
  
  //2.检测新节点插入后,红黑树的性质是否造到破坏。
  //如果上一次循环中grandparent为根节点,此次循环parent == nullptr,结束调整。
  //如果其父节点的颜色是黑色,没有违反红黑树的任何性质,则不需要调整;
  while(parent != nullptr && parent->_color == RED) 
  {
    Node *grandparent = parent->_parent;
    //增加断言,是为了方便找出一般错误。
    assert(grandparent != nullptr); //因为父节点是红色的,所以祖父节点一定不为空,性质2
    assert(grandparent->_color == BLACK); //因为父节点是红色的,所以祖父节点一定是黑色,性质3

    Node *uncle = grandparent->_left;
    if(parent == grandparent->_left)
      uncle = grandparent->_right;

    if(uncle != nullptr && uncle->_color == RED) //情况一:uncle存在且为红
    {
      //p,u变黑,g变红,继续向上调整。
      parent->_color = uncle->_color = BLACK;
      grandparent->_color = RED;
      cur = grandparent;
      parent = cur->_parent;
    }
    else //情况二、三:uncle不存在或uncle存在且为黑
    {
      //需要进行旋转变色处理,先要判断旋转方式。
      if(parent == grandparent->_left)
      {
        if(cur == parent->_left) //左左
        {
          RotateR(grandparent);
          parent->_color = BLACK;
          grandparent->_color = RED;
        }
        else{ //左右
          RotateL(parent);
          RotateR(grandparent);
          cur->_color = BLACK;
          grandparent->_color = RED;
        }
      }
      else{
        if(cur == parent->_right) //右右
        {
          RotateL(grandparent);
          parent->_color = BLACK;
          grandparent->_color = RED;
        }
        else{ //右左
          RotateR(parent);
          RotateL(grandparent);
          cur->_color = BLACK;
          grandparent->_color = RED;
        }
      }
      break; //完成旋转变色后每条路径的黑节点数量相同且根节点为黑色不需要继续往上处理。
    } //end of else
  } //end of while
    
  //如果上一次循环中grandparent为根节点,循环结束后要将根节点再改为黑色,性质2。
  if(cur == _root)
    cur->_color = BLACK;

  return true;

}

五、红黑树的验证

红黑树的检测分为两步:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质
bool IsValidRBTree(){
  //空树也是红黑树
  if(_root == nullptr) return true;
  //检查性质2:
  if(_root->_color != BLACK)
  {
    cout << "违反性质2:根节点不为黑色!" << endl;
    return false;
  }
  //检查性质3,4:
  int benchmark = 0;
  return PrevCheck(_root, 0, benchmark);
}

//blacknum:用于记录当前路径的黑色节点个数,不能传引用。
//benchmark:用于记录第一条路径的黑色节点个数。需要传引用,返回给上层递归。
bool _IsValidRBTree(Node *root, int blacknum, int &benchmark){
  if(root == nullptr)
  {
    if(benchmark == 0) //表示第一条路径遍历完
    {
      benchmark = blacknum; //记录第一条路径的黑色节点个数
      return true;
    }
    else{
      if(blacknum != benchmark) //如果其他路径的blacknum与第一条路径不同,说明违反性质4
      {
        cout << "违反性质4:从任意节点到每个叶子节点的所有路径都包含相同数目的黑色节点!" << endl;
        return false;
      }
      else{
        return true;
      }
    }
  }
    
  //检查性质3:
  if(root->_color == RED && root->_parent->_color == RED)
  {
    cout << "违反性质3:路径上有两个连续的红色节点!" << endl;
    return false;
  }

  if(root->_color == BLACK)
  {
    ++blacknum; 
  }
  return PrevCheck(root->_left, blacknum, benchmark)
      && PrevCheck(root->_right, blacknum, benchmark);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/968749.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【多线程案例】生产者消费者模型(堵塞队列)

文章目录 1. 什么是堵塞队列&#xff1f;2. 堵塞队列的方法3. 生产者消费者模型4. 自己实现堵塞队列 1. 什么是堵塞队列&#xff1f; 堵塞队列也是队列&#xff0c;故遵循先进先出的原则。但堵塞队列是一种线程安全的数据结构&#xff0c;可以避免线程安全问题&#xff0c;当队…

数学建模--时间序列预测模型的七种经典算法的Python实现

目录 1.开篇版权提示 2.时间序列介绍 3.项目数据处理 4.项目数据划分可视化 5.时间预测序列经典算法1&#xff1a;朴素法 6.时间预测序列经典算法2&#xff1a; 简单平均法 7.时间预测序列经典算法3&#xff1a;移动平均法 8.时间预测序列经典算法4&#xff1a;简单指…

pytest自动化测试两种执行环境切换的解决方案

目录 一、痛点分析 方法一&#xff1a;Hook方法pytest_addoption注册命令行参数 1、Hook方法注解 2、使用方法 方法二&#xff1a;使用插件pytest-base-url进行命令行传参 一、痛点分析 在实际企业的项目中&#xff0c;自动化测试的代码往往需要在不同的环境中进行切换&am…

windows-nessus安装

1、下载 路径&#xff1a;Download Tenable Nessus | Tenable 2、获取active code 路径&#xff1a;Tenable Nessus Essentials Vulnerability Scanner | Tenable 3、安装 challenge code:上图马赛克位置 active code:获取active code第二张图片的马赛克位置 4、激活 5、安装…

Docker从认识到实践再到底层原理(三)|Docker在Centos7环境下的安装和配置

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

企业架构LNMP学习笔记10

1、Nginx版本&#xff0c;在实际的业务场景中&#xff0c;需要使用软件新版本的功能、特性。就需要对原有软件进行升级或重装系统。 Nginx的版本需要升级迭代。那么如何进行升级呢&#xff1f;线上服务器如何升级&#xff0c;我们选择稳定版本。 从nginx的1.14版本升级到ngin…

【数据库】MySQL基础知识全解

系列综述&#xff1a; &#x1f49e;目的&#xff1a;本系列是个人整理为了秋招面试的&#xff0c;整理期间苛求每个知识点&#xff0c;平衡理解简易度与深入程度。 &#x1f970;来源&#xff1a;材料主要源于拓跋阿秀、小林coding等大佬博客进行的&#xff0c;每个知识点的修…

计算机的存储规则(ASCII,GBK,Unicode)

不爱生姜不吃醋⭐️⭐️⭐️ 声明&#xff1a; &#x1f33b;本文写的是关于计算机的存储规则 ❗️ &#x1f33b;看完之后觉得不错的话麻烦动动小手点个赞赞吧&#x1f44d; &#x1f33b;如果本文有什么错误的话欢迎在评论区中指正哦&#x1f497; &#x1f33b;与其明天开始…

count(1)与count(*)的区别、ROUND函数

部分问题 1. count(1)与count(*)的区别2. ROUND函数3. SQL19 分组过滤练习题4. Mysql bigdecimal 与 float的区别5. 隐式内连接与显示内连接 &#xff08;INNER可省略&#xff09; 1. count(1)与count(*)的区别 COUNT(*)和COUNT(1)有什么区别&#xff1f; count(*)包括了所有…

【网络】路由配置实践1

网络实践-路由篇 本文使用vmware虚拟机进行路由表配置实践&#xff0c;通过配置路由表连接两个不同的网络&#xff0c;不涉及路由协议&#xff0c;全手动配置&#xff0c;旨在理解路由表的概念 网络规划&#xff1a; 准备三台centos7虚拟机&#xff0c;其中一台作为路由设备ro…

【个人博客系统网站】注册与登录 · 加盐加密验密算法 · 上传头像

【JavaEE】进阶 个人博客系统&#xff08;3&#xff09; 文章目录 【JavaEE】进阶 个人博客系统&#xff08;3&#xff09;1. 加盐加密验密算法原理1.1 md5加密1.2 md5验密1.3 md5缺漏1.4 加盐加密1.5 后端的盐值拼接约定1.6 代码实现1.6.1 加密1.6.2 验密1.6.3 测试 2. 博客…

探究IP路由的工作原理与路由表查找规则

文章目录 一、定义二、IP连通的前提三、路由表1. 作用2. 路由表字段内容3. 路由表查表规则4. 路由信息的来源5. 路由表写表规则6. 路由优先级 四、常用命令 首先可以看下思维导图&#xff0c;以便更好的理解接下来的内容。 一、定义 路由器是网络中负责将数据报文在不同IP网段…

css 左右宽固定,中间自适应——双飞翼布局

最近面试的时候遇到一个提问说&#xff0c;如何做到一个左右宽度固定&#xff0c;中间自适应的布局&#xff0c;我的答案不重要&#xff0c;重要的是不是面试官想听到的答案&#xff0c;这样问大概率他想听到的答案一定是双飞翼布局&#xff0c;所以今天就手敲一个双飞翼布局让…

设计模式-原则篇-01.开闭原则

简介 ​ 可以把设计模式理解为一套比较成熟并且成体系的建筑图纸&#xff0c;经过多次编码检验目前看来使用效果还不错的软件设计方案。适用的场景也比较广泛&#xff0c;在使用具体的设计模式之前先要学习软件设计的基础 “软件设计原则”&#xff0c;后面的23个设计模式都是…

Mybatis学习|多对一、一对多

有多个学生&#xff0c;没个学生都对应&#xff08;关联&#xff09;了一个老师&#xff0c;这叫&#xff08;多对一&#xff09; 对于每个老师而言&#xff0c;每个老师都有N个学生&#xff08;学生集合&#xff09;&#xff0c;这叫&#xff08;一对多&#xff09; 测试环境…

《TCP/IP网络编程》阅读笔记--Socket类型及协议设置

目录 1--协议的定义 2--Socket的创建 2-1--协议族&#xff08;Protocol Family&#xff09; 2-2--Socket类型&#xff08;Type&#xff09; 3--Linux下实现TCP Socket 3-1--服务器端 3-2--客户端 3-3--编译运行 4--Windows下实现 TCP Socket 4-1--TCP服务端 4-2--TC…

B. Consecutive Points Segment - 思维

分析&#xff1a; 思维还是不够发散&#xff0c;太容易一种方法走到死了&#xff0c;一直在模拟一直WA&#xff0c;看完题解发现一个数组的整段所有数组共同移动的距离最多只能是2&#xff0c;那么a[0]到a[n - 1]就是之间应该有多少个数&#xff0c;然后本来需要n个连续的数&am…

【vue2第十一章】v-model的原理详解 与 如何使用v-model对父子组件的value绑定 和修饰符.sync

v-model的原理详解 v-model的本质就是一个语法糖&#xff0c;实际上就是 :value"msg" 与 input"msg $event.target.value" 的简写。 :value"msg" 从数据单向绑定到input框&#xff0c;当data数据中的msg内容一旦改变&#xff0c;而input框数据…

yum 、rpm、yumdownloader、repotrack 学习笔记

1 Linux 包管理器概述 rpm的使用&#xff1a; rpm -ivh filename.rpm#这列出该packageName&#xff08;包名&#xff09;安装的所有文件列表。 rpm -ql packageName #查询已安装的该packageName的详细信息&#xff0c;包括版本、发布日期等。 rpm -qi packageName #列出该pac…

剑指 Offer 62. 圆圈中最后剩下的数字(简单)

题目&#xff1a; class Solution { public:int lastRemaining(int n, int m) {int pos 0;for(int i2;i<n;i){pos (posm)%i;}return pos;} };作者&#xff1a;想吃火锅的木易 链接&#xff1a;详细题解 来源&#xff1a;力扣&#xff08;LeetCode&#xff09;