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

C++《红黑树》

在之前的篇章当中我们已经了解了基于二叉搜索树的AVL树,那么接下来在本篇当中将继续来学习另一种基于二叉搜索树的树状结构——红黑树,在此和之前学习AVL树类似还是通过先了解红黑树是什么以及红黑树的结构特点,接下来在试着实现红黑树的结构以及实现红黑树插入新节点、进行节点查询的功能,相信通过本篇的学习能让你了解红黑树,一起加油把!!!


1. 红黑树的概念

在此红黑树是基于二叉搜索树进行改进的,因此红黑树的中序遍历也是有序的

红黑树是⼀棵二叉搜索树,他的每个结点增加⼀个存储位来表示结点的颜色,可以是红色或者黑色。通过对任何⼀条从根到叶子的路径上各个结点的颜⾊进行约束,红黑树确保没有⼀条路径会比其他路径长出2倍,因而是接近平衡的

1.1 红黑树的规则

只有同时满足以下的几点要求时才是在红黑树:
1. 每个结点不是红色就是黑色
2. 根结点是黑色的
3. 如果⼀个结点是红色的,则它的两个孩⼦结点必须是黑色的,也就是说任意⼀条路径不会有连续的红色结点。
4. 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点

以上的要求看起来是规律的,但是其实这几点规则之间是相互协调的,接下来我们就通过几个示例来看看这些规则是怎么使得红黑树当中的最长路径的长度不大于其他路径的两倍的。 

来看以下的示例:

以上图示的二叉树当中,根节点为黑,并且不存在连续的红节点,那么接下来就是要知道二叉树当中每个路径上黑节点的个数;在此之前需要我们先找出以上的二叉树有几条路径,可能你会简单的认为以上不就是有4条路径吗?如下所示

但是其实以上求路径个数的方式是错误的,在此要一条路径的需要到空节点为止,因此以上的正确的路径应如下所示:


 

此时就可以看出以上的二叉树有9条路径,并且每条的路径上黑色节点的个数都是2两个,这也是满足红黑树的要求的。

以上的二叉树当中满足了以上所示的红黑树的1、2、4点规则,但是对应规则3以上的所示的二叉树的叶子节点为红时不就不满足规则了吗?

这其实在在此通常情况下我们会忽略这种情况,在《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是黑色的规则。他这⾥所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了方便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道⼀下这个概念即可。

那么在满足红黑树的规则下,就可以使得没有⼀条路径会比其他路径长出2倍,因而是接近平衡的

以上所示的红黑树当中最长路径为3,最短的为2,这时二叉搜索树就是接近平衡的

以下的二叉搜索树也是满足以上的红黑树规则,也是红黑树

 这是就可以看出红黑树当中其实是完全没有红色节点的,这是也是满足红黑树的规则的

1.2 红黑树如何保持接近平衡 

在此我们就要思考在红黑树是如何确保基本平衡的,也就是在红黑树当中是如何确保最长的路径不超过最短路径的两倍的?

• 由规则4可知,从根到NULL结点的每条路径都有相同数量的黑色结点,所以极端场景下,最短路径就就是全是⿊⾊结点的路径,假设最短路径长度为bh(black height)。
• 由规则2和规则3可知,任意⼀条路径不会有连续的红色结点,所以极端场景下,最长的路径就是一黑一红间隔组成,那么最长路径的长度为2*bh
• 综合红黑树的4点规则而言,理论上的全⿊最短路径和一黑一红的最长路径并不是在每棵红⿊树都存在的。假设任意⼀条从根到NULL结点路径的⻓度为x,那么bh <= h <= 2*bh

1.3 红黑树的效率 

以上我们了解了红黑树的概率,以及要使得二叉搜索树为红黑树要满足什么样的要求,那么接下来就来分析在红黑树当中进行查找的效率。

假设N是红⿊树树中结点数量,h最短路径的长度,那么 2^{h}-1<=N<2^{2h}-1, 由此推出
h ≈ logN ,也就是意味着红⿊树增删查改最坏也就是走最长路径为 2*logN,那么时间复杂度就为O(\log N)

 

在此红黑树其实相比之前学习的AVL树控制平衡的方式不用,AVL树是通过子树的控制高度差进行整体的平衡控制,而红黑树是通过4条规则的颜色约束间接的实现近似平衡,他们效率都是同⼀档次,但是相对而言,插⼊相同数量的结点,红黑树的旋转次数是更少的,因为他对平衡的控制没那么严格。在大量节点时AVL树的高度相比红黑树会低一些。

2. 红黑树实现

以上我们了解了红黑树的结构特点之后接下来就来实现红黑树的代码

在此创建两个文件BRTRee.h和test.cpp,在BRTRee.h内实现红黑树的结构以及各种的功能,在test.cpp内对实现的红黑树进行测试,看是否满足我们的要求

2.1 红黑树节点实现

在实现红黑树的结构之前我们先要实现红黑树节点的结构体,在此创建一个名为colour的枚举来表示节点的颜色,创建一个名为RBTree的struct结构体来表示红黑树的节点,实现代码如下所示:

enum colour
{RED,BLACK
};template<class K, class V>
struct RBTreeNode
{pair<K, V> _val;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;colour _col;RBTreeNode(const pair<K, V>& val):_val(val), _left(nullptr), _right(nullptr), _parent(nullptr){}};

2.2 红黑树结构实现 

在实现了红黑树节点的结构之后,接下来我们抽奖一个名为RBTree的类,在该类内实现红黑树的结构以及各种功能,接下来就先实现结构,实现的代码如下所示:

template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;public://各种功能……private:Node* root = nullptr;};

以上我们时使用类模板的方式来创建对应的二叉树,这样就可以使得创建的二叉树内节点的数据的类型是任意的,不过要存储在红黑树内的数据类型是需要能进行大小的比较,如果默认不支持也是需要我们自己实现的。

2.3 红黑树插入实现

以上实现了红黑树的大体结构之后接下来就来实现红黑树当中的插入功能,接下来在实现插入的代码之前先来分析在红黑树当中插入新的节点会有几种情况。

在插入新的节点之后,新形成的也是要满足红黑树的4条规则的。

1.在空树当中插入节点,那么新增的节点需要是黑色节点。

在非空的树当中插入新的节点,此时我们就要思考应将新的节点的颜色置为黑的还是红色?

如果我们插入的节点是红色的那么就只需要在其父节点为红色是进行调整,但是如果插入的节点为黑色的,这就会使得新产生的路径内的黑色节点数比其他的路径要多一个,这时要进行调整是很困难的,因此插入的节点应该初始化为红节点

2.在非空树当中插入新的节点,将该节点初始化为红

在非空树内插入新节点之后接下来就要看其父节点是否为红,是的话此时的树就违背了红黑树的规则3,就需要进行调整。调整直到没有应该红色节点的父节点为红为止。如果一开始插入的节点的发节点就为黑;那么插入完就可以停止操作

以下就是实现的不包含调整调整代码的插入代码:

bool Insert(const pair <K, V> kv)
{//当插入节点时的树为空时就将新插入的节点置为树的根节点,且颜色变为黑if (root == nullptr){root = new Node(kv);root->_col = BLACK;return true;}//寻找合适的插入位置Node* cur = root;Node* Parent = nullptr;while (cur){if (cur->_val.first < kv.first){Parent = cur;cur = cur->_right;}else if (cur->_val.first > kv.first){Parent = cur;cur = cur->_left;}else{return false;}}//创建新节点cur = new Node(kv);if (Parent->_val.first < kv.first){Parent->_right = cur;}else{Parent->_left = cur;}cur->_parent = Parent;cur->_col = RED;//进行调整使得树满足红黑树的规则……//无论以上是否进行调整都将根结点的颜色置为黑root->_col = BLACK;return true;}


 

接下来就来讲解插入的节点的父节点为红时进行调整的3种情况

注:接下来的讲解当中假设我们把新增结点标识为c (cur),c的父亲标识为p(parent),p的父亲标识为g(grandfather),p的兄弟标识为u(uncle)。

1.情况1:只需变色

当插入的节点的为父亲节点为红时,而且父亲的父亲的另一个节点也是的,也就是c节点的叔叔节点u也为红时。因为在插入新的节点c之前当前的树一定是满足红黑树的规则的,那么此时p节点的父亲节点f一定是为黑,也就是c节点的祖父节点一定为黑

 

例如以下示例:


新插入的节点为x,此时c、p、u的节点颜色情况就满足以上描述的形式。那么要让该节点变回满足红黑树的规则,就只需要将p、u变黑;g变红即可。这时调整完之后就可以使得树当中每条路径内的黑色节点的个数都相同。

以下是只进行变色情况的抽象表达,d/e/f代表每条路径拥有hb个黑色结点的子树,a/b代表每条路径拥有hb-1个黑色结点的根为红的子树,hb>=0。和之前学习AVL树一样接下来来通过看看几种只进行变色的情况。

那么接下来就开看看 hb==0 、hb==1、hb==2的情况 ,其中当hb等于2时,这⾥组合情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理方式⼀样的,变⾊再继续往上处理即可,所以我们只需要看抽象图即可。

通过以上的示例就可以看出在处理只需要变色的情况时,新出现的红色节点可能是新插入的也可能是下部分的节点调整上来的,此时只需要一直进行调整直到对应的p为黑时就停止 

实现的代码如下所示:

以下以p节点分别为g节点的左节点和右节点的两种情况分别进行分析
 

bool Insert(const pair <K, V> kv)
{//当插入节点时的树为空时就将新插入的节点置为树的根节点,且颜色变为黑if (root == nullptr){root = new Node(kv);root->_col = BLACK;return true;}//寻找合适的插入位置Node* cur = root;Node* Parent = nullptr;while (cur){if (cur->_val.first < kv.first){Parent = cur;cur = cur->_right;}else if (cur->_val.first > kv.first){Parent = cur;cur = cur->_left;}else{return false;}}//创建新节点cur = new Node(kv);if (Parent->_val.first < kv.first){Parent->_right = cur;}else{Parent->_left = cur;}cur->_parent = Parent;cur->_col = RED;while (Parent && Parent->_col == RED){Node* grandfather = Parent->_parent;//父节点为祖父节点的左节点时//    g//  p  u//cif (grandfather->_left == Parent){Node* uncle = grandfather->_right;//叔叔存在且为红if (uncle && uncle->_col == RED){Parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;Parent = cur->_parent;}//其他情况……}//父节点为祖父节点的右节点时//    g//  u   p//       celse{Node* uncle = grandfather->_left;//叔叔存在且为红if (uncle && uncle->_col == RED){Parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;Parent = cur->_parent;}//其他情况……}}//无论以上是否进行调整都将根结点的颜色置为黑root->_col = BLACK;return true;}

2.情况2:单旋+变色

以上情况1当中是叔叔存在且为红的情况,那么如果叔叔不存在或者是不为红又要怎么进行调整呢?

首先是c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。
 

在此p是必须变为黑的,但是由于u为黑或者不存在,那么这就使得将p变为黑时p节点所在的路径的黑色节点的个数就与其他的路径不相同,那么这时就需要进行旋转来解决问题。

在此也是以p节点分别为g节点的左节点和右节点的两种情况分别进行分析

     gp    u
c

如果p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变⿊,g变红即可。p变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是黑色还是红色或者空都不违反规则。

     gu    pc

如果p是g的右,c是p的右,那么以g为旋转点进行左单旋,再把p变⿊,g变红即可。p变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则。

3.情况3:双旋+变色
 

以上我们分析了单旋的情,那么接下来就来分析双旋的情况,当插入c之后红黑树的子树结构如下所示时只使用单旋是无法实现调整之后的树满足红黑树的规则的,在此接下来要使用到双旋才能调整至满足要求。

c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c⼀定是新增结点,u存在且为黑,则c⼀定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变⾊将c从黑色变成红⾊,更新上来的。


在此p必须变黑,才能解决,连续红色结点的问题,u不存在或者是⿊⾊的,但是这⾥单纯的变色⽆法解决问题,需要旋转+变色。

     gp    uc

如果p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变
黑,g变红即可。c变成课这颗树新的根,这样子树黑⾊结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。

 

     gu    pc

如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变
⿊,g变红即可。c变成课这颗树新的根,这样⼦树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的⽗亲是黑色还是红色或者空都不违反规则。

2.4 插入完整代码

以上我们就进行插入节点的三种情况的分析,那么接下来就将以上的插入代码补充完整

bool Insert(const pair <K, V> kv)
{//当插入节点时的树为空时就将新插入的节点置为树的根节点,且颜色变为黑if (root == nullptr){root = new Node(kv);root->_col = BLACK;return true;}//寻找合适的插入位置Node* cur = root;Node* Parent = nullptr;while (cur){if (cur->_val.first < kv.first){Parent = cur;cur = cur->_right;}else if (cur->_val.first > kv.first){Parent = cur;cur = cur->_left;}else{return false;}}//创建新节点cur = new Node(kv);if (Parent->_val.first < kv.first){Parent->_right = cur;}else{Parent->_left = cur;}cur->_parent = Parent;cur->_col = RED;while (Parent && Parent->_col == RED){Node* grandfather = Parent->_parent;//父节点为祖父节点的左节点时//    g//  p  u//cif (grandfather->_left == Parent){Node* uncle = grandfather->_right;//叔叔存在且为红if (uncle && uncle->_col == RED){Parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;Parent = cur->_parent;}//叔叔不存在或者为黑else{//当c为p的左节点时//    g//  p  u//cif (Parent->_left == cur){RotateR(grandfather);grandfather->_col = RED;Parent->_col = BLACK;}//当c为p的右节点时//     g//   p   u//    celse{RotateL(Parent);RotateR(grandfather);grandfather->_col = RED;cur->_col = BLACK;}break;}}//父节点为祖父节点的右节点时//    g//  u   p//       celse{Node* uncle = grandfather->_left;//叔叔存在且为红if (uncle && uncle->_col == RED){Parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;Parent = cur->_parent;}//叔叔不存在或者为黑else{//当c为p的右节点时//    g//  u   p//       cif (Parent->_right == cur){RotateL(grandfather);grandfather->_col = RED;Parent->_col = BLACK;}//当c为p的左节点时//    g//  u   p//     celse{RotateR(Parent);RotateL(grandfather);grandfather->_col = RED;cur->_col = BLACK;}break;}}}//无论以上是否进行调整都将根结点的颜色置为黑root->_col = BLACK;return true;}void RotateR(Node* Parent)
{Node* subL = Parent->_left;Node* subLR = subL->_right;if (subLR != nullptr){subLR->_parent = Parent;}Node* tmpNode = Parent->_parent;Parent->_left = subLR;Parent->_parent = subL;subL->_right = Parent;if (tmpNode != nullptr){if (tmpNode->_left == Parent){tmpNode->_left = subL;}else{tmpNode->_right = subL;}subL->_parent = tmpNode;}else{root = subL;subL->_parent = nullptr;}}void RotateL(Node* Parent)
{Node* subR = Parent->_right;Node* subRL = subR->_left;if (subRL != nullptr){subRL->_parent = Parent;}Node* tmpNode = Parent->_parent;Parent->_right = subRL;Parent->_parent = subR;subR->_left = Parent;if (tmpNode != nullptr){if (tmpNode->_left == Parent){tmpNode->_left = subR;}else{tmpNode->_right = subR;}subR->_parent = tmpNode;}else{root = subR;subR->_parent = nullptr;}}

2.4 红黑树查找

在此红黑树的查找实现和之前实现的二叉搜索树和AVL树类似,对你来说实现查找的代码肯定是 so easy 的,在此就不再进行讲解,直接奉上代码:

Node* Find(const K& val)
{if (root == nullptr){return nullptr;}Node* cur = root;while (cur){if (cur->_val.first < val){cur = cur->_left;}else if (cur->_val.first > val){cur = cur->_right;}else{return cur;}}return nullptr;
}

2.5 红黑树删除

在此红黑树的删除较为复杂且不是很重要,在此就不进行讲解。

 

2.6 红黑树验证

以上我们实现了红黑树的插入以及查找,那么接下来就来实现验证的代码

首先来分析验证的代码该如何实现:
这力获取最长路径和最短路径,检查最长路径不超过最短路径的2倍是不可行的,因为就算满足这个条件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查4点规则,满足这4点规则,⼀定能保证最长路径不超过最短路径的2倍。

1. 规则1枚举颜⾊类型,天然实现保证了颜色不是黑色就是红色。
2. 规则2直接检查根即可
3. 规则3前序遍历检查,遇到红色结点查孩子不太方便,因为孩⼦有两个,且不⼀定存在,反过来检查父亲的颜色就方便多了。
4. 规则4前序遍历,遍历过程中用形参记录跟到当前结点的blackNum(黑色结点数量),前序遍历遇到黑色结点就++blackNum,走到空就计算出了⼀条路径的⿊⾊结点数量。再任意⼀条路径黑色结点数量作为参考值,依次比较即可。

实现代码如下所示:

bool Check(Node* root, int blackNum, const int refNum)
{if (root == nullptr){// 前序遍历⾛到空时,意味着⼀条路径⾛完了//cout << blackNum << endl;if (refNum != blackNum){cout << "存在⿊⾊结点的数量不相等的路径" << endl;return false;}return true;}// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了if (root->_col == RED && root->_parent->_col == RED){cout << root->_val.first << "存在连续的红⾊结点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);
}bool IsBalance()
{if (root == nullptr)return true;if (root->_col == RED)return false;// 参考值int refNum = 0;Node* cur = root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(root, 0, refNum);
}

测试用例:

void TestAVLTree1()
{RBTree<int, int> t;// 常规的测试⽤例int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };// 特殊的带有双旋场景的测试⽤例//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (auto e : a){t.Insert({ e, e });}t.InOrder();//cout << t.IsBalanceTree() << endl;cout<<t.IsBalance();
}

运行以上代码输出如下所示:
 

这时只能是说明对以上示例的测试用例进行插入是没问题的,要更严谨的验证就需要有更多的测试用例,这时使用以上的代码进行验证 ,并且测试进行插入的效率以及查找效率

void TestAVLTree2()
{const int N = 1000000;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; i++){v.push_back(rand() + i);}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;cout << "Insert:" << end2 - begin2 << endl;cout << "Height:" << t.Height() << endl;cout << "Size:" << t.Size() << endl;size_t begin1 = clock();// 确定在的值/*for (auto e : v){t.Find(e);}*/// 随机值for (size_t i = 0; i < N; i++){t.Find((rand() + i));}size_t end1 = clock();cout << "Find:" << end1 - begin1 << endl;
}

将vs的调整为release模式之后,通过输出的结果就可以看出我们实现的红黑树的插入代码是没问题的,并且实现的红黑树的插入效率以及查找效率都是很高的,效率基本和AVL树不差上下。 

2.7 完整代码

RBTree.c

#pragma once
#include<iostream>using namespace std;enum colour
{RED,BLACK
};template<class K, class V>
struct RBTreeNode
{pair<K, V> _val;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;colour _col;RBTreeNode(const pair<K, V>& val):_val(val), _left(nullptr), _right(nullptr), _parent(nullptr){}};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* cur = root;Node* Parent = nullptr;while (cur){if (cur->_val.first < kv.first){Parent = cur;cur = cur->_right;}else if (cur->_val.first > kv.first){Parent = cur;cur = cur->_left;}else{return false;}}//创建新节点cur = new Node(kv);if (Parent->_val.first < kv.first){Parent->_right = cur;}else{Parent->_left = cur;}cur->_parent = Parent;cur->_col = RED;while (Parent && Parent->_col == RED){Node* grandfather = Parent->_parent;//父节点为祖父节点的左节点时//    g//  p  u//cif (grandfather->_left == Parent){Node* uncle = grandfather->_right;//叔叔存在且为红if (uncle && uncle->_col == RED){Parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;Parent = cur->_parent;}//叔叔不存在或者为黑else{//当c为p的左节点时//    g//  p  u//cif (Parent->_left == cur){RotateR(grandfather);grandfather->_col = RED;Parent->_col = BLACK;}//当c为p的右节点时//     g//   p   u//    celse{RotateL(Parent);RotateR(grandfather);grandfather->_col = RED;cur->_col = BLACK;}break;}}//父节点为祖父节点的右节点时//    g//  u   p//       celse{Node* uncle = grandfather->_left;//叔叔存在且为红if (uncle && uncle->_col == RED){Parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;Parent = cur->_parent;}//叔叔不存在或者为黑else{//当c为p的右节点时//    g//  u   p//       cif (Parent->_right == cur){RotateL(grandfather);grandfather->_col = RED;Parent->_col = BLACK;}//当c为p的左节点时//    g//  u   p//     celse{RotateR(Parent);RotateL(grandfather);grandfather->_col = RED;cur->_col = BLACK;}break;}}}//无论以上是否进行调整都将根结点的颜色置为黑root->_col = BLACK;return true;}Node* Find(const K& val){if (root == nullptr){return nullptr;}Node* cur = root;while (cur){if (cur->_val.first < val){cur = cur->_left;}else if (cur->_val.first > val){cur = cur->_right;}else{return cur;}}return nullptr;}void InOrder(){_InOrder(root);cout << endl;}int  Height(){return _Height(root);}int Size(){return _Size(root);}bool Check(Node* root, int blackNum, const int refNum){if (root == nullptr){// 前序遍历⾛到空时,意味着⼀条路径⾛完了//cout << blackNum << endl;if (refNum != blackNum){cout << "存在⿊⾊结点的数量不相等的路径" << endl;return false;}return true;}// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了if (root->_col == RED && root->_parent->_col == RED){cout << root->_val.first << "存在连续的红⾊结点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);}bool IsBalance(){if (root == nullptr)return true;if (root->_col == RED)return false;// 参考值int refNum = 0;Node* cur = root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(root, 0, refNum);}private:Node* root = nullptr;void RotateR(Node* Parent){Node* subL = Parent->_left;Node* subLR = subL->_right;if (subLR != nullptr){subLR->_parent = Parent;}Node* tmpNode = Parent->_parent;Parent->_left = subLR;Parent->_parent = subL;subL->_right = Parent;if (tmpNode != nullptr){if (tmpNode->_left == Parent){tmpNode->_left = subL;}else{tmpNode->_right = subL;}subL->_parent = tmpNode;}else{root = subL;subL->_parent = nullptr;}}void RotateL(Node* Parent){Node* subR = Parent->_right;Node* subRL = subR->_left;if (subRL != nullptr){subRL->_parent = Parent;}Node* tmpNode = Parent->_parent;Parent->_right = subRL;Parent->_parent = subR;subR->_left = Parent;if (tmpNode != nullptr){if (tmpNode->_left == Parent){tmpNode->_left = subR;}else{tmpNode->_right = subR;}subR->_parent = tmpNode;}else{root = subR;subR->_parent = nullptr;}}void _InOrder(Node* cur){if (cur == nullptr){return;}_InOrder(cur->_left);cout << cur->_val.first << ":"<<cur->_val.second<<endl;_InOrder(cur->_right);}int _Height(Node* cur){if (cur == nullptr){return 0;}int Left = _Height(cur->_left);int Right = _Height(cur->_right);return Left > Right ? Left + 1 : Right + 1;}int _Size(Node* cur){if (cur == nullptr){return 0;}return 1 + _Size(cur->_left) + _Size(cur->_right);}};

test.cpp

#include<iostream>
#include<vector>
#include"BRTree.h"using namespace std;void TestAVLTree1()
{RBTree<int, int> t;// 常规的测试⽤例int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };// 特殊的带有双旋场景的测试⽤例//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };for (auto e : a){t.Insert({ e, e });}t.InOrder();//cout << t.IsBalanceTree() << endl;cout<<t.IsBalance();
}void TestAVLTree2()
{const int N = 1000000;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; i++){v.push_back(rand() + i);}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;cout << "Insert:" << end2 - begin2 << endl;cout << "Height:" << t.Height() << endl;cout << "Size:" << t.Size() << endl;size_t begin1 = clock();// 确定在的值/*for (auto e : v){t.Find(e);}*/// 随机值for (size_t i = 0; i < N; i++){t.Find((rand() + i));}size_t end1 = clock();cout << "Find:" << end1 - begin1 << endl;
}int main()
{//TestAVLTree1();TestAVLTree2();return 0;
}

以上就是本篇的全部内容了,在实现了红黑树之后接下来我们就可以基于红黑树来自己实现封装set和map,未完待续……

相关文章:

C++《红黑树》

在之前的篇章当中我们已经了解了基于二叉搜索树的AVL树&#xff0c;那么接下来在本篇当中将继续来学习另一种基于二叉搜索树的树状结构——红黑树&#xff0c;在此和之前学习AVL树类似还是通过先了解红黑树是什么以及红黑树的结构特点&#xff0c;接下来在试着实现红黑树的结构…...

Axios 请求取消:从原理到实践

Axios 请求取消&#xff1a;从原理到实践 在现代前端开发中&#xff0c;网络请求是不可或缺的一部分。Axios 是一个基于 Promise 的 HTTP 客户端&#xff0c;广泛应用于浏览器和 Node.js 环境中。然而&#xff0c;在某些场景下&#xff0c;我们可能需要取消正在进行的请求&…...

【css酷炫效果】纯CSS实现照片堆叠效果

【css酷炫效果】纯CSS实现照片堆叠效果 缘创作背景html结构css样式完整代码基础版进阶版(增加鼠标悬停查看) 效果图 想直接拿走的老板&#xff0c;链接放在这里&#xff1a;https://download.csdn.net/download/u011561335/90492022 缘 创作随缘&#xff0c;不定时更新。 创…...

论文精度:Transformers without Normalization

前言 论文题目:Transformers without Normalization 作者:Jiachen Zhu 1,2 , Xinlei Chen 1 , Kaiming He 3 , Yann LeCun 1,2 , Zhuang Liu 1,4,† 论文地址:https://arxiv.org/pdf/2503.10282 摘要 这篇论文探讨了现代神经网络中广泛使用的归一化层是否是必不可少的。…...

基于香橙派 KunpengPro学习CANN(3)——pytorch 模型迁移

通用模型迁移适配可以分为四个阶段&#xff1a;迁移分析、迁移适配、精度调试与性能调优。 迁移分析 迁移支持度分析&#xff1a; 准备NPU环境&#xff0c;获取模型的源码、权重和数据集等文件&#xff1b;使用迁移分析工具采集目标网络中的模型/算子清单&#xff0c;识别第三方…...

微软远程桌面即将下架?Splashtop:更稳、更快、更安全的 RDP 替代方案

近日&#xff0c;Windows 官方博客宣布&#xff1a;将于2025年5月27日起&#xff0c;在 Windows 10 和 Windows 11 应用商店中下架“Microsoft 远程桌面”应用&#xff0c;建议用户迁移至新的 Windows App。这一变动引发了广大用户对远程访问解决方案的关注。作为全球领先的远程…...

【Python】Python与算法有应用关系吗?

李升伟 整理 是的&#xff0c;Python与算法有着密切的应用关系。Python作为一种高级编程语言&#xff0c;因其简洁的语法和强大的库支持&#xff0c;被广泛应用于算法设计、实现和应用中。以下是Python与算法之间的一些主要应用关系&#xff1a; 1. 算法学习与教学&#xff1…...

js,html,css,vuejs手搓级联单选

<!DOCTYPE html> <html lang"zh"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>级联选择器</title><script src"h…...

将Django连接到mysql

将Django连接到mysql 文章目录 将Django连接到mysql一.按照我的文章 在Django模型中的Mysql安装 此篇 的步骤完成mysql的基础配置二.Django配置 一.按照我的文章 ‘在Django模型中的Mysql安装’ 此篇 的步骤完成mysql的基础配置 基础配置具体内容 1.打开PowerShell 安装mysql的…...

每天五分钟深度学习框架pytorch:基于pytorch搭建循环神经网络RNN

本文重点 我们前面介绍了循环神经网络RNN,主要分析了它的维度信息,其实它的维度信息是最重要的,一旦我们把维度弄清楚了,一起就很简单了,本文我们正式的来学习一下,如何使用pytorch搭建循环神经网络RNN。 RNN的搭建 在pytorch中我们使用nn.RNN()就可以创建出RNN神经网络…...

【力扣刷题实战】无重复的最长字串

大家好&#xff0c;我是小卡皮巴拉 文章目录 目录 力扣题目&#xff1a; 无重复的最长字串 题目描述 解题思路 问题理解 算法选择 具体思路 解题要点 完整代码&#xff08;C&#xff09; 兄弟们共勉 &#xff01;&#xff01;&#xff01; 每篇前言 博客主页&#x…...

vulhub/joker 靶机----练习攻略

1. 靶机下载地址 https://download.vulnhub.com/ha/joker.zip 下载下来是ova文件&#xff0c;直接双击&#xff0c;在VMware打开&#xff0c;选择保存位置&#xff0c;点击导入。 2. 设置网卡模式为NAT&#xff0c;打开靶机 3.老规矩&#xff0c;打开kali&#xff0c;扫同C…...

Nuxt2 vue 给特定的页面 body 设置 background 不影响其他页面

首先认识一下 BODY_ATTRS 他可以在页面单独设置 head () {return {bodyAttrs: {form: form-body}};},设置完效果是只有这个页面会加上 接下来在APP.vue中添加样式...

【Go】运算符笔记

基本数学运算 Go 语言支持常见的 算术运算符&#xff0c;用于执行数学计算。 运算符说明加法-减法*乘法/除法%取余自增--自减 整数运算只能得到整数部分 package mainimport ("fmt""math" )func main() {go_math() }func go_math() {x, y : 8, 5fmt.Pr…...

常见的前端安全问题

前端安全是 Web 开发中至关重要的一环&#xff0c;以下是常见的前端安全问题及对应的防御措施&#xff1a; 1. XSS&#xff08;跨站脚本攻击&#xff09; 攻击原理 攻击者向页面注入恶意脚本&#xff08;如 JavaScript&#xff09;&#xff0c;在用户浏览器中执行&#xff0c;…...

基于Spring Boot的项目申报系统的设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…...

SPI驱动(九) -- SPI_Master驱动程序

文章目录 参考资料&#xff1a;一、SPI传输概述二、SPI传输的两种方法2.1 旧方法2.2 新方法 参考资料&#xff1a; 参考资料&#xff1a; 参考内核源码: drivers\spi\spi.c 一、SPI传输概述 SPI控制器的作用是发起与它下面挂接的SPI设备之间的数据传输&#xff0c;那么控制…...

Transformer网络发展概述2025.3.18

一.Transformer概述 1.1 定义与原理 Transformer是一种基于自注意力机制的深度学习模型&#xff0c;在处理序列数据时表现卓越。其核心原理包括&#xff1a; 自注意力机制 &#xff1a;允许模型同时考虑输入序列中的所有位置&#xff0c;捕捉语义关系多头注意力 &#xff1a…...

3.4 二分查找专题:LeetCode 69. x 的平方根

1. 题目链接 LeetCode 69. x 的平方根 2. 题目描述 给定一个非负整数 x&#xff0c;计算并返回 x 的平方根的整数部分&#xff08;向下取整&#xff09;。 示例&#xff1a; 输入&#xff1a;x 4 → 输出&#xff1a;2输入&#xff1a;x 8 → 输出&#xff1a;2&#xff0…...

机器人曲面跟踪Surface-Tracking

定义 机器人曲面跟踪&#xff08;Surface-Tracking&#xff09;是指机器人通过实时感知工件曲面的三维形貌&#xff0c;动态调整运动轨迹和位姿&#xff0c;以精确跟随曲面进行加工&#xff08;如打磨、抛光、喷涂等&#xff09;的技术。 力 - 位姿协同控制 力控模式&#xff…...

opencv中stitch图像融合

openv版本: opencv249 vs &#xff1a;2010 qt : 4.85 #include "quanjing.h"#include <iostream> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <open…...

深入解析ES6+新语法:复杂的迭代器与生成器

一、迭代器&#xff08;Iterator&#xff09;&#xff1a;数据遍历的统一协议 1. 迭代器协议的本质 **迭代器协议&#xff08;Iterator Protocol&#xff09;** 是一种标准化的数据访问接口&#xff0c;它要求对象实现一个 next() 方法&#xff0c;每次调用返回包含 { valu…...

【C语言】自定义类型:结构体

一、结构体类型的声明 我们前面学习操作符的时候已经接触过结构体了&#xff0c;下面我们回顾一下结构体的基本内容。 创建结构体的语法如上所示&#xff1a; struct是创建结构体的关键字&#xff0c;然后tag就是我们结构体的名称&#xff0c;member-list是结构体的成员列表&…...

微服务即时通信系统---(五)框架学习

目录 ODB 介绍 安装 build2安装 odb-compiler安装 ODB运行时库安装 mysql和客户端开发包安装 boost profile库安装 总体打包安装 总体卸载 总体升级 头文件包含和编译时指明库 ODB常见操作介绍 类型映射 ODB编程 类与接口介绍 mysql连接池对象类 mysql客户端…...

蓝桥杯练习day1:自除数

前言 自除数 是指可以被它包含的每一位数整除的数。 例如&#xff0c;128 是一个 自除数 &#xff0c;因为 128 % 1 0&#xff0c;128 % 2 0&#xff0c;128 % 8 0。 自除数 不允许包含 0 。 给定两个整数 left 和 right &#xff0c;返回一个列表&#xff0c;列表的元素…...

0基础 | 上下拉电阻典型应用场景

三极管典型运用&#xff1a; 上拉电阻 下拉电阻 限流电阻 此处开关为三极管 左侧电阻&#xff1a;驱动电阻【限流电阻】 &#xff08;控制mos管&#xff09; 下面电阻&#xff1a;下拉电阻【关断电阻】 %%作用1&#xff1a; &#xff08;因为IO口输出信号分为低电平&…...

MySQL数据高效同步到Elasticsearch的四大方案

目录 引言 一、为什么需要MySQL到ES的同步&#xff1f; 二、四大同步方案对比 三、方案详解与代码实战 1. 应用层双写&#xff1a;简单但强耦合 2. 定时任务同步&#xff1a;可控的准实时 3. Logstash JDBC&#xff1a;离线迁移利器 4. Binlog监听&#xff1a;生产级实…...

Docker详解

云是一种服务理念。在云里docker是业务的最小载体 doker是管理容器的引擎&#xff0c;为应用打包、部署平台、而非单纯的虚拟化技术 1.轻量级虚拟化 2.一致性 4.高效的资源利用 5.易于部署和扩展 docker和虚拟机的区别&#xff1a; 虚拟机&#xff1a;真机需要一个操作系…...

清晰易懂的Maven安装教程(含自定义依赖包位置)

初学者也能看懂的 Maven 安装教程&#xff08;含自定义依赖包位置&#xff09; Maven 是一个强大的项目管理和构建工具&#xff0c;广泛用于 Java 项目的依赖管理和构建自动化。本教程将手把手教你如何在 Windows 系统上安装 Maven&#xff0c;并配置环境变量&#xff0c;同时…...

王道数据结构6.3

顶点x的第一个邻接点以及下一个邻接点 int first_neighbor(Algraph G, int x){if(G.vertices[x].firstarc! nullptr){return G.vertices[x].firstarc->adjvex;}else return -1; }int next_neighbor(Algraph G,int x,int y){ArcNode *pG.vertices[x].firstarc;while(p! null…...

【Linux操作系统——学习笔记二】Linux简单导航命令操作

一、前言 学习Linux&#xff0c;本质上是学习在命令行下熟练使用Linux的各类命令。 命令行&#xff1a;是一种通过输入命令和参数与计算机系统进行交互的方式&#xff0c;可以使用各种字符化命令对系统发出操作指令&#xff0c;打开Linux终端&#xff0c;进入命令行界面。 …...

贪心算法——c#

贪心算法通俗解释 贪心算法是一种"每一步都选择当前最优解"的算法策略。它不关心全局是否最优&#xff0c;而是通过局部最优的累积来逼近最终解。优点是简单高效&#xff0c;缺点是可能无法得到全局最优解。 一句话秒懂 自动售货机找零钱&#xff1a;用最少数量的…...

SPI 总线协议

1、协议介绍 SPI&#xff0c;是英语 Serial Peripheral interface 的缩写&#xff0c;顾名思义就是串行外围设备接口。是 Motorola 首先在其 MC68HCXX 系列处理器上定义的。 SPI&#xff0c;是一种高速的&#xff0c;全双工&#xff0c;同步的通信总线。主节点或子节点的数据在…...

单片机开发资源分析的实战——以STM32G431RBT6为例子的单片机资源分析

目录 第一点&#xff1a;为什么叫STM32G431RBT6 从资源手册拿到我们的对STM32G431RBT6的资源描述 第二件事情&#xff0c;关心我们的GPIO引脚输出 第三件事情&#xff1a;去找对应外设的说明部分 第一点&#xff1a;为什么叫STM32G431RBT6 对于命名规则不太熟悉的朋友看这里…...

物联网(IoT)架构中,平台层的应用与技术

在物联网(IoT)架构中,平台层是连接物理设备(感知层)和应用服务(应用层)的核心部分。它负责数据的采集、处理、存储、分析以及设备管理等功能,是物联网系统的“大脑”。以下是平台层的主要功能及其技术实现手段: 平台层的主要功能 设备管理: 功能:管理物联网设备的注…...

大语言模型的压缩技术

尽管人们对越来越大的语言模型一直很感兴趣&#xff0c;但MistralAI 向我们表明&#xff0c;规模只是相对而言的&#xff0c;而对边缘计算日益增长的兴趣促使我们使用小型语言获得不错的结果。压缩技术提供了一种替代方法。在本文中&#xff0c;我将解释这些技术&#xff0c;并…...

JVM 2015/3/15

定义&#xff1a;Java Virtual Machine -java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&#xff0c;到处运行 自动内存管理&#xff0c;垃圾回收 数组下标越界检测 多态 比较&#xff1a;jvm/jre/jdk 常见的JVM&…...

DeepSeek辅助学术写作中期能力及提示词分享

目录 确立三论 收集资料 选取论据 展开论证 大家好这里是AIWritePaper官方账号&#xff01;更多内容&#x1f449;AIWritePaper~在如今这个学术圈的“快车道”上&#xff0c;时间就像是一场永不停歇的赛跑&#xff0c;而论文质量则是那颗我们拼命追逐的“金苹果”。最近一款…...

Git 实战指南:本地客户端连接 Gitee 全流程

本文将以 Gitee(码云)、系统Windows 11 为例,详细介绍从本地仓库初始化到远程协作的全流程操作 目录 1. 前期准备1.1 注册与配置 Gitee1.2 下载、安装、配置客户端1.3 配置公钥到 Gitee2. 本地仓库操作(PowerShell/Git Bash)2.1 初始化本地仓库2.2 关联 Gitee 远程仓库3. …...

汇编基础知识

机器语言 1、机器语言是机器指令的集合&#xff0c;机器指令就是机器可以正确执行的命令&#xff0c;由二进制数组成 2、当今我们常用的是pc机&#xff0c;由一个芯片完成上述功能&#xff0c;即CPU是一种微处理器&#xff0c;每一种微处理器由于自身硬件设计和内部构造不同都…...

线程池的拒绝策略适用场景思考

ThreadPoolExecutor有四种拒绝策略。刚开始学习线程池的时候我就觉得&#xff0c;就是应该当任务饱和&#xff08;达到拒绝策略&#xff09;时&#xff0c;就应该拒绝任务&#xff0c;抛出异常。最近仔细思考了下&#xff0c;既然线程池这么设计&#xff0c;也应该有一定的道理…...

on-policy对比off-policy

目录 持续更新。。。 on-policy与off-policy的定义 Q-learning属于on-policy算法还是off-policy算法&#xff1f; 为什么off-policy适用于从离线经验或多种探索策略中学习&#xff0c;明明 On-policy 也可以基于探索学习的啊&#xff1f; 重要性权重方法 off-policy方法可…...

如何记录Matlab程序运行过程中所占用的最大内存(续)

在上一篇博客中&#xff0c;我们讨论了如何记录Matlab程序运行过程中所占用的最大内存。 博客原文&#xff1a;如何记录Matlab程序运行过程中所占用的最大内存-CSDN博客 但经过测试发现&#xff0c;这与实际有非常大的差异。运行如下例子&#xff1a; clear;clc; profile on…...

解决MySQL字符集冲突引发的“Illegal mix of collations”错误

引言 在开发过程中&#xff0c;我们常常会遇到数据库层面的字符集兼容性问题。本文将通过一个典型的案例&#xff0c;分析因字符集不匹配导致的 Illegal mix of collations 错误&#xff0c;并提供完整的解决方案&#xff0c;帮助开发者彻底规避此类问题。 问题现象 假设我们…...

Vue3:F12后,页面弹出runtime errors及提示的解决办法

解决&#xff1a; vue.config.jsdevServer: {client: {overlay: false}, },关闭提示 main.js // 定义特性标志 window.__VUE_PROD_DEVTOOLS__ false window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ false...

学习笔记:黑马程序员JavaWeb开发教程(2025.3.17)

11.5 案例-文件上传-阿里云OSS-入门 出现报错&#xff1a;Process exited with an error: 1 (Exit value: 1)&#xff0c;点击exec那一行&#xff0c;出现错误原因&#xff1a;Command execution failed. 在CSDN上找到了解决方法&#xff1a; 之后出现新的报错&…...

EDAS:投稿经验-word版本-问题解决

1. 字体不对&#xff0c;字体未嵌入问题 问题&#xff1a;word转PDF后&#xff0c;总是显示有字体格式不对&#xff08;忘记截图了&#xff09;。 办法&#xff1a;1. EDAS投稿PDF格式问题-CSDN博客-PDF上修改 IEEE论文检测的字体未嵌入问题Times New Ro…...

【数据结构初阶第十九节】八大排序系列(下篇)—[详细动态图解+代码解析]

hello&#xff0c;好久不见&#xff01; 云边有个稻草人-CSDN博客 上篇内容&#xff0c;回顾一下吧【数据结构初阶第十八节】八大排序系列(上篇)—[详细动态图解代码解析]-CSDN博客 今天我们来学习下篇 目录 &#xff08;2&#xff09;快速排序 【挖坑法】 —思路 —思路…...

不可不知的分布式数据库-TiDB

不可不知的分布式数据库-TiDB 介绍TiDb架构TiDb与Mysql的区别功能特性性能表现数据可靠性运维管理成本 Docker部署TiDB1. 获取 TiDB 配置文件2. 启动 TiDB 集群3. 连接到 TiDB4. 停止和清理 TiDB 集群注意事项 实用案例TiDB实现分布式事务实现原理实现方式SQL 方式编程方式 注意…...

BUUCTF Pwn babyheap_0ctf_2017 Unsorted bin attack部分

checksec exeinfo 开启了全保护 64位 查看函数&#xff1a; 堆题 增删查改齐了 可以在编辑堆的时候重新设置大小 存在堆溢出 delete函数的指针清零了 无UAF 想法是通过unsorted bin泄露libc基址&#xff1a; from pwn import *p process(./babyheap) #p remote("node…...