【C++进阶】哈希(万字详解)—— 学习篇(上) 缺乏、安全感 2024-04-20 06:32 74阅读 0赞 > **?C++学习历程:入门** > > -------------------- > > * **博客主页:**[一起去看日落吗][Link 1] > * **持续分享博主的C++学习历程** > * **`博主的能力有限,出现错误希望大家不吝赐教`** > * **分享给大家一句我很喜欢的话:** 也许你现在做的事情,暂时看不到成果,但不要忘记,树?成长之前也要扎根,也要在漫长的时光?中沉淀养分。静下来想一想,哪有这么多的天赋异禀,那些让你羡慕的优秀的人也都曾默默地翻山越岭?。 > > -------------------- > > ![在这里插入图片描述][eabca17da9704379a5f15832a495b4cc.jpeg_pic_center] ? ? ? ? -------------------- #### 目录 #### * ? 1. unordered系列关联式容器 * * ? 1.1 unordered\_map * * ? 1.1.1 unordered\_map的文档介绍 * ? 1.1.2 unordered\_map的接口说明 * ? 1.1.3 unordered\_set的文档介绍 * ? 1.1.4 unordered\_map和unordered\_set的使用 * ? 1.2 在线OJ * ? 2. 底层结构 * * ? 2.1 哈希概念 * ? 2.2 哈希冲突 * ? 2.3 哈希函数 * ? 2.4 哈希冲突解决 * * ? 2.4.1 闭散列 * ? 2.4.2 闭散列的实现 * ? 2.4.3 开散列 * ? 2.4.4 开散列的实现 ## ? 1. unordered系列关联式容器 ## 在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log\_2 N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered\_map和unordered\_set进行介绍, unordered\_multimap和unordered\_multiset学生可查看文档介绍。 map和set底层是红黑树实现的,map是KV模型,set是K模型,而unordered\_map和unordered\_set底层是哈希表实现的,unordered\_set是K模型,unordered\_map是KV模型 ![在这里插入图片描述][e1bf936c0d034968befd204c79bd4272.png] unordered\_map和unordered\_set的命名体现特点,在功能和map/set是一样的,区别在于,它遍历出来是无序的,另外,它们的迭代器是单向迭代器 -------------------- ### ? 1.1 unordered\_map ### #### ? 1.1.1 unordered\_map的文档介绍 #### [unordered\_map在线文档说明][unordered_map] 1. unordered\_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。 2. 在unordered\_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键 映射值的类型可能不同。 3. 在内部,unordered\_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered\_map将相同哈希值的键值对放在相同的桶中。 4. unordered\_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。 5. unordered\_maps实现了直接访问操作符(operator\[\]),它允许使用key作为参数直接访问value。 6. 它的迭代器至少是前向迭代器。 -------------------- #### ? 1.1.2 unordered\_map的接口说明 #### * unordered\_map的构造 <table> <thead> <tr> <th>函数声明</th> <th>功能介绍</th> </tr> </thead> <tbody> <tr> <td>unordered_map</td> <td>构造不同格式的unordered_map对象</td> </tr> </tbody> </table> * unordered\_map的容量 <table> <thead> <tr> <th>函数声明</th> <th>功能介绍</th> </tr> </thead> <tbody> <tr> <td>bool empty() const</td> <td>检测unordered_map是否为空</td> </tr> <tr> <td>size_t size() const</td> <td>获取unordered_map的有效元素个数</td> </tr> </tbody> </table> * unordered\_map的迭代器 <table> <thead> <tr> <th>函数声明</th> <th>功能介绍</th> </tr> </thead> <tbody> <tr> <td>begin</td> <td>返回unordered_map第一个元素的迭代器</td> </tr> <tr> <td>end</td> <td>返回unordered_map最后一个元素下一个位置的迭代器</td> </tr> <tr> <td>cbegin</td> <td>返回unordered_map第一个元素的const迭代器</td> </tr> <tr> <td>cend</td> <td>返回unordered_map最后一个元素下一个位置的const迭代器</td> </tr> </tbody> </table> * unordered\_map的元素访问 <table> <thead> <tr> <th>函数声明</th> <th>功能介绍</th> </tr> </thead> <tbody> <tr> <td>operator[]</td> <td>返回与key对应的value,没有一个默认值</td> </tr> </tbody> </table> 注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,将key对应的value返回。 * unordered\_map的查询 <table> <thead> <tr> <th>函数声明</th> <th>功能介绍</th> </tr> </thead> <tbody> <tr> <td>iterator find(const K& key)</td> <td>返回key在哈希桶中的位置</td> </tr> <tr> <td>size_t count(const K& key)</td> <td>返回哈希桶中关键码为key的键值对的个数</td> </tr> </tbody> </table> 注意:unordered\_map中key是不能重复的,因此count函数的返回值最大为1 * unordered\_map的修改操作 <table> <thead> <tr> <th>函数声明</th> <th>功能介绍</th> </tr> </thead> <tbody> <tr> <td>insert</td> <td>向容器中插入键值对</td> </tr> <tr> <td>erase</td> <td>删除容器中的键值对</td> </tr> <tr> <td>void clear()</td> <td>清空容器中有效元素个数</td> </tr> <tr> <td>void swap(unordered_map&)</td> <td>交换两个容器中的元素</td> </tr> </tbody> </table> * unordered\_map的桶操作 <table> <thead> <tr> <th>函数声明</th> <th>功能介绍</th> </tr> </thead> <tbody> <tr> <td>size_t bucket_count()const</td> <td>返回哈希桶中桶的总个数</td> </tr> <tr> <td>size_t bucket_size(size_t n)const</td> <td>返回n号桶中有效元素的总个数</td> </tr> <tr> <td>size_t bucket(const K& key)</td> <td>返回元素key所在的桶号</td> </tr> </tbody> </table> -------------------- #### ? 1.1.3 unordered\_set的文档介绍 #### [unordered\_set的在线文档介绍][unordered_set] -------------------- #### ? 1.1.4 unordered\_map和unordered\_set的使用 #### * unordered\_set #include<iostream> #include<unordered_set> #include<unordered_map> using namespace std; void test_unordered_set() { unordered_set<int> s; s.insert(3); s.insert(4); s.insert(5); s.insert(3); s.insert(1); s.insert(2); s.insert(6); unordered_set<int>::iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; ++it; } cout << endl; } int main() { test_unordered_set(); return 0; } ![在这里插入图片描述][9c9baa8e324543dfa48439146bb9f200.png] **`可以看到它遍历出来是无序的,并且相同的数只会插入一次`** * unordered\_map #include<iostream> #include<unordered_map> using namespace std; void test_unordered_map() { unordered_map<string, string> dict; dict.insert(make_pair("string", "字符串")); dict.insert(make_pair("sort", "排序")); dict.insert(make_pair("string", "字符串")); dict.insert(make_pair("string", "字符串")); auto it = dict.begin(); while (it != dict.end()) { cout << it->first << ":" << it->second << endl; it++; } } int main() { test_unordered_map(); return 0; } ![在这里插入图片描述][d62c5853b0324a4993429f36f4f4d4f5.png] **`它遍历出来也是无序的,并且相同的数只会插入一次`** -------------------- ### ? 1.2 在线OJ ### [在长度2N的数组中找出重复N次的元素][2N_N] ![在这里插入图片描述][22b61ada1ff546e9a6fcd5a830abe52a.png] class Solution { public: int repeatedNTimes(vector<int>& nums) { size_t N = nums.size() / 2; unordered_map<int,int> m; for(auto e : nums) m[e]++; for(auto &e : m) { if(e.second == N) return e.first; } return 0; } }; -------------------- [两个数组的交集][Link 2] ![在这里插入图片描述][f5af47c5f86244f08cb68a4e3a2de66a.png] class Solution { public: vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { // 用unordered_set对nums1中的元素去重 unordered_set<int> s1; for (auto e : nums1) s1.insert(e); // 用unordered_set对nums2中的元素去重 unordered_set<int> s2; for (auto e : nums2) s2.insert(e); // 遍历s1,如果s1中某个元素在s2中出现过,即为交集 vector<int> vRet; for (auto e : s1) { if (s2.find(e) != s2.end()) vRet.push_back(e); } return vRet; } }; -------------------- [两个数组的交集 ||][Link 3] ![在这里插入图片描述][a96864813e8b40c98e85f60e2dbbd59e.png] class Solution { public: vector<int> intersect(vector<int>& nums1, vector<int>& nums2) { if (nums1.size() > nums2.size()) { return intersect(nums2, nums1); } unordered_map <int, int> m; for (int num : nums1) { ++m[num]; } vector<int> intersection; for (int num : nums2) { if (m.count(num)) { intersection.push_back(num); --m[num]; if (m[num] == 0) { m.erase(num); } } } return intersection; } }; -------------------- [存在重复元素][Link 4] ![在这里插入图片描述][3233eade74814d2a9988000e0a5900ec.png] class Solution { public: bool containsDuplicate(vector<int>& nums) { unordered_set<int> s; for (int x: nums) { if (s.find(x) != s.end()) { return true; } s.insert(x); } return false; } }; -------------------- [两句话中的不常见单词][Link 5] ![在这里插入图片描述][146a39a286df4ee8b4472bb1feef275c.png] class Solution { public: vector<string> uncommonFromSentences(string s1, string s2) { unordered_map<string, int> freq; auto insert = [&](const string& s) { stringstream ss(s); string word; while (ss >> word) { ++freq[move(word)]; } }; insert(s1); insert(s2); vector<string> ans; for (const auto& [word, occ]: freq) { if (occ == 1) { ans.push_back(word); } } return ans; } }; -------------------- ## ? 2. 底层结构 ## unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。 ### ? 2.1 哈希概念 ### 顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log\_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。 理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。 当向该结构中: * 插入元素 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放 * 搜索元素 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表) 例如:数据集合\{1,7,6,4,5,9\}; 哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。 ![在这里插入图片描述][45e59b86520c4d80beb87284c5d6e7ce.png] 用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题? > **发现4这个位置已经被占用了** -------------------- ### ? 2.2 哈希冲突 ### 对于两个数据元素的关键字 k i k\_i ki和 k j k\_j kj(i != j),有 k i k\_i ki != k j k\_j kj,但有:Hash( k i k\_i ki) == Hash( k j k\_j kj), 即:**不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞。** 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。 发生哈希冲突该如何处理呢? ### ? 2.3 哈希函数 ### 引起哈希冲突的一个原因可能是:**哈希函数设计不够合理。** 哈希函数设计原则: * 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间 * 哈希函数计算出来的地址能均匀分布在整个空间中 * 哈希函数应该比较简单 **常见哈希函数:** 1. 直接定址法–(常用) 取关键字的某个线性函数为散列地址:Hash(Key)= A\*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况 2. 除留余数法–(常用) 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址 3. 平方取中法–(了解) 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况 4. 折叠法–(了解) 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。 **折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况** 5. 随机数法–(了解) 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。 **通常应用于关键字长度不等时采用此法** 6. 数学分析法–(了解) 设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。 例如: ![在这里插入图片描述][d594b3222fa245f48d8b1997e8dbbbff.png]假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。 数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况 **注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突** -------------------- ### ? 2.4 哈希冲突解决 ### **解决哈希冲突两种常见的方法是:闭散列和开散列** #### ? 2.4.1 闭散列 #### 闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢? 1. 线性探测 比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。 **线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。** 2. 二次探测 线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H\_i Hi = ( H 0 H\_0 H0 \+ i 2 i^2 i2 )% m, 或者: H i H\_i Hi = ( H 0 H\_0 H0 \- i 2 i^2 i2 )% m。其中:i = 1,2,3…, H 0 H\_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。 对于2.1中如果要插入44,产生冲突,使用解决后的情况为: 研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。 ![在这里插入图片描述][dcd06c47e7234b71b14ab8d4e2c5d8fb.png] **因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。** -------------------- #### ? 2.4.2 闭散列的实现 #### #pargma once namespace close_hash { enum Status { EMPTY,//空 EXIST,//存在 DELETE//删除 }; template<class K,class V> struct HashData { pair<K,V> _kv; Status _status = EMPTY; //状态 }; template<class K> struct HashFunc { size_t operator()(const K&key) { return key; } }; //特化 template<> struct HashFunc<string> { size_t operator()(const string& key) { //BKDR Hash思想 size_t hash = 0; for(size_t i = 0;i<key.size();++i) { hash*=131; hash += key[i];//转成整形 } return hash; } }; template<class K,class V,class Hash = HashFunc<K>> class HashTable { public: bool Erase(const K& key) { HashData<K,V>* ret = Find(key); if(ret == nullptr) { //没有这个值 return false; } else { //伪删除 ret->_status = DELETE; _n--; return true; } } HashData<K,V>* Find(const K& key) { if(_table.size() == 0) { //防止除0错误 return nullptr; } Hash hf; size_t index = hf(key) % _table.size(); size_t i = 0; size_t index = start + i; while(_tables[index]._status != EMPTY) { if(_tabled[index]._kv.first == key && _table[index]._status == EXIST) { return &_tabled[index]; } else { ++i; //index = start+i;//线性探测 index = start+i*i;//二次探测 index %= _tables.size(); } } return nullptr; } //插入 bool Insert(const pair<K,V>& kv) { if(Find(kv.first)) { return false; } if(_table.size() == 0 || (double)(_n / _table.size()) > 0.7) { //扩容 //方法二: size_t newSize = _table.size()==0? 10 : _table.size()*2; HashTable<K,V> newHT;//建立一个临时新表 newHT._tables.resize(newSize);//给新表开扩容后的空间 for(auto& e:_tables) { if(e._status == EXIST) { newHT.Insert(e._kv);//将旧表的数据插入新表 } } _table.swap(newHT._tables);//将新表和旧表交换 } Hash hf; size_t start = hf(kv.first) % _tables.size(); //线性探测 size_t i = 0; size_t index = start + i; while(_table[index]._status == EXIST) { ++index; if(index == _tables.size()) { //当index到达最后的时候,让它回到起始 index = 0; } //插满的时候会死循环 } //走到这里要么是空要么是删除 _tables[index]._kv = kv; _tables[index]._status = EXIST; ++_n; return true; } private: vector<HashData<K,V>> _tables;//vector来存储HashData size_t _n = 0;//存储有效数据的个数 } } -------------------- #### ? 2.4.3 开散列 #### 1. 开散列概念 开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。 ![在这里插入图片描述][b460f881fb9542f484b74626bbdc83ac.png]![在这里插入图片描述][656fbfcb6dd74617be06c5c7021e3e16.png] 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。 #### ? 2.4.4 开散列的实现 #### namespace bucket_hash { template<class K, class V> struct HashNode { pair<K, V> _kv; HashNode<K, V>* _next; HashNode(const pair<K, V>& kv) :_kv(kv) , _next(nullptr) { } }; size_t GetNextPrime(size_t prime) { const int PRIMECOUNT = 28; static const size_t primeList[PRIMECOUNT] = { 53ul, 97ul, 193ul, 389ul, 769ul, 1543ul, 3079ul, 6151ul, 12289ul, 24593ul, 49157ul, 98317ul, 196613ul, 393241ul, 786433ul, 1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul, 50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul, 1610612741ul, 3221225473ul, 4294967291ul }; size_t i = 0; for (; i < PRIMECOUNT; ++i) { if (primeList[i] > prime) return primeList[i]; } return primeList[i]; } template<class K, class V, class Hash = HashFunc<K>> class HashTable { typedef HashNode<K, V> Node; public: // 拷贝 和 赋值 需要自己实现桶的拷贝 ~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; } _n = 0; } bool Erase(const K& key) { if (_tables.size() == 0) { return false; } Hash hf; // 素数 size_t index = hf(key) % _tables.size(); Node* prev = nullptr; Node* cur = _tables[index]; while (cur) { if (cur->_kv.first == key) { // 1、cur是头结点 // 2、非头节点 if (prev == nullptr) { _tables[index] = cur->_next; } else { prev->_next = cur->_next; } delete cur; --_n; return true; } else { prev = cur; cur = cur->_next; } } return false; } Node* Find(const K& key) { if (_tables.size() == 0) { return nullptr; } Hash hf; size_t index = hf(key) % _tables.size(); Node* cur = _tables[index]; while (cur) { if (cur->_kv.first == key) { return cur; } else { cur = cur->_next; } } return nullptr; } bool Insert(const pair<K, V>& kv) { Hash hf; //当负载因子到1时,进行扩容 if (_n == _tables.size()) { //size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2; size_t newSize = GetNextPrime(_tables.size()); //HashTable<K, V> newHT; vector<Node*> newtables; newtables.resize(newSize, nullptr); for (size_t i = 0; i < _tables.size(); ++i) { Node* cur = _tables[i]; while (cur) { Node* next = cur->_next; size_t index = hf(cur->_kv.first) % newSize; cur->_next = newtables[index]; newtables[index] = cur; cur = next; } _tables[i] = nullptr; } newtables.swap(_tables); } size_t index = hf(kv.first) % _tables.size(); Node* cur = _tables[index]; while (cur) { if (cur->_kv.first == kv.first) { return false; } else { cur = cur->_next; } } Node* newnode = new Node(kv); newnode->_next = _tables[index]; _tables[index] = newnode; ++_n; return true; } private: vector<Node*> _tables; size_t _n = 0; // 存储多少有效数据 }; -------------------- [Link 1]: https://blog.csdn.net/m0_60338933?type=blog [eabca17da9704379a5f15832a495b4cc.jpeg_pic_center]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/b239f800de2249ddbb63897709dbc246.jpeg [e1bf936c0d034968befd204c79bd4272.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/2e693d7f24224567a16c125820736e94.png [unordered_map]: https://cplusplus.com/reference/unordered_map/unordered_map/?kw=unordered_map [unordered_set]: https://cplusplus.com/reference/unordered_set/unordered_set/?kw=unordered_set [9c9baa8e324543dfa48439146bb9f200.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/0e4cf65acb3146aca07607074ac5207e.png [d62c5853b0324a4993429f36f4f4d4f5.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/c9c24df0d510491c91d434dc0485b1a5.png [2N_N]: https://leetcode.cn/problems/n-repeated-element-in-size-2n-array/description/ [22b61ada1ff546e9a6fcd5a830abe52a.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/29d65c3f446343c3b5b1c80dda512ebe.png [Link 2]: https://leetcode.cn/problems/intersection-of-two-arrays/submissions/388102700/ [f5af47c5f86244f08cb68a4e3a2de66a.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/98da4df90b284eabb6b083cee13fcb3b.png [Link 3]: https://leetcode.cn/problems/intersection-of-two-arrays-ii/description/ [a96864813e8b40c98e85f60e2dbbd59e.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/dd7b278a144c421f8daf2c8d0b497f28.png [Link 4]: https://leetcode.cn/problems/contains-duplicate/description/ [3233eade74814d2a9988000e0a5900ec.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/c9bbb9be184642e4be9f368b7cdb0a0a.png [Link 5]: https://leetcode.cn/problems/uncommon-words-from-two-sentences/description/ [146a39a286df4ee8b4472bb1feef275c.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/cb54fcd616b846ec96e30eadb4f68312.png [45e59b86520c4d80beb87284c5d6e7ce.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/98e908e498f14f6f8351f2c3c5a51ea3.png [d594b3222fa245f48d8b1997e8dbbbff.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/98295e7323eb4cd4b4d1e7398a5e0307.png [dcd06c47e7234b71b14ab8d4e2c5d8fb.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/1adbf88bca27404a8dabd39587a00e44.png [b460f881fb9542f484b74626bbdc83ac.png]: https://img-blog.csdnimg.cn/b460f881fb9542f484b74626bbdc83ac.png [656fbfcb6dd74617be06c5c7021e3e16.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/04/20/2ce39a5f67924217a4af07203dd9527c.png
相关 【C++进阶】map和set( 万字详解)—— 上篇 C++map和set详解上篇,很快就会更新下篇,实现avl树红黑树和封装map和set 旧城等待,/ 2024年04月20日 06:24/ 0 赞/ 68 阅读
相关 【C++进阶】:哈希 哈希 一.unordered\_map 二.底层结构 1.哈希概念 2.解决哈希冲突 1.闭散列 ゞ 浴缸里的玫瑰/ 2024年03月03日 09:06/ 0 赞/ 102 阅读
相关 [C++] 哈希详解 目录 1. 哈希概念 2. 实现机制 2.1 插入时 2.2 查找时 2.3 缺陷 2.4 常见哈希函数 我就是我/ 2022年09月09日 02:19/ 0 赞/ 261 阅读
还没有评论,来说两句吧...