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

【C++详解】用哈希表封装实现myunordered_map和 myunordered_set

文章目录

  • 一、框架分析
  • 二、封装框架,解决KeyOfT
  • 三、⽀持iterator的实现
  • 四、const迭代器
  • 五、实现key不支持修改
  • 六、operator[ ]
  • 七、一些补充(reserve和rehash)
  • 八、源码


一、框架分析

SGI-STL30版本源代码中没有unordered_map和unordered_set,SGI-STL30版本是C++11之前的STL版本,这两个容器是C++11之后才更新的。但是SGI-STL30实现了哈希表,只容器的名字是hash_map和hash_set,他是作为⾮标准的容器出现的,⾮标准是指⾮C++标准规定必须实现的。
通过源码可以看到,结构上hash_map和hash_set跟map和set的完全类似,复⽤同⼀个hashtable实现key和key/value结构,hash_set传给hash_table的是两个key,hash_map传给hash_table的是key和pair<const
key, value> 需要注意的是源码⾥⾯跟map/set源码类似,命名⻛格⽐较乱,所以我们模拟⼀份⾃⼰的出来,就按⾃⼰的⻛格⾛了。

实现步骤也和红黑树的类似:
1、实现哈希表
2、封装unordered_map和unordered_set的框架 解决KeyOfT
3、iterator
4、const_iterator
5、key不⽀持修改的问题
6、operator[]

二、封装框架,解决KeyOfT

我们这⾥相⽐源码调整⼀下,key参数就⽤K,value参数就⽤V,哈希表中存储的数据类型,我们使⽤T。
其次跟map和set相⽐⽽⾔unordered_map和unordered_set的模拟实现类结构更复杂⼀点,但是⼤框架和思路是完全类似的。因为HashTable实现了泛型导致不知道T参数是K,还是pair<K,
V>,那么insert内部进⾏插⼊时要⽤K对象转换成整形取模和K⽐较相等,因为pair的value不参与计算取模,且默认⽀持的是key和value⼀起⽐较相等,我们需要任何时候只需要⽐较K对象,包括find和erase内部比较相等需要只比较key,所以我们在unordered_map和unordered_set层分别实现⼀个MapKeyOfT和SetKeyOfT的仿函数传给HashTable的KeyOfT,然后HashTable中通过KeyOfT仿函数取出T类型对象中的K对象,再转换成整形取模和K⽐较相等。有了KeyOfT之后就可以封装上层实现insert,具体细节参考如下代码实现。

//unordered_map.h
template<class K, class V>
class unordered_map
{struct MapKeyOfT{const K& operator()(const pair<K, V>& data){return data.first;}};
public:bool insert(const pair<K, V>& kv){return _ht.Insert(kv);}
private:hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};//unordered_set.h
template<class K>
class unordered_set
{struct SetKeyOfT{const K& operator()(const K& data){return data;}};
public:bool insert(const K& key){return _ht.Insert(key);}
private:hash_bucket::HashTable<K, K, SetKeyOfT> _ht;
};//hashtable.h
bool Insert(const T& data)
{KeyOfT kot;if (Find(kot(data))) //不允许冗余return false;Hash hf;//扩容if (_n == _table.size()) //负载因子为1时触发扩容{vector<Node*> newtable(__stl_next_prime(_table.size() + 1), nullptr);for (size_t i = 0; i < _table.size(); i++) //遍历旧表{Node* cur = _table[i];while (cur) //cur不会空时依次取该桶元素重新映射到新表{Node* next = cur->_next; //提前保存size_t hashi = hf(kot(cur->_data)) % newtable.size();//将遍历到的值头插到新表cur->_next = newtable[hashi];newtable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newtable);}size_t hashi = hf(kot(data)) % _table.size();Node* newnode = new Node(data);//头插newnode->_next = _table[hashi]; //_table[hashi]存的链表头结点指针_table[hashi] = newnode;++_n;return true;
}Node* Find(const K& key){KeyOfT kot;Hash hf;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];while (cur){if (kot(cur->_data) == key)return cur;cur = cur->_next;}return nullptr;}

三、⽀持iterator的实现

⽀持iterator的思路也和以前一样,先在hashtable.h文件里定义一个HTIterator的类,因为哈希表是单向迭代器,所以在类里实现各种有关单向迭代器的方法,再在HashTable类里实现迭代器的接口Begin()和End()。最后在unordered_map和unordered_set里再套一层begin()和end()就行了。
下面是一些迭代器实现的注意细节和相比红黑树封装时的区别
1、迭代器多了一个指向哈希表的成员变量,而哈希表内部又包含迭代器,两者相互包含,不能单纯的交换定义位置,这里就在迭代器前面前置声明哈希表,注意哈希表有模板参数,前置声明时需要把模板参数带上,并且因为哈希表的仿函数模板参数有缺省值,要把前置声明的缺省值去掉,因为缺省值只能在第一次声明中出现,后续声明 / 定义中不能重复,我们这里为了可读性把模板参数的缺省值放在哈希表定义的位置。
2、这里我们实现的迭代器里需要访问哈希表的私有成员变量_table,所以需要用到友元或者实现get函数,但是为什么我们封装红黑树时在迭代器内部也访问了底层红黑树的私有_root为什么那里没有用到友元呢?其实是因为在那里迭代器的初始化充当了get函数的功能,它把红黑树的_root通过迭代器的初始化传给了迭代器内部的_root,所以在迭代器里访问的_root并不是直接访问的红黑树内部的那个_root,而是访问的一份拷贝变量。在哈希表这里我们没有传递变量所以需要用友元,HTIterator要访问HashTable的私有,那么HTIterator要成为HashTable的友元,所以在HashTable内写友元声明,注意友元也需要把模板参数带上。
3、1和2中不论是前置声明还是友元声明都是声明的一个类模板,所以声明写法如下:

 //正确写法template<class K, class T, class KeyOfT, class Hash>class HashTable;//错误写法template<class K, class T, class KeyOfT, class Hash>class HashTable<K, T, KeyOfT, Hash>;

错误写法的含义是模板特化,我们这里只是声明,模板特化是使用模板时才需要做的事。
4、容器迭代器的仿函数模板参数一般不建议加缺省值,而容器的仿函数模板参数一般推荐加缺省值。
5、这里也和封装红黑树一样,把nullptr当作end()的返回值。

HashTable中的Begin()和End()
1、Begin()和End()都不能传引用返回,因为都是返回的临时对象,出了作用域就销毁了。
2、Begin()返回的是第一个不为空的桶里的第一个结点,如果哈希表为空则返回End(),而End()就是把nullptr充当_node返回,迭代器的第二个参数哈希表的指针就传HashTable的this指针。

//hashtable.h//前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class KeyOfT, class Hash>
struct HTIterator
{typedef HTIterator<K, T, KeyOfT, Hash> Self;typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;Node* _node;HT* _ht;HTIterator(Node* node, HT* ht): _node(node), _ht(ht){ }T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self& operator++(){//若当前桶没遍历完,继续遍历当前桶if (_node->_next){_node = _node->_next;}else{//当前桶遍历完了,找下一个不为空的桶的第一个结点KeyOfT kot;Hash hf;//找当前结点所在桶的序号size_t hashi = hf(kot(_node->_data)) % _ht->_table.size();++hashi;while (hashi < _ht->_table.size()){if (_ht->_table[hashi]){//找到不为空的桶了_node = _ht->_table[hashi];break;}else{++hashi;}if (hashi == 51)int a = 0;}//出循环可能找到不为空的桶了也可能把哈希表遍历完了if (hashi == _ht->_table.size())                                {//哈希表遍历完了_node = nullptr;}}return *this;}bool operator==(const Self& it){return this->_node == it._node;}bool operator!=(const Self& it){return this->_node != it._node;}
};

四、const迭代器

const迭代器思路也和以前一样,先在HTIterator类里加两个模板参数Ref 和 Ptr, 修改类里operator*() 和operator->() 的返回值,然后在底层哈希表和上层map set 里都实现一份const的begin() 和 end()。
这里实现哈希表的const迭代器会出现一个新问题,就是实现的const成员函数Begin()和End()中return Const_Iterator(_table[i], this);,其中的this指针被HTIterator(Node* node, HT* ht) 中的ht接受会发生权限的放大,因为在const成员函数中this指针会由 HashTable<K, T, KeyOfT, Hash>* const this 变为 const HashTable<K, T, KeyOfT, Hash>* const this。
为什么封装红黑树const迭代器时没有出现这个问题呢?我们仔细看红黑树代码:

__Tree_Iterator(Node* node, Node* root):_node(node), _root(root){}ConstIterator Begin() const//找整棵树最左结点{Node* minleft = _root;while (minleft && minleft->_left){minleft = minleft->_left;}return ConstIterator(minleft, _root);}ConstIterator End() const{return ConstIterator(nullptr, _root);}//...private:Node* _root = nullptr;

语法规定const成员函数会自动将类的非const指针成员变量视为 “指针本身不可修改”(即T*
const),而不会改变局部变量minleft,所以这里会把成员变量_root的类型Node* 变为 Node*
const,所以这里把_root传给迭代器初始化不会发生权限的放大,因为_root指向的对象一直都可以被修改,而哈希表那里把const T* const 类型的this指针传给迭代器初始化就会发生权限的放大。
这里的解决思路就是把HTIterator类里的成员变量直接改成const HT*类型,这样const对象和非const对象都能传递过来。
代码这里小编就不演示了,看最后的源码。

五、实现key不支持修改

实现思路和红黑树一样,通过上层unordered_set和unordered_map里配置底层哈希表的第二个模板参数就行了,unordered_set是_data整体不能被修改,unordered_map的_data是pair,要求pair的first不能被修改,而second可以被修改。
注意不要忘了把typedef iterator时指定类域的模板参数也改了。

//unordered_set
typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT>::Const_Iterator const_iterator;private:hash_bucket::HashTable<K, const K, SetKeyOfT> _ht;
//unordered_map
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::Const_Iterator const_iterator;private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT> _ht;

六、operator[ ]

  • 这里也和红黑树一样unordered_map独有的operator[ ]实现要依托于底层哈希表Insert的返回值,我们目前Insert的返回值的bool类型,所以我们首先需要调整Insert的接口。
  • 第一步修改insert的返回值为pair<Iterator,bool>,修改完返回值后insert实现里每一个返回bool值的地方也需要跟着调整,一共要调整两处,一个是插入冗余时返回{
    it, false },一个是插入成功后返回{ { newnode, this }, true }。
  • 现在我们实现了迭代器顺便把find的返回值也改成迭代器。
  • 改了底层哈希表Insert的返回值后,上层的unordered_map和unordered_set的返回值也要跟着改成pair,否则匹配不上会报错。
  • 最后在上层unordered_map里把insert套一层,再复用insert把operator[]实现出来就行了。
//unordered_map
pair<iterator, bool> insert(const pair<K, V>& kv)
{return _ht.Insert(kv);
}V& operator[](const K& key)
{pair<iterator, bool> ret = insert({ key, V() });return ret.first->second;
}//hashtable
pair<Iterator, bool> Insert(const T& data)
{KeyOfT kot;Iterator it = Find(kot(data));if (it != End()) //不允许冗余return { it, false };Hash hf;//扩容if (_n == _table.size()) //负载因子为1时触发扩容{vector<Node*> newtable(__stl_next_prime(_table.size() + 1), nullptr);for (size_t i = 0; i < _table.size(); i++) //遍历旧表{Node* cur = _table[i];while (cur) //cur不会空时依次取该桶元素重新映射到新表{Node* next = cur->_next; //提前保存size_t hashi = hf(kot(cur->_data)) % newtable.size();//将遍历到的值头插到新表cur->_next = newtable[hashi];newtable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newtable);}size_t hashi = hf(kot(data)) % _table.size();Node* newnode = new Node(data);//头插newnode->_next = _table[hashi]; //_table[hashi]存的链表头结点指针_table[hashi] = newnode;++_n;return { { newnode, this }, true };
}Iterator Find(const K& key)
{KeyOfT kot;Hash hf;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];while (cur){if (kot(cur->_data) == key)return Iterator(cur, this);cur = cur->_next;}return End();
}

七、一些补充(reserve和rehash)

  • 解决key不能取模问题的Hash仿函数模板参数不应该在底层hashtable哪里给一个HashFunc< K >的缺省值,应该在上层unordered_map和unordered_set配置底层hashtable时自己传递Hash仿函数,Hash仿函数模板参数的缺省值也应该在上层实现unordered_map和unordered_set类的那里加,因为上层unordered_map和unordered_set里的元素类型只有上层自己知道,比如Date,这时需要上层自己实现对应的Hash仿函数并传递给底层hashtable。
  • 这里我们了解了哈希表实现底层后后再来看库里unordered_map和unordered_set里的的一些接口会更清晰,其中还有两个重要的接口reserve和rehash,它们都是用于调整内部存储空间的方法。
  • reserve(n)是预分配空间,优化插入性能:提前为哈希表预留足够的空间,确保其能容纳至少n个元素,避免后续插入过程中频繁触发自动扩容(重哈希),从而提升插入效率。
  • rehash(m)是强制调整桶数,立即重哈希:直接将哈希表的桶数设置为m(或不小于m的最小质数,相当于实际桶数可能比m更大),并立即对所有现有元素执行重哈希(重新计算哈希值并分配到新桶中)。
  • 举例说明: 若要插入 1000 个元素,先调用reserve(1000),哈希表会提前准备足够的桶,插入时无需多次扩容,效率更高。 若哈希表原本有 1000 个元素,删除了 800 个后,调用rehash(200)可减少桶数,避免空间浪费(需确保 200 ≥ 剩余元素数,否则又会触发insert的扩容机制)。
  • 还有几个不太重要的接口,小编快速介绍一下:
    load_factor()返回当前哈希表的负载因子。
    max_load_factor()返回当前哈希表的最大负载因子,大于它就会触发扩容。
    bucket_count()返回当前哈希表的桶数。
    max_bucket_count()返回当前哈希表的最大桶数。
    bucket_size(n)返回n号桶有多少个数据。
    bucket ( const K& k )返回数据k在第几号桶。

八、源码

hashtable.h

#pragma once
using namespace std;
#include <iostream>
#include <vector>
#include <string>
#include <algorithm> static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{53,         97,         193,       389,       769,1543,       3079,       6151,      12289,     24593,49157,      98317,      196613,    393241,    786433,1572869,    3145739,    6291469,   12582917,  25165843,50331653,   100663319,  201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291
};inline unsigned long __stl_next_prime(unsigned long n)
{const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos; //当pos取end()时返回数组最后一个值也就是429...
}template<class K>
struct HashFunc
{size_t operator()(const K& k){return size_t(k);}
};template<>
struct HashFunc<string>
{size_t operator()(const string& str){size_t hashi = 0;for (auto e : str){hashi *= 131;hashi += e;}return hashi;}
};namespace hash_bucket
{template<class T>struct HashNode{T _data;HashNode<T>* _next;HashNode(const T& data): _data(data), _next(nullptr){}};//前置声明template<class K, class T, class KeyOfT, class Hash>class HashTable;template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>struct HTIterator{typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;Node* _node;const HT* _ht;HTIterator(Node* node, const HT* ht): _node(node), _ht(ht){ }Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){//若当前桶没遍历完,继续遍历当前桶if (_node->_next){_node = _node->_next;}else{//当前桶遍历完了,找下一个不为空的桶的第一个结点KeyOfT kot;Hash hf;//找当前结点所在桶的序号size_t hashi = hf(kot(_node->_data)) % _ht->_table.size();++hashi;while (hashi < _ht->_table.size()){if (_ht->_table[hashi]){//找到不为空的桶了_node = _ht->_table[hashi];break;}else{++hashi;}}//出循环可能找到不为空的桶了也可能把哈希表遍历完了if (hashi == _ht->_table.size())                                {//哈希表遍历完了_node = nullptr;}}return *this;}bool operator==(const Self& it){return this->_node == it._node;}bool operator!=(const Self& it){return this->_node != it._node;}};template<class K, class T, class KeyOfT, class Hash>class HashTable{//友元声明template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>friend struct HTIterator;typedef HashNode<T> Node;public:typedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash> Const_Iterator;Iterator Begin(){for (int i = 0; i < _table.size(); i++){if (_table[i])return Iterator(_table[i], this);}return End();     //能根据this指针匹配各自的End}Iterator End(){return Iterator(nullptr, this);}//这里const修饰this指针,也就是hashtable对象是const对象的话就调用该Begin()和End()返回Const_IteratorConst_Iterator Begin() const {for (int i = 0; i < _table.size(); i++){if (_table[i])return Const_Iterator(_table[i], this);}return End();     }Const_Iterator End() const{return Const_Iterator(nullptr, this);}HashTable(size_t size = __stl_next_prime(0)):_table(size, nullptr){}~HashTable(){for (int i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}pair<Iterator, bool> Insert(const T& data){KeyOfT kot;Iterator it = Find(kot(data));if (it != End()) //不允许冗余return { it, false };Hash hf;//扩容if (_n == _table.size()) //负载因子为1时触发扩容{vector<Node*> newtable(__stl_next_prime(_table.size() + 1), nullptr);for (size_t i = 0; i < _table.size(); i++) //遍历旧表{Node* cur = _table[i];while (cur) //cur不会空时依次取该桶元素重新映射到新表{Node* next = cur->_next; //提前保存size_t hashi = hf(kot(cur->_data)) % newtable.size();//将遍历到的值头插到新表cur->_next = newtable[hashi];newtable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newtable);}size_t hashi = hf(kot(data)) % _table.size();Node* newnode = new Node(data);//头插newnode->_next = _table[hashi]; //_table[hashi]存的链表头结点指针_table[hashi] = newnode;++_n;return { { newnode, this }, true };}Iterator Find(const K& key){KeyOfT kot;Hash hf;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];while (cur){if (kot(cur->_data) == key)return Iterator(cur, this);cur = cur->_next;}return End();}bool Erase(const K& key){KeyOfT kot;Hash hf;size_t hashi = hf(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];while (cur){if (kot(cur->_data) == key){//若key在桶的链表头结点if (cur == _table[hashi]){_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;}private:vector<Node*> _table;size_t _n = 0;};
}

unordered_set.h

#pragma once
#include "hashtable.h"namespace wusaqi
{template<class K, class Hash = HashFunc<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& data){return data;}};public://类模板里取嵌套类型加typenametypedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Const_Iterator const_iterator;iterator begin(){return _ht.Begin();}iterator end(){return _ht.End();}const_iterator begin() const{return _ht.Begin();}const_iterator end() const{return _ht.End();}pair<iterator, bool> insert(const K& key){return _ht.Insert(key);}bool erase(const K& key){return _ht.Erase(key);}private:hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;};void print(const unordered_set<int>& us){for (auto& e : us){cout << e << " ";}cout << endl;}void test_unordered_set(){unordered_set<int> us;us.insert(1103);us.insert(1);us.insert(88);us.insert(8);us.insert(666);us.insert(5);us.insert(50);us.insert(3);us.insert(5);unordered_set<int>::iterator it = us.begin();while (it != us.end()) {cout << *it << " ";++it;}cout << endl;us.erase(5);us.erase(1103);print(us);}
}

unordered_map.h

#pragma once
#include "hashtable.h"namespace wusaqi
{template<class K, class V, class Hash = HashFunc<K>>class unordered_map{struct MapKeyOfT{const K& operator()(const pair<K, V>& data){return data.first;}};public:typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Const_Iterator const_iterator;iterator begin(){return _ht.Begin();}iterator end(){return _ht.End();}const_iterator begin() const{return _ht.Begin();}const_iterator end() const{return _ht.End();}pair<iterator, bool> insert(const pair<K, V>& kv){return _ht.Insert(kv);}bool erase(const K& key){return _ht.Erase(key);}V& operator[](const K& key){pair<iterator, bool> ret = insert({ key, V() });return ret.first->second;}private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;};void print(const unordered_map<string, string>& us){for (auto& e : us){cout << e.first << ":" << e.second << " ";cout << endl;}}void test_unordered_map(){unordered_map<string, string> dict;dict.insert({ "string", "字符串"});dict.insert({ "sort", "排序"});dict.insert({ "tree", "树"});dict.insert({ "sort", "排序" });dict["vector"];dict["left"] = "左边,剩余";dict["sort"] = "排序算法";unordered_map<string, string>::iterator it = dict.begin();while (it != dict.end()){cout << (*it).first << ":" << (*it).second << " ";cout << endl;++it;}cout << endl;dict.erase("tree");dict.erase("vector");print(dict);}
}

test.cpp

#include "unorderedmap.h"
#include "unorderedset.h"#include <unordered_map>
#include <unordered_set>void test01()
{unordered_set<int> us;us.insert(42);us.insert(421);us.insert(2);//us.insert(312);//us.insert(313);//us.insert(314);//us.insert(315);//us.insert(316);//us.insert(317);cout << us.bucket_count() << endl;cout << us.max_bucket_count() << endl;cout << us.load_factor() << endl;cout << us.max_load_factor() << endl;
}int main()
{//wusaqi::test_unordered_set();//wusaqi::test_unordered_map();test01();return 0;
}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

http://www.xdnf.cn/news/1378801.html

相关文章:

  • Kubernetes一Prometheus概述
  • [linux仓库]透视文件IO:从C库函数的‘表象’到系统调用的‘本质’
  • [调试][实现][原理]用Golang实现建议断点调试器
  • 获取小红书某个用户列表
  • 【LeetCode 热题 100】32. 最长有效括号——(解法二)动态规划
  • 集成电路学习:什么是TensorFlow
  • AI实时故障诊断系统(实时采集信号)
  • Python 正则表达式完全指南:从基础语法到实战案例
  • 【从0带做】基于Springboot3+Vue3的呱呱同城(微服务项目)
  • 实现微信小程序的UniApp相机组件:拍照、录像与双指缩放
  • ARM相关的基础概念和寄存器
  • PCIe 5.0 SSD连续读写缓存用完速度会骤降吗?
  • 2024年09月 Python(三级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • 帕累托优化:多目标决策的智慧与艺术
  • 【重学 MySQL】九十二、 MySQL8 密码强度评估与配置指南
  • 关于virtual camera
  • 【C++游记】模板升级
  • 【半导体制造流程概述】
  • windows 子系统 wsl 命令的用法
  • vue3 字符 居中显示
  • SpringBoot整合Redis:从入门到实战的完整指南
  • 关于DTO、DO、BO、VO
  • 工业 DCS 全面科普:从入门到 AI 赋能的未来
  • mybatis-plus实现苍穹外卖项目-分类操作,不定期更新-day2
  • 【和春笋一起学C++】(三十七)类的析构函数
  • 死锁产生的条件是什么? 如何进行死锁诊断?
  • leetcode 974 和可被K整除的子数组
  • 集成电路学习:什么是YOLO一次性检测器
  • 关于国产 RAC 和分布式研讨
  • 【Python学习笔记】whl包打包