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

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

🔥 个人主页:大耳朵土土垚
🔥 所属专栏:C++从入门至进阶

这里将会不定期更新有关C/C++的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉

文章目录

  • 前言
  • 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都能使用:

  1. 首先红黑树存储节点的类需要从只能存储键值对改为能够存储任意数据
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){}
};
  1. 相应的,红黑树的模板参数也需要修改:

 修改前:

//红黑树类
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再写一个红黑树了,如下图所示:


  1. 插入函数的参数也得从键值对改为任意数据:
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.hset.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. 结语

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

http://www.lryc.cn/news/418078.html

相关文章:

  • 终于有人把客户成功讲明白了
  • [新械专栏] 肾动脉射频消融仪及一次性使用网状肾动脉射频消融导管获批上市
  • leetcode-119-杨辉三角II
  • 【第八节】python正则表达式
  • 三大浏览器Google Chrome、Edge、Firefox内存占用对比
  • 【wiki知识库】08.添加用户登录功能--后端SpringBoot部分
  • vue中nextTick的作用
  • 计算机网络面试-核心概念-问题理解
  • go语言创建协程
  • RabbitMQ之基于注解声明队列交换机:使用@RabbitListener实现消息监听
  • 【grafana 】mac端grafana配置的文件 grafana.ini 及login
  • 程序员如何在人工智能时代保持核心竞争力
  • 回溯排列+棋盘问题篇--代码随想录算法训练营第二十三天| 46.全排列,47.全排列 II,51. N皇后,37. 解数独
  • ESXI加入VMware现有集群提示常规性错误
  • 数字噪音计(声级计)【AR814数字噪音计】
  • 【Vue3】图片未加载成功前占位
  • AbstractQueuedSynchronizer之AQS
  • <数据集>起重机识别数据集<目标检测>
  • 04--Docker
  • MiniCPM-V: A GPT-4V Level MLLM on Your Phone 手机上的 GPT-4V 级多模态大模型
  • Unity初识
  • 【游戏引擎之路】登神长阶(九)——《3D游戏编程大师技巧》:我想成为游戏之神!
  • Linux:线程同步之信号量
  • GPT-SoVITS-文本转语音(你的声音不再是唯一)
  • C语言深度剖析(部分)--剩下随缘更新
  • 计算机毕业设计选题推荐-电缆行业生产管理系统-Java/Python项目实战
  • Linux 下查看 CPU 使用率
  • 数理基础知识
  • Java解决lombok和mapstruct编译模块的问题
  • 大模型场景应用全集:持续更新中