【C++深度探索】红黑树实现Set与Map的封装


文章目录
- 前言
- 1. 修改红黑树
- 2. 迭代器
- ✨const迭代器
- 3. map的[]访问
- 4. set和map封装
- ✨修改后的红黑树
- ✨map类
- ✨set类
- 5. 结语
前言
前面我们学习过map、set、multimap、multiset的使用,这四种容器都是使用红黑树作为其底层结构。红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),但是红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对AVL树而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
今天我们就可以利用之前实现过的红黑树来对C++STL库中的set和map进行模拟实现。
1. 修改红黑树
我们之前模拟实现过红黑树,插入的节点是键值对pair
类型,而如果要使用红黑树来对set和map封装的话,set存储的应该是单个值,而不是键值对,所以我们就需要对红黑树进行修改,使得set和map都能使用:
- 首先红黑树存储节点的类需要从只能存储键值对改为能够存储任意数据:
template<class T>
struct RBTreeNode
{T _data; //存放数据,不再只能存放键值对:pair<K, V> _kv; RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Colour _col; //保存颜色RBTreeNode(const T& data):_data(data), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED){}
};
- 相应的,红黑树的模板参数也需要修改:
修改前:
//红黑树类
template<class K, class V>
class RBTree
{
public:typedef RBTreeNode<K, V> Node;Node* Find(const K& key);//查找函数private:Node* _pHead = nullptr;
};
因为节点类只有一个模板参数了,所以红黑树两个模板参数有点多余,但是如果将红黑树的模板参数也改为一个,如下面代码所示:
//红黑树类
template<class T>
class RBTree
{
public:typedef RBTreeNode<T> Node;Node* Find(const T& key);//?查找函数private:Node* _pHead = nullptr;
};
那么在实现查找上面代码中的Find函数时,对于map查找时Find函数参数就得传一个完整的键值对,我们不可能把完整的键值对全部传过去查找,这样查找就没有意义,我们只需要获得键值对的键查找到相应的键值对即可,所以我们还应该传一个模板参数:
template<class K, class T>//K存储键,T存储键值对
class RBTree
{
public:typedef RBTreeNode<T> Node;//传入键值对Node* Find(const K& key);//查找函数,只传键private:Node* _pHead = nullptr;
};
这样对于不同函数的需求就可以传入不同的模板参数了🥳🥳
如果是map存储的是键值对,我们就可以往红黑树的两个模板参数中传入一个键和一个键值对:
//map类
template<class K,class V>
class map {
private:RBTree<K, pair<K,V>> _t;
}
这样红黑树的第一个模板参数帮助我们解决查找、删除函数传参的问题,第二个则是必须的,除了存储数据外,插入等函数也需要第二个模板参数
如果是set存储的不是键值对,我们也可以复用上面的代码,传入两个一样的参数即可:
//set类
template<class K>
class set{
private:RBTree<K, K> _t;
}
虽然set看起来传了两个一模一样的参数是无意义的,但是这样就可以实现对红黑树的复用,不用单独为了set再写一个红黑树了,如下图所示:

- 插入函数的参数也得从键值对改为任意数据:
bool Insert(const T& data)//之前:bool Insert(const pair<K, V>& data)
除此以外,我们在寻找插入的位置时不可避免需要比较节点中存储的_data
的大小:
while (cur){if (cur->_kv.first > data.first)//使用键值对的键比较大小{parent = cur;cur = cur->_left;}else if (cur->_kv.first < data.first)//使用键值对的键比较{parent = cur;cur = cur->_right;}elsereturn false;//没找到返回false}//2.找到,插入节点Node* newnode = new Node(data);//判断插入父节点左侧还是右侧if (parent->_kv.first > data.first)//使用键值对的键比较parent->_left = newnode;elseparent->_right = newnode;
我们发现之前插入键值对都是使用键值对的键来比较大小,当我们将红黑树改成可以存储任意数据后,就不支持上述比较大小的方式了。
因此为了实现代码的复用,我们需要传入一个新的模板参数,以便在不同情况下都能按照我们需要的方式进行比较,因为如果是map需要通过键来比较,set则直接通过数据进行比较,所以我们可以在set类和map类中各定义一个类来获取要比较的数据,再传给红黑树:
//map类
template<class K,class V>
class map {
//定义一个类获取键值对里面的键
struct MapKeyOfT {const K& operator()(const pair<K, V>& data){return data.first;}
};private:RBTree<K, pair<K,V>, MapKeyOfT> _t;//传给红黑树}
//set类
template<class K>
class set{
//也定义一个类返回data即可,虽然有点多此一举,但是可以实现代码复用
struct SetKeyOfT {const K& operator()(const K& data){return data;}
};private:RBTree<K, K, SetKeyOfT> _t;//传给红黑树
}
看起来set定义的类似乎做了无用功,但是这一切都是为了能够实现红黑树的复用,map多传了一个参数,set也要多传。
那么红黑树的模板参数也要修改一下:
template<class K, class T,class KeyOfT>
class RBTree
{
public:typedef RBTreeNode<T> Node;private:Node* _pHead = nullptr;
};
这样我们在插入函数进行比较时,就可以通过类模板KeyOfT定义一个对象然后使用括号来获取需要进行比较的数了:
bool Insert(const T& data)
{KeyOfT kot;//使用类模板,定义一个对象//1.先找到插入位置//如果是空树...//如果不是空树...Node* cur = _pHead;Node* parent = nullptr;while (cur){if (kot(cur->_data) > kot(data))//使用括号获取想要比较的值进行比较{parent = cur;cur = cur->_left;}else if (kot(cur->_data) < kot(data))//使用括号获取想要比较的值进行比较{parent = cur;cur = cur->_right;}elsereturn false;//没找到返回false}//2.找到,插入节点Node* newnode = new Node(data);//判断插入父节点左侧还是右侧if (kot(parent->_data) > kot(data))//使用括号获取想要比较的值进行比较parent->_left = newnode;elseparent->_right = newnode;//更新newnode父节点和颜色...}
这样就可以使用类模板定义的对象通过重载括号获取你想要进行比较的值,如果插入类型是键值对,那么就获得键值对中的键进行比较;如果不是键值对,括号返回的也是数据本身直接进行比较即可;这样不同的数据都可以进行比较🥳🥳
2. 迭代器
红黑树的迭代器与链表的迭代器类似,都是使用指针来构造:
//迭代器
template<class T>
struct RBTreeIterator {typedef RBTreeNode<T> Node;typedef RBTreeIterator<T> self;T& operator*(){return _node->_data;}T* operator->(){return &(_node->_data);}bool operator!=(const self& t){return _node != t._node;}bool operator==(const self& t){return _node == t._node;}self& operator++(){//...}//构造RBTreeIterator(Node* node):_node(node){}Node* _node;
};
✨对于operator++
:
因为红黑树中序遍历是有序的,所以如果使用迭代器遍历,我们也希望它是有序的,例如下面的红黑树:

如果当前节点是13,它的右子树不为空,那么下一个节点就应该是它右子树的最左节点15;如果当前节点是15,它的右子树为空,那么下一个节点就应该是它的父亲17,但是这是因为15是它父亲的左孩子;如果是右孩子,那么说明这棵子树已经遍历完了,需要继续向上找父亲,代码如下:
self& operator++()
{if (_node->_right){// 右不为空,右子树最左节点就是中序第一个Node* leftMost = _node->_right;while (leftMost->_left){leftMost = leftMost->_left;}_node = leftMost;}else{// 孩子是父亲左的那个祖先Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right){cur = parent;parent = cur->_parent;}_node = parent;}return *this;
}
✨const迭代器
相较于普通迭代器,const迭代器指向的内容不可以被修改,也就是说operator*
和operator->
返回值不可以修改,所以只要在其返回值前加const
修饰即可,为了与普通迭代器复用同一个迭代器类,我们需要在迭代器类的模板参数中多穿两个:
//迭代器
template<class T,class Ref,class Ptr>
struct RBTreeIterator {typedef RBTreeNode<T> Node;typedef RBTreeIterator<T, Ref, Ptr> self;Ref operator*(){return _node->_data;}Ptr operator->(){return &(_node->_data);}
};
这样在红黑树中如果是普通迭代器就传T,T&,T*
这三个模板参数,如果是const迭代器就传T,const T&,const T*
这三个模板参数,这样就很好限制了const迭代器修改指向的内容:
template<class K, class T,class KeyOfT>
class RBTree
{
public:typedef RBTreeNode<T> Node;typedef RBTreeIterator<T,T&,T*> Iterator;typedef RBTreeIterator<T, const T&, const T*> ConstIterator;//普通迭代器Iterator Begin(){Node* leftMost = _pHead;while (leftMost && leftMost->_left){leftMost = leftMost->_left;}return Iterator(leftMost);}Iterator End(){return Iterator(nullptr);}//const迭代器ConstIterator Begin() const {Node* leftMost = _pHead;while (leftMost && leftMost->_left){leftMost = leftMost->_left;}return ConstIterator(leftMost);}ConstIterator End() const{return ConstIterator(nullptr);}private:Node* _pHead = nullptr;
};
有了迭代器之后,Find查找函数的返回值就可以使用迭代器了🥳🥳:
// 检测红黑树中是否存在值为key的节点,存在返回该节点的迭代器,否则返回End()
Iterator Find(const K& key)
{KeyOfT kot;//使用类模板,定义一个对象//1.先找到插入位置//如果是空树if (_pHead == nullptr)return End();//如果不是空树Node* cur = _pHead;while (cur){if (kot(cur->_data) > key){cur = cur->_left;}else if (kot(cur->_data) < key){cur = cur->_right;}elsereturn Iterator(cur);//找到返回}return End();//没找到返回End()
}
这里同样要注意,查找函数需要通过比较特定的值来实现,所以我们可以利用之前在插入函数中使用的类模板继续创建一个对象来获取需要比较的值。
3. map的[]访问
在map的使用介绍中,我们知道可以用[]来访问修改键值对以及插入数据:
//迭代器构造
std::vector<pair<string, string>> v = { {"上", "up"}, { "下", "down"}, { "左", "left"}, { "右", "right"} };
std::map<string, string> m(v.begin(), v.end());//[]访问
cout << m["上"] << endl;
cout << m["下"] << endl;
cout << m["左"] << endl;
cout << m["右"] << endl;//[]修改
m["右"] += "tutu";//[]插入
m["中"] = "center";cout << endl;
cout << "修改插入后:" << endl;
std::map<string, string>::iterator it = m.begin();
while (it != m.end())
{cout << it->first << ":" << it->second << endl;++it;
}
cout << endl;
结果如下:

map的[]能够插入数据是因为其复用了插入函数,如果[]里面引用的值不存在map中就会插入并返回键值对的值,存在就直接返回键值对的值,而插入函数中恰好会先寻找合适的插入位置,并返回bool值,所以我们只需对插入函数返回的值进行修改:
我们将插入函数的返回值设为pair类型,如果插入成功就返回新节点的迭代器和true;如果插入失败,那么map中肯定以及有相同的值,那么返回该位置的迭代器和false
这样在[]中就可以复用插入函数完成插入和修改操作了:
V& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;}
插入函数只需要修改返回值即可:
pair<Iterator,bool> Insert(const T& data)
{//...简写//1.插入成功return make_pair(Iterator(newnode),true);//2.插入失败return make_pair(Iterator(node), false);//node:已有位置的指针
}
4. set和map封装
我们对于map和set封装需要各种新开一个头文件map.h
和set.h
来进行,并且都需要包含RBTree.h
头文件,放在自己的命名空间内,避免与STL标准库中的map和set弄混。
✨修改后的红黑树
#include<iostream>
using namespace std;
#include<assert.h>//枚举颜色
enum Colour
{RED,BLACK
};//节点类
template<class T>
struct RBTreeNode
{T _data; //存放数据RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Colour _col; //保存颜色RBTreeNode(const T& data):_data(data), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED){}
};//迭代器
template<class T,class Ref,class Ptr>
struct RBTreeIterator {typedef RBTreeNode<T> Node;typedef RBTreeIterator<T, Ref, Ptr> self;Ref operator*(){return _node->_data;}Ptr operator->(){return &(_node->_data);}self& operator++(){if (_node->_right){// 右不为空,右子树最左节点就是中序第一个Node* leftMost = _node->_right;while (leftMost->_left){leftMost = leftMost->_left;}_node = leftMost;}else{// 孩子是父亲左的那个祖先Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right){cur = parent;parent = cur->_parent;}_node = parent;}return *this;}bool operator!=(const self& t){return _node != t._node;}bool operator==(const self& t){return _node == t._node;}//构造RBTreeIterator(Node* node):_node(node){}Node* _node;
};//红黑树类
template<class K, class T,class KeyOfT>
class RBTree
{
public:typedef RBTreeNode<T> Node;typedef RBTreeIterator<T,T&,T*> Iterator;typedef RBTreeIterator<T,const T&,const T*> ConstIterator;Iterator Begin(){Node* leftMost = _pHead;while (leftMost && leftMost->_left){leftMost = leftMost->_left;}return Iterator(leftMost);}//直接使用空指针构造Iterator End(){return Iterator(nullptr);}//const迭代器ConstIterator Begin() const {Node* leftMost = _pHead;while (leftMost && leftMost->_left){leftMost = leftMost->_left;}return ConstIterator(leftMost);}ConstIterator End() const{return ConstIterator(nullptr);}RBTree() = default;//拷贝构造RBTree(const RBTree<K, T,KeyOfT>& t){_pHead = Copy(t._pHead);}//赋值运算符重载RBTree<K, T, KeyOfT>& operator=(RBTree<K, T, KeyOfT> t){swap(_pHead, t._pHead);return *this;}//析构函数~RBTree(){Destroy(_pHead);_pHead = nullptr;}//求树的高度int Height(){return _Height(_pHead);}//树中节点个数int Size(){return _Size(_pHead);}// 注意:为了简单起见,本次实现红黑树不存储重复性元素pair<Iterator,bool> Insert(const T& data){KeyOfT kot;//使用类模板,定义一个对象//1.先找到插入位置//如果是空树if (_pHead == nullptr){Node* newnode = new Node(data);newnode->_col = BLACK;_pHead = newnode;return make_pair(Iterator(newnode),true);}//如果不是空树Node* cur = _pHead;Node* parent = nullptr;while (cur){if (kot(cur->_data) > kot(data)){parent = cur;cur = cur->_left;}else if (kot(cur->_data) < kot(data)){parent = cur;cur = cur->_right;}elsereturn make_pair(Iterator(nullptr), false);//没找到返回false}//2.找到,插入节点Node* newnode = new Node(data);//判断插入父节点左侧还是右侧if (kot(parent->_data) > kot(data))parent->_left = newnode;elseparent->_right = newnode;//更新newnode父节点和颜色newnode->_parent = parent;if (parent->_col == BLACK){//父节点是黑色,插入成功return make_pair(Iterator(newnode),true);}if (parent->_col == RED){//父节点是红色cur = newnode;while (parent && parent->_col == RED){Node* grandparent = parent->_parent;//parent是红色,肯定不是根节点,所以grandparent不是空节点,而且是黑色//找叔叔节点Node* uncle = grandparent->_left;if (parent == grandparent->_left)uncle = grandparent->_right;if (uncle&&uncle->_col == RED){//如果uncle是红色//将unlcle和parent节点都变为黑色,grandparent节点变为红色parent->_col = uncle->_col = BLACK;//即可保证所有路径上黑色一样多grandparent->_col = RED;//继续往上更新cur = grandparent;parent = cur->_parent;}else if (uncle==nullptr||uncle->_col == BLACK){//如果uncle不存在或者存在且为黑色if (grandparent->_left == parent && parent->_left == cur){//右单旋,再将grandparent改为红色,parent改为黑色RotateR(grandparent);grandparent->_col = RED;parent->_col = BLACK;}else if (grandparent->_right == parent && parent->_right == cur){//左单旋,再将grandparent改为红色,parent改为黑色RotateL(grandparent);grandparent->_col = RED;parent->_col = BLACK;}else if (grandparent->_right == parent && parent->_left == cur){RotateR(parent);//先右单旋RotateL(grandparent);//再左单旋//再将grandparent的颜色改为红色,cur改为黑色grandparent->_col = RED;cur->_col = BLACK;}else if (grandparent->_left == parent && parent->_right == cur){RotateL(parent);//先左单旋RotateR(grandparent);//后右单旋//再将grandparent的颜色改为红色,parent改为黑色grandparent->_col = RED;cur->_col = BLACK;}elseassert(false);//插入成功,跳出循环break;}}}_pHead->_col = BLACK;return make_pair(Iterator(newnode), true);}// 检测红黑树是否为有效的红黑树bool IsValidRBTRee(){if (_pHead == nullptr)return true;if (_pHead->_col == RED){return false;}// 先求一条路径上黑色节点数量作为参考值int refNum = 0;Node* cur = _pHead;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_pHead, 0, refNum);}// 检测红黑树中是否存在值为key的节点,存在返回该节点的迭代器,否则返回End()Iterator Find(const K& key){KeyOfT kot;//使用类模板,定义一个对象//1.先找到插入位置//如果是空树if (_pHead == nullptr)return End();//如果不是空树Node* cur = _pHead;while (cur){if (kot(cur->_data) > key){cur = cur->_left;}else if (kot(cur->_data) < key){cur = cur->_right;}elsereturn Iterator(cur);//找到返回}return End();//没找到返回End()}private: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->_kv.first << "存在连续的红色节点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);}// 左单旋void RotateL(Node* parent){Node* cur = parent->_right;//将cur的左边给parent的右边,cur的左边再指向parentparent->_right = cur->_left;cur->_left = parent;//链接cur与parent的父节点if (parent->_parent == nullptr){//如果parent是根节点cur->_parent = nullptr;_pHead = cur;}else if (parent->_parent->_left == parent)parent->_parent->_left = cur;elseparent->_parent->_right = cur;//更新父节点cur->_parent = parent->_parent;parent->_parent = cur;if (parent->_right)//判断parent的右边是否存在parent->_right->_parent = parent;}// 右单旋void RotateR(Node* parent){Node* cur = parent->_left;//将cur的右边给parent的左边,cur的右边再指向parentparent->_left = cur->_right;cur->_right = parent;//链接cur与parent的父节点if (parent->_parent == nullptr){//如果parent是根节点cur->_parent = nullptr;_pHead = cur;}else if (parent->_parent->_left == parent)parent->_parent->_left = cur;elseparent->_parent->_right = cur;//更新父节点cur->_parent = parent->_parent;parent->_parent = cur;if (parent->_left)parent->_left->_parent = parent;}int _Size(Node* root){return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;}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;}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}Node* Copy(Node* root){if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_data);newRoot->_left = Copy(root->_left);newRoot->_right = Copy(root->_right);return newRoot;}Node* _pHead = nullptr;
};
✨map类
#include"RBTree.h"
namespace tutu {template<class K,class V>class map {public://定义类获取data.first进行比较struct MapKeyOfT {const K& operator()(const pair<K, V>& data){return data.first;}};typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::Iterator iterator;typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::ConstIterator const_iterator;iterator begin(){return _t.Begin();}iterator end(){return _t.End();}const_iterator begin() const{return _t.Begin();}const_iterator end() const{return _t.End();}pair<iterator,bool> insert(const pair<K,V>& data){return _t.Insert(data);}iterator find(const K& key){return _t.Find(key);}V& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;}private:RBTree<K, pair<K,V>, MapKeyOfT> _t;};//测试函数void Print(const map<string, int>& m){map<string, int>::const_iterator it = m.begin();while (it != m.end()){cout << it->first << ":" << it->second << endl;++it;}cout << endl;}void TestMap(){map<string,int> m;m.insert({ "111",1 });m.insert({ "444",4 });m.insert({ "222",2 });m.insert({ "666",6 });m.insert({ "333",3 });m.insert({ "777",7 });map<string, int>::iterator it = m.begin();while (it != m.end()){cout << it->first << ":" << it->second << endl;++it;}cout << endl;/*cout << "“777”:" << endl;cout << m.find("777")->second << endl;*/cout << "const迭代器遍历:" << endl;Print(m);}
}
结果如下:

✨set类
#include"RBTree.h"namespace tutu {template<class K>class set{public:struct SetKeyOfT {const K& operator()(const K& data){return data;}};typedef typename RBTree<K, K, SetKeyOfT>::Iterator iterator;typedef typename RBTree<K, K, SetKeyOfT>::ConstIterator const_iterator;iterator begin(){return _t.Begin();}iterator end(){return _t.End();}const_iterator begin() const{return _t.Begin();}const_iterator end() const{return _t.End();}pair<iterator, bool> insert(const K& data){return _t.Insert(data);}iterator find(const K& key){return _t.Find(key);}private:RBTree<K, K, SetKeyOfT> _t;};
结果如下:

5. 结语
map和set的底层都是使用一颗红黑树来实现的,然后在外面套了一层壳,为了能够更好的实现代码复用,我们对红黑树进行了很多修改还使用了仿函数,最终用一颗红黑树模拟实现除了map和set。以上就是今天所有的内容啦~ 完结撒花 ~🥳🎉🎉
相关文章:

【C++深度探索】红黑树实现Set与Map的封装
🔥 个人主页:大耳朵土土垚 🔥 所属专栏:C从入门至进阶 这里将会不定期更新有关C/C的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉 文章目录…...

终于有人把客户成功讲明白了
作者:沈建明 对ToB企业来说,只有客户成功才能带来持久增长,在SaaS企业下行大背景下,客户成功是唯一的救命稻草。大家是不是都听过这样的说法? ToB和SaaS企业的老客户贡献对于企业至关重要。因为获取新客户的成本是留…...

[新械专栏] 肾动脉射频消融仪及一次性使用网状肾动脉射频消融导管获批上市
近日,国家药品监督管理局批准了上海魅丽纬叶医疗科技有限公司“肾动脉射频消融仪”和“一次性使用网状肾动脉射频消融导管”两个创新产品注册申请。 肾动脉射频消融仪由主机、脚踏开关、主机连接线、中性电极连接线以及电源线组成。一次性使用网状肾动脉射频消融导…...

leetcode-119-杨辉三角II
原理: 1、初始化每行一维数组nums[1]; 2、从第2行开始,在nums的头插入0(因为杨辉三角每行的第一个1相当于是上一行的1与其前面的0相加之和)后进行相加操作。 代码:...

【第八节】python正则表达式
目录 一、python中的re模块 1.1 基本匹配和搜索 1.2 替换和分割 1.3 编译正则表达式 二、正则表达式对象 2.1 re.RegexObject 和 re.MatchObject 2.2 正则表达式修饰符 - 可选标志 2.3 正则表达式模式 2.4 正则表达式实例 一、python中的re模块 正则表达式是一种独特的…...

三大浏览器Google Chrome、Edge、Firefox内存占用对比
问题 Chrome、Edg、Firefox三家究竟谁的占用少 结论 打开一个页面内存占用 Firefox>Edge>Chrome 打开打量页面内存占用 Firefox>Chrome>Edge 从监视器可以看到Edge增加一个页面增加一个页面不到100M而其它浏览器需要150M左右;Firefox浏览器主线程内存占用800M比…...

【wiki知识库】08.添加用户登录功能--后端SpringBoot部分
目录 一、今日目标 二、SpringBoot后端实现 2.1 新增UserLoginParam 2.2 修改UserController 2.3 UserServiceImpl代码 2.4 创建用户上下文工具类 2.5 通过token校验用户(重要) 2.6 创建WebMvcConfig 2.7 用户权限校验拦截器 一、今日目标 上篇…...
vue中nextTick的作用
nextTick是Vue.js提供的一个非常有用的方法,其主要作用是在DOM更新之后执行延迟回调函数。以下是nextTick的具体作用及其实现原理的详细解析: nextTick的作用 确保DOM更新完成: 当Vue实例的数据发生变化时,Vue会异步地更新DOM。…...

计算机网络面试-核心概念-问题理解
目录 1.计算机网络OSI协议七层结构功能分别是什么?如何理解这些功能 2.物理层、数据链路层、网络层、传输层和应用层,这五个层之间功能的关系,或者说是否存在协调关系 3. 数据链路层功能理解 4.MAC地址和以太网协议 5.以太网协议中的CSMA…...
go语言创建协程
前言 Go 语言中,协程是通过 go 关键字来创建的,这使得 Go 语言成为实现并发程序的一个非常直观和强大的工具。Go 运行时管理着协程,这些协程在内部被称为 goroutine。 协程(goroutines)本身是轻量级的线程,…...
RabbitMQ之基于注解声明队列交换机:使用@RabbitListener实现消息监听
文章目录 什么是RabbitListener?队列和交换机的基本概念使用RabbitListener注解声明队列和交换机代码解析1. QueueBinding2. 消费者方法 运行原理应用场景总结 在现代的微服务架构中,消息队列是一种重要的异步通信机制。RabbitMQ作为一种流行的消息代理软…...
【grafana 】mac端grafana配置的文件 grafana.ini 及login
brew services start grafana 以后,怎么知道mac端的配置文件的路径 brew services restart grafana#brew services start grafana在macOS上使用Homebrew安装并启动Grafana服务后,通常的配置文件路径是在以下两个位置之一: Homebrew默认配置文件路径:/usr/local/etc/grafana…...

程序员如何在人工智能时代保持核心竞争力
目录 1.概述 1.1. 技术深度与广度的平衡 1.2. 软技能的培养 1.3. 持续学习和适应性 1.4. 理解和应用AI 1.5. 伦理和责任意识 2.AI辅助编程对程序员工作的影响 2.1.AI工具对编码实践的积极影响 2.2.AI工具的潜在风险 2.3.如何平衡利与弊 3.程序员应重点发展的核心能力…...

回溯排列+棋盘问题篇--代码随想录算法训练营第二十三天| 46.全排列,47.全排列 II,51. N皇后,37. 解数独
46.全排列 题目链接:. - 力扣(LeetCode) 讲解视频: 组合与排列的区别,回溯算法求解的时候,有何不同? 题目描述: 给定一个不含重复数字的数组 nums ,返回其 所有可能…...

ESXI加入VMware现有集群提示常规性错误
集群内有vSphere6.5和6.7的版本,都开启了EVC 这台老服务器是DELL R710添加时报错,网上查了些资料说要重装ESXI或者关闭EVC等等 最终解决方法是,给这台ESXI配置一个NTP服务器,同步系统时间,之后即可正常加入集群 往期文…...

数字噪音计(声级计)【AR814数字噪音计】
系统介绍 声级计,又叫噪音计,是噪声测量中最基本的仪器。声级计一般由电容式传声器、前置放大器、衰减器、放大器、频率计权网络以及有效值指示表头等组成。 声级计的工作原理是:由传声器将声音转换成电信号,再由前置放大器放大…...

【Vue3】图片未加载成功前占位
背景 在写项目时,加载图片未成功前,会出现空白页面,太影响美观和体验感 解决方案 1. element ui通过slot占位符解决 2. 自定义指令 原生img标签可以通过自定义指令解决,img标签有onload和onerror事件,都是在渲染成…...

AbstractQueuedSynchronizer之AQS
目录 AQS简单入门为什么说AQS是JUC包下的重要基石AQS能干嘛?实际实现原理AQS自身成员变量Node内部类的成员变量源码解读总结 AQS简单入门 AQS是抽象的队列同步器,是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,是重量级基础框架及…...

<数据集>起重机识别数据集<目标检测>
数据集格式:VOCYOLO格式 图片数量:2984张 标注数量(xml文件个数):2984 标注数量(txt文件个数):2984 标注类别数:1 标注类别名称:[cranes] 使用标注工具:labelImg 标注规则:对…...

04--Docker
前言:前面写过关于DockerKubernetes的部署,主要是针对国产化linux系统的适配问题,并没有对docker进行复习。这里整理一下docker的知识点,用作容器化微服务的起点,主要为日常工作配置使用,本章可能有点长&am…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...

UE5 学习系列(三)创建和移动物体
这篇博客是该系列的第三篇,是在之前两篇博客的基础上展开,主要介绍如何在操作界面中创建和拖动物体,这篇博客跟随的视频链接如下: B 站视频:s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...

佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill
视觉语言模型(Vision-Language Models, VLMs),为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展,机器人仍难以胜任复杂的长时程任务(如家具装配),主要受限于人…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...

从物理机到云原生:全面解析计算虚拟化技术的演进与应用
前言:我的虚拟化技术探索之旅 我最早接触"虚拟机"的概念是从Java开始的——JVM(Java Virtual Machine)让"一次编写,到处运行"成为可能。这个软件层面的虚拟化让我着迷,但直到后来接触VMware和Doc…...