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

【C++闯关笔记】哈希表模拟实现unordered_map与unordered_set

系列文章目录

【C++闯关笔记】使用红黑树简单模拟实现map与set-CSDN博客

【C++闯关笔记】unordered_map与unordered_set的底层:哈希表(哈希桶)-CSDN博客


文章目录

目录

系列文章目录

文章目录

前言

unordered_map、unordered_set与map、set的区别与联系

一、核心框架

1.unordered_map、unordered_set结构分析

HashData结构

KeyOfT

2.框架实现

二、迭代器iterator的模拟实现

1.思路分析

operator++

2.模拟实现

三、重载unordered_map的[ ]

1.insert返回pair< >的原因解答

2.模拟实现operator [ ]

四、代码整合

1.底层哈希表代码

2.模拟实现的unordered_set

3.模拟实现的unordered_map

本文总结



前言

unordered_map、unordered_set与map、set的区别与联系

        unordered_map、unordered_set与map、set都属于C++标准模板库中的关联式容器,即通过 来访问的容器。map 和 set 是有序容器,而 unordered_map 和 unordered_set 自C++11后引入的无序容器

特性map/set (有序)unordered_map/unordered_set (无序)
底层结构红黑树哈希表
时间复杂度O(log n)平均O(1),最坏O(n)
元素顺序按键排序无序(由哈希函数决定)
键的要求必须定义 < 或自定义比较器必须定义 == 和自定义哈希函数
内存使用通常更紧凑因预分配桶而可能有额外开销
迭代器稳定性插入/删除稳定(除被删除元素)插入可能导致全部迭代器失效(rehash时)
主要用途需要有序遍历、范围查询需要极速单点访问、不关心顺序

一、核心框架

1.unordered_map、unordered_set结构分析

        上面说到unordered_map、unordered_set与map、set都是靠来访问的关联式容器,实际中使用unordered_map、unordered_set与map、set的方法过程也几乎完全类似。

        观察上图,可以发现在结构上unordered_map、unordered_set和map、set的完全类似。

我们已经知道了map与set的底层是用的红黑树,unordered_map与unordered_set底层则用的哈希表。

        现在问题是:unordered_map与unordered_set能否像map与set那样复用一套底层实现呢?

HashData结构

        假设复用同一个Hashtable实现key和key/value结构,那么unordered_set就应该传给hashtable的是两个 keyunordered_map传给hashtable的是是pair<key,value>。

        这能实现吗?能,HashTable的底层数据即HashNode可以用模板T代替数据类型,上面传的是key,那就存储key;上面传的是pair<key,value>那就存储pair。

namespace karsen
{template<class T>struct HashNode{T _data;HashNode* _next = nullptr;HashNode(const T& data):_data(data), _next(nullptr){}};
}

KeyOfT

        上面的HashNode解决了unordered_map与unordered_det用同一个容器存储,可是紧随其后的又是另一个问题:哈希表维持结构的关键是通过哈希函数计算得出key与内存的映射关系,因为HashTable实现了泛型不知道T参数导致是K,还是pair<K,V>,那么u_set与u_map怎么复用同一个一个函数insert实现插入呢?

        因为HashTable实现了泛型不知道T参数导致是K,还是pair<K,V>, 而insert内部进行插入时要用key转换成整形取模构建与内存的映射,而这里如果是u_set还好直接用HashData 中的key即可,可是u_map怎么办?

        所以我们在unordered_map和unordered_set中分别实现一个MapKeyOfT和SetKeyOfT的仿函数传给 HashTable的KeyOfT,然后HashTable中通过KeyOfT仿函数取出T类型对象中的K对象,再转换成整形取模供给哈希函数比较使用。

如下所示:

unordered_set:

namespace karsen
{template<class K,class hash=HashFunc<K>>class unordered_set{struct set_KeyOfT{const K& operator()(const K& key){return key;}};private:HashTable<K, K, hash, set_KeyOfT> _set;};
}

unordered_map:

namespace karsen
{template<class K, class V, class Hash = HashFunc<K>>class unordered_map{struct map_KeyOfT{const K& operator()(const std::pair<const K, V>& kv){return kv.first;}};private:karsen::HashTable<K, std::pair<K, V>, Hash, map_KeyOfT> _map;};
}

        读者可能会好奇上面代码中HashTable中传入的 Hash = HashFunc<K>是什么,这在上一篇笔记【C++闯关笔记】unordered_map与unordered_set的底层:哈希表(哈希桶)-CSDN博客 中提到HashFunc的作用是将关键字转为整数,以便哈希函数使用key与内存进行映射。

        下方的代码框架实现是建立在哈希表之上的,如果读者对哈希表不太熟悉,可以点击上方蓝字了解哈希表的概念与实现,考虑到篇幅问题,本文不再赘述哈希表的逻辑与实现。

2.框架实现

        综合上述我们就可以用哈希表简单搭建unordered_map与unordered_set框架了。

#pragma once
#include<iostream>
#include<vector>//insert扩容时取得下一次空间的大小
//返回一个不小于n的质数
size_t next_prime(size_t n)
{// Note: assumes long is at least 32 bits.static const int nums = 28;static const unsigned long _prime_list[nums] ={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};const unsigned long* first = _prime_list;const unsigned long* last = _prime_list + nums;//  [first,last)const unsigned long* pos = std::lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}//将key键转化为整数
template<class K>
struct HashFunc
{size_t operator()(const K& key){return size_t(key);}
};//针对string特化
template<>
struct HashFunc<std::string>
{size_t ret = 0;size_t operator()(const std::string& key){for (auto& ch : key)ret += ch;return ret;}
};namespace karsen
{template<class T>struct HashNode{T _data;HashNode* _next = nullptr;HashNode(const T& data):_data(data), _next(nullptr){}};//这里将V视作原来的Ttemplate<class K, class V,class HashFunc, class KeyOfT>class HashTable{typedef HashNode<V> Node;public:HashTable():_tables(next_prime(0)), _cnt(0){}//拷贝构造HashTable(const HashTable<K, V, HashFunc, KeyOfT>& kv){_tables.resize(kv._tables.size(), nullptr);_cnt = kv._cnt;for (size_t i = 0; i < kv._tables.size(); ++i){//头插if (kv._tables[i]){Node* cur = kv._tables[i];while (cur){Node* newNode = new Node(cur->_data);newNode->_next = _tables[i];_tables[i] = newNode;cur = cur->_next;}}}}HashTable<K,V,HashFunc,KeyOfT>& operator=(HashTable<K, V, HashFunc, KeyOfT> kv){std::swap(kv._tables, _tables);std::swap(kv._cnt, _cnt);return *this;}~HashTable(){for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i] != nullptr){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}}std::pair<Iterator,bool> Insert(const V& data){HashFunc ht;KeyOfT kot;Iterator it = Find(kot(data));if (it != End())return{ it,false };//扩容if (_cnt == _tables.size()){std::vector<Node*> newTable(next_prime(_tables.size() + 1));for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;//头插size_t newPos = ht(kot(cur->_data)) % newTable.size();//这里跳过第一次,就能明显看出头插逻辑了cur->_next = newTable[newPos];newTable[newPos] = cur;cur = next;}}_tables.swap(newTable);}size_t hashi = ht(kot(data)) % _tables.size();//头插Node* newNode = new Node(data);newNode->_next = _tables[hashi];_tables[hashi] = newNode;_cnt++;return { Iterator(newNode,this),true };}Iterator Find(const K& key){HashFunc ht;KeyOfT kot;size_t pos = ht(key) % _tables.size();Node* cur = _tables[pos];while (cur){if (ht(kot(cur->_data)) == key)return Iterator(cur,this);cur = cur->_next;}return End();}bool Erase(const K& key){//if (Find(key) == End())return false;HashFunc ht;KeyOfT kot;size_t goalPos = ht(key) % _tables.size();Node* cur = _tables[goalPos];Node* prev = nullptr;while (cur){if (kot(cur->_data) == key){if (!prev){_tables[goalPos] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_cnt;return true;}prev = cur;cur = cur->_next;}return false;}private:std::vector<HashNode<V>*>_tables;size_t _cnt = 0;};}

二、迭代器iterator的模拟实现

1.思路分析

        由于哈希表中的结构是vector + 单链表,所以哈希表的迭代器是单向迭代器。

        哈希表iterator实现的思路架跟list的iterator思路是基本一致:用一个类封装结点的指针,再通过重载运算符实现迭代器像指针一样访问的行为。

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

        再考虑unordered_map与unordered_set迭代器的差异:unordered_set的iterator也不支持修改,unordered_map的iterator不支持修改key但是可以修改value。所以我们可以将把unordered_set的第二个模板参数改成const K;把unordered_map的第二个 模板参数pair的第一个参数改成const K即可。如下方代码所示:
unordered_set:

namespace karsen
{template<class K,class hash=HashFunc<K>>class unordered_set{private:HashTable<K, const K, hash, set_KeyOfT> _set;};
}

unordered_map:

namespace karsen
{template<class K, class V, class Hash = HashFunc<K>>class unordered_map{private:karsen::HashTable<K, std::pair<const K, V>, Hash, map_KeyOfT> _map;};
}

operator++

        最麻烦的还是是operator++的实现。因为这里需要分两种情况:

①如果当前桶的该节点下面还有结点, 则结点的指针指向下一个结点即可;

②如果当前桶走完了,则需要想办法计算找到下一个不为空的桶

        这里的难点在于需要访问哈希表本身,也就是说iterator中得又哈希表对象的指针,这样当前桶走完了,才能找到下一个桶:用key值计算出当前桶位置,依次往后找下一个不为空的桶即可。

2.模拟实现

        上面将可能的情况分析清楚了,现在直接模拟实现iterator,代码中夹杂着注释,方便读者理解它们的作用。

        class Ref,class Ptr。这里将V类型当作T,Ref则是T&,Ptr这是T*,这样写的目的在于可以用同一个迭代器类实现普通迭代器和const_iterator迭代器,仅需在实例化时传不同的参数。

        typedef HashTableIterator<K, V, V&, V*, HashFunc, KeyOfT> Iterator;
        typedef HashTableIterator<K, V, const V&, const V*, HashFunc, KeyOfT> ConstIterator;

iterator代码实现

namespace karsen
{//迭代器中要访问HashTable,所以这里提前声明template<class K, class V, class HashFunc, class KeyOfT>class HashTable;//这里将V视作原来的Ttemplate<class K,class V,class Ref,class Ptr,class HashFunc,class KeyOfT>struct HashTableIterator{typedef HashNode<V> Node;typedef HashTableIterator<K,V, Ref, Ptr, HashFunc, KeyOfT> Self;//将哈希表类型typedef为HTtypedef HashTable<K, V, HashFunc, KeyOfT> HT;Node* _node = nullptr;//哈希表指针const HT* _ht = nullptr;		HashTableIterator(Node * node , HT*ht):_node(node),_ht(ht){ }Self& operator++(){if (_node->_next){_node = _node->_next;return *this;}else{//这个桶走完了,找下一个有数据的桶HashFunc hf;KeyOfT kot;size_t hashi = hf(kot(_node->_data)) % _ht->_tables.size();hashi++;while (hashi < _ht->_tables.size()){_node = _ht->_tables[hashi];if (_node)return *this;else hashi++;}//后面所有桶都为空,着返回end()即nullptr;if (hashi == _ht->_tables.size()){_node = nullptr;}}return *this;}Ref operator*(){return _node->_data;}Ptr operator->(){return &(_node->_data);}bool operator==(const Self& it){return it._node == _node;}bool operator!=(const Self& it){return it._node != _node;}};
}

        实现了iterator之后,上述的核心框架也可以加入begin、end等函数了。

namespace karsen
{//这里将V视作原来的Ttemplate<class K, class V,class HashFunc, class KeyOfT>class HashTable{typedef HashNode<V> Node;//在迭代器operator++中需要访问私有成员tables,所以需要友元template<class K, class V, class Ref, class Ptr, class HashFunc, class KeyOfT>friend struct HashTableIterator;public:typedef HashTableIterator<K, V, V&, V*, HashFunc, KeyOfT> Iterator;typedef HashTableIterator<K, V, const V&, const V*, HashFunc, KeyOfT> ConstIterator;//找到第一个桶中第一个节点,没找到直接返回End()Iterator Begin(){if (_cnt == 0)return End();size_t hashPos = 0;while (hashPos < _tables.size()){if (_tables[hashPos])return Iterator(_tables[hashPos], this);else hashPos++;}return End();}Iterator End(){return Iterator(nullptr, this);}ConstIterator Begin()const{if (_cnt == 0)return End();size_t hashPos = 0;while (hashPos < _tables.size()){if (_tables[hashPos])return ConstIterator(_tables[hashPos], this);else hashPos++;}return End();}ConstIterator End()const{return ConstIterator(nullptr, this);}
}

三、重载unordered_map的[ ]

1.insert返回pair< >的原因解答

        库中unordered_map的insert函数与map的insert函数一样,返回一个pair<iterator, bool>对象。

        其中第一个数据成员first,存储的是插入成功或失败(数据已存在)后数据在哈希表中的位置封装成的迭代器;第二个数据成员second,存储的是表示是否插入成功的布尔值,成功即为true,失败为false。

        值得一提的是,find函数正是利用了insert的返回值实现的查找。

2.模拟实现operator [ ]

        值得注意的是仅unordered_map支持[ ]修改,所以重载的operator[ ]在模拟实现的unordered_map类中。

namespace karsen
{template<class K, class V, class Hash = HashFunc<K>>class unordered_map{V& operator[](const K& key){std::pair<iterator, bool> it = insert({ key,V() });return it.first->second;}private:karsen::HashTable<K, std::pair<const K, V>, Hash, map_KeyOfT> _map;};
}

四、代码整合

1.底层哈希表代码

#pragma once
#include<iostream>
#include<vector>size_t next_prime(size_t n)
{// Note: assumes long is at least 32 bits.static const int nums = 28;static const unsigned long _prime_list[nums] ={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};const unsigned long* first = _prime_list;const unsigned long* last = _prime_list + nums;//  [first,last)const unsigned long* pos = std::lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}template<class K>
struct HashFunc
{size_t operator()(const K& key){return size_t(key);}
};//针对string特化
template<>
struct HashFunc<std::string>
{size_t ret = 0;size_t operator()(const std::string& key){for (auto& ch : key)ret += ch;return ret;}
};namespace karsen
{template<class T>struct HashNode{T _data;HashNode* _next = nullptr;HashNode(const T& data):_data(data), _next(nullptr){}};//声明template<class K, class V, class HashFunc, class KeyOfT>class HashTable;//这里将V视作原来的Ttemplate<class K,class V,class Ref,class Ptr,class HashFunc,class KeyOfT>struct HashTableIterator{typedef HashNode<V> Node;typedef HashTableIterator<K,V, Ref, Ptr, HashFunc, KeyOfT> Self;typedef HashTable<K, V, HashFunc, KeyOfT> HT;Node* _node = nullptr;const HT* _ht = nullptr;		HashTableIterator(Node * node , HT*ht):_node(node),_ht(ht){ }Self& operator++(){if (_node->_next){_node = _node->_next;return *this;}else{//这个桶走完了,找下一个有数据的桶HashFunc hf;KeyOfT kot;size_t hashi = hf(kot(_node->_data)) % _ht->_tables.size();hashi++;while (hashi < _ht->_tables.size()){_node = _ht->_tables[hashi];if (_node)return *this;else hashi++;}//后面所有桶都为空,着返回end()即nullptr;if (hashi == _ht->_tables.size()){_node = nullptr;}}return *this;}Ref operator*(){return _node->_data;}Ptr operator->(){return &(_node->_data);}bool operator==(const Self& it){return it._node == _node;}bool operator!=(const Self& it){return it._node != _node;}};//这里将V视作原来的Ttemplate<class K, class V,class HashFunc, class KeyOfT>class HashTable{typedef HashNode<V> Node;//在迭代器operator++中需要访问私有成员tablestemplate<class K, class V, class Ref, class Ptr, class HashFunc, class KeyOfT>friend struct HashTableIterator;public:typedef HashTableIterator<K, V, V&, V*, HashFunc, KeyOfT> Iterator;typedef HashTableIterator<K, V, const V&, const V*, HashFunc, KeyOfT> ConstIterator;Iterator Begin(){if (_cnt == 0)return End();size_t hashPos = 0;while (hashPos < _tables.size()){if (_tables[hashPos])return Iterator(_tables[hashPos], this);else hashPos++;}return End();}Iterator End(){return Iterator(nullptr, this);}ConstIterator Begin()const{if (_cnt == 0)return End();size_t hashPos = 0;while (hashPos < _tables.size()){if (_tables[hashPos])return ConstIterator(_tables[hashPos], this);else hashPos++;}return End();}ConstIterator End()const{return ConstIterator(nullptr, this);}HashTable():_tables(next_prime(0)), _cnt(0){}HashTable(const HashTable<K, V, HashFunc, KeyOfT>& kv){_tables.resize(kv._tables.size(), nullptr);_cnt = kv._cnt;for (size_t i = 0; i < kv._tables.size(); ++i){//头插if (kv._tables[i]){Node* cur = kv._tables[i];while (cur){Node* newNode = new Node(cur->_data);newNode->_next = _tables[i];_tables[i] = newNode;cur = cur->_next;}}}}HashTable<K,V,HashFunc,KeyOfT>& operator=(HashTable<K, V, HashFunc, KeyOfT> kv){std::swap(kv._tables, _tables);std::swap(kv._cnt, _cnt);return *this;}~HashTable(){for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i] != nullptr){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}}std::pair<Iterator,bool> Insert(const V& data){HashFunc ht;KeyOfT kot;Iterator it = Find(kot(data));if (it != End())return{ it,false };//扩容if (_cnt == _tables.size()){std::vector<Node*> newTable(next_prime(_tables.size() + 1));for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;//头插size_t newPos = ht(kot(cur->_data)) % newTable.size();//这里跳过第一次,就能明显看出头插逻辑了cur->_next = newTable[newPos];newTable[newPos] = cur;cur = next;}}_tables.swap(newTable);}size_t hashi = ht(kot(data)) % _tables.size();//头插Node* newNode = new Node(data);newNode->_next = _tables[hashi];_tables[hashi] = newNode;_cnt++;return { Iterator(newNode,this),true };}Iterator Find(const K& key){HashFunc ht;KeyOfT kot;size_t pos = ht(key) % _tables.size();Node* cur = _tables[pos];while (cur){if (ht(kot(cur->_data)) == key)return Iterator(cur,this);cur = cur->_next;}return End();}bool Erase(const K& key){//if (Find(key) == End())return false;HashFunc ht;KeyOfT kot;size_t goalPos = ht(key) % _tables.size();Node* cur = _tables[goalPos];Node* prev = nullptr;while (cur){if (kot(cur->_data) == key){if (!prev){_tables[goalPos] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_cnt;return true;}prev = cur;cur = cur->_next;}return false;}private:std::vector<HashNode<V>*>_tables;size_t _cnt = 0;};}

2.模拟实现的unordered_set

#pragma once
#include"HashTable.h"namespace karsen
{template<class K,class hash=HashFunc<K>>class unordered_set{struct set_KeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename karsen::HashTable<K, const K, hash, set_KeyOfT>::Iterator iterator;typedef typename karsen::HashTable<K, const K, hash, set_KeyOfT>::ConstIterator const_iterator;iterator begin(){return _set.Begin();}iterator end(){return _set.End();}const_iterator begin()const{return _set.Begin();}const_iterator end()const{return _set.End();}std::pair<iterator, bool>insert(const K& key){return _set.Insert(key);}iterator find(const K& key){return _set.Find(key);}bool erase(const K& key){return _set.Erase(key);}private:HashTable<K, const K, hash, set_KeyOfT> _set;};
}

3.模拟实现的unordered_map

#pragma once
#include"HashTable.h"namespace karsen
{template<class K, class V, class Hash = HashFunc<K>>class unordered_map{struct map_KeyOfT{const K& operator()(const std::pair<const K, V>& kv){return kv.first;}};public:typedef typename karsen::HashTable<K, std::pair<const K, V>, Hash, map_KeyOfT>::Iterator iterator;typedef typename karsen::HashTable<K, std::pair<const K, V>, Hash, map_KeyOfT>::ConstIterator const_iterator;iterator begin(){return _map.Begin();}iterator end(){return _map.End();}const_iterator begin()const{return _map.Begin();}const_iterator end()const{return _map.End();}std::pair<iterator, bool> insert(const std::pair<const K, V>& kv){return _map.Insert(kv);}V& operator[](const K& key){std::pair<iterator, bool> it = insert({ key,V() });return it.first->second;}iterator find(const K& key){return _map.Find(key);}bool erase(const K& key){return _map.Erase(key);}private:karsen::HashTable<K, std::pair<const K, V>, Hash, map_KeyOfT> _map;};
}


本文总结

        本文首先总结了unordered_map、unordered_set与map、set的区别与联系,其次分析了unordered_map、unordered_set的共同底层——哈希表的结构,其中着重介绍了HashData的结构,以及KeyOfT的原理。之后,本文又尝试模拟实现哈希表的迭代器Iterator,并介绍loperator++的实现原理,以及unordered_map中[ ]与insert的复用关系。最后,总结了哈希表HashTable、unordered_map、unordered_set的实现代码。

        希望本文能对你有所帮助。

        读完点赞,手留余香~

http://www.dtcms.com/a/572963.html

相关文章:

  • 【Agentic RL 专题】四、深入浅出RAG原理与实战项目
  • 开源力量:GitCode+昇腾NPU 部署Mistral-7B-Instruct-v0.2模型的技术探索与经验总结
  • 网站被人做跳转了做网站横幅的软件
  • 暖色调网站什么网站上面能接点小活做
  • 网站栏目是什么上海城隍庙必吃美食
  • 智慧康养人形机器人——银发科技的革命者及在日本超老龄化社会的实验(中)
  • 微算法科技(NASDAQ MLGO)“自适应委托权益证明DPoS”模型:重塑区块链治理新格局
  • 小康AI家庭医生:以科技之翼,守陪伴之初心
  • 司马阅与铨亿科技达成生态战略合作,AI赋能工业领域智能化转型
  • 【旋智科技】SPC1158 MCU 参数要点和开发资料
  • 天元建设集团有限公司管理工资发放2个网站 同意内容 百度优化
  • 算法26.0
  • 二十二、STM32的ADC(二)(ADC单通道)
  • 芯谷科技--D3915高性能点阵/条形显示驱动器,点亮智能显示新时代
  • 空间革命:智慧档案馆三维立体一体化科技监控系统方案
  • 苏州网站建设代理装饰设计资质乙级
  • layui窗口标题
  • Linux(docker)安装搭建CuteHttpFileServer/chfs文件共享服务器
  • ubuntu 系统下 将 ROS2 apt 存储库添加到系统,用apt授权我们的GPG 密钥
  • 网站域名注册基本流程微网站移交
  • 线性代数 - 正交矩阵
  • Flink DataStream × Table API 融合双向转换、变更流、批流一体与执行模型
  • 汽车配件 AI 系统:重构汽车配件管理与多语言内容生成新范式
  • 使用Requests和加密技术实现淘宝药品信息爬取
  • 分享|智能决策,精准增长:企业数据挖掘关键策略与应用全景
  • (Azure)PGSQL和redis 连通性测试 --code 备份
  • 重构增长:生成式AI如何将CRM打造为企业的销售大脑
  • 唯品会一家做特卖的网站 分析陕西印象信息技术有限公司
  • Scala与Spark算子:大数据处理的黄金搭档
  • mac Android Studio配置adb环境(使用adb报错 adb: command not found)