当前位置: 首页 > news >正文

数据结构进阶——红黑树

数据结构进阶——红黑树

  • 1. 红黑树的概念
  • 2. 红黑树的性质
  • 3. 红黑树节点的定义
  • 4. 红黑树的插入
  • 5. 红黑树的验证
  • 6. 红黑树的删除
  • 7. 红黑树完整代码+测试


1. 红黑树的概念


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

在这里插入图片描述


2. 红黑树的性质


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

其中,第5条性质,是为了帮大家更好的理解性质4,举个例子:下面这棵树符合红黑树的定义吗?
在这里插入图片描述
很显然,性质1,2,3都是符合的,但是性质4符合吗?所有路径上,是否有相同数量的黑色节点?是不是大眼一看感觉也符合性质4,别急,下面我们把NIL节点也带上:

在这里插入图片描述

可以看到,用蓝色线条画出的这条路径,是不是很容易被忽略?从根节点向下看,其他路径上的黑色节点数都是2个(不算NIL节点),唯独蓝色线路上的黑色节点只有1个,所以上面这棵树是不符合红黑树定义的,它不是红黑树!

满足了如上5条性质,就可以保证,最长路径不超过最短路径的两倍,但是这是为什么?感兴趣的同学可以下去思考一下,只能说,这是天才的设计,很抽象,从学习者的角度来讲,只要能使用并控制红黑树的结构,这就够了,没必要向天才看齐。


3. 红黑树节点的定义

下面我们先实现一颗kv结构的红黑树。


// 颜色定义
enum Colour
{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;					// 节点保存的数据Colour _col;					// 节点颜色RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _col(RED){}
};

4. 红黑树的插入


1. 思考,如果我们要新插入一个节点,这个节点应该是红色还是黑色?

  • 先假设我们插入黑色节点,这会导致什么?会导致一整条路径上的黑色节点数量+1,这代价是不是太大了,我们要进行很多调整结构的操作,使得树重新满足性质4。

  • 如果我们插入红色节点呢?

    • 如果新节点的父节点是黑色的,那么就结束了,不用做什么其他调整,就算插入成功了,因为这完全不会影响树的整体结构。
      在这里插入图片描述
    • 如果新节点的父节点是红色,那么为了满足性质3(红色节点的子节点必须是黑色),就需要做出一些调整。
      在这里插入图片描述
  • 单单分析到这,就发现,插入红色节点,要比插入黑色节点代价小的多,很多情况下竟然只需要变色,就可以使红黑树结构平衡,而不用调整节点间的父子关系(旋转)。

2. 确定了新节点必须是红色节点,接下来就是分情况讨论

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

  • 但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论;

  • 约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点(cur不一定是新插入节点,也可能是调整上去的)。
    在这里插入图片描述

  • 如果一颗子树需要被调整,那么至少有三个节点的颜色,我们是确定的1. cur当前节点为红色。2. cur的父节点一定是红色。3. cur的爷爷节点,也就是父节点的父节点,一定是黑色。 因为调整之前,以cur为根节点的子树的上一级树必然是符合规则的,如果不符合,还调整什么?这棵树就是有问题的。

  • 所以接下来,我们讨论的实际上是叔叔节点不同颜色or存在/不存在的情况 + cur在p的左节点or右节点的情况 + p在g的左节点or右节点的情况(叔叔节点也有可能不存在)。

在这里插入图片描述

  • 将上面这些情况全部综合起来,一共有 4 * 4 = 16 种情况需要讨论u节点存在or不存在or红色or黑色,共四种 X 上图中四种节点位置状态,共四种。
  • 有同学可能会想,如果u节点是黑色,那么整个树就不满足规则4了啊,平衡被打破了啊。所以u节点要么是红色,要么不存在。这就是没有考虑到,cur可能不是新插入节点,可能是原来是黑色的,只是调整的过程中,由黑变红了。这种情况下,整棵树的黑节点数量还是平衡的。

下面的讨论中有一些情况会合并成一种情况(好编程),最终呈现出来,会小于16种。但是我们需要知道,本质上,就是对这16种情况进行处理。

2. 开始讨论

  • 情况一(叔叔红):

    • 情况1.1:pg的左节点,curp的左节点,u存在且为红(左左红)。

      • 解决方案:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
        在这里插入图片描述
    • 情况1.2:pg的右节点,curp的右节点,u存在且为红(右右红)。

      • 解决方案和情况一完全相同,这里不画图了,大家对着情况一的图,自行脑补一下。
    • 情况1.3:pg的左节点,curp的右节点,u存在且为红(左右红)。

      • 解决方案和情况一完全相同
    • 情况1.4:pg的右节点,curp的左节点,u存在且为红(右左红)。

      • 解决方案和情况一完全相同
    • 将上面所有的情况1.*,总结为一种,即叔叔为红色(叔叔红),统一解决方案:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整

  • 情况二(左左黑):pg的左节点,curp的左节点,u不存在/u存在且为黑。

    • 解决方案:先对g节点进行右单旋,然后将p,g变色--p变黑,g变红。
      在这里插入图片描述
  • 情况三(右右黑):pg的右节点,curp的右节点,u不存在/u存在且为黑。

    • 解决方案: 与情况三相比,唯一的不同是,需要对g进行左单旋。变色部分完全一样,p,g变色--p变黑,g变红
  • 情况四(左右黑):pg的左节点,curp的右节点,u不存在/u存在且为黑。

    • 解决方案:先对p进行左单旋,转化为情况二(左左黑),再根据情况二进行处理。 总结一下就是,先左右双旋,再将cur变黑,g变红。
      在这里插入图片描述
  • 情况五(右左黑):pg的右节点,curp的左节点,u存在且为红。

    • 解决方案:先对p进行右单旋,转化为情况三(右右黑),再根据情况三进行处理。 总结一下就是,先进行右左双旋,再将cur变黑,g变红。
template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
private:Node* _root = nullptr;public:bool Insert(const pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;	// 根节点必须是黑色return true;}Node* parent = nullptr;Node* cur = _root;// 先找到要插入的位置while (cur){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);cur->_col = RED;if (kv.first > parent->_kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}// 如果父节点存在并且是红色,再处理,是黑色不用处理while(parent && parent->_col == RED){Node* grandFather = parent->_parent;if (parent == grandFather->_left){// 这里只是说父亲在爷爷左边,而cur有可能在父亲的左节点也有可能在右节点// 后面还要再做讨论//		g//	  p	  u//	  c// 父亲是爷爷的左节点,那么叔叔就是爷爷的右节点Node* uncle = grandFather->_right;// 叔叔存在,并且叔叔是红色(叔叔红)if (uncle && uncle->_col == RED){// 变色uncle->_col = parent->_col = BLACK;grandFather->_col = RED;// 继续往上处理cur = grandFather;parent = cur->_parent;}else{if (cur == parent->_left){// cur 在父亲左边,右单旋(左左黑)//		g//	  p//	cRotateR(grandFather);parent->_col = BLACK;grandFather->_col = RED;}else{// cur 在父亲右边,左右双旋(左右黑)//		g//	  p//		cRotateL(parent);RotateR(grandFather);grandFather->_col = RED;cur->_col = BLACK;}// 旋转之后直接就平衡了,直接breakbreak;}}else // parent == grandFather->_right{// 这里只是说父亲在爷爷右边,而cur有可能在父亲的左节点也有可能在右节点// 后面还要再做讨论//		g//	  u	  p//		  c// 父亲是爷爷的右节点,那么叔叔就是左节点Node* uncle = grandFather->_left;// 叔叔存在,并且叔叔是红色(叔叔红)if (uncle && uncle->_col == RED){// 变色uncle->_col = parent->_col = BLACK;grandFather->_col = RED;// 继续向上处理cur = grandFather;parent = cur->_parent;}else{if (cur == parent->_right){// cur 在父亲右边,左单旋(右右黑)//		g//		  p//			cRotateL(grandFather);parent->_col = BLACK;grandFather->_col = RED;}else{// cur 在父亲左边,右左双旋(右左黑)//		g//		  p//		cRotateR(parent);RotateL(grandFather);grandFather->_col = RED;cur->_col = BLACK;}// 旋转之后直接就平衡了,直接breakbreak;}}}// 不管前面如何处理,最后都要把根节点变黑_root->_col = BLACK;return true;}
}

5. 红黑树的验证


红黑树的检测分为两步:

  • 检测其是否满足二叉搜索树(中序遍历是否为有序序列);
  • 检测其是否满足红黑树的性质。
template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
private:Node* _root = nullptr;public:...// 中序遍历void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}void InOrder(){_InOrder(_root);cout << endl;}// 检查//	1. 红色节点的子节点是否为黑色节点//	2. 根节点->当前节点这条路径的黑色节点的数量bool Check(Node* root, int blacknum, const int refVal){if (root == nullptr){if (blacknum != refVal){ cout << "存在黑色节点数量不相等的路径" << endl;return false;}return true;}// 子节点情况太多了,我们直接反向检查父节点,只要红色节点的父节点不是红色,就是符合要求的if (root->_col == RED && root->_parent->_col == RED){cout << "有连续的红色节点" << endl;return false;}if (root->_col == BLACK){blacknum++;}return Check(root->_left, blacknum, refVal)&& Check(root->_right, blacknum, refVal);}// 看看红黑树是否符合要求bool IsBalance(){return _IsBalance(_root);}bool _IsBalance(Node* root){if (root == nullptr)return true;if (root->_col == RED)return false;// 参考值,算出最左路径的黑节点数量int refVal = 0;Node* cur = root;while (cur) {if (cur->_col == BLACK)refVal++;cur = cur->_left;}int blacknum = 0;	return Check(root, blacknum, refVal);}
};

6. 红黑树的删除


红黑树的删除本节不做讲解,有兴趣的同学可参考:《算法导论》或者《STL源码剖析》http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html


7. 红黑树完整代码+测试


1. 完整代码

#pragma once#include <iostream>enum Colour
{RED,BLACK
};using namespace std;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;
private:Node* _root = nullptr;public:bool Insert(const pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;	// 根节点必须是黑色return true;}Node* parent = nullptr;Node* cur = _root;// 先找到要插入的位置while (cur){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);cur->_col = RED;if (kv.first > parent->_kv.first){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}// 如果父节点存在并且是红色,再处理,是黑色不用处理while(parent && parent->_col == RED){Node* grandFather = parent->_parent;if (parent == grandFather->_left){// 这里只是说父亲在爷爷左边,而cur有可能在父亲的左节点也有可能在右节点// 后面还要再做讨论//		g//	  p	  u//	  c// 父亲是爷爷的左节点,那么叔叔就是爷爷的右节点Node* uncle = grandFather->_right;// 叔叔存在,并且叔叔是红色(叔叔红)if (uncle && uncle->_col == RED){// 变色uncle->_col = parent->_col = BLACK;grandFather->_col = RED;// 继续往上处理cur = grandFather;parent = cur->_parent;}else{if (cur == parent->_left){// cur 在父亲左边,右单旋(左左黑)//		g//	  p//	cRotateR(grandFather);parent->_col = BLACK;grandFather->_col = RED;}else{// cur 在父亲右边,左右双旋(左右黑)//		g//	  p//		cRotateL(parent);RotateR(grandFather);grandFather->_col = RED;cur->_col = BLACK;}// 旋转之后直接就平衡了,直接breakbreak;}}else // parent == grandFather->_right{// 这里只是说父亲在爷爷右边,而cur有可能在父亲的左节点也有可能在右节点// 后面还要再做讨论//		g//	  u	  p//		  c// 父亲是爷爷的右节点,那么叔叔就是左节点Node* uncle = grandFather->_left;// 叔叔存在,并且叔叔是红色(叔叔红)if (uncle && uncle->_col == RED){// 变色uncle->_col = parent->_col = BLACK;grandFather->_col = RED;// 继续向上处理cur = grandFather;parent = cur->_parent;}else{if (cur == parent->_right){// cur 在父亲右边,左单旋(右左黑)//		g//		  p//			cRotateL(grandFather);parent->_col = BLACK;grandFather->_col = RED;}else{// cur 在父亲左边,右左双旋(左右黑)//		g//		  p//		cRotateR(parent);RotateL(grandFather);grandFather->_col = RED;cur->_col = BLACK;}// 旋转之后直接就平衡了,直接breakbreak;}}}// 不管前面如何处理,最后都要把根节点变黑_root->_col = BLACK;return true;}// 左单旋void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;// 更新左右节点parent->_right = subRL;subR->_left = parent;Node* parentParent = parent->_parent;// 更新父节点parent->_parent = subR;if (subRL != nullptr){subRL->_parent = parent;}// 将子树链接进整体if (_root == parent){_root = subR;subR->_parent = nullptr;}else if (parentParent->_left == parent){parentParent->_left = subR;subR->_parent = parentParent;}else{parentParent->_right = subR;subR->_parent = parentParent;}}// 右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;// 更新左右节点parent->_left = subLR;subL->_right = parent;Node* parentParent = parent->_parent;// 更新父节点parent->_parent = subL;if (subLR != nullptr){subLR->_parent = parent;}// 将子树链接进整体if (_root == parent){_root = subL;subL->_parent = nullptr;}else if (parentParent->_left == parent){parentParent->_left = subL;subL->_parent = parentParent;}else{parentParent->_right = subL;subL->_parent = parentParent;}}// 中序遍历void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}void InOrder(){_InOrder(_root);cout << endl;}// 检查//	1. 红色节点的子节点是否为黑色节点//	2. 根节点->当前节点这条路径的黑色节点的数量bool Check(Node* root, int blacknum, const int refVal){if (root == nullptr){if (blacknum != refVal){ cout << "存在黑色节点数量不相等的路径" << endl;return false;}return true;}// 子节点情况太多了,我们直接反向检查父节点,只要红色节点的父节点不是红色,就是符合要求的if (root->_col == RED && root->_parent->_col == RED){cout << "有连续的红色节点" << endl;return false;}if (root->_col == BLACK){blacknum++;}return Check(root->_left, blacknum, refVal)&& Check(root->_right, blacknum, refVal);}// 看看红黑树是否符合要求bool IsBalance(){return _IsBalance(_root);}bool _IsBalance(Node* root){if (root == nullptr)return true;if (root->_col == RED)return false;// 参考值,算出最左路径的黑节点数量int refVal = 0;Node* cur = root;while (cur) {if (cur->_col == BLACK)refVal++;cur = cur->_left;}int blacknum = 0;	return Check(root, blacknum, refVal);}bool Find(const K& key){Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;}else if (cur->_kv.first > key){cur = cur->_left;}else{return true; // 找到了,返回真}}return false; // 找不到,返回假}int _Height(Node* root){if (root == nullptr)return 0;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}int Height(){return _Height(_root);}int _Size(Node* root){if (root == nullptr){return 0;}return _Size(root->_left) + _Size(_root->_right) + 1;}int Size(){_Size(_root);}
};

2. 测试Insert和Find的效率

#include <iostream>
#include <chrono>
#include <vector>using namespace std;#include "RBTree.h"// 小范围测试
void Test1()
{int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };RBTree<int, int> t;for (auto e : a){t.Insert(make_pair(e, e));}t.InOrder();cout << "IsBalance: " << t.IsBalance() << endl;
}// 大范围测试
void Test2()
{const int N = 100 * 10000;  // 测试100w条数据的插入vector<int> v;v.reserve(N);srand((unsigned int)time(0));for (size_t i = 0; i < N; i++){v.push_back(rand() + (int)i);}RBTree<int, int> t;// 测试插入耗时auto start1 = chrono::high_resolution_clock::now();for (auto e : v){t.Insert(make_pair(e, e));}auto end1 = chrono::high_resolution_clock::now();double insertTime = chrono::duration_cast<chrono::microseconds>(end1 - start1).count() / 1e6;// 测试查找耗时auto start2 = chrono::high_resolution_clock::now();if (t.Find(v[100])) cout << "Find Success!!!" << endl;auto end2 = chrono::high_resolution_clock::now();double findTime = chrono::duration_cast<chrono::microseconds>(end2 - start2).count() / 1e6;cout << "IsBalance: " << t.IsBalance() << endl;cout << "Heigh: " << t.Height() << endl;cout << "Insert cost: " << insertTime << " s" << endl;cout << "Find cost: " << findTime << " s" << endl;
}int main()
{Test2();return 0;
}
  • Test2大范围测试的结果,数据量100w:
Find Success!!!
IsBalance: 1
Heigh: 27
Insert cost: 0.304353 s
Find cost: 0.00027 s
  • 可以看到插入和查找效率非常高。

http://www.dtcms.com/a/610406.html

相关文章:

  • 开源数据同步中间件(Dbsyncer)简单玩一下 mysql to mysql 的增量,全量配置
  • 在 VS Code 中用 MyBatis 操作数据库的 Spring Boot 示例
  • 唐山网站建设开发专业网站建设经费申请
  • Java 抽象类
  • 基于Unity YooAsset自动化资源管理框架,附源代码
  • 域名注册要多少钱信阳新网站做seo优化
  • 做网站盈利方式开发一个简单的app需要多少钱
  • RPA 重构财务新生态:自动化驱动的转型革命
  • Kubernetes调度器深度解析:从资源分配到亲和性策略的架构师之路
  • 具身智能-一文详解视觉-语言-动作(VLA)大模型(3
  • 【Linux C/C++开发】libusb库操作-获取USB设备信息
  • LeetCode 刷题【154. 寻找旋转排序数组中的最小值 II】
  • 大视频秒级同步:高性能跨系统视频数据迁移实战方案
  • 手机网站制作哪家公司好wordpress 查件
  • 优化 TDengine IDMP 面板编辑的几种方法​
  • 定制开发AI智能名片S2B2C预约服务小程序的定制开发与优势分析
  • 哪个做网站平台好电子商务与网站建设实践论文
  • 填充每个节点的下一个右侧节点指针(一)
  • 基于RFSOC47DR的射频采集卡
  • 东莞齐诺做网站网站做服装那个平台好一点
  • 长春模板网站建设企业网站开发难吗
  • 国外网站不需要备案吗看wordpress导出文章
  • DeepSeek-OCR私有化部署—从零构建OCR服务环境
  • Navicat 17 连接 SQL Server 后在导航栏中没有显示数据库表对象,如何解决?
  • 官方网站下载手电筒网站设置在哪
  • 如何建设阿里巴巴网站东莞现代建设有限公司
  • 【openGauss】让gsql和sqlplus输出包含有SQL及数据的完全一致的文本文件
  • LingJing(灵境)桌面级靶场平台新增靶机:加密攻防新挑战:encrypt-labs靶场,全面提升安全研究者的实战能力!
  • 高通SMD450 pop音问题回顾
  • 【LeetCode】将 x 减到 0 的最小操作数