[数据结构]红黑树的原理及其实现

文章目录

  • 红黑树的特性
  • 红黑树的时间复杂度
    • 推导:
    • 结论
    • 红黑树与AVL树比较
  • 红黑树的插入
    • 红黑树的节点定义
    • 调整策略
      • 思考情况2:
      • 思考情况3:
  • 代码实现
    • myBTRee.h
    • test.cpp

红黑树的特性

红黑树最常用的平衡二叉搜索树。跟AVL树不同的是,红黑树是依靠节点的颜色来维护平衡的。虽然任意节点不具备严格平衡,但是数据的查找、插入、删除等操作效率依旧出色。下面是红黑树的一些特性:

  1. 每个节点的颜色要么是红色要么是黑色
  2. 根节点的颜色是黑色
  3. 如果一个节点的颜色是红色的,那么它的两个孩子节点的颜色一定是黑色的
  4. 任意节点到其所能到达的叶子节点之间的黑色节点的数量相同
  5. 叶节点是黑色的空节点

根据以上红黑树的特性,我们可以总结出以下结论:

结论:红黑树中最长路径的节点数量不超过最短路径节点数量的2倍
证明:假设每条路径黑色节点的数量为n(假设不包括空叶子节点),则红色节点的数量最多是n。任意路径节点数量最少为n(只有黑节点),最多为2*n

这样一来,任意一条路径的长度之差都保证在了一个有限的范围内,这也是红黑树具有平衡性的原因。

红黑树的时间复杂度

由于红黑树底层还是一颗二叉搜索树,根据二叉搜索树的特性:查找的效率取决于树的高度

推导:

现假设红黑树的某一路径黑色节点数量为bh。由于任意路径黑色节点数目相同,我们可以把路径上所有的红色节点删除,将删除节点的父节点和其子节点相连,于是我们得到了一颗纯黑色节点的树(四叉树),如下图:
在这里插入图片描述
(该图截自b站动画讲编程)
这颗黑树的高度显然就是bh,且我们可以得到之前红黑树的节点数最少为2^bh-1(最少就是全是黑色)。假设这棵红黑树的节点总数为N,则N>=2^bh-1,两边取对数得bh<=log(N+1)(以2为底)。

设h为红黑树得最大高度,则有h=2*hb=2log(N+1)(最长路径节点数是2*bh)

结论

红黑树的高度最大为2log(N+1),N表示树的节点总数。这个证明基于红黑树的性质。
得到了节点数与树的高度的关系,我们也就能得到红黑树查找数据的效率为O(logN)。此外,红黑树的插入效率和删除效率取决于查找效率,也是O(logN)。

红黑树与AVL树比较

相对来说,红黑树的内部实现细节没有AVL树这么复杂,虽然AVL树保证具备严格平衡性,但是其插入元素时调整平衡的操作相对也就繁琐。插入和删除的时候,AVL树可能会进行更多的旋转操作来维持平衡,导致实际的执行时间可能高于红黑树

总的来说,红黑树适用于读写均衡的场景,插入和删除操作较为高效。而AVL树适用于查询频繁的场景,查询效率更高,但是插入和删除的维护成本较高。即使是这样,两者的差别依旧不大,查找、删除、插入的时间复杂度都是OlogN。

红黑树的插入

上面我们已经了解了红黑树的特性,接下来谈谈红黑树插入数据时是如何维护上述特性的。

红黑树的节点定义

与AVL实现类似,红黑树的节点依旧有三个指针分别指向父节点、左孩子节点、右孩子节点。且有一个变量表示当前节点的颜色。初始默认是红色。为什么默认设置为红色呢?这是因为插入节点时,新节点一定与空叶子节点相邻。而根据红黑树的性质,空叶子节点的颜色是黑色,这就使得,如果新插入节点是黑色,那么每插入一次这条路径就一定会多出来一个黑色节点,即一定要调整。虽然新节点是红色也可能会需要调整,但影响会少很多。

template<class K,class V>
struct RBTreeNode {RBTreeNode<T, V>* _parent;RBTreeNode<T, V>* _left;RBTreeNode<T, V>* _right;pair<K, V> _kv;//键值对Colour _col;//颜色RBTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr),_left(nullptr),_right(nullptr),_col(RED){}
};

调整策略

如果新插入一个节点,破坏了红黑树的性质,那么我们需要进行调整。具体需要调整的情况如下:

  1. 如果插入节点是根节点:那么只需要将根节点变黑
  2. 插入节点的叔叔节点是红色:父亲节点和叔叔节点变成黑色,爷爷变成红色,且爷爷节点变成新插入节点
  3. 插入节点的叔叔节点是黑色(为空也是黑色):需要先旋转,然后变色

上述2,3情况一定是出现两个相邻节点是红色。

思考情况2:

为什么我们要把父亲节点和叔叔节点变成黑色,爷爷变成红色,且爷爷节点变成新插入节点?为什么这样做一定能维护红黑树的性质呢?
在这里插入图片描述

  • 首先,把父亲节点变黑是因为相邻两个节点是红色,那为什么要把叔叔节点也变色呢
    这是因为新节点插入的这条路径可能会多一个黑色节点。为了让所有路径黑色节点数都能加一,每次调节父节点颜色的同时,也要调节叔叔节点颜色。这样一来,即使最后新节点插入的这条路径黑色节点数加一,其它所有路径的黑色节点也能同步加一。

  • 其实将爷爷节点变色也是为了维护所有路径黑色节点数一致这一特性
    设想我们不将爷爷节点变成红色,此时整个红黑树确实没有出现“相邻的红色节点”,但是经过爷爷节点的路径的黑色节点数目就会比其它路径黑色节点数目多(parent和uncle都是变黑了)。除非此时爷爷节点是根节点(所有路径都经过根节点)。所以我们选择继续向上调整,把爷爷节点变成红色,并更新cur指针指向grandparent节点,看是否还会出现“红红”的俩节点。把爷爷节点变红是基于贪心思路:先不让路径的黑色节点数加一试试能不能平衡

在这里插入图片描述

什么时候停止调整呢?父节点的颜色是黑色为止。此时红黑树平衡

思考情况3:

如果叔叔节点本来就是黑色的呢?(空节点也是黑色)此时无论如何调节叔叔节点颜色,都有可能使得其它路径(比如叔叔这条路径)黑色节点数少于当前新节点插入路径的黑色节点数。此时考虑旋转。旋转的方式和AVL树是一致的。
旋转一共有四种方式:左旋、右旋、左右双旋、右左双旋。
细分情况3,一共有以下四种情况:
1.curparent的左孩子,parentgrandparent的左孩子。
在这里插入图片描述
将grandparent节点右旋转,旋转之后parent顶替了原来的grandparent,并变成黑色。grandparent成为parent的右孩子,变成红色
在这里插入图片描述
旋转是不会改变二叉搜索树的特性的。那么思考这样一个问题,旋转之后还是平衡的红黑树吗?答案是–是的。因为我们观察旋转之后经过parent的所有路径的黑色节点数压根就没有变。

2.curparent的右孩子,parentgrandparent的右孩子
这种情况和情况1一样,只不过方向相反,是左旋。不做过多徐述。
在这里插入图片描述
3.curparent孩子,parentgrandparent孩子

此时这种情况对grandparent节点进行左单旋还是右单旋都不能保证红黑树平衡,于是考虑双旋,即旋转两次。
在这里插入图片描述
先对parent节点左旋,发现左旋之后就变成了情况1
在这里插入图片描述
此时我们就可以像情况1一样,对grandparent进行右旋,对cur节点和grandparent节点进行变色
在这里插入图片描述
这样红黑树就平衡了,整个过程并没有增加路径的黑色节点的数量,因此也就不需要继续向上调整。

  1. curparent孩子,parentgrandparent孩子
    思路跟情况3一致,只不过旋转方向相反。向对parent节点进行右旋,再对grandparent节点进行左旋,再对cur和grandparent节点进行变色。

在这里插入图片描述

代码实现

myBTRee.h

封装了红黑树的类模板,包括各种功能的声明于定义

#pragma once#include<vector>
#include<iostream>
using namespace std;enum Colour{RED,BLACK
};template<class K,class V>
struct RBTreeNode {RBTreeNode<K, V>* _parent;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;pair<K, V> _kv;//键值对Colour _col;//颜色RBTreeNode(const pair<K, V>& kv):_kv(kv),_parent(nullptr),_left(nullptr),_right(nullptr),_col(RED){}
};template<class K,class V>
class RBTree {typedef RBTreeNode<K, V> Node;typedef Node* pNode;
public:bool Insert(const pair<K, V>& kv) {if (_root == nullptr) {_root = new Node(kv);_root->_col = BLACK;return true;}//找到一个合适的插入位置pNode cur = _root;pNode parent = nullptr;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为nullptrcur = new Node(kv);if (cur->_kv.first < parent->_kv.first) {//插入节点parent->_left = cur;cur->_parent = parent;}else {parent->_right = cur;cur->_parent = parent;}//插入之后需要调节颜色平衡while (parent && parent->_col == RED) {//找叔叔节点pNode grandparent = parent->_parent;//祖父节点一定不为空,因为父节点为红色if (parent == grandparent->_left) {//如果叔叔在右边pNode uncle = grandparent->_right;//叔叔存在且为红,直接都变成黑色if (uncle && uncle->_col == RED) {parent->_col = BLACK;uncle->_col = BLACK;grandparent->_col = RED;//继续往上处理cur = grandparent;//cur往上跳两个节点parent = cur->_parent;}//叔叔节点不存在或者为黑色else {if (cur == parent->_left) {RotateR(grandparent);parent->_col = BLACK;grandparent->_col = RED;}else if (cur == parent->_right) {RotateL(parent);RotateR(grandparent);cur->_col = BLACK;grandparent->_col = RED;}break;}}//如果叔叔在左边if (parent == grandparent->_right) {pNode uncle = grandparent->_left;//叔叔存在且为红,直接都变成黑色if (uncle && uncle->_col == RED) {parent->_col = BLACK;uncle->_col = BLACK;//继续往上处理grandparent->_col = RED;cur = grandparent;//cur往上跳两个节点parent = cur->_parent;}//叔叔节点不存在或者为黑色else {if (cur == parent->_left) {RotateR(parent);RotateL(grandparent);cur->_col = BLACK;grandparent->_col= RED;}else if (cur == parent->_right) {RotateL(grandparent);parent->_col = BLACK;grandparent->_col = RED;}break;}}}_root->_col = BLACK;return true;}void RotateR(Node* parent)//右旋{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;subL->_right = parent;Node* ppNode = parent->_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;}}void RotateL(Node* parent)//左旋{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;Node* ppNode = parent->_parent;parent->_parent = subR;if (parent == _root){_root = subR;_root->_parent= nullptr;}else{if (ppNode->_right == parent){ppNode->_right = subR;}else{ppNode->_left = subR;}subR->_parent = ppNode;}}bool IsBalance() {if (_root->_col == RED)return false;int targetnum = 0;pNode cur = _root;while (cur) {if (cur->_col == BLACK)targetnum++;cur = cur->_left;}return _Check(_root, targetnum, 0);}void InOrder() {_InOrder(_root);}
private:bool _Check(pNode root,int targetnum,int blacknum) {if (root == nullptr) {if (blacknum != targetnum) {//路径的黑色节点数不相等return false;}else return true;}if (root->_left && root->_col == RED && root->_left->_col == RED)return false;if (root->_right && root->_col == RED && root->_right->_col == RED)return false;if (root->_col == BLACK)blacknum++;return _Check(root->_left,targetnum,blacknum) && _Check(root->_right,targetnum,blacknum);}void _InOrder(pNode root) {if (root == nullptr) {return;}_InOrder(root->_left);cout << root->_kv.first << " " << root->_kv.second << endl;_InOrder(root->_right);}pNode _root=nullptr;};

test.cpp

用于测试代码的正确性。给出一组随机数,插入到红黑树中之后进行平衡检查。平衡检查内容为,检查是否出现连个相邻的红色节点,且所有路径的黑节点·数目是否相等。
代码:

void TestRBTree2()
{const int N = 100000;vector<int> v;v.reserve(N);srand((unsigned int)time(0));for (size_t i = 0; i < N; i++){v.push_back(rand() + i);//cout << v.back() << endl;}size_t begin2 = clock();RBTree<int, int> t;for (auto e : v){t.Insert(make_pair(e, e));}size_t end2 = clock();cout << t.IsBalance() << endl;
}

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Blender雕刻建模_笔画,镜像,动态拓扑

笔画 笔画选项&#xff0c;一般是对刷子&#xff08;自由线&#xff09;工具设置 描边方法如下&#xff1a;标红的为常用 -间隔&#xff1a;按一定间隔应用笔画的结果 例如&#xff1a;笔刷半径50&#xff0c;笔画间隔100%&#xff08;笔刷直径的百分比&#xff09;&#x…

C++ 中重写重载和隐藏的区别

重写&#xff08;override&#xff09;、重载&#xff08;overload&#xff09;和隐藏&#xff08;overwrite&#xff09;在C中是3个完全不同的概念。我们这里对其进行详细的说明 1、重写&#xff08;override&#xff09;是指派生类覆盖了基类的虚函数&#xff0c;这里的覆盖必…

GD32F103RCT6/GD32F303RCT6-UCOSIII底层移植(1)工程建立

本文章基于兆易创新GD32 MCU所提供的2.2.4版本库函数开发 后续项目主要在下面该专栏中发布&#xff1a; 手把手教你嵌入式国产化_不及你的温柔的博客-CSDN博客 感兴趣的点个关注收藏一下吧! 电机驱动开发可以跳转&#xff1a; 手把手教你嵌入式国产化-实战项目-无刷电机驱动&am…

OpenAI推出旗舰AI模型GPT-4o并免费开放

&#x1f989; AI新闻 &#x1f680; OpenAI推出旗舰AI模型GPT-4o并免费开放 摘要&#xff1a; OpenAI 未来的产品将以免费为优先&#xff0c;以让更多人使用为目标。OpenAI 发布了桌面版本的程序和更新后的 UI&#xff0c;更加简单自然。推出了新一代大模型 GPT-4o&#xf…

Qt自定义QpushButton分别在c++/python中实现

//.h文件#ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QPainter> #include<QMouseEvent> #include<QPropertyAnimation> #include<QResizeEvent>QT_BEGIN_NAMESPACE namespace Ui { class Widget; }class Widget : public QWi…

React Native 开发心得分享

有一段时间没更新了&#xff0c;花了点时间研究了下 React Native&#xff08;后续用 RN 简称&#xff09;&#xff0c;同时也用该技术作为我的毕设项目(一个校园社交应用&#xff0c;仿小红书)&#xff0c;经过了这段时间的疯狂折腾&#xff0c;对 RN 生态有了一定的了解&…

C语言/数据结构——栈的实现

一.前言 今天我们讲解新的领域——栈。 二.正文 1.栈 1.1栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其允许在固定的一段进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#…

【漏洞复现】Secnet-智能路由系统 actpt_5g.data信息泄露

0x01 产品简介 Secnet安网智能AC管理系统是广州安网通信技术有限公司(简称“安网通信”)的无线AP管理系统 0x02 漏洞描述 Secnet智能路由系统 acipt 5g.data 接口存在信息泄露漏洞&#xff0c;未经身份验证的远程攻击者可以利用此漏洞获取系统账户名密码等重要凭据&#xff…

OFDM 802.11a的FPGA实现(十六)长训练序列:LTS(含Matlab和verilog代码)

目录 1.前言2.原理3.Matlab生成长训练序列4.硬件实现5.ModelSim仿真6.和Matlab仿真结果对比 原文链接&#xff08;相关文章合集&#xff09;&#xff1a; OFDM 802.11a的xilinx FPGA实现 1.前言 在之前已经完成了data域数据的处理&#xff0c;在构建整个802.11a OFDM数据帧的时…

如何启用WooCommerce商城快捷结帐:3 种简单方法

使用WooCommerce商城快捷结帐可帮助您提高商店的转化率。 70%的顾客同意在线商店的快速结账流程会鼓励他们完成购买。 在结账过程中您让购物者完成的步骤越多&#xff0c;他们完成该流程的可能性就越小。 解决方案是什么&#xff1f; 通过跳过默认的WooCommerce商城购物车页…

从RTTR谈Reflection机制

虽然C11引入了RTTI、Metaprogramming 等技术&#xff0c;但C在Reflection编程方面依旧功能有限。在社区上&#xff0c;RTTR则提供了一套C编写的反射库&#xff0c;补充了C在Reflection方面的缺陷。 零、环境 操作系统Windows 11Visual StudioVisual Studio Community 2022 CMa…