【C++】手撕红黑树
文章目录
- 前言
- 一、红黑树的概念
- 二、红黑树的节点结构
- 三、红黑树的插入
- 四、红黑树的调整
- 1、叔叔存在且为红
- 2、叔叔不存在或存在且为黑
- 3、插入完整代码
- 4、总结
- 五、红黑树的验证
- 六、红黑树的删除
- 七、红黑树与 AVL 树的比较
- 八、红黑树的代码实现
前言
在网络上流传着这样一张图片:
这张图片表达的侧面意思是:红黑树非常难!!!但如果认真阅读了这篇的博客,并且你有 AVL 树的基础的话 (重点是 AVL 树的旋转),其实你会发现,红黑树难只是指红黑树比较抽象,但它的逻辑其实是比 AVL 树要简单的,并且红黑树的代码也不难写。
一、红黑树的概念
什么是红黑树
红黑树是一种平衡二叉搜索树,但和 AVL 树使用高度来控制平衡不同,红黑树在每个结点上增加一个存储位来表示结点的颜色,可以是Red或Black,然后通过对任何一条从根到叶子的路径上各个结点着色方式的限制来达到接近平衡;
红黑树通过对每个节点颜色的限制,从而使得整棵树的最长路径不超过最短路径的两倍 (注意是整棵树,对子树没有要求),这样红黑树就可以达到接近平衡。
注意:因为红黑树只要求整棵树中最长路径是最短路径的两倍,所以红黑树是近似平衡的,即子树中某一路径可能比另一条路径长两倍不止;但 AVL 是通过高度控制平衡的,且 AVL 能达到的平衡已经是二叉树中能够达到的最好平衡了,所以 AVL 树是绝对平衡的。
红黑树的性质
红黑树有如下性质:
- 每个结点不是红色就是黑色;
- 根节点是黑色的;
- 如果一个节点是红色的,则它的两个孩子结点是黑色的 – 不允许出现连续的红色节点;
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 – 每条路径都包含相同数量的黑色节点;
每个叶子结点都是黑色的 (此处的叶子结点指的是空结点)
特别注意第五点中的叶子节点并不是指我们平时说的叶子节点,而是指的空节点 (图中的NIL节点),它之所以要加这么一条规则是为了更好的标识每条路径 (比如认为图中的13 8 1 NIL 也是一条路径);但是我认为我们完全可以不用管第五点,而且这样也不会造成什么影响,因为如果不计算 NIL 节点,那么树中所有路径的黑色节点数量都会减一,并不会违反规则4,所以可以看到第五点我是用删除线进行修饰的。
思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?
我们可以在极端场景下来思考这个问题 – 极端场景下,当一条路径中的全部节点都为黑色时该路径为最短路径,当一条路径中黑色节点与红色节点交替时该路径为最长路径,此时最长路径刚好为最短路径的两倍;如下:
二、红黑树的节点结构
红黑树的节点结构和 AVL 树整体上类似,三个节点指针分别指向左孩子、右孩子和父亲,然后还包含一个 pair 键值对,和 AVL 树不同的时,红黑树不再需要 bf 变量来作为树的平衡因子,而是需要一个 col 变量来标识节点的颜色:
//节点颜色
enum Color {RED,BLACK
};template<class K, class V>
struct RBTreeNode {RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;std::pair<K, V> _kv;Color _col; //节点颜色RBTreeNode(const std::pair<K, V>& kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) //默认给成红色{}
};
可以看到,在构造函数的初始化列表中,我们将节点的颜色默认初始化为 RED,这是因为新增节点的颜色默认给成红色更合适,原因如下:
- 如果新增节点的颜色为红色,那么这里存在三种情况:一是新增节点为根节点,此时我们需要将节点颜色改为黑色;二是新增节点的父节点颜色为红色,由于红黑树中不能出现连续的红色节点,所以我们需要进行调整;三是新增节点的父节点颜色为黑色,此时我们不需要做任何事,因为新增节点没有违反红黑树的性质。
- 而如果新增节点的颜色为黑色,那我们一定需要对该节点进行调整,因为新增节点会导致这条路径的黑色节点数量比其他所有路径的黑色节点数量多一,违反了红黑树的性质4。
- 综合考量上面两种情况,我们认为将新增节点颜色默认设置为红色更合适 (黑色必调整且很难调整,红色可能调整且调整较易)。
三、红黑树的插入
红黑树插入的前面部分很简单,就是一般二叉搜索树的插入逻辑,需要注意的是如果插入的节点是根节点,则我们需要将节点的颜色改为黑色,因为新增节点默认是被初始化为红色的;
红黑树插入的难点在于检测插入后红黑树的性质是否被破坏,其一共可以分为两种情况:
- 父节点的颜色为黑色,此时没有违反红黑树任何性质,不需要调整;
- 父节点的颜色为红色,此时出现了连续红色节点,需要进行调整。
bool insert(const std::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->_left;}else if (kv.first > cur->_kv.first) {parent = cur;cur = cur->_right;}else {return false; //不允许重复节点}}//走到空开始插入,注意这里还需要修改父节点的指向//新增节点的颜色为默认被初始化为红色,所以这里不需要再显式赋值Node* newNode = new Node(kv);if (kv.first < parent->_kv.first)parent->_left = newNode;elseparent->_right = newNode;newNode->_parent = parent; //修改父节点指向//如果父节点颜色为红色,则需要进行调整//红黑树的调整
}
而红黑树的调整又分为两种情况:(约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点)
- 叔叔存在且为红,此时我们只需要进行变色;
- 叔叔不存在或者叔叔存在且为黑,此时我们需要进行旋转+变色。
下面我们来具体探讨这两种情况。
四、红黑树的调整
1、叔叔存在且为红
如果cur为红,p为红,g为黑,u存在且为红,此时我们只需要将p和u变黑,再将g变红即可 (约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点):
可以看到,当p为红时,由于g是p的父亲,所以g一定为黑,如果此时叔叔存在且为红,那么我们只需要将p和u变黑,再将g变红即可;这样做不会改变g这棵子树黑色节点的数量,所以不会影响到其他路径,同时g的子树中每条路径的黑色节点数量也是相同的,符合红黑树的性质。
节点颜色修改完毕之后,这里又分为两种情况:
- g是根节点,由于根节点必须为黑,所以我们需要将g的颜色置为黑;
- g不是根节点,由于我们将g变成了红色,而我们并不知道g的父亲是不是红色,所以我们需要将g作为新增节点 继续向上调整,然后再下一层循环中再进行判断与调整。
另外,和 AVL 树中旋转的图一样,这里给出的也是抽象图,其中 a/b/c/d/e 代表的是高度为 h 的红黑树子树,h 可以取任何非负整数,所以这张图代表的是一类情况,我们以 h == 1 为例 (图中给出的组合数并不一定是准确的):
注意:上面我们是以左左为例子进行讨论的,但其实上面的处理方式也适用于 右右、左右、右左 的情况 (注意:这里的左左、右右、左右、右左和上一节 AVL 树里面的其实是差不多的,只是红黑树不通过平衡因子来控制平衡,所以进行 if 条件需要进行相应的调整而已);
也就是说,只要叔叔存在且为红,那么无论是插入节点的父亲在祖父节点的左边还是右边,插入节点在父亲的左边还是右边都没有关系,统一将父节点和叔叔节点变黑,将祖父节点变红。
2、叔叔不存在或存在且为黑
之所以将叔叔不存在与叔叔存在且为黑分为一类情况是因为它们的处理方式是相同的 (约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点):
-
当u不存在时,cur一定是新插入节点,因为如果cur不是新插入节点,则cur和p中一定有一个节点的颜色是黑色,否则不满足性质4 – 每条路径黑色节点个数相同;此时我们的操作是旋转 + 变色,其中旋转会根据父节点位置和插入节点位置的不同分为四种情况,而变色是统一将旋转后子树的根节点变为黑色,将根的左右节点变为红色。
-
如果u存在且为黑,那么cur节点原来的颜色一定是黑色的,现在看到其是红色是因为cur的子树之前为情况1,然后情况1调整后将祖父节点也就是cur节点变成了红色,否则g左子树的黑色节点数量会比右子树黑色节点数量少1,违反性质4;此时我们的操作和u不存在时一模一样 – 旋转 + 变色,旋转分为四种情况,变色统一将旋转后子树的根节点变为黑色,将根的左右节点变为红色;所以说我们可以将u不存在和u存在且为黑分同一类 (本质是我们不会改变u及其子树的颜色)。
红黑树的旋转 – 和 AVL 树一样,红黑树会根据父节点位置的不同和插入节点位置的不同选择不同的旋转方式来进行调整,旋转一共分为四类:
- 右单旋 – 父节点在祖父节点的左侧,cur 在父节点的左侧;
- 左单旋 – 父节点在祖父节点的右侧,cur 在父节点的右侧;
- 左右双旋 – 父节点在祖父节点的左侧,cur 在父节点的右侧;
- 右左双旋 – 父节点在祖父节点的右侧,cur 在父节点的左侧。
可以看到,红黑树的旋转其实就是 AVL 树中的四种旋转,只不过红黑树中不再需要更新平衡因子,而是需要更新节点颜色而已;不过红黑树中叔叔不存在或存在且为黑情况下节点颜色的更新十分简单 – 统一将旋转后子树的根节点变为黑色,将根的左右节点变为红色即可。
需要注意的是,和情况1 – 叔叔存在且为红不同,由于这里我们将祖父节点的颜色改为黑色 (旋转后子树的根节点为祖父节点),所以不用考虑祖父的父节点为红色从而违反性质4的问题,即 这里我们不需要继续向上调整。
最后,由于左单旋、右单旋、左右双旋和右左双旋这四种旋转我们在上一节 AVL 树中已经讲解的十分清楚,所以这里我们不再重复讲解,而仅仅是给出左右双旋和右左双旋两种情况的例图 (由于右单旋的例图在上面我们已经给出,所以下面只给出双旋的例图),如果看不懂例图或对旋转的细节有疑问的同学可以再回顾一下上一节;
左右双旋的情况如下:
右左双旋的情况如下:
3、插入完整代码
旋转的代码如下:
bool insert(const std::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->_left;}else if (kv.first > cur->_kv.first) {parent = cur;cur = cur->_right;}else {return false; //不允许重复节点}}//走到空开始插入,注意这里还需要修改父节点的指向//新增节点的颜色为默认被初始化为红色,所以这里不需要再显式赋值Node* newNode = new Node(kv);if (kv.first < parent->_kv.first)parent->_left = newNode;elseparent->_right = newNode;newNode->_parent = parent; //修改父节点指向cur = newNode;//如果父节点颜色为红色,则需要进行调整,且最多调整到根while (parent && parent->_col == RED) {Node* grandfater = parent->_parent; //祖父节点if (grandfater == nullptr)break;//如果父节点在祖父节点的左边if (parent == grandfater->_left) {Node* uncle = grandfater->_right; //叔叔节点//情况一:叔叔存在且为红--父和叔叔变黑,爷爷变红,继续向上调整if (uncle && uncle->_col == RED) {parent->_col = BLACK;uncle->_col = BLACK;grandfater->_col = RED;//继续向上调整cur = grandfater;parent = cur->_parent;}//情况二:叔叔不存在或叔叔存在且为黑else {//如果cur在parent的左边,则右单旋--注意走到这里p已经是g的左边了if (cur == parent->_left) {rotateR(grandfater);//更新节点颜色parent->_col = BLACK;cur->_col = grandfater->_col = RED;}else { //左右双旋//先以parent为轴进行左单旋rotateL(parent);//再以grandfather进行右单旋rotateR(grandfater);//更新节点颜色--此时cur为子树的根节点cur->_col = BLACK;parent->_col = grandfater->_col = RED;}//最后需要跳出循环,因为此时不会再影响上层break;}}//如果父节点在祖父节点的右边else {Node* uncle = grandfater->_left; //叔叔节点//情况一:叔叔存在且为红--父和叔叔变黑,爷爷变红,继续向上调整if (uncle && uncle->_col == RED) {parent->_col = BLACK;uncle->_col = BLACK;grandfater->_col = RED;//继续向上调整cur = grandfater;parent = cur->_parent;}//情况二:叔叔不存在或叔叔存在且为黑else {//如果cur在parent的右边,则左单旋--注意走到这里p已经是g的右边了if (cur == parent->_right) {rotateL(grandfater);//更新节点颜色parent->_col = BLACK;cur->_col = grandfater->_col = RED;}else { //右左双旋//先以parent为轴进行右单旋rotateR(parent);//再以grandfather进行左单旋rotateL(grandfater);//更新节点颜色--此时cur为子树的根节点cur->_col = BLACK;parent->_col = grandfater->_col = RED;}//最后需要跳出循环,因为此时不会再影响上层break;}}}//在循环外面将_root的颜色重新置黑,避免循环内部将根节点的颜色变红了_root->_col = BLACK;return true;
}
4、总结
红黑树节点的调整其实很简单,一共就两种情况:(注意新增节点一定要初始化为红色)
- 父节点的颜色为黑色,此时没有违反红黑树任何性质,不需要调整;
- 父节点的颜色为红色,此时出现了连续红色节点,需要进行调整;调整又分为两种情况:
- 若叔叔存在且为红,则将父亲和叔叔置黑、祖父置红,然后继续往上调整;
- 叔叔不存在或者叔叔存在且为黑,此时我们需要进行旋转+变色 – 旋转根据父节点和cur节点位置的不同分为左单旋、右单旋、左右双旋和右左双旋;变色统一将旋转后的子树的根节点置黑、根节点的左右节点置红即可;调整完成后不需要继续往上调整。
简单来说,红黑树如何调整取决于叔叔节点。
五、红黑树的验证
红黑树的验证和 AVL 树一样,验证一共分为两部分:
- 验证是否为二叉搜索树;
- 验证其是否满足红黑树的性质;
验证二叉搜索树很简单,我们向树中插入数据,然后实现一个 InOrder 函数看遍历得到的数据是否有序即可。
//中序遍历
void InOrder() {return _inOrder(_root);
}void _inOrder(Node* root) {if (root == nullptr)return;_inOrder(root->_left);std::cout << root->_kv.first >> root->_kv.second >> std::endl;_inOrder(root->_right);
}
验证平衡则比较麻烦,因为我们需要分别验证红黑树的几个性质 – 根节点为黑色,不允许出现连续的红色,每条路径黑色节点的数量是红色节点数量的两倍;同时,在验证的过程中我们可以顺便将不符合要求的地方的节点的key值打印出来,方便发生错误时进行调试
bool Check(Node* root, int blackCount, int baseCount) {//如果走到空,说明这条路径结束了,此时看count与baseCount是否相等if (root == nullptr) {if (blackCount != baseCount) {std::cout << "此路径黑色节点数量有误,节点key值:" << root->_kv.first << std::endl;return false;}return true;}//检查是否出现连续红色节点if (root->_col == RED) {if (root->_parent->_col == RED) { //红色节点一定有父亲,因为根节点为黑色std::cout << "出现连续红色节点,节点key值:" << root->_kv.first << std::endl;}}if (root->_col == BLACK)blackCount++;return Check(root->_left, blackCount, baseCount) && Check(root->_right, blackCount, baseCount);
}//验证红黑树的性质
bool IsBanlance() {if (_root == nullptr)return true;//检查根节点if (_root->_col == RED) {std::cout << "根节点为红" << std::endl;return false;}//以最左路径黑色节点数量为基准,同其他路径的黑色节点数量相比较,看是否相等int baseCount = 0;Node* cur = _root;while (cur) {if (cur->_col == BLACK)baseCount++;cur = cur->_left;}//检查红黑树的其他性质return Check(_root, 0, baseCount);
}
如果我们用几千上万甚至几十上百万个随机数测试出来的结果都是真,那么我们的 红黑树 就基本上没问题了,当然前提是你的 IsBanlance 函数没写错。
六、红黑树的删除
和 AVL 树一样,红黑树的删除我们作为了解内容即可,如果对其特别感兴趣的同学可以参考一下《算法导论》或者《STL源码剖析》,也可以看看下面这位大佬的博客:红黑树 - Never - 博客园 (cnblogs.com)
七、红黑树与 AVL 树的比较
红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是 O(logN),但由于红黑树不追求绝对平衡,只需要保证最长路径不超过最短路径的2倍,所以在某些极端情况下红黑树的查询效率相较于 AVL 树要低一点点;例如当树左子树全部为黑色节点,右子树全部为一黑一红交替时,红黑树的高度差不多是 AVL 树的两倍,但是此时红黑树的查询效率仍然属于 O(logN) 这个量级;
不过也正是由于红黑树不要求左右子树绝对平衡,所以红黑树相较于 AVL 树在插入和删除时的旋转次数要少一些,所以在经常进行增删的结构中红黑树的性能比 AVL 树更优,而且红黑树实现比较简单,所以在实际业务中一般都使用红黑树,而不使用 AVL 树。
八、红黑树的代码实现
RBTree.h:
#pragma once//节点颜色
enum Color {RED,BLACK
};template<class K, class V>
struct RBTreeNode {RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;std::pair<K, V> _kv;Color _col; //节点颜色RBTreeNode(const std::pair<K, V>& kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) //默认给成红色{}
};template<class K, class V>
class RBTree {typedef RBTreeNode<K, V> Node;
public:bool insert(const std::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->_left;}else if (kv.first > cur->_kv.first) {parent = cur;cur = cur->_right;}else {return false; //不允许重复节点}}//走到空开始插入,注意这里还需要修改父节点的指向//新增节点的颜色为默认被初始化为红色,所以这里不需要再显式赋值Node* newNode = new Node(kv);if (kv.first < parent->_kv.first)parent->_left = newNode;elseparent->_right = newNode;newNode->_parent = parent; //修改父节点指向cur = newNode;//如果父节点颜色为红色,则需要进行调整,且最多调整到根while (parent && parent->_col == RED) {Node* grandfater = parent->_parent; //祖父节点if (grandfater == nullptr)break;//如果父节点在祖父节点的左边if (parent == grandfater->_left) {Node* uncle = grandfater->_right; //叔叔节点//情况一:叔叔存在且为红--父和叔叔变黑,爷爷变红,继续向上调整if (uncle && uncle->_col == RED) {parent->_col = BLACK;uncle->_col = BLACK;grandfater->_col = RED;//继续向上调整cur = grandfater;parent = cur->_parent;}//情况二:叔叔不存在或叔叔存在且为黑else {//如果cur在parent的左边,则右单旋--注意走到这里p已经是g的左边了if (cur == parent->_left) {rotateR(grandfater);//更新节点颜色parent->_col = BLACK;cur->_col = grandfater->_col = RED;}else { //左右双旋//先以parent为轴进行左单旋rotateL(parent);//再以grandfather进行右单旋rotateR(grandfater);//更新节点颜色--此时cur为子树的根节点cur->_col = BLACK;parent->_col = grandfater->_col = RED;}//最后需要跳出循环,因为此时不会再影响上层break;}}//如果父节点在祖父节点的右边else {Node* uncle = grandfater->_left; //叔叔节点//情况一:叔叔存在且为红--父和叔叔变黑,爷爷变红,继续向上调整if (uncle && uncle->_col == RED) {parent->_col = BLACK;uncle->_col = BLACK;grandfater->_col = RED;//继续向上调整cur = grandfater;parent = cur->_parent;}//情况二:叔叔不存在或叔叔存在且为黑else {//如果cur在parent的右边,则左单旋--注意走到这里p已经是g的右边了if (cur == parent->_right) {rotateL(grandfater);//更新节点颜色parent->_col = BLACK;cur->_col = grandfater->_col = RED;}else { //右左双旋//先以parent为轴进行右单旋rotateR(parent);//再以grandfather进行左单旋rotateL(grandfater);//更新节点颜色--此时cur为子树的根节点cur->_col = BLACK;parent->_col = grandfater->_col = RED;}//最后需要跳出循环,因为此时不会再影响上层break;}}}//在循环外面将_root的颜色重新置黑,避免循环内部将根节点的颜色变红了_root->_col = BLACK;return true;}//左单旋--右右void rotateL(Node* parent) {Node* subR = parent->_right; //右子树Node* subRL = subR->_left; //右子树的左子树//右子树的左子树链接到parent的右边parent->_right = subRL;if (subRL) //subR可能为空(h==0)subRL->_parent = parent;//parent链接到右子树的左边subR->_left = parent;Node* pparent = parent->_parent; //保存parent的父节点parent->_parent = subR;//让subR成为子树的根if (pparent) {if (pparent->_left == parent) {pparent->_left = subR;subR->_parent = pparent;}else {pparent->_right = subR;subR->_parent = pparent;}}else { //如果parent的parent为空,说明parent是整棵树根节点,此时subR成为新的根节点subR->_parent = nullptr;_root = subR;}}//右单旋--左左void rotateR(Node* parent) {Node* subL = parent->_left; //左子树Node* subLR = subL->_right; //左子树的右子树//将左子树的右子树链接到根的左边parent->_left = subLR;if (subLR) //应对h==0的情况subLR->_parent = parent;//将根链接到左子树的右边subL->_right = parent;Node* pparent = parent->_parent; //记录父节点的父节点parent->_parent = subL;//让subL成为子树的根if (pparent) {if (pparent->_left == parent) {pparent->_left = subL;subL->_parent = pparent;}else {pparent->_right = subL;subL->_parent = pparent;}}else { //如果parent的parent为空,说明parent是整棵树的根节点,此时subL成为新的根节点subL->_parent = nullptr;_root = subL;}}//中序遍历void InOrder() {return _inOrder(_root);}bool Check(Node* root, int blackCount, int baseCount) {//如果走到空,说明这条路径结束了,此时看count与baseCount是否相等if (root == nullptr) {if (blackCount != baseCount) {std::cout << "此路径黑色节点数量有误,节点key值:" << root->_kv.first << std::endl;return false;}return true;}//检查是否出现连续红色节点if (root->_col == RED) {if (root->_parent->_col == RED) { //红色节点一定有父亲,因为根节点为黑色std::cout << "出现连续红色节点,节点key值:" << root->_kv.first << std::endl;}}if (root->_col == BLACK)blackCount++;return Check(root->_left, blackCount, baseCount) && Check(root->_right, blackCount, baseCount);}//验证红黑树的性质bool IsBanlance() {if (_root == nullptr)return true;//检查根节点if (_root->_col == RED) {std::cout << "根节点为红" << std::endl;return false;}//以最左路径黑色节点数量为基准,同其他路径的黑色节点数量相比较,看是否相等int baseCount = 0;Node* cur = _root;while (cur) {if (cur->_col == BLACK)baseCount++;cur = cur->_left;}//检查红黑树的其他性质return Check(_root, 0, baseCount);}private:void _inOrder(Node* root) {if (root == nullptr)return;_inOrder(root->_left);std::cout << root->_kv.first << ":" << root->_kv.second << std::endl;_inOrder(root->_right);}private:Node* _root = nullptr;
};
test.cpp:
#define _CRT_SECURE_NO_WARNINGS 1#include <iostream>
using namespace std;
#include "RBTree.h"//验证搜索树
void RBTree_test1() {int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };RBTree<int, int> rb;for (auto e : a)rb.insert(std::make_pair(e, e));rb.InOrder();
}//验证红黑树的性质
void RBTree_test2() {//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };RBTree<int, int> rb;for (auto e : a)rb.insert(std::make_pair(e, e));rb.InOrder();cout << rb.IsBanlance() << endl;
}//增大测试用例--采用N个随机数来验证
void RBTree_test3() {srand((size_t)time(0));const int N = 10000;RBTree<int, int> rb;for (int i = 0; i < N; i++) {int x = rand();rb.insert(make_pair(x, x));}cout << rb.IsBanlance() << endl;
}int main() {//RBTree_test1();//RBTree_test2();RBTree_test3();return 0;
}
相关文章:

【C++】手撕红黑树
文章目录前言一、红黑树的概念二、红黑树的节点结构三、红黑树的插入四、红黑树的调整1、叔叔存在且为红2、叔叔不存在或存在且为黑3、插入完整代码4、总结五、红黑树的验证六、红黑树的删除七、红黑树与 AVL 树的比较八、红黑树的代码实现前言 在网络上流传着这样一张图片&am…...

Java中的CAS实现原理
文章目录一、什么是CAS?二、JAVA中如何实现CAS操作三、CAS在JUC中的运用四、ABA问题一、什么是CAS? 在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行…...

什么是华为云对象存储OBS?它有什么优势?
华为对象存储OBS(Object Storage Service)是一种高可用、高可靠、高性能的云存储服务,能够为企业和个人用户提供强大的数据存储和管理功能。本文将对华为对象存储OBS的特点、优势和未来发展进行详细介绍。 一、华为对象存储OBS的特点 1.对象…...

你知道照片怎么变清晰吗?增强照片清晰度的方法
相信很多小伙伴都会有这种的经历,去游玩时高高兴兴的拍照留念,结果拍出来的照片不是很尽人意。或者是画面还没聚焦好,就按下快门,导致拍摄出来的照片变模糊了。很多小伙伴遇到这种情况都很烦恼,照片丢了可惜࿰…...

NOIP模拟赛 轰炸(bomb)
题目描述 有nnn座城市,城市之间建立了mmm条有向的地下通道。 你需要发起若干轮轰炸,每轮可以轰炸任意多的城市。但在每次轰炸城市中,不能同时存在两个城市i,ji,ji,j满足可以通过地下通道从城市iii到达城市jjj。你需要求出最少需要多少轮可以…...

Linux系统之安装PHP环境
Linux系统之安装PHP环境 一、PHP介绍1.PHP简介2.PHP优势3.php7版本特点二、本地环境介绍1.环境规划2.检查操作系统版本3.检查当前yum仓库三、安装PHP5.4版本1.查看可安装php版本2.使用yum安装php3.安装httpd服务4.关闭selinux和设置防火墙5.编辑index.php测试文件6.测试php环境…...

MySQL8的安装教程
MySQL8的安装教程 1.安装包的下载 如果不想去官网下载的话可以去百度网盘进行下载。 MySQL :: Download MySQL Community Server mysql-8.0.28-winx64.zip_免费高速下载|百度网盘-分享无限制 (baidu.com) 提取码:0001 2.解压 3.创建一个my.ini的文件 最好是创建…...

日入500+的程序员都在用的“接私活”平台
网上总说程序员的薪资很高,这我可就不同意了: 程序员的薪资哪里是很高,而是非常高!而会接私活的程序员更是能拿到更高的收入!作为一个程序员,这些接私活的网站一定要收藏起来,让你在“八小时外…...

MySQL表设计思路(一对多、多对多...)
要开始单独负责需求了,捋一捋表设计的思路。 文章目录一、MySQL中的数据类型二、一对一的关系设计二、一对多的关系设计三、多对多的关系设计四、经验总结一、MySQL中的数据类型 字符串类型 varchar:即variable char ,可边长度的字符串&#…...

内存对齐:C/C++编程中的重要性和技巧
C/C中的内存对齐前言基本概念 什么是内存对齐?内存对齐的定义内存对齐的作用数据类型的大小ARM 64 位架构和 x86_64 架构下的数据类型大小ARM 32 位架构下的数据类型大小内存对齐的边界填充字节的作用内存对齐的原理结构体中的内存对齐结构体的定义和使用结构体中成…...

C++ Primer第五版_第七章习题答案(41~50)
文章目录练习7.411、头文件2、源文件3、主函数练习7.42练习7.43练习7.44练习7.45练习7.46练习7.47练习7.48练习7.49练习7.50练习7.41 使用委托构造函数重新编写你的Sales_data 类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的…...

python玄阶斗技--NumPy入门
目录 一.NumPy介绍 二.创建数组 1.一维数组创建 2.二维数组创建 3.zeros函数 4.ones函数 5.empty函数 6.arange函数 三.NumPy的数学操作 1.基本运算 2.矩阵运算 3.ndarray类的方法 四.数组堆叠 五.数组分隔 一.NumPy介绍 在这里对NumPy的介绍我不想扯太多…...

VR黑科技丨远离拥挤,VR直播开启沉浸式赏樱新姿势
春光兮婉转,珞樱兮盛绽,又是一年樱花季,全国各地大部分地区的樱花进入盛花期,尤其是武汉,东湖樱园踏青赏花的游人如织、摩肩擦踵,勾勒一幅“人人人人人人人花人人人人人”的盛景。 为了一睹樱花“芳容”&am…...

ts的一些用法
1.交叉类型 & ---多个类型属性的集合 1.1类型别名实现 type Person {name:string} type Children Person & {age:number} let newPerson:Children {// name:hahah,name:hhaah,age:18 } 1.2 接口类型实现 interface Inter1{name:string } interface Inter2{name:…...

云计算面试总结
shell脚本对日志进行备份 shell 对日志备份 #!/bin/bash if [ -d /log/bak/ ] || mkdir -p /log/bak/ thentar Pcf /log/bak/log_$(date %Y%m%d)$(date %H%M%S).tar.gz /var/log/*.logecho "干完!可以约会啦" fi存在的问题: 说的太快&a…...

(DP)买不到的数目【蓝桥杯】(裴蜀定理)
买不到的数目 小明开了一家糖果店。 他别出心裁:把水果糖包成4颗一包和7颗一包的两种。 糖果不能拆包卖。 小朋友来买糖的时候,他就用这两种包装来组合。 当然有些糖果数目是无法组合出来的,比如要买 10 颗糖。 你可以用计算机测试一下&#…...

Docker使用DockerFile部署Go项目
Docker使用DockerFile部署Go项目1. 文章说明2. Go项目打包到Linux2.1 学习链接与知识点2.2. 打包生成 main 文件2.3 Docker部署Go项目1. 文章说明 目的:将打包生成的 main 文件,在Docker里面,使用Dockerfile文件,生成镜像与容器&…...

C++ Primer第五版_第七章习题答案(31~40)
文章目录练习7.31练习7.32练习7.33练习7.34练习7.35练习7.36练习7.37练习7.38练习7.29练习7.40练习7.31 定义一对类X 和Y,其中X 包含一个指向 Y 的指针,而Y 包含一个类型为 X 的对象。 class Y;class X{Y* y nullptr; };class Y{X x; };练习7.32 定义你…...

基于springboot实现学生成绩管理系统【源码+论文】分享
基于springboot实现学生成绩管理系统演示开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7 数据库工具:Navicat11 开发软件:eclipse/myeclipse/idea Maven包&…...

Linux diff 命令
Linux diff 命令用于比较文件的差异。 diff 以逐行的方式,比较文本文件的异同处。如果指定要比较目录,则 diff 会比较目录中相同文件名的文件,但不会比较其中子目录。 语法 diff [-abBcdefHilnNpPqrstTuvwy][-<行数>][-C <行数&g…...

unity动画状态机
介绍: 动画状态机(Animation State Machine)是Unity中用于控制动画状态转换的工具,它由多个状态(State)和转换(Transition)组成,可以通过状态转换来控制动画的播放行为。…...

溯源(五)之攻击源的获取
溯源(一)之溯源的概念与意义 溯源(二)之 windows-还原攻击路径 溯源(三)之Linux-入侵排查 溯源(四)之流量分析-Wireshark使用 溯源整体流程的思维导图 攻击源的获取 1、获取哪些数…...

【redis】redis淘汰策略
一、说明 1.redis key没有设置过期时间被redis主动删除了 2.当redis已用内存超过maxmemory限定时,触发主动清理策略 3.主动清理策略在redis4.0之前一共实现了6种内存淘汰策略,在4.0之后,增加了2种,总共8种 二、淘汰策略 2.1 针对…...

指针和数组(二)
目录 指针和数组 数组名和指针的区别 多维数组 数组指针 语法 作用 内存大小 自增运算 【】运算 指针和数组 结论:数组的本质就是指针。数组的【】运算同样可以用指针来运算 证明 C代码 int array[5];int* ptr{ &array[0] };*ptr 5;array[0] 5;arr…...

Linux WIFI 驱动实验
目录WIFI 驱动添加与编译向Linux 内核添加WIFI 驱动配置Linux 内核编译WIFI 驱动驱动加载测试wireless tools 工具移植与测试wireless tools 移植wireless tools 工具测试wpa_supplicant 移植openssl 移植libnl 库移植wpa_supplicant 移植WIFI 联网测试RTL8188 USB WIFI 联网测…...

UART驱动情景分析-write
一、write过程分析 App写: 使用行规程来写数据最终存入uart_state->xmit的buffer里 硬件发送: 使用硬件驱动中uart_ops->start_tx开始发送具体的发送方式有两种:通过DMA、通过中断 中断方式: 方法1:直接使能tx …...

Metasploit入门到高级【第四章】
来自公粽号:Kali与编程预计更新第一章:Metasploit 简介 Metasploit 是什么Metasploit 的历史和发展Metasploit 的组成部分 第二章:Kali Linux 入门 Kali Linux 简介Kali Linux 安装和配置常用命令和工具介绍 第三章:Metasploi…...

java 继承super
在java继承中,如果子类继承父类,在子类中要给用构造器给父类的属性赋值,需要用到 super 举例,Son类继承Father 类,便于理解 在 new Son(String name, int age) 传入name,和age的值 将会调用Son这个构造器…...

Java学习笔记——多态
2.1 概述 引入 多态是继封装、继承之后,面向对象的第三大特征。 生活中,比如跑的动作,小猫,小狗和大象,跑起来都是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来是不一样的。可见&#x…...

Python处理JSON数据
Python和JSON JavaScript Object Notation (JSON) 是一种轻量级的数据交换格式,通常用于Web应用程序之间的数据交换。JSON的设计使得它非常易于人和机器阅读和编写。JSON数据格式与Python数据结构非常相似,因此Python中提供了一个json模块,用…...