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

C++ 红黑树万字详解(含模拟实现(两种版本))

目录

红黑树的概念

红黑树的性质

红黑树的删除

红黑树与AVL树的比较

红黑树的应用

红黑树的模拟实现


红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。

通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

最长路径 <= 最短路径*2

注:虽然严格上来看,红黑树不如AVL树(红黑树的高度一般要比AVL树高),但是实际上并没有太大的影响(因为最多也就是多出一倍的高度),通过之前AVL树那里的测试能知道,插入 几千万 个值,AVL树的高度也就 二十几,即使高度翻一倍,查找效率还是超级快。

红黑树出现的原因:AVL树虽然已经很优秀了,但是AVL树为了控制其严格的平衡性,付出了很多的代价,比如插入和删除操作时可能需要很多次的旋转调节。

红黑树的性质

1. 每个结点不是红色就是黑色

2. 根节点是黑色的 

3(重点). 如果一个节点是红色的,则它的两个孩子结点是黑色的(不存在连续的红色结点)

4(重点). 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点(每条路径都存在相同数量的黑色结点) 

5. 每个叶子结点(NIL)都是黑色的(此处的叶子结点(NIL)指的是空结点)

思考:为什么满足上面的(1、2、3、4)性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?

答:满足上面的性质时有:1、最短的路径就是全黑的路径。2、最长的路径就是一黑一红间隔的路径。

注:红黑树的路径指的是从根走到NIL(空)才算一条路径。

注:红黑树中一定要永远保证性质4不能被破坏。-- 所以新插入的节点一定要默认为红色

红黑树的删除

红黑树的删除这里不做讲解,有兴趣的可参考:《算法导论》或者《STL源码剖析》

https://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html

红黑树与AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

红黑树的应用

1. C++ STL库 -- map / set、mutil_map / mutil_set

2. Java 库

3. linux内核

4. 其他一些库

红黑树的模拟实现

红黑树插入时单纯的颜色调整

红黑树插入时颜色调整+单旋 

 红黑树插入时颜色调整+双旋

RBTree.h

#pragma once#include<iostream>
#include<vector>
#include<assert.h>enum Color // 颜色就定义成枚举值
{BLACK,RED
};// 注:这个版本的RBTree是写死的,只适合用来给map封装,库里的操作则更nb,是一种泛型编程。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; // AVL树用的是平衡因子,红黑树用的是颜色RBTreeNode(const std::pair<K, V>& kv,Color color = RED) // 默认新增的节点为红色:_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_col(color){}
};template<class K,class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
public:// 红黑树的插入和AVL树的插入是差不多的,都要遵循着二叉搜索树的规则插入,只不过AVL还可能需要调整平衡因子,而红黑树还可能需要调整颜色// 注:新插入的结点,默认为红色(可能违法红黑树的性质3),默认为黑色的话,必然会违反红黑树的性质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){parent = cur;if ((cur->_kv).first < kv.first){cur = cur->_right;}else if ((cur->_kv).first > kv.first){cur = cur->_left;}else{return false;}}cur = new Node(kv); // 新增结点默认为红色if ((parent->_kv).first > kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent; // 上面只是完成了新节点的插入// 因为新节点的默认颜色是红色。// 因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;// 但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:// 约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点// 情况一:cur为红,p为红,g为黑,u存在且为红(还需要继续向上调整,因为调整一次之后这颗(子)树的根结点还是红色)// 解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。//	 	    (如果g本身就是这整棵树的根结点,那么g可以就保持为黑色,不用变红)//          (不过模拟实现的过程还是直接就不管三七二十一先把g变红了再说,如果最后发现g就是整棵树的根,再把g变为黑)// 注:看截图:红黑树插入时单纯的颜色调整// 情况二:cur为红,p为红,g为黑,u不存在/u存在且为黑(无需继续向上调整,因为调整一次之后这颗(子)树的根结点已经是黑色了) // 此时又有两种情况:// 情况一(单旋):若p为g的左孩子,cur为p的左孩子,则进行右单旋转;//			     相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转;//         所以:单旋要先将p变为黑,g变为红。(谁最后变成这颗子树的根,谁就要变黑)// 注:看截图:红黑树插入时颜色调整+单旋// 情况二(双旋):若p为g的左孩子,cur为p的右孩子,则进行左右双旋;//			     相反,p为g的右孩子,cur为p的左孩子,则进行右左双旋;//		   注意:双旋这里会先对p进行一个单旋,单旋完之后p就变到了cur的位置,cur变到了p的位置//         所以:双旋要先将cur变为黑,g变为红。(谁最后变成这颗子树的根,谁就要变黑)// 注:看截图:红黑树插入时颜色调整+双旋// 补充:如果 u结点 不存在,则 cur结点 一定是新插入的结点,//	     因为如果 cur结点 不是新插入结点,//       则 cur结点 和 p结点 中一定有一个结点的颜色是黑色,这就会导致不满足性质4:每条路径黑色结点个数相同。//		 如果 u结点 存在且为黑,则 cur结点 原来的颜色一定是黑色的,//		 现在看到 cur结点 为红色的原因是因为 cur结点 的子树在调整的过程中将 cur结点 的颜色由黑色改为了红色。while (parent && parent->_col == RED) // 当新增结点的父亲存在且颜色也是红色的时候才需要进行调整{Node* g = parent->_parent;if (g->_left == parent){Node* u = g->_right;if (u && u->_col == RED) // 情况一{parent->_col = u->_col = BLACK;g->_col = RED;cur = g;parent = cur->_parent;}else // 情况二{if (cur == parent->_left) // 右单旋{parent->_col = BLACK;g->_col = RED;RotateR(g);}else // 左右双旋(因为红黑树没有平衡因子,所以这里的双旋没有什么其他的处理){cur->_col = BLACK;g->_col = RED;RotateL(parent);RotateR(g);}break;}}else{Node* u = g->_left;if (u && u->_col == RED) // 情况一{ parent->_col = u->_col = BLACK;g->_col = RED;cur = g;parent = cur->_parent;}else // 情况二{if (cur == parent->_right) // 左单旋{parent->_col = BLACK;g->_col = RED;RotateL(g);}else // 右左双旋(因为红黑树没有平衡因子,所以这里的双旋没有什么其他的处理){cur->_col = BLACK;g->_col = RED;RotateR(parent);RotateL(g);}break;}}}// 注:这一部分调整可以看截图_root->_col = BLACK; // 不管上面的循环最终是调整到哪结束的(可能有调整到根,可能没有),都把根的颜色始终变为黑return true;}void RotateR(Node* parent) // 右单旋 {// 要抬高左边,降低右边Node* subL = parent->_left; Node* subLR = subL->_right; // subL 的右子树 要给给 parent,当 parent 的左子树// subL的值 < subLR的值 < parent的值parent->_left = subLR; // 将 subL 的右子树给给 parentif (subLR) // 这边要注意 subLR 可能是 nullptrsubLR->_parent = parent;subL->_right = parent; // 抬高左边subL->_parent = parent->_parent; // 要注意,要先把 parent 的 parent 给给 subLif (parent->_parent) // 还要注意改 parent 的 parent // 因为 parent 可能就是根,那么 parent 的 parent 就是 nullptr,所以这里要 if 判断一下{if (parent->_parent->_left == parent){parent->_parent->_left = subL;}else{parent->_parent->_right = subL;}}else // 如果原本的 parent 是根的话,就更新一下根{_root = subL;}parent->_parent = subL; // 降低右边}void RotateL(Node* parent) // 左单旋 {// 要抬高右边,降低左边Node* subR = parent->_right; Node* subRL = subR->_left; // subR 的左子树 要给给 parent,当 parent 的右子树// parent的值 < subRL的值 < subR的值parent->_right = subRL; // 将 subR 的右子树给给 parentif (subRL) // 这边要注意 subRL 可能是 nullptrsubRL->_parent = parent;subR->_left = parent; // 抬高右边subR->_parent = parent->_parent; // 要注意,要先把 parent 的 parent 给给 subRif (parent->_parent) // 还要注意改 parent 的 parent // 因为 parent 可能就是根,那么 parent 的 parent 就是 nullptr,所以这里要 if 判断一下{if (parent->_parent->_left == parent){parent->_parent->_left = subR;}else{parent->_parent->_right = subR;}}else // 如果原本的 parent 是根的话,就更新一下根{_root = subR;}parent->_parent = subR; // 降低右边}void InOrder(){_InOrder(_root);std::cout << std::endl;}bool IsBalance(){if (_root->_col == RED) return false; // 检查根是否为黑int ReferenceValues = 0; // 参考值,记录第一次算出的任意一条路径的黑子数量 -- 因为理论上所有路径的黑子数量要相等// 这个参考值也可以弄成全局变量(是真正的全局,不是在这个类里面定义),但这样就需要你在每一次调用 IsBalance() 这个函数之前,把它置为0,才能保证该函数逻辑正确Node* cur = _root;while (cur){if (cur->_col == BLACK) ++ReferenceValues;cur = cur->_left; // 这里就记录一下最左路径的黑子数,记录其他路径的也行,随你。}return Check(_root, 0, ReferenceValues);}private:bool Check(Node* root, int BlackNum, const int ReferenceValues){if (root == nullptr){if (BlackNum != ReferenceValues){ std::cout << "存在黑子数不相等的路径" << std::endl; // 违法规则4return false;}return true;}if (root->_col == RED && root->_parent->_col == RED){std::cout << "结点值为:" << root->_kv.first << "处存在连续的红色结点" << std::endl; // 违法规则3return false; // 检查有没有连续的红色结点}if (root->_col == BLACK) ++BlackNum; // 记录黑子的数量 // BlackNum要定义成函数参数,不要是全局变量,且不能用传址/传引用,不然 回溯 的时候,就需要你手动“恢复现场”return Check(root->_left, BlackNum, ReferenceValues) && Check(root->_right, BlackNum, ReferenceValues);}void _InOrder(Node* root /* = _root */) // 注意:这里不能给缺省值 _root,因为 _root 需要this指针调用,但是this指针本身就是形参,这样写玩不了。{if (root == nullptr)return;_InOrder(root->_left); // 左std::cout << (root->_kv).first << ":" << (root->_kv).second << ":" << root->_col << std::endl; //根_InOrder(root->_right); // 右}private:Node* _root=nullptr;size_t _Size = 0; // 这个 _Size 根据实际情况,可加可不加
};void Test_RBTree1()
{int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };int a1[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };int a2[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };RBTree<int, int> t;for (auto& x : a2){t.insert({ x,x });std::cout << x << "->" << t.IsBalance() << std::endl;}// t.InOrder(); // 这里有个问题,没法给 InOrder() 这个函数传参,因为 _root 是私有函数,你在这里调不动。// 那么该怎么解决呢?// 给个缺省值吗? 这是不行的,给不了// 那该怎么办?// 三种方法:1、把这个测试函数定义成友元。(这个方法很不好,就一个测试函数又不是要经常用,定义成友元有点太没边界感了)//           2、学Java,弄一个 Get() 函数,把 _root 拿出来。//			 3、看上面的操作。(封装一下,套一层)
}void Test_RBTree2()
{const int N = 1000000;srand((unsigned int)time(nullptr));std::vector<int> v(N);RBTree<int, int> t;for (int i = 0; i < N; ++i){v[i] = rand() + i;}for (auto x : v){t.insert({ x,x });}std::cout << "t.IsBalance():" << t.IsBalance() << std::endl;
}

RBTree - 优化版.h

#pragma once// 注:这里是优化版(泛型编程)#include<iostream>
#include<vector>
#include<assert.h>enum Color // 颜色就定义成枚举值
{BLACK,RED
};template<class D>
struct RBTreeNode
{RBTreeNode<D>* _left;RBTreeNode<D>* _right;RBTreeNode<D>* _parent; D _data;Color _col;RBTreeNode(const D& data,Color color = RED):_left(nullptr),_right(nullptr),_parent(nullptr),_data(data),_col(color){}
};template<class D, class Ref, class Ptr>
struct __RBTreeIterator
{typedef RBTreeNode<D> Node;typedef __RBTreeIterator<D, Ref, Ptr> Self;Node* _node;__RBTreeIterator(Node* node):_node(node){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& s){return _node != s._node;}// ++ 只需要考虑中序的下一个!!Self& operator++() // 重难点!!!{// ++也是遵循着中序遍历,左 根 右 来走的if (_node->_right) // 如果右不为空{// 就去找右的最左节点Node* leftMin = _node->_right;while (leftMin && leftMin->_left){leftMin = leftMin->_left;}_node = leftMin;}else // 如果右为空(右访问完了,这颗(子)树也访问完了),就一直向上找,找到 cur == parent->_left(左访问完了,就该访问根了) 的时候{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right) // 注意:当parent为nullptr时,说明整棵树都遍历完了{cur = parent;parent = cur->_parent;}_node = parent;}return *this;}Self& operator--() // 思路就是跟++反过来{// -- 也是遵循着中序遍历,左 根 右 来走的if (_node == nullptr) // 因为按照自己写的这棵树的构造,end()会是nullptr,所以无法实现 --end()的操作,库里的可以,因为库里是用了一个哨兵位的头节点去充当这个end()。{// 要返回整棵树的最右节点,那么就需要先找到这棵树的根,但因为这里没法获取到树的根,所以实现不了	}else if (_node->_left) // 如果左不为空{// 就去找左的最右节点Node* rightMin = _node->_left;while (rightMin && rightMin->_right){rightMin = rightMin->_right;}_node = rightMin;}else // 如果左为空,就一直向上找,找到 cur==parent->_right 的时候{ Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left) // 注意:当parent为nullptr时,说明整棵树都遍历完了{cur = parent;parent = cur->_parent;}_node = parent;}return *this;}
};// 注:第二个模板参数D的设计是一种泛型编程的体现,第三个模板参数KeyOfD(一个仿函数,用于获取 Key)的设计也是一种泛型编程的体现。
template<class K,class D,class KeyOfD> // 为了避免混淆,把第二个参数取名叫D(Data,它可能是Key,也可能是pair<K,V>)
class RBTree
{typedef RBTreeNode<D> Node;
public:typedef __RBTreeIterator<D, D&, D*> iterator;typedef __RBTreeIterator<D, const D&, const D*> const_iterator;RBTree() = default; // C++11加的一个关键字,强制编译器去生成一个默认的构造函数RBTree(const RBTree<K, D, KeyOfD>& t){_root = Copy(t._root); // 这里复用 insert 是不太好的,因为插入的顺序不同,最后得到的树的形状可能也会不同(尽管值都是一样的)} RBTree<K, D, KeyOfD>& operator=(RBTree<K, D, KeyOfD> t) // 注意,赋值要用传值,不能用引用,因为下面写的是现代写法{swap(_root, t._root);return *this;}~RBTree(){Destroy(_root); // 写一个Destroy()函数去递归析构_root = nullptr;}iterator begin() // 返回中序遍历的第一个{Node* leftMin = _root;while (leftMin && leftMin->_left) // 第一个判断是处理空树的情况{leftMin = leftMin->_left;}return iterator(leftMin);}iterator end() // 返回中序遍历的最后一个的下一个(按照自己写的这棵树的构造,这个end()会是nullptr,库里是用了一个哨兵位的头节点去充当这个end()){return iterator(nullptr);}const_iterator begin() const // 返回中序遍历的第一个{Node* leftMin = _root;while (leftMin && leftMin->_left) // 第一个判断是处理空树的情况{leftMin = leftMin->_left;}return const_iterator(leftMin);}const_iterator end() const // 返回中序遍历的最后一个的下一个(按照自己写的这棵树的构造,这个end()会是nullptr,库里是用了一个哨兵位的头节点去充当这个end()){return const_iterator(nullptr);}iterator Find(const K& key) // 第一个模板参数K的作用就是体现在这种地方 -- 查找的时候只需要 Key,是按照 Key 来查找的,并不需要 Value,如果没有第一个模板参数 K,那么这里的 Find() 就要写两份,一份的参数就是 K,另一份的参数是 pair<K, V>。{KeyOfD kod;Node* cur = _root;while (cur){if (kod(cur->_data) < key){cur = cur->_right;}else if (kod(cur->_data) > key){cur = cur->_left;}else{return iterator(cur);}}return end();}// 红黑树的插入和AVL树的插入是差不多的,都要遵循着二叉搜索树的规则插入,只不过AVL还可能需要调整平衡因子,而红黑树还可能需要调整颜色// 注:新插入的结点,默认为红色(可能违法红黑树的性质3),默认为黑色的话,必然会违反红黑树的性质4。pair<iterator,bool> Insert(const D& data){if (_root == nullptr){_root = new Node(data);_root->_col = BLACK; //根结点是黑色return make_pair(iterator(_root), true);}KeyOfD kod; // RBTree是不知道这个传过来的D是Key还是pair<K,V>,所以需要在map和set中加一个仿函数用于获取D类型的data的keyNode* parent = nullptr;Node* cur = _root; while (cur){parent = cur;if (kod(cur->_data) < kod(data)){cur = cur->_right;}else if (kod(cur->_data) > kod(data)){cur = cur->_left;}else{return make_pair(iterator(cur), false);}}cur = new Node(data); // 新增结点默认为红色Node* newnode = cur; // 保存一下最初的cur用于最下面的返回值,不然下面的颜色调整,调整两下,cur都不知道变成哪个节点了if (kod(parent->_data) > kod(data)){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent; // 因为新节点的默认颜色是红色。// 因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;// 但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:// 约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点// 情况一:cur为红,p为红,g为黑,u存在且为红(还需要继续向上调整,因为调整一次之后这颗(子)树的根结点还是红色)// 解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。//		    (如果g本身就是这整棵树的根结点,那么g可以就保持为黑色,不用变红)//          (不过模拟实现的过程还是直接就不管三七二十一先把g变红了再说,如果最后发现g就是整棵树的根,再把g变为黑)// 情况二:cur为红,p为红,g为黑,u不存在/u存在且为黑(无需继续向上调整,因为调整一次之后这颗(子)树的根结点已经是黑色了) // 此时又有两种情况:// 情况一(单旋):若p为g的左孩子,cur为p的左孩子,则进行右单旋转;//			     相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转;//         所以:单旋要先将p变为黑,g变为红。// 情况二(双旋):若p为g的左孩子,cur为p的右孩子,则进行左右双旋;//		 	     相反,p为g的右孩子,cur为p的左孩子,则进行右左双旋;//		   注意:双旋这里会先对p进行一个单旋,单旋完之后p就变到了cur的位置,cur变到了p的位置//         所以:双旋要先将cur变为黑,g变为红。// 补充:如果 u结点 不存在,则 cur结点 一定是新插入的结点,//	     因为如果 cur结点 不是新插入结点,//       则 cur结点 和 p结点 中一定有一个结点的颜色是黑色,这就会导致不满足性质4:每条路径黑色结点个数相同。//		 如果 u结点 存在且为黑,则 cur结点 原来的颜色一定是黑色的,//		 现在看到 cur结点 为红色的原因是因为 cur结点 的子树在调整的过程中将 cur结点 的颜色由黑色改为了红色。while (parent && parent->_col == RED) // 当新增结点的父亲存在且颜色也是红色的时候才需要进行调整{Node* g = parent->_parent;if (g->_left == parent){Node* u = g->_right;if (u && u->_col == RED) // 情况一{parent->_col = u->_col = BLACK;g->_col = RED;cur = g;parent = cur->_parent;}else // 情况二{if (cur == parent->_left) // 右单旋{parent->_col = BLACK;g->_col = RED;RotateR(g);}else // 左右双旋(因为红黑树没有平衡因子,所以这里的双旋没有什么其他的处理){cur->_col = BLACK;g->_col = RED;RotateL(parent);RotateR(g);}break;}}else{Node* u = g->_left;if (u && u->_col == RED) // 情况一{parent->_col = u->_col = BLACK;g->_col = RED;cur = g;parent = cur->_parent;}else // 情况二{if (cur == parent->_right) // 左单旋{parent->_col = BLACK;g->_col = RED;RotateL(g);}else // 右左双旋(因为红黑树没有平衡因子,所以这里的双旋没有什么其他的处理){cur->_col = BLACK;g->_col = RED;RotateR(parent);RotateL(g);}break;}}}// 注:这一部分调整可以看截图_root->_col = BLACK; // 不管上面的循环最终是调整到哪结束的(可能有调整到根,可能没有),都把根的颜色始终变为黑return make_pair(iterator(newnode), true);}bool IsBalance(){if (_root->_col == RED) return false; // 检查根是否为黑int ReferenceValues = 0; // 参考值,记录第一次算出的任意一条路径的黑子数量 // 这个参考值也可以弄成全局变量(是真正的全局,不是在这个类里面定义),但这样就需要你在每一次调用 IsBalance() 这个函数之前,把它置为0,才能保证该函数逻辑正确Node* cur = _root;while (cur){if (cur->_col == BLACK) ++ReferenceValues;cur = cur->_left; // 这里就记录一下最左路径的黑子数,记录其他路径的也行,随你。}return Check(_root, 0, ReferenceValues);}private:bool Check(Node* root, int BlackNum, const int ReferenceValues){if (root == nullptr){if (BlackNum != ReferenceValues){ std::cout << "存在黑子数不相等的路径" << std::endl;return false;}return true;}if (root->_col == RED && root->_parent->_col == RED){std::cout << "结点值为:" << KeyOfD()(root->_data) << "处存在连续的红色结点" << std::endl;return false; // 检查有没有连续的红色结点}if (root->_col == BLACK) ++BlackNum; // 记录黑子的数量 // BlackNum要定义成函数参数,不要是全局变量,且不能用传址/传引用,不然 回溯 的时候,就需要你手动“恢复现场”return Check(root->_left, BlackNum, ReferenceValues) && Check(root->_right, BlackNum, ReferenceValues);}void RotateR(Node* parent) //右单旋 {//要抬高左边,降低右边Node* subL = parent->_left;Node* subLR = subL->_right; //subL 的右子树 要给给 parent,当 parent 的左子树//subL的值 < subLR的值 < parent的值parent->_left = subLR; //将 subL 的右子树给给 parentif (subLR) //这边要注意 subLR 可能是 nullptrsubLR->_parent = parent;subL->_right = parent; //提高左边subL->_parent = parent->_parent; //要注意,要先把 parent 的 parent 给给 subLif (parent->_parent) //还要注意改 parent 的 parent  //因为 parent 可能就是根,那么 parent 的 parent 就是 nullptr,所以这里要 if 判断一下{if (parent->_parent->_left == parent){parent->_parent->_left = subL;}else{parent->_parent->_right = subL;}}else //如果原本的 parent 是根的话,就更新一下根{_root = subL;}parent->_parent = subL; //降低右边}void RotateL(Node* parent) //左单旋 {//要抬高右边,降低左边Node* subR = parent->_right;Node* subRL = subR->_left; //subR 的左子树 要给给 parent,当 parent 的右子树//parent的值 < subRL的值 < subR的值parent->_right = subRL; //将 subR 的右子树给给 parentif (subRL) //这边要注意 subRL 可能是 nullptrsubRL->_parent = parent;subR->_left = parent; //提高右边subR->_parent = parent->_parent; //要注意,要先把 parent 的 parent 给给 subRif (parent->_parent) //还要注意改 parent 的 parent  //因为 parent 可能就是根,那么 parent 的 parent 就是 nullptr,所以这里要 if 判断一下{if (parent->_parent->_left == parent){parent->_parent->_left = subR;}else{parent->_parent->_right = subR;}}else //如果原本的 parent 是根的话,就更新一下根{_root = subR;}parent->_parent = subR; //降低右边}// 前序拷贝Node* Copy(Node* root){if (root == nullptr) return nullptr;Node* newroot = new Node(root->_data);newroot->_col = root->_col; // 记得也要拷贝颜色Node* leftchild = Copy(root->_left);Node* rightchild = Copy(root->_right);// 父亲的指向也要记得拷贝,但要记得判断一下,左右孩子是有可能为空的if(leftchild) leftchild->_parent = newroot; if(rightchild) rightchild->_parent = newroot;newroot->_left = leftchild;newroot->_right = rightchild;return newroot;}// 后序析构void Destroy(Node* root){if (root == nullptr) return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}private:Node* _root = nullptr;size_t _Size = 0; // 这个 _Size 根据实际情况,可加可不加
};//void Test_RBTree()
//{
//	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
//	int a1[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
//	int a2[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
//	RBTree<int, int> t;
//	for (auto& x : a2)
//	{
//		t.insert({ x,x });
//		std::cout << x << "->" << t.IsBalance() << std::endl;
//	}
//
//	//t.InOrder(); //这里有个问题,没法给 InOrder() 这个函数传参,因为 _root 是私有函数,你在这里调不动。
//	//那么该怎么解决呢?
//	//给个缺省值吗? 这是不行的,给不了
//	//那该怎么办?
//	//三种方法:1、把这个测试函数定义成友元。(这个方法很不好,就一个测试函数又不是要经常用,定义成友元有点太没边界感了)
//	//          2、学Java,弄一个 Get() 函数,把 _root 拿出来。
//	//			 3、看上面的操作。(封装一下,套一层)
//}
//
//void Test_RBTree2()
//{
//	const int N = 1000000;
//	srand((unsigned int)time(nullptr));
//
//	std::vector<int> v(N);
//
//	RBTree<int, int> t;
//
//	for (int i = 0; i < N; ++i)
//	{
//		v[i] = rand() + i;
//	}
//
//	for (auto x : v)
//	{
//		t.insert({ x,x });
//	}
//	std::cout << "t.IsBalance():" << t.IsBalance() << std::endl;
//}

相关文章:

C++ 红黑树万字详解(含模拟实现(两种版本))

目录 红黑树的概念 红黑树的性质 红黑树的删除 红黑树与AVL树的比较 红黑树的应用 红黑树的模拟实现 红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或Black。 通过对任何一条从根到叶…...

使用 Spring Boot 和 Keycloak 的 OAuth2 快速指南

1. 概述 本教程是关于使用 Spring Boot 和 Keycloak 通过 OAuth2 配置后端的。 我们将使用 Keycloak 作为 OpenID 提供程序。我们可以将其视为负责身份验证和用户数据&#xff08;角色、配置文件、联系信息等&#xff09;的用户服务。它是最完整的 OpenID Connect &#xff0…...

4个小时开发DeepSeek+baiduNaotu一键生成思维导图

一、引言 最近发现AI生成思维导图的解决方案普遍存在两个断层&#xff1a;用户需手动复制模型输出的JSON数据到脑图软件&#xff0c;且缺乏实时可视化反馈。基于日常使用的BaiduNaotu框架&#xff08;其轻量级架构与简洁的UI设计已满足基础需求&#xff09;&#xff0c;我决定…...

DeepSeek 开源狂欢周(一)FlashMLA:高效推理加速新时代

上周末&#xff0c;DeepSeek在X平台&#xff08;Twitter&#xff09;宣布将开启连续一周的开源&#xff0c;整个开源社区为之沸腾&#xff0c;全球AI爱好者纷纷为关注。没错&#xff0c;这是一场由DeepSeek引领的开源盛宴&#xff0c;推翻了传统推理加速的种种限制。这周一&…...

视频批量分段工具

参考原文&#xff1a;视频批量分段工具 选择视频文件 当您启动这款视频批量分段工具程序后&#xff0c;有两种便捷的方式来选择要处理的视频文件。其一&#xff0c;您可以点击程序界面中的 “文件” 菜单&#xff0c;在下拉选项里找到 “选择视频文件” 按钮并点击&#xff1b…...

【OMCI实践】ONT上线过程的omci消息(五)

引言 在前四篇文章中&#xff0c;主要介绍了ONT上线过程的OMCI交互的第一、二、三个阶段omci消息&#xff0c;本篇介绍第四个阶段&#xff0c;OLT下发配置到ONT。前三个阶段&#xff0c;每个厂商OLT和ONT都遵循相同标准&#xff0c;OMCI的交换过程大同小异。但第四个阶段&…...

git从零学起

从事了多年java开发&#xff0c;一直在用svn进行版本控制&#xff0c;如今更换了公司&#xff0c;使用的是git进行版本控制&#xff0c;所以打算记录一下git学习的点滴&#xff0c;和大家一起分享。 百度百科&#xff1a; Git&#xff08;读音为/gɪt/&#xff09;是一个开源…...

服务器间迁移conda环境

注意&#xff1a;可使用迁移miniconda文件 or 迁移yaml文件两种方式&#xff0c;推荐前者&#xff0c;基本无bug&#xff01; 一、迁移miniconda文件&#xff1a; 拷贝旧机器的miniconda文件文件到新机器: 内网拷贝&#xff1a;scp -r mazhf192.168.1.233:~/miniconda3 ~/ 外…...

计算机毕业设计SpringBoot+Vue.js精准扶贫管理系统(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…...

[RH342]tcpdump

[RH342]tcpdump 1. 题目2. 解题 1. 题目 服务器serverc 和 servera 之间有进程定期发送一个明文密码,找出它2. 解题 找出通信端口 抓包分析 tcpdump -X -vv port 6644红框中就是密码,所以密码是root123...

LeetCode-Hot100-001两数之和

给出个人解答&#xff0c;不懂的可以在评论区问 代码 使用的手写的hash函数 class Hash{ public:static const int MAXN 10007;int num;struct Data{int key;int v; int nxt;};vector<Data> data;vector<int> head;Hash(): num(0), data(3*MAXN), head(3*MAXN)…...

(2.26 “详细分析示例“ 暴力+位运算 最长优雅子数组)leetcode 2401

a&b0说明a和b的每一位都是一个0和一个1 不存在两个均为1的位次 a|0a 0与任何数|都等于它本身 &#xff08;mask&#xff09;的作用&#xff1a; 担心两数的1在用一位导致mask覆盖了&#xff1f; 答&#xff1a;出现这种情况说明mask与nums j后就直接break 由&#xff1a;…...

【Go】十六、protobuf构建基础服务信息、grpc服务启动的基础信息

商品服务 服务结构 创建 goods 服务&#xff0c;将之前 user 服务的基本结构迁移到 goods 服务上&#xff0c;完整目录是&#xff1a; mxshop_srvs user_srv … tmp … goods_srv config config.go 配置的读取表 global global.go 数据库、日志初始化、全局变量定义 handler …...

ONNX转RKNN的环境搭建

将ONNX模型转换为RKNN模型的过程记录 工具准备 rknn-toolkit:https://github.com/rockchip-linux/rknn-toolkit rknn-toolkit2:https://github.com/airockchip/rknn-toolkit2 rknn_model_zoo:https://github.com/airockchip/rknn_model_zoo ultralytics_yolov8:https://github…...

解决npm run dev报错

解决&#xff1a;Node.js 版本更新后与 OpenSSL 不兼容导致的npm报错“Error: error:0308010C:digital envelope routines::unsupported” 方法一&#xff1a;更改系统环境变量方法二&#xff1a;更改项目环境变量方法三&#xff1a;更换 Node.js 版本方法四&#xff1a;升级依…...

【Kubernetes】对资源进行PATCH

文章目录 1 更新资源的方式2 PATCH的三种方式2.1 JSON Patch2.2 Merge Patch2.3 Strategic Merge Patch 3 kubectl中的patch命令4 PATCH的优势和问题5 参考文档 1 更新资源的方式 K8S的核心就是各种资源以及针对资源的控制器&#xff0c;为了能够操作资源对象&#xff0c;apis…...

打破关节动力桎梏!杭州宇树科技如何用“一体化设计”重塑四足机器人性能?

核心价值&#xff1a;通过集成电机与行星减速器、创新双联齿轮结构&#xff0c;实现机器人关节动力单元体积缩小50%&#xff0c;力矩控制精度提升30%。&#xff08;申请人&#xff1a;杭州宇树科技有限公司&#xff0c;申请号&#xff1a;201821267397.0&#xff09; 一、技术解…...

一劳永逸解决vsocde模块import引用问题

这里写目录标题 原因解决方案 原因解决方案 原因&#xff1a; VSCode中需要显式地声明PYTHONPATH&#xff0c;不然根本找不到本项目内的模块和包的路径。 解决方法&#xff0c;加入到setting。json里当前Project路径&#xff0c;以后运行就自动添加了&#xff1a; 打开设置 …...

在 Vue 组件中,如何确认父组件在 add 模式下传入 value 的情况及其对子组件 getProducts() 方法的触发影响?

文章目录 父组件中 <ave-form> 的使用add 模式下触发逻辑value 的传入情况是否触发 getProducts()&#xff1f; 验证 add 模式下 getProducts() 是否触发结论&#xff1a; 检查父组件传入 value 的完整情况如何明确知道父组件传入的 value最终回答 父组件 index.vue子组件…...

Unity XR-XR Interaction Toolkit开发使用方法(十)组件介绍(XR Interaction Group)

目录 一、插件介绍 二、主要组件 XR Interaction Manager XR Controller XR Interactor XR Direct Interactor XR Ray Interactor XR Socket Interactor XR Gaze Interactor 三、XR Interaction Group 1、组件介绍 2、核心功能与特点 优先级与冲突管理 动态交互切…...

docker简介-学习与参考

docker Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言并遵从 Apache2.0 协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。 容器是完全使用沙箱…...

3dtiles平移旋转工具制作

3dtiles平移旋转缩放原理及可视化工具实现 背景 平时工作中&#xff0c;通过cesium平台来搭建一个演示场景是很常见的事情。一般来说&#xff0c;演示场景不需要多完善的功能&#xff0c;但是需要一批三维模型搭建&#xff0c;如厂房、电力设备、园区等。在实际搭建过程中&…...

【第十节】C++设计模式(结构型模式)-Flyweight( 享元)模式

目录 一、问题背景 二、模式选择 三、代码实现 四、总结讨论 一、问题背景 享元模式&#xff08;Flyweight Pattern&#xff09;在对象存储优化中的应用 在面向对象系统的设计与实现中&#xff0c;创建对象是最常见的操作之一。然而&#xff0c;如果一个应用程序使用了过多…...

VScode在windows10上使用clang-format

用途&#xff1a;自动调整代码格式&#xff0c;如缩进等。 clang-format官方文档&#xff1a;ClangFormat — Clang 21.0.0git documentation 前提&#xff1a;有一个.clang-format文件 下载LLVM&#xff1a;https://github.com/llvm/llvm-project/releases&#xff0c;将可…...

青少年编程与数学 02-010 C++程序设计基础 11课题、程序结构

青少年编程与数学 02-010 C程序设计基础 11课题、程序结构 一、C程序结构二、main函数1. main 函数的基本形式1.1 无参数形式1.2 带参数形式 2. 参数解释3. 示例3.1 无参数形式3.2 带参数形式 4. 编译和运行4.1 编译4.2 运行 5. main 函数的返回值6. 总结 三、预处理指令1. #in…...

Node.js与MySQL的深入探讨

Node.js与MySQL的深入探讨 引言 Node.js,一个基于Chrome V8引擎的JavaScript运行时环境,以其非阻塞、事件驱动的方式在服务器端应用中占据了一席之地。MySQL,作为一款广泛使用的开源关系型数据库管理系统,凭借其稳定性和高效性,成为了许多应用的数据库选择。本文将深入探…...

【洛谷贪心算法题】P2240部分背包问题

【解题思路】 贪心策略选择 对于部分背包问题&#xff0c;关键在于如何选择物品放入背包以达到最大价值。由于物品可以分割&#xff0c;遍历排序后的物品数组&#xff0c;根据物品重量和背包剩余容量的关系&#xff0c;决定是将整个物品放入背包还是分割物品放入背包&#xff…...

ArcGIS Pro技巧实战:高效矢量化天地图地表覆盖图

在地理信息系统&#xff08;GIS&#xff09;领域&#xff0c;地表覆盖图的矢量化是一项至关重要的任务。天地图作为中国国家级的地理信息服务平台&#xff0c;提供了丰富且详尽的地表覆盖数据。然而&#xff0c;这些数据通常以栅格格式存在&#xff0c;不利于进行空间分析和数据…...

AF3 pair_sequences函数解读

AlphaFold3 msa_pairing模块的pair_sequences函数的核心目标是基于 MSA(多序列比对)中的物种信息,在多条链之间建立 MSA 配对索引,从而帮助 AlphaFold3 捕捉共进化信息,提升蛋白复合物预测的准确性。函数pair_sequences 通过调用 _make_msa_df、 _create_species_dict 以…...

在VSCode 中使用通义灵码最新版详细教程

在 VSCode 中使用通义灵码&#xff1a;最新版详细教程与使用场景 Visual Studio Code&#xff08;简称 VSCode&#xff09;是一款由微软开发的轻量级、功能强大的开源代码编辑器&#xff0c;支持多种编程语言&#xff0c;深受开发者喜爱。而通义灵码&#xff08;TONGYI Lingma…...

ssh和rdp踩坑

ssh和rdp&#xff08;远程桌面&#xff09;踩坑 使用微软账号登录windows的话&#xff0c;ssh的用户名是本地用户名&#xff08;就是c盘用户文件夹下的用户名&#xff09;&#xff0c;rdp的用户名是微软账号用户名&#xff0c;但是密码都是微软账号的密码&#xff0c;跟登录密…...

Linux驱动学习(四)--字符设备注册

上一节讲到的字符设备注册与销毁是通过cdev_init、cdev_add、cdev_del等函数分步执行的&#xff0c;本小节用一种更简单的方式&#xff0c;来注册字符设备 register_chrdev 如果major为0&#xff0c;该函数将动态的分配一个主设备号并且返回对应的值如果major > 0&#xff…...

vue3 下载文件 responseType-blob 或者 a标签

在 Vue 3 中&#xff0c;你可以使用 axios 或 fetch 来下载文件&#xff0c;并将 responseType 设置为 blob 以处理二进制数据。以下是一个使用 axios 的示例&#xff1a; 使用 axios 下载文件 首先&#xff0c;确保你已经安装了 axios&#xff1a; npm install axios然后在你…...

Meta最新研究:从单张照片到3D数字人的革命性突破

随着人工智能技术的发展,3D建模和虚拟人物生成逐渐变得更加普及和高效。Meta(前身为Facebook)的最新研究成果展示了如何仅通过一张普通手机拍摄的照片就能生成高质量、全方位的3D数字人。这项技术不仅适用于虚拟试衣、游戏角色建模,还能广泛应用于AR/VR内容生成等领域。本文…...

大中型虚拟化园区网络设计

《大中型虚拟化园区网络设计》属于博主的“园区网”专栏&#xff0c;若想成为HCIE&#xff0c;对于园区网相关的知识需要非常了解&#xff0c;更多关于园区网的内容博主会更新在“园区网”专栏里&#xff0c;请持续关注&#xff01; 一.前言 华为云园区网络解决方案(简称Cloud…...

Vue 项目中配置代理的必要性与实现指南

Vue 项目中配置代理的必要性与实现指南 在 Vue 前端项目的开发过程中&#xff0c;前端与后端地址通常不同&#xff0c;可能引发跨域问题。为了在开发环境下顺畅地请求后端接口&#xff0c;常常会通过配置**代理&#xff08;proxy&#xff09;**来解决问题。这篇文章将详细解析…...

chromadb向量数据库使用 (1)

目录 完整代码代码解释 完整代码 import chromadb chroma_client chromadb.Client()collection chroma_client.create_collection(name"my_collection")collection.add(documents["This is a document about pineapple","This is a document about…...

玩机日记 12 fnOS使用lucky反代https转发到外网提供服务

目录 1、安装lucky 2、更新lucky 3、上传ssl证书 4、设置安全入口&#xff0c;替换fnOS的应用url 5、添加https反代 这一篇主要是解决一下飞牛反代https的问题。可以先看玩机日记 12.5 在PVE Windows11上部署本地AI模型&#xff0c;使用群晖反代https转发到外网提供服务&a…...

5分钟学会SpringAI

引言 要开发一个Spring AI的入门案例&#xff0c;我们可以从一个简单的Spring Boot项目开始&#xff0c;然后集成Spring AI的功能来实现基本的生成式AI任务。下面是一个步骤指南&#xff0c;帮助你快速启动并运行一个简单的Spring AI应用。 步骤 1: 准备环境 首先&#xff0…...

专业的UML开发工具StarUML

专业的UML开发工具StarUML 可靠的软件建模软件StarUML StarUML 是一款支持统一建模语言 (UML)框架的开源建模软件。它提供了几种类型的图表&#xff0c;并允许用户生成多种语言的代码。在它的帮助下&#xff0c;软件开发人员可以创建设计、概念和编码解决方案。但是&#xff0…...

go语言环境下载与配置(Windows)

下载 Go下载 - Go语言中文网 - Golang中文社区 建议在D盘中创建文件夹安装到 D 盘 &#xff0c;方便进行管理&#xff0c;然后进行傻瓜式安装。 安装 验证安装 go version 安装成功 配置环境变量 winE --> 右击此电脑 --> 选择属性 --> 高级系统设置 --> 点击…...

矩阵系列 题解

1.洛谷 P1962 斐波那契数列 题意 大家都知道&#xff0c;斐波那契数列是满足如下性质的一个数列&#xff1a; F n { 1 ( n ≤ 2 ) F n − 1 F n − 2 ( n ≥ 3 ) F_n \left\{\begin{aligned} 1 \space (n \le 2) \\ F_{n-1}F_{n-2} \space (n\ge 3) \end{aligned}\right. …...

C++ 正则表达式分组捕获入门指南

在 C 中&#xff0c;正则表达式&#xff08;regex&#xff09;是一种用于匹配字符串模式的强大工具。正则表达式不仅能帮助你查找符合特定模式的字符&#xff0c;还能捕获匹配的子字符串&#xff08;即分组捕获&#xff09;。这篇文章将介绍 C 正则表达式中的分组捕获机制&…...

动态数据表格:基于 PrimeFaces 的运行时列选择实现

在现代的 Web 应用开发中&#xff0c;动态数据表格是一个非常实用的功能&#xff0c;它允许用户根据自己的需求选择显示哪些列。这种灵活性不仅提升了用户体验&#xff0c;还能适应不同的数据展示需求。今天&#xff0c;我们将通过一个具体的实现案例&#xff0c;展示如何使用 …...

Plugin ‘mysql_native_password‘ is not loaded`

Plugin ‘mysql_native_password’ is not loaded mysql_native_password介绍1. 使用默认的认证插件2. 修改 my.cnf 或 my.ini 配置文件3. 加载插件&#xff08;如果确实没有加载&#xff09;4. 重新安装或检查 MySQL 版本 遇到错误 ERROR 1524 (HY000): Plugin mysql_nativ…...

Kotlin 知识点二 延迟初始化和密封类

对变量延迟初始化 Kotlin 语言的许多特性&#xff0c;包括变量不可变&#xff0c;变量不可为空&#xff0c;等等。这些特性 都是为了尽可能地保证程序安全而设计的&#xff0c;但是有些时候这些特性也会在编码时给我们带来不 少的麻烦。 比如&#xff0c;如果你的类中存在很多…...

DeepSeek开源周第二弹:DeepEP如何用RDMA+FP8让MoE模型飞起来?

一、引言&#xff1a;MoE模型的通信瓶颈与DeepEP的诞生 在混合专家&#xff08;MoE&#xff09;模型训练中&#xff0c;专家间的全对全&#xff08;All-to-All&#xff09;通信成为性能瓶颈。传统方案在跨节点传输时带宽利用率不足50%&#xff0c;延迟高达300μs以上。DeepSee…...

C++ Primer 成员访问运算符

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…...

无人机遥控器的亮度 和 两个工作频率

工作频率 2.4000-2.4835 GHz &#xff0c; 5.725-5.850 GHz 1.这是一个无人机的遥控器的两个工作频率&#xff0c;为什么会有两个工作频率&#xff1f; 无人机的遥控器采用双频段设计&#xff08;2.4GHz 和 5.8GHz&#xff09;&#xff0c;主要是为了解决以下问题并优化性…...

ubuntu20.04安装docker

3台主机&#xff0c;2台都能正确安装&#xff0c;第三台怎么都安装不成功&#xff1b; 3台主机都是一样的配置和系统&#xff1b; 后来看来是其外网的ip不一样&#xff0c;导致第三台主机可能被Qiang&#xff0c;不过错误只是提示签名不正确&#xff0c;在设置签名时好像没有…...