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

C++——STL——unordered_map与unordered_set的使用以及使用哈希表封装unordered_map/set

目录

1.unordered_map与unordered_set的使用

1.1 unordered_set类的介绍

1.2 unordered_set和set的使用差异

 1.3 unordered_map和map的使用差异

1.4 unordered_multimap/unordered_multiset

2. 使用哈希表封装unordered_map/set

2.1 模拟实现unordered_map和unordered_set

2.1.1 实现出复用哈希表的框架,并支持insert 

2.1.2 封装iterator

 2.1.3 const_iterator

2.1.4 key不可修改问题:

 2.1.5 map[]的实现

2.1.6 封装总代码:


1.unordered_map与unordered_set的使用

1.1 unordered_set类的介绍

1.unordered_set的声明如上,Key就是unordered_set底层关键字的类型

2. unordered_set默认要求Key支持转换为整形,如果不支持或者想按自己的需求走可以自行实现支 持将Key转成整形的仿函数传给第二个模板参数

3.unordered_set默认要求Key支持比较相等,如果不支持或者想按自己的需求走可以自行实现支持 将Key比较相等的仿函数传给第三个模板参数

4.unordered_set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给 第四个参数。

5.⼀般情况下,我们都不需要传后三个模板参数

6. unordered_set底层是用哈希桶实现,增删查平均效率是 O(1) ,迭代器遍历不再有序(为什么不再有序了呢?咱们知道map,set的底层是红黑树,红黑树又是二叉平衡树,自然,它的它的迭代器遍历一定是有序的。但是unordered_map/set的底层是哈希桶啊,哈希桶可不是二叉平衡树,它的迭代器本身也不是按照顺序走的,所以自然它肯定也是无序的),为了跟set区分,所以叫做unordered_set。

7.这里的迭代器是单向迭代器,只支持++,(记住这里的模板参数以及迭代器类型,后面实现部分会用到)。

8. 咱们前面已经学习了set容器的使用,set和unordered_set的功能高度相似,只是底层结构不 同,有一些性能和使用的差异,这里我们只讲他们的差异部分。

1.2 unordered_set和set的使用差异

1.现unordered_set的增删查且跟set的使用⼀模⼀样。所以这部分就不多说了。

2.unordered_set和set的第⼀个差异是对key的要求不同,set要求Key支持小于比较,而 unordered_set要求Key支持转成整形且支持等于比较(这个咱们后面的实现可能会省略这一个)。

3.unordered_set和set的第二个差异是迭代器的差异,set的iterator是双向迭代器,unordered_set 是单向迭代器,其次set底层是红黑树,红黑树是二叉搜索树,走中序遍历是有序的,所以set迭代 器遍历是有序+去重。而unordered_set底层是哈希表,迭代器遍历是无序+去重。

4.unordered_set和set的第三个差异是性能的差异,整体而言大多数场景下,unordered_set的增删 查改更快⼀些,因为红黑树增删查改效率是O(logN),而哈希表增删查平均效率是O(1)。

记住这几个主要的成员函数,咱们待会实现的时候会用到。 

性能测试代码: 

#include<unordered_set>
#include<unordered_map>
#include<set>
#include<iostream>using namespace std;void test_set2()
{const size_t N = 1000000;unordered_set<int> us;set<int> s;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){//v.push_back(rand()); // N比较大时,重复值比较多//v.push_back(rand() + i); // 重复值相对少v.push_back(i); // 没有重复,有序}size_t begin1 = clock();for (auto e : v){s.insert(e);}size_t end1 = clock();cout << "set insert:" << end1 - begin1 << endl;size_t begin2 = clock();//us.reserve(N);for (auto e : v){us.insert(e);}size_t end2 = clock();cout << "unordered_set insert:" << end2 - begin2 << endl;int m1 = 0;size_t begin3 = clock();for (auto e : v){auto ret = s.find(e);if (ret != s.end()){++m1;}}size_t end3 = clock();cout << "set find:" << end3 - begin3 << "->" << m1 << endl;int m2 = 0;size_t begin4 = clock();for (auto e : v){auto ret = us.find(e);if (ret != us.end()){++m2;}}size_t end4 = clock();cout << "unorered_set find:" << end4 - begin4 << "->" << m2 << endl;cout << "插入数据个数:" << s.size() << endl;cout << "插入数据个数:" << us.size() << endl << endl;size_t begin5 = clock();for (auto e : v){s.erase(e);}size_t end5 = clock();cout << "set erase:" << end5 - begin5 << endl;size_t begin6 = clock();for (auto e : v){us.erase(e);}size_t end6 = clock();cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
}
int main()
{test_set2();return 0;
}

1.当N比较大时,重复值比较多。

2.当重复值相对少

 

3.当没有重复,有序。

 

可以看出无论哪种插入方式,都是unordered系列的性能更优,所以,咱们以后也使用unordered系列吧。

 1.3 unordered_map和map的使用差异

1.unordered_map的的增删查改且跟map的使用⼀模⼀样。这里就不过多的阐释了。

2.unordered_map和map的第⼀个差异是对key的要求不同,map要求Key支持小于比较,而 unordered_map要求Key支持转成整形支持等于比较。(这个咱们模拟实现的时候,并没有实现这个)

3.unordered_map和map的第二个差异是迭代器的差异,map的iterator是双向迭代器, unordered_map是单向迭代器,其次map底层是红黑树,红黑树是⼆叉搜索树,走中序遍历是有 序的,所以map迭代器遍历是Key有序+去重。而unordered_map底层是哈希表,迭代器遍历是 Key无序+去重。

4.unordered_map和map的第三个差异是性能的差异,整体而言大多数场景下,unordered_map的 增删查改更快⼀些,因为红黑树增删查改效率是O(logN)而哈希表增删查平均效率是O(1)。

记住这些成员函数,待会咱们实现底层的时候,去实现它们几个。

1.4 unordered_multimap/unordered_multiset

1.unordered_multimap/unordered_multiset跟multimap/multiset功能完全类似,支持Key冗余。

2.unordered_multimap/unordered_multiset跟multimap/multiset的差异也是三个方面的差异, key的要求的差异,iterator及遍历顺序的差异,性能的差异。

OK,其实这个unordered系列的使用跟map与set基本相同,所以说这里没什么好阐述的,大家要是对map与set的使用生疏了,可以去看我的那篇《C++STL——map与set的使用这篇文章》。

2. 使用哈希表封装unordered_map/set

在这里,咱们需要实现三个部分:unordered_map.h   unordered_set.h   HashTable.h

在看他们之前,得了解,这三者之间有什么联系?

HashTable是基础数据结构,提供通用的哈希表功能;unordered_map和unordered_set则通过组合HashTable,并配置不同的模板参数和提取键的方式,实现了标准库中的映射和集合容器。它们的关联在于复用HashTable的核心逻辑,通过模板和策略模式来适应不同的数据类型和操作需求。

看着是不是有点熟悉?跟咱们的使用红黑树封装map与set的逻辑一模一样的。以至于里面有很多东西都是一样的。那么咱们接下来就来一点一点的看吧:

咱们按照这六个顺序来实现哈希表对unordered_map/set的封装。

2.1 模拟实现unordered_map和unordered_set

对于第二点,咱们就不需要再多阐述了,也不需要再看源码了,原理跟封装map与set一样一样的。

2.1.1 实现出复用哈希表的框架,并支持insert 

1.unordered_map和unordered_set复用之前我们实现的哈希桶。

2.key参数就用K,value参数就用V,哈希表中的数据类型,我们使用T。

3.其次跟map和set相比而言unordered_map和unordered_set的模拟实现类结构更复杂⼀点,但是 大框架和思路是完全类似的。因为HashTable实现了泛型不知道T参数导致是K,还是pair, 那么insert内部进行插入时要用K对象转换成整形取模和K比较相等,因为pair的value不参与计算取 模,且默认支持的是key和value⼀起比较相等,我们需要时的任何时候只需要比较K对象,所以我 们在unordered_map和unordered_set层分别实现⼀个MapKeyOfT和SetKeyOfT的仿函数传给 HashTable的KeyOfT,然后HashTable中通过KeyOfT仿函数取出T类型对象中的K对象,再转换成 整形取模和K比较相等,具体细节参考如下代码实现。

//unordered_set.h
这个的参数情况完全是按照库里面的来实现的,这里省略了一个比较相等
template<class K, class Hash = HashFunc<K>>
class unordered_set
{struct SetKeyOfT{const K& operator()(const K& key){return key;}};
//unordered_map.h
//这个的参数情况完全是按照库里面的来实现的,这里省略了一个比较相等
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};
//HashTable.h
pair<Iterator, bool> Insert(const T& data)
{KeyOfT kot;Iterator it = Find(kot(data));if (it != End())return { it, false };//多参数的隐式类型转换需要用到花括号Hash hs;// 负载因子到1扩容if (_n == _tables.size()){// 扩容vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);// 遍历旧表,将旧表的数据全部重新映射到新表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// cur头插到新表size_t hashi = hs(kot(cur->_data)) % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}size_t hashi = hs(kot(data)) % _tables.size();// 头插Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return { Iterator(newnode, this), true };
}

这里的hashtable只展示插入代码,大家看hs(kot(cur->_data)),可别被他搞晕了,这个的意思是,先看内层:取出cur->_data中的key元素,外层:之后再将这个key元素转换为整形,以方便咱们计算哈希值。

2.1.2 封装iterator

iterator实现的大框架跟list的iterator思路是⼀致的,用⼀个类型封装结点的指针,再通过重载运算 符实现,迭代器像指针⼀样访问的行为,要注意的是哈希表的迭代器是单向迭代器。

这个部分的封装iterator,与前面的map与set的封装有异曲同工之妙。并且这里只实现++,不实现--。但是++:对于同一个桶来说,找到下一个原宿好办,但是如果当前桶遍历完了呢?下一个桶该如何去找?那么此时源码中就给出了,定义了两个成员函数。

 

一个是指向当前节点的指针,另外一个是指向哈希表的指针。这样的话,就可以很轻松的实现在不同的桶之间进行切换。用key值计算出当前 桶位置,依次往后找下一个不为空的桶即可。

 

Self& operator++()
{if (_node->_next){// 当前桶还没走完_node = _node->_next;}else // 21:05{// 当前桶走完了,需要找下一个不为空的桶里面的第一个节点KeyOfT kot;Hash hs;size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();hashi++;while (hashi < _pht->_tables.size()){if (_pht->_tables[hashi]){_node = _pht->_tables[hashi];break;}++hashi;}if (hashi == _pht->_tables.size()){// 所有桶都走完了,置为end()_node = nullptr;}}return *this;
}

咱们先来看这个++的逻辑是个怎么样的:

1.当前桶没有走完,那么直接继续往下走就可以了。

2.当前桶走完了:2.1 去找当前桶的位置hashi  2.2  让hashi从下一个位置开始寻找,寻找不为空的桶的位置,找到后,节点挪到不为空的那个桶的位置。   2.3   别忘了处理,如果找了一圈还没找到,那就得将节点置为空了。(end()的情况)

begin()返回第⼀个桶中第⼀个节点指针构造的迭代器,这⾥end()返回迭代器可以用空表示。

Iterator Begin()
{//如果一个元素都没有if (_n == 0)return End();for (size_t i = 0; i < _tables.size(); i++){//找到第一个有元素的桶的位置if (_tables[i]){return Iterator(_tables[i], this);//前面Iterator就有两个成员变量//所以这个也是返回带两个参数构造的Iterator//而这里代表哈希表的指针就是this指针}}return End();//得保证每条路径都有返回值
}
Iterator End()
{return Iterator(nullptr, this);
}

封装迭代器代码总结:

//当你自定义类型的时候,你真正需要用到几种类型的模板参数,那么你就定义几种就可以了!
template<class T>
struct HashNode
{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){}
};// 前置声明
//因为在HTIterator的定义中,引用了HashTable类,而HashTable类又在后面定义
// ,所以需要前置声明HashTable,告诉编译器有这个类存在。否则,
// 编译器在编译HTIterator的时候不知道HashTable是什么,导致错误。
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
//这个设计表中,需要用到几个模板参数(即泛型),就设计几个class
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct HTIterator
{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;Node* _node;//当前节点的指针const HT* _pht;//因为这里是 pht 是 const HT*的,表示既可以接受普通的指针又可以妾受const指针,而这里// pht 是const修饰的//那么_pht(pht)这个行为就需要保证_pht也是const修饰的,不然就会报错//因为存在权限放大//而为什么需要在HTIterator构造函数中用const修饰pht,是因为HTIerator会在// HashTable 类的 普通 begin()函数中使用,同时也会在 const成员函数// begin()和end()中使用//一旦是 const成员函数,那么this指针就是const的,所以就需要用const修饰的pht接收//即this指针是const的,那么无法用普通的pht接收,因为会造成权限的放大//所以,const修饰的this指针,只可以用const修饰的pht接收。//不是const修饰的指针,可以用不是const修饰的pht接收,也可以用const修饰的pht接收,//所以干脆就都用const修饰的pht喽。HTIterator(Node* node, const HT* pht):_node(node), _pht(pht){}Self& operator++(){if (_node->_next){// 当前桶还没走完_node = _node->_next;}else // 21:05{// 当前桶走完了,需要找下一个不为空的桶里面的第一个节点KeyOfT kot;Hash hs;size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();hashi++;while (hashi < _pht->_tables.size()){if (_pht->_tables[hashi]){_node = _pht->_tables[hashi];break;}++hashi;}if (hashi == _pht->_tables.size()){// 所有桶都走完了,置为end()_node = nullptr;}}return *this;}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}
};

 2.1.3 const_iterator

这个其实在这里无法很好的体现,咱们到了下面总的代码再一起来看:

2.1.4 key不可修改问题:

 unordered_set的iterator也不支持修改,我们把unordered_set的第二个模板参数改成constK即 可。

unordered_map的iterator不支持修改key但是可以修改value,我们把unordered_map的第二个 模板参数pair的第⼀个参数改成constK即可。

 2.1.5 map[]的实现

其实这个跟咱们的前面那一章的map支持[]是差不多的:

V& operator[](const K& key)
{pair<iterator, bool> ret = insert({ key, V() });iterator it = ret.first;//it->second,这里的it应该是pair<const K, V>,//迭代器的operator->应该返回指向该pair的指针,因此it->second应该是V& 类型,允许修改return it->second;
}

底层也是依靠了插入。

2.1.6 封装总代码:

下面就展示总代码 ,这样一个一个的分割着看,效果不太好,下面的总代吗注释我都写上面的,大家自行观看。

HashTable.h

这里咱们只看哈希桶里面的。(因为unordered_map/set底层就是哈希桶)

//当你自定义类型的时候,你真正需要用到几种类型的模板参数,那么你就定义几种就可以了!
template<class T>
struct HashNode
{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){}
};// 前置声明
//因为在HTIterator的定义中,引用了HashTable类,而HashTable类又在后面定义
// ,所以需要前置声明HashTable,告诉编译器有这个类存在。否则,
// 编译器在编译HTIterator的时候不知道HashTable是什么,导致错误。
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
//这个设计表中,需要用到几个模板参数(即泛型),就设计几个class
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct HTIterator
{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;Node* _node;//当前节点的指针const HT* _pht;//因为这里是 pht 是 const HT*的,表示既可以接受普通的指针又可以妾受const指针,而这里// pht 是const修饰的//那么_pht(pht)这个行为就需要保证_pht也是const修饰的,不然就会报错//因为存在权限放大//而为什么需要在HTIterator构造函数中用const修饰pht,是因为HTIerator会在// HashTable 类的 普通 begin()函数中使用,同时也会在 const成员函数// begin()和end()中使用//一旦是 const成员函数,那么this指针就是const的,所以就需要用const修饰的pht接收//即this指针是const的,那么无法用普通的pht接收,因为会造成权限的放大//所以,const修饰的this指针,只可以用const修饰的pht接收。//不是const修饰的指针,可以用不是const修饰的pht接收,也可以用const修饰的pht接收,//所以干脆就都用const修饰的pht喽。HTIterator(Node* node, const HT* pht):_node(node), _pht(pht){}Self& operator++(){if (_node->_next){// 当前桶还没走完_node = _node->_next;}else // 21:05{// 当前桶走完了,需要找下一个不为空的桶里面的第一个节点KeyOfT kot;Hash hs;size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();hashi++;while (hashi < _pht->_tables.size()){if (_pht->_tables[hashi]){_node = _pht->_tables[hashi];break;}++hashi;}if (hashi == _pht->_tables.size()){// 所有桶都走完了,置为end()_node = nullptr;}}return *this;}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}
};template<class K, class T, class KeyOfT, class Hash>
class HashTable
{// 友元声明//在 HashTable 类中添加 friend struct HTIterator 的目的是// 允许迭代器访问哈希表的私有成员:/*访问权限需求:HTIterator 的 operator++ 需要访问 HashTable 的私有成员 _tables(桶数组),以跳转到下一个非空桶。如果 _tables 是 private 或 protected 成员,非友元类无法访问。*/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> ConstIterator;Iterator Begin(){//如果一个元素都没有if (_n == 0)return End();for (size_t i = 0; i < _tables.size(); i++){//找到第一个有元素的桶的位置if (_tables[i]){return Iterator(_tables[i], this);//前面Iterator就有两个成员变量//所以这个也是返回带两个参数构造的Iterator//而这里代表哈希表的指针就是this指针}}return End();//得保证每条路径都有返回值}Iterator End(){return Iterator(nullptr, this);}ConstIterator Begin() const{if (_n == 0)return End();for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]){return ConstIterator(_tables[i], this);}}return End();}ConstIterator End() const{return ConstIterator(nullptr, this);}HashTable(size_t n = __stl_next_prime(0)):_tables(n, nullptr), _n(0){}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}pair<Iterator, bool> Insert(const T& data){KeyOfT kot;Iterator it = Find(kot(data));if (it != End())return { it, false };//多参数的隐式类型转换需要用到花括号Hash hs;// 负载因子到1扩容if (_n == _tables.size()){// 扩容vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);// 遍历旧表,将旧表的数据全部重新映射到新表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// cur头插到新表size_t hashi = hs(kot(cur->_data)) % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}size_t hashi = hs(kot(data)) % _tables.size();// 头插Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return { Iterator(newnode, this), true };}Iterator Find(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key)return Iterator(cur, this);cur = cur->_next;}return End();}bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();KeyOfT kot;Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}--_n;delete cur;return true;}prev = cur;cur = cur->_next;}return false;}
private:vector<Node*> _tables;size_t _n;				// 实际存储有效数据个数
};

 unordered_map.h

//这个的参数情况完全是按照库里面的来实现的,这里省略了一个比较相等
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};public://因为迭代器是在HashTable中的。//不管是Iterator还是const Iterator,key都是不可以修改的,所以要加const//迭代器中的第二个模板参数是T,但在这里就不是泛型了,是对应的元素类型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>::ConstIterator 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);}iterator find(const K& key){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}V& operator[](const K& key){pair<iterator, bool> ret = insert({ key, V() });iterator it = ret.first;//it->second,这里的it应该是pair<const K, V>,//迭代器的operator->应该返回指向该pair的指针,因此it->second应该是V& 类型,允许修改return it->second;}private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};

 unordered_set.h

这个的参数情况完全是按照库里面的来实现的,这里省略了一个比较相等
template<class K, class Hash = HashFunc<K>>
class unordered_set
{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:因为迭代器是在HashTable中的。//不管是Iterator还是const Iterator,key都是不可以修改的,所以要加const//迭代器中的第二个模板参数是T,但在这里就不是泛型了,是对应的元素类型typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator 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);}iterator find(const K& key){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}private:hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
};

大家不要看到代码就以为万事大吉了,不是这样的,精髓都在代码里呢,大家好好看一下! 

本篇完..................

相关文章:

  • DIY 自己的 MCP 服务-核心概念、基本协议、一个例子(Python)
  • ChatGPT 如何工作——提示工程、对话记忆与上下文管理解析
  • 最新Spring Security实战教程(十六)微服务间安全通信 - JWT令牌传递与校验机制
  • 从“无我”到“无生法忍”:解构执着的终极智慧
  • Godot的RichTextLabel富文本标签,鼠标拖拽滚动,方向键滚动,底部吸附,自动滚动
  • 时序模型上——ARIMA/MA/AR
  • OpenCV图像认知(二)
  • 编程中优秀大模型推荐:特点与应用场景深度分析
  • JAVA Apache POI实战:从基础Excel导出入门到高级功能拓展
  • java写一个简单的冒泡排序
  • vue实例 与组件实例
  • 视频存储开源方案
  • Flutter Web 3.0革命:用WebGPU实现浏览器端实时光追渲染,性能提升300%
  • 论文分享之Prompt优化
  • C++模板与字符串:从入门到精通
  • 什么是HTTP HTTP 和 HTTPS 的区别
  • SQL进阶之旅 Day 4:子查询与临时表优化
  • vue3获取两个日期之间的所有时间
  • PostgreSQL日志管理完整方案(AI)
  • 关于Python编程语言学习的入门总结
  • php 动态网站/2345网址导航官网
  • 网站开发制作报价单/口碑营销的前提及好处有哪些
  • 明年做哪个网站能致富/搜索引擎营销案例分析
  • wordpress分类加密/重庆seo按天收费
  • 网站怎么在百度搜到/注册网站免费注册
  • 长葛做网站/网站优化排名方案