C++修炼:unordered_map和unordered_set的利用和封装

[复制链接]
发表于 2025-7-3 15:36:43 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
        Hello各人好!很高兴我们又见面啦!给生存添点passion,开始今天的编程之路!
  

  我的博客:<但凡.
  我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》
  接待点赞,关注!
          这一期我们来讲解并模拟实现unordered_map和unordered_set。有了前面的铺垫,这一篇就要相对简单很多了。
 
1、unordered_set和unordered_map的利用

        在C++标准模板库(STL)中,unordered_set和unordered_map是两种非常紧张的关联容器,它们基于哈希表实现,提供了高效的查找、插入和删除操作。本文将详细讲解这两种容器的特性、用法、内部实现原理。
      1.1、unordered_set的利用

        unordered_set是一个存储唯一元素的容器,此中每个元素的值同时也是它的键。
        我们把unordered_set的利用快速过一遍。因为在日常利用中,unordered_set的利用和set的利用很相似。接口都是雷同的。
  1. #include <unordered_set>
  2. #include <iostream>
  3. int main() {
  4.     // 创建unordered_set
  5.     std::unordered_set<int> numbers = {1, 2, 3, 4, 5};
  6.    
  7.     // 插入元素
  8.     numbers.insert(6);
  9.     numbers.emplace(7);  // 更高效的插入方式
  10.     //下两篇会详细讲解emplace接口
  11.    
  12.     // 查找元素
  13.     if (numbers.find(3) != numbers.end()) {
  14.         std::cout << "3 found in the set\n";
  15.     }
  16.    
  17.     // 删除元素
  18.     numbers.erase(4);
  19.    
  20.     // 遍历元素
  21.     for (const auto& num : numbers) {
  22.         std::cout << num << " ";
  23.     }
  24.     std::cout << "\n";
  25.    
  26.     // 获取大小
  27.     std::cout << "Size: " << numbers.size() << "\n";
  28.    
  29.     return 0;
  30. }
复制代码
         unordered_set的主要特性:
                唯一性:容器中不允许有重复的元素
                无序性:元素不以任何特定顺序存储
                哈希实现:基于哈希表实现,均匀环境下提供O(1)的查找复杂度
                动态大小:容器可以根据必要动态扩展或收缩
        我们可以把unordered_set理解成不能排序的set,而且unordered_set的插入,删除,查找均匀时间复杂度都是O(1),效率比set要高。
         1.2、unordered_map的利用

        unordered_map是存储键值对的关联容器,此中每个键都是唯一的。与map不同unordered_map中的元素不以任何特定顺序排序。
        操作我们也不细讲了,因为常用接口和map的接口都差不多。
  1. #include <unordered_map>
  2. #include <iostream>
  3. #include <string>
  4. int main() {
  5.     // 创建unordered_map
  6.     std::unordered_map<std::string, int> word_counts = {
  7.         {"apple", 5},
  8.         {"banana", 3}
  9.     };
  10.    
  11.     // 插入元素
  12.     word_counts.insert({"orange", 2});
  13.     word_counts["pear"] = 4;  // 使用下标操作符插入
  14.    
  15.     // 访问元素
  16.     std::cout << "apple count: " << word_counts["apple"] << "\n";
  17.    
  18.     // 查找元素
  19.     auto it = word_counts.find("banana");
  20.     if (it != word_counts.end()) {
  21.         std::cout << "banana count: " << it->second << "\n";
  22.     }
  23.    
  24.     // 删除元素
  25.     word_counts.erase("orange");
  26.    
  27.     // 遍历元素
  28.     for (const auto& pair : word_counts) {
  29.         std::cout << pair.first << ": " << pair.second << "\n";
  30.     }
  31.    
  32.     // 获取大小
  33.     std::cout << "Size: " << word_counts.size() << "\n";
  34.    
  35.     return 0;
  36. }
复制代码
        unordered_map的主要特性:
                键唯一性:每个键只能在容器中出现一次
                无序性:键值对不以任何特定顺序存储
                哈希实现:基于哈希表实现,均匀环境下提供O(1)的访问复杂度
                快速访问:通过键可以直接访问对应的值
               假如我们想要支持自定义范例的比较和存储,必要让自定义范例支持哈希映射和==的比较。unordered_map和unordered_set同理。
  1. #include<iostream>
  2. #include<unordered_map>
  3. #include<unordered_set>
  4. using namespace std;
  5. struct node
  6. {
  7.         int _a;
  8.         int _b;
  9.         bool operator==(const node& x) const
  10.         {
  11.                 return (_a == x._a) && (_b == x._b);
  12.         }
  13. };
  14. class A
  15. {
  16. public:
  17.         //需要注意仿函数的重载()一定是public:
  18.         size_t operator()(const node& x) const
  19.         {
  20.                 int ret = x._a;
  21.                 ret *= 131;
  22.                 ret += x._b;
  23.                 ret *= 131;
  24.                 return ret;
  25.         }
  26. };
  27. int main()
  28. {
  29.         unordered_set<node, A> s;
  30.         unordered_map<node, string, A> mp;
  31.         s.insert({ 1,1 });
  32.         s.insert({ 2,2 });
  33.         node x = { 1,1 };
  34.         mp[{1, 2}] = "11";
  35.         node x1 = { 2,3 };
  36.         mp.insert({x1, "ss"});//传入pair(key,value)
  37.         return 0;
  38. }
复制代码
 2、unordered_map和unordered_set的封装

        基于我们上节课实现的哈希表,我们对unordered_set和unordered_map进行封装。必要注意的是我们利用的是链地址法进行封装,因为stl中的unordered_map和unordered_set也是链地址法。
        2.1、KeyOfT

        上期我们的哈希表查找key值,我们是通过直接访问kv.first来实现的。但是由于封装的时间我们想让unordered_set和unordered_map用同一个哈希表,以是我们对哈希表的模板参数中传入KeyOfT,然后在访问key值时利用KeyOfT的方式:
  1. template<class K,class T,class KeyOfT,class Hash>
复制代码
        这个KeyOfT是跟着unordered_set和unordered_map的时间写的,以是等我们进一步封装的时间再去实现KeyOfT。 
         2.2、迭代器

        我们对上一次的哈希表进行升级,是他支持迭代器:
  1. template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
  2. struct HTIterator
  3. {
  4.         typedef HashNode<T> Node;
  5.         typedef HashTable<K, T, KeyOfT, Hash> HT;
  6.         typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
  7.         Node* _node;
  8.         const HT* _pht;
  9.         HTIterator(Node* node,const HT* pht)
  10.                 :_node(node),
  11.                 _pht(pht)
  12.         {}
  13.         Self& operator++()
  14.         {
  15.                 if (_node->_next)
  16.                 {
  17.                         _node = _node->_next;
  18.                 }
  19.                 else
  20.                 {
  21.                         //根据哈希表找到下一个下挂节点的地方
  22.                         KeyOfT kot;
  23.                         Hash hs;
  24.                         size_t hashi = hs(kot(_node->_data)) % _pht->_tables.size();
  25.                         hashi++;
  26.                         while (hashi < _pht->_tables.size())
  27.                         {
  28.                                 if (_pht->_tables[hashi])
  29.                                 {
  30.                                         _node = _pht->_tables[hashi];
  31.                                         break;
  32.                                 }
  33.                                 //这个地方和线性探测不一样,不要让他成环
  34.                                 ++hashi;
  35.                         }
  36.                         if (hashi == _pht->_tables.size())
  37.                         {
  38.                                 _node = nullptr;
  39.                                 //如果跳出循环发现这个位置根本不存在说明没有下一个节点了
  40.                         }
  41.                         //注意这是前置++
  42.                         return *this;
  43.                 }
  44.         }
  45.         Ref operator*()
  46.         {
  47.                 return _node->_data;
  48.         }
  49.         Ptr operator->()
  50.         {
  51.                 return &_node->_data;
  52.         }
  53.         bool operator!=(const Self& s) const
  54.         {
  55.                 return  _node != s._node;
  56.         }
  57.         bool operator==(const Self& s) const
  58.         {
  59.                 return _node == s._node;//比较地址
  60.         }
  61. };
复制代码
        我们模板参数有6个,K,T是存储范例不多说了,和我们哈希表的一样,ref是引用,ptr是指针,各人应该也都知道,keyOfT是提取key值,hash是哈希映射函数。
        对于operator++,首先假如这个节点是链中的一个节点,而且他还有下一个节点,那我们直接加加就可以了。假如没有,那么我们就去找下一个链。首先我们通过哈希哈希函数找到当前节点在的链子,记作hashi。然后把hashi加加跳过当前的链子,之后一直往后遍历知道找到下一个链子的位置。
        接下来我们让哈希表支持迭代器。
  1.         template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
  2.         friend struct HTIterator;
  3.         typedef HashNode<T> Node;
  4. public:
  5.         typedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
  6.         typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;
  7.         Iterator Begin()
  8.         {
  9.                 if (_n == 0)
  10.                         return End();
  11.                 for (size_t i = 0;i < _tables.size();i++)
  12.                 {
  13.                         if (_tables[i])
  14.                         {
  15.                                 return Iterator(_tables[i],this);
  16.                         }
  17.                 }
  18.                 return End();
  19.         }
  20.         Iterator End()
  21.         {
  22.                 return Iterator(nullptr,this);
  23.         }
  24.         ConstIterator Begin() const
  25.         {
  26.                 if (_n == 0)
  27.                         return End();
  28.                 for (size_t i = 0;i < _tables.size();i++)
  29.                 {
  30.                         if (_tables[i])
  31.                         {
  32.                                 return ConstIterator(_tables[i], this);
  33.                         }
  34.                 }
  35.                 return End();
  36.         }
  37.         ConstIterator End() const
  38.         {
  39.                 return ConstIterator(nullptr, this);
  40.         }
复制代码
        因为我们在写迭代器类的时间用了hashTable中的私有成员变量,以是要在hashTable中对迭代器类进行友元声明。 
        对于begin来说,我们就通过遍历,找到第一个出现值的位置就行了,假如走到最后了都没出现值,那阐明我们当前哈希表是空的,直接返回end(),而end()现实上是返回一个nullptr构造的迭代器。
      2.3、插入

        我们对插入操作改造升级,使他和源码的返回值雷同:
  1. pair<Iterator, bool> Insert(const T& data)
  2. {
  3.         KeyOfT kot;
  4.         Iterator it = Find(kot(data));
  5.         if (it != End())
  6.                 return { it,false };
  7.         Hash hs;
  8.         if (_n == _tables.size())
  9.         {
  10.                 //扩容
  11.                 //HashTable<K, V> newht(__stl_next_prime(_tables.size() + 1));
  12.                  遍历旧表,将旧表的数据全部重新映射到新表
  13.                 //for (size_t i = 0; i < _tables.size(); i++)
  14.                 //{
  15.                 //        Node* cur = _tables[i];
  16.                 //        while (cur)
  17.                 //        {
  18.                 //                newht.Insert(cur->_kv);
  19.                 //                cur = cur->_next;
  20.                 //        }
  21.                 //}
  22.                 //_tables.swap(newht._tables);
  23.                 //我们不采用上面的方式,因为上面的方式每个节点都得拷贝一次
  24.                 vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
  25.                 for (size_t i = 0;i < _tables.size();i++)
  26.                 {
  27.                         Node* cur = _tables[i];
  28.                         while (cur)
  29.                         {
  30.                                 Node* next = cur->_next;
  31.                                 size_t hashi = hs(kot(cur->_data)) % newtables.size();
  32.                                 cur->_next = newtables[hashi];
  33.                                 newtables[hashi] = cur;
  34.                                 cur = next;
  35.                         }
  36.                         _tables[i] = nullptr;
  37.                 }
  38.                 _tables.swap(newtables);//浅交换
  39.         }
  40.         size_t hashi = hs(kot(data)) % _tables.size();
  41.         Node* newnode = new Node(data);
  42.         newnode->_next = _tables[hashi];
  43.         _tables[hashi] = newnode;
  44.         ++_n;
  45.         return { Iterator(newnode,this),true };
  46. }
复制代码
        这段内容没什么难点,各人看看就好了。 
         2.4、unordered_map的封装

        现在我们新建一个头文件unordered_map.h,然后写一下unordered_map的类:
  1. #include"HashTable.h"
  2. template<class K,class V,class hash=HashFunc<K>>
  3. class unordered_map
  4. {
  5.         struct MapKeyOfT
  6.         {
  7.                 const K& operator()(const pair<K, V>& kv) const //仿函数记得加const
  8.                 {
  9.                         return kv.first;
  10.                 }
  11.         };
  12. public:
  13.         typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, hash>::Iterator iterator;
  14.         typedef typename hash_bucket::HashTable< K, pair<const K, V>, MapKeyOfT, hash>::ConstIterator const_iterator;
  15.         iterator begin()
  16.         {
  17.                 return _ht.Begin();
  18.         }
  19.         iterator end()
  20.         {
  21.                 return _ht.End();
  22.         }
  23.         const_iterator begin() const
  24.         {
  25.                 return _ht.Begin();
  26.         }
  27.         const_iterator end() const
  28.         {
  29.                 return _ht.End();
  30.         }
  31.         pair<iterator, bool> insert(const pair<K, V>& kv)
  32.         {
  33.                 return _ht.Insert(kv);
  34.         }
  35.         iterator find(const K& key)
  36.         {
  37.                 return _ht.Find(key);
  38.         }
  39.         bool erase(const K& key)
  40.         {
  41.                 return _ht.Erase(key);
  42.         }
  43.         V& operator[](const K& key)
  44.         {
  45.                 pair<iterator, bool> ret = insert({ key,V() });
  46.                 return ret.first->second;
  47.         }
  48. private:
  49.         hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, hash> _ht;
  50. };
复制代码
        由于我们要包管unordered_map中存储的值的key值不被修改,以是哈希表的私有变量我们第二个传入的模版参数是pair<const K,V>。
        2.5、unordered_set的封装

        说实话这也没啥好说的,各人看看就好。
  1. #include"HashTable.h"
  2. template<class K,class Hash=HashFunc<K>>
  3. class unordered_set
  4. {
  5.         struct SetKeyOfT
  6.         {
  7.                 const K& operator()(const K& key)
  8.                 {
  9.                         return key;
  10.                 }
  11.         };
  12. public:
  13.         //注意迭代器都是大类里面封装好的迭代器,不能拿最开始定义的那个类的迭代器
  14.         typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
  15.         typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;
  16.         iterator begin()
  17.         {
  18.                 return _ht.Begin();
  19.         }
  20.         iterator end()
  21.         {
  22.                 return _ht.Begin();
  23.         }
  24.         const_iterator begin() const
  25.         {
  26.                 return _ht.Begin();
  27.         }
  28.         const_iterator end() const
  29.         {
  30.                 return _ht.End();
  31.         }
  32.         pair<iterator, bool> insert(const K& key)
  33.         {
  34.                 return _ht.Insert(key);
  35.         }
  36.         iterator find(const K& key)
  37.         {
  38.                 return _ht.Find(key);
  39.         }
  40.         bool erase(const K& key)
  41.         {
  42.                 return _ht.Erase(key);
  43.         }
  44. private:
  45.         hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
  46. };
复制代码
3、总结 

        unordered_set常常用于去重以及存在性检查。unordered_map常用于快速键值查找以及频率统计。在后续做算法题时,常常会用到这两个容器。
特性unordered_setunordered_map存储内容仅 KeyKey-Value 对哈希冲突处理链地址法链地址法时间复杂度均匀 O(1),最坏 O(n)均匀 O(1),最坏 O(n)动态扩容必要必要迭代器支持可以自定义可以自定义         好了,今天的内容就分享到这,我们下期再见! 
 
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表