【C++】——深入探析与封装:探索unordered系列容器的高效应用

[复制链接]
发表于 2025-12-10 03:35:52 | 显示全部楼层 |阅读模式

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

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

×
低头赶路

敬事如仪


目次
1、Unordered 系列容器:哈希桶与高效查找
unordered_map
unordered_set
2、 哈希桶实现的高级封装
2.1 模板参数与范例泛化
2.2 迭代器的筹划与实现
3、 容器的上层抽象与封装
3.1 unordered_set 的高效封装
3.2 unordered_map 的高效封装与扩展
4、 口试题剖析与实践履历


1、Unordered 系列容器:哈希桶与高效查找

unordered_map 和 unordered_set 是以哈希桶为底层布局的容器,紧张用于快速查找指定命据。与其他容器相比,它们在查找元素时提供了明显的性能提升,但也有一些特别的限定和特点。
unordered_map

unordered_map 是一种基于哈希表实现的容器,用于存储键值对 <key, value>。其紧张特点如下:


  • 快速查找:通过 key 快速查找对应的 value,查找服从明显高于 map。
  • 无序存储:元素的存储次序并非按插入次序,而是根据 key 的哈希值决定的数组下标,因此是无序的。
  • 修改规则:

    • key 一旦插入后不可修改,value 可以修改。
    • 提供 [ ] 运算符来访问或插入元素。

  • 迭代器:仅支持正向迭代器,无法反向迭代。
  • 性能特点:只管查找速率非常快,但迭代速率较慢。
unordered_set

unordered_set 与 unordered_map 雷同,但只存储 key,没有对应的 value。其紧张特点如下:


  • 快速查找:通过 key 快速判断元素是否存在,查找服从远高于 set。
  • 无序存储:与 unordered_map 一样,元素按照 key 的哈希值存储,存储次序无特定次序。
  • 修改规则:

    • 只存储 key,不能修改 key 的值。
    • 不提供 [ ] 运算符举行元素访问。

  • 迭代器:同样只提供正向迭代器,不支持反向迭代。
常用接口
无论是 unordered_map 照旧 unordered_set,它们都提供了一些相似的接口,便于用户举行高效利用。
迭代器接口:
函数功能先容begin()返回容器第一个元素的迭代器end()返回容器末了一个元素之后的位置的迭代器cbegin()返回容器第一个元素的 const 迭代器cend()返回容器末了一个元素之后位置的 const 迭代器常勤劳能函数:
函数功能先容find(const K& key)查找指定 key,返回对应元素的迭代器count(const K& key)返回容器中 key 对应的元素个数insert()向容器中插入元素erase()删除指定元素clear()清空容器中的全部元素swap(unordered_map&)交换两个容器的元素哈希桶利用:
函数功能先容bucket_count()返回哈希桶的总数bucket_size(size_t n)返回指定桶 n 中的元素数目bucket(const K& key)返回指定 key 所在的桶编号
总结


  • unordered_map 实用于须要根据键值对举行快速查找的场景,支持对 value 的修改,提供高效的查找性能,但迭代利用相对较慢。
  • unordered_set 实用于去重或判断元素是否存在的场景,只存储 key,不支持随机访问,通过哈希表提供快速查找。
两者都以哈希桶为底层布局,提供高效的查找本领,得当大数据量的快速查找场景,但其性能和功能之间也存在肯定的折衷。
2、 哈希桶实现的高级封装

2.1 模板参数与范例泛化

底层哈希桶的泛型编程改造:适配差别数据范例
unordered_map 和 unordered_set 都基于开散列(哈希桶)实现,其底层数据存储固然相似,但存储的数据范例却有所差别。unordered_map 存储的是键值对 pair<k, v>,而 unordered_set 只存储键值 key。为了让哈希桶可以大概适配差别的数据存储需求,我们通过泛型编程举行改造,利用模板参数来实现这种适配。
四个关键模板参数
为了实现数据存储的机动性,我们须要引入四个模板参数,确保底层哈希桶可以大概根据差别的需求举行设置:
   

  • class K:表现键值范例 key,这是最根本的模板参数,决定了哈希表的键范例。
  • class T:表现存储的数据范例,可以是 pair<k, v>(用于 unordered_map)大概仅为 key(用于 unordered_set)。
  • class KeyOfT:这是一个关键的模板参数,通过仿函数界说怎样从存储的数据范例 T 中提取 key。这一环节是适配差别数据范例的焦点,非常奥妙地利用了仿函数来举行转换,使得哈希桶可以大概机动应对差别存储方式。
  • class HashFunc:这是用来将 key 转换为哈希值(size_t 范例),进而映射到哈希桶的数组下标。它是哈希表的底子,通过该函数确定 key 的分布。
通过模板参数实现适配
通过这四个模板参数的组合,底层哈希桶就可以大概根据上层 unordered_map 或 unordered_set 的需求,机动存储差别范例的数据。比方,在 unordered_map 中,T 为 pair<k, v>,而在 unordered_set 中,T 仅为 key。而 KeyOfT 则通过仿函数来确保可以大概从 T 中精确地提取出 key。
这种方式通过机动的模板参数,使得底层哈希桶可以大概根据上层容器的差别需求举行调解,极大地进步了代码的复用性和适配性。
总结
通过引入四个模板参数(K、T、KeyOfT、HashFunc),我们实现了一个通用的哈希桶布局,可以大概根据需求机动存储 key 或 key-value 对。这种筹划将数据的存储与哈希桶的实现分离,使得容器可以大概高效且机动地适配差别的应用场景。
  1. template<class K>
  2. struct HashFunc
  3. {
  4.         size_t operator()(const K& key)
  5.         {
  6.                 return (size_t)key;
  7.         }
  8. };
  9. template<>
  10. struct HashFunc<string>
  11. {
  12.         size_t operator()(const string& s)
  13.         {
  14.                 // BKDR
  15.                 size_t hash = 0;
  16.                 for (auto ch : s)
  17.                 {
  18.                         hash += ch;
  19.                         hash *= 131;
  20.                 }
  21.                 return hash;
  22.         }
  23. };
  24. template<class T>
  25. struct HashNode
  26. {
  27.         T _data;
  28.         HashNode<T>* _next;
  29.         HashNode(const T& data)
  30.                 :_data(data)
  31.                 , _next(nullptr)
  32.         {}
  33. };
  34. template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
  35. class HashTable
  36. {   //设置为友元关系否则 迭代器++无法访问 私有成员 _tables 友元声明和前置声明都需要加上模板参数
  37.         template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
  38.         friend struct HTIterator;
  39.         typedef HashNode<T> Node;
  40. public:
  41.         typedef HTIterator<K,T, T&, T*, KeyOfT, Hash> Iterator;
  42.         typedef HTIterator<K,T, const T&, const T*, KeyOfT, Hash> ConstIterator;
  43.         Iterator Begin()
  44.         {
  45.         }
  46.         Iterator End()
  47.         {
  48.         }       
  49.         ConstIterator Begin()const
  50.         {
  51.         }
  52.         ConstIterator End()const
  53.         {
  54.         }
  55.         inline unsigned long __stl_next_prime(unsigned long n)
  56.         {
  57.                 static const int __stl_num_primes = 28;
  58.                 static const unsigned long __stl_prime_list[__stl_num_primes] =
  59.                 {
  60.                 53, 97, 193, 389, 769,
  61.                 1543, 3079, 6151, 12289, 24593,
  62.                 49157, 98317, 196613, 393241, 786433,
  63.                 1572869, 3145739, 6291469, 12582917, 25165843,
  64.                 50331653, 100663319, 201326611, 402653189, 805306457,
  65.                 1610612741, 3221225473, 4294967291
  66.                 };
  67.                 const unsigned long* first = __stl_prime_list;
  68.                 const unsigned long* last = __stl_prime_list +
  69.                         __stl_num_primes;
  70.                 const unsigned long* pos = lower_bound(first, last, n);
  71.                 return pos == last ? *(last - 1) : *pos;
  72.         }
  73. public:
  74.         HashTable()
  75.                 :_tables(__stl_next_prime(0))
  76.                 ,_n(0)
  77.         {}
  78.         // 拷贝构造 赋值重载
  79.        
  80.         ~HashTable()
  81.         {
  82.         }
  83.         pair<Iterator,bool> Insert(const T& data)
  84.         {       
  85.         }
  86.         Iterator Find(const K& key)
  87.         {
  88.         }
  89.         bool Erase(const K& key)
  90.         {       
  91.         }
  92. private:
  93.         vector<Node*> _tables; // 指针数组
  94.         size_t _n = 0; // 表中存储数据个数
  95. };
复制代码
我们的模版参数修改之后,我们的函数体也要举行改造,不能直接写死,要符合泛型编程:
函数根本都是修改了本来的cur->_kv.first 变为 kot(cur->_kv),通过仿函数来获取key值,而且返回值设置为迭代器。如许无论我们传入的是pair<k , v> 或 key,都可以通过仿函数获取对应的key值!
下面给出插入函数的代码,别的函数的改造可以参考【C++】——风雅化哈希表架构:理论与实践的综合分析-CSDN博客 中的开散列实现战略,焦点加了仿函数获取key值!!
  
  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.        
  8.         Hash hash;
  9.         // 负载因子等于1时候 进行扩容
  10.         if (_n==_tables.size())
  11.         {
  12.                 vector<Node*> newtable(__stl_next_prime(_tables.size() + 1));
  13.                 for (size_t i = 0; i < _tables.size(); i++)
  14.                 {
  15.                        
  16.                         Node* cur = _tables[i];
  17.                         while (cur)
  18.                         {
  19.                                 Node* next = cur->_next;
  20.                                 // 头插到新链表
  21.                                 size_t hashi = hash(kot(cur->_data)) % newtable.size();
  22.                                 cur->_next = newtable[hashi];
  23.                                 newtable[hashi] = cur;
  24.                                 cur = next;
  25.                         }
  26.                         _tables[i] = nullptr;
  27.                 }
  28.                 _tables.swap(newtable);
  29.         }
  30.         size_t hashi = hash(kot(data)) % _tables.size();
  31.         // 头插
  32.         Node* newnode = new Node(data);
  33.         newnode->_next = _tables[hashi];
  34.         _tables[hashi] = newnode;
  35.         ++_n;
  36.         return {Iterator(newnode,this),true};
  37. }
复制代码
2.2 迭代器的筹划与实现

实现迭代器:访问与修改数据的强大武器
在实现容器的封装时,迭代器是不可或缺的一部门。迭代器不但让我们可以大概利用基于范围的 for 循环,还可以方便地访问和修改容器中的数据。因此,筹划一个高效的迭代器是必不可少的一环。
哈希表的迭代器筹划
哈希表的迭代器与传统容器的迭代器略有差别,紧张体如今遍历规则和内部机制。我们须要构建一个迭代器框架,来支持高效的元素访问和遍历。
焦点要素

  • 节点指针:迭代器的焦点是节点指针,用于访问哈希表中的数据。每个节点表现哈希桶中的一个元素。
  • 支持++运算符:迭代器必须支持 ++ 运算符,用于遍历哈希表中的元素。移动规则如下:

    • 假如当前桶未遍历完,直接移动到桶中的下一个元素。
    • 假如当前桶已遍历完,则跳转到下一个桶的第一个元素。为了实现这一点,迭代器内部须要访问哈希表(HashTable),以便获取下一个桶。

  • 根本运算符:

    • != 和 ==:用于比力两个迭代器是否指向雷同元素或是否差别。
    • * 和 ->:提供对元素的访问,用于解引用和成员访问。

构造函数筹划
在迭代器的构造函数中,我们利用 const HT* _ht 范例的指针作为参数,以包管只能读取哈希表数据而不能修改。这也是出于对容器封装的安全思量,克制上层传入 const HT*,从而影响迭代器的举动。
  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* _ht; //为什么变为const 下面的 const End() 传的this 是const类型 如果这里 _ht不是const那么权限放大不通过
  9.         HTIterator(Node* node, const HT* ht)
  10.                 :_node(node)
  11.                 , _ht(ht)
  12.         {}
  13.        
  14.         Ref operator*()
  15.         {
  16.                 return _node->_data;
  17.         }
  18.         Ptr operator->()
  19.         {
  20.                 return &(_node->_data);
  21.         }
  22.         bool operator!=(const Self& s)
  23.         {
  24.                 return _node != s._node;
  25.         }
  26.         bool operator==(const Self& s)
  27.         {
  28.                 return _node == s._node;
  29.         }
  30.         Self& operator++()
  31.         {
  32.     //....
  33.         }
  34.        
  35. };
复制代码
办理迭代器的前置声明与 ++ 运算符标题
在筹划哈希表的迭代器时,遇到了一些常见的标题。特别是当我们将迭代器放在哈希表类的外部时,会出现编译错误:编译器无法辨认 HashTable 范例。这是由于 HashTable 类在迭代器界说之后才举行声明,因此编译器在迭代器类的界说时无法访问到 HashTable 类。
办理方法
我们有两种办理方案来克制这个标题:

  • 前置声明:在迭代器之前为 HashTable 添加前置声明,如许编译器就能辨认 HashTable 的存在。
    1. // 前置声明 告诉编译器 HashTable 是类模板后面有定义
    2. template<class K,class T, class KeyOfT, class Hash>
    3. class HashTable;
    复制代码

  • 将迭代器放入 HashTable 内部:我们也可以将迭代器类界说为 HashTable 的内部类,如许就不须要前置声明确。内部类直接访问外部类的成员,因此迭代器可以大概直接访问 HashTable 的数据和方法。
    1. template <typename Key, typename Value>
    2. class HashTable {
    3. public:
    4.     class Iterator {
    5.         // 迭代器类定义
    6.     };
    7. };
    复制代码

第二种方法将迭代器类与哈希表类细密团结,是一种常见的做法,可以大概克制前置声明的贫苦,同时也更符合对象的封装头脑。但是为了兼容之前哈希架构,我们选择前置声明。
++ 运算符的实现:遍历哈希表
实现 ++ 运算符是哈希表迭代器中的关键部门。我们须要处置惩罚两个紧张的环境:

  • 当前桶内有多个元素:此时,迭代器只须要移动到当前桶中的下一个节点。
  • 当前桶已遍历完:此时,迭代器须要跳转到下一个桶,并从该桶的第一个元素开始遍历。假如当前哈希表中没有更多桶可供访问,则迭代器表现遍历完成。
++ 运算符的逻辑


  • 当前桶未遍历完:直接移动到当前桶中的下一个元素,即通过 cur = cur->next 访问下一个节点。
  • 当前桶已遍历完:假如 cur->next 为 nullptr,分析当前桶已遍历完,须要跳转到下一个桶的第一个元素。我们须要从哈希表中查找下一个有用的桶。
  • 竣事遍历:假如找到了下一个桶,继承遍历;假如没有找到,分析已经遍历完全部元素,迭代器指向容器末了。
代码实现:++ 运算符
  1. Self& operator++()
  2. {
  3.         if (_node->_next)
  4.         {
  5.                 //当前哈希桶还有数据,那就走到当前桶下一个节点
  6.                 _node = _node->_next;
  7.         }
  8.         else
  9.         {
  10.                 //当前哈希桶走完了,找下一个不为空的桶
  11.                 KeyOfT kot;
  12.                 Hash hash;
  13.                 size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
  14.                 ++hashi;
  15.                 while (hashi < _ht->_tables.size())
  16.                 {
  17.                         _node = _ht->_tables[hashi];
  18.                         if (_node)
  19.                                 break;
  20.                         else
  21.                                 ++hashi; //没有这个陷入死循环
  22.                 }
  23.                 // 走完了所以桶  end()给的空来标识_node
  24.                 if (hashi == _ht->_tables.size())
  25.                 {
  26.                         _node = nullptr;
  27.                 }
  28.         }
  29.         return *this;
  30. }
复制代码
如许我们的迭代器就完成了,再在hashtable中实例化平常迭代器和const迭代器:
  1. public:
  2.         typedef HTIterator<K,T, T&, T*, KeyOfT, Hash> Iterator;
  3.         typedef HTIterator<K,T, const T&, const T*, KeyOfT, Hash> ConstIterator;
  4.         Iterator Begin()
  5.         {
  6.                 if (_n == 0)
  7.                         return End();
  8.         //找到第一个不为空的桶
  9.                 for (size_t i = 0; i < _tables.size(); i++)
  10.                 {
  11.                         Node* cur = _tables[i];
  12.                         if (cur)
  13.                         {
  14.                                 return Iterator(cur, this);
  15.                         }
  16.                 }//遍历完了都为空直接返回
  17.                 return End();
  18.         }
  19.         Iterator End()
  20.         {
  21.                 return Iterator(nullptr, this);
  22.         }       
  23.         ConstIterator Begin()const
  24.         {
  25.                 if (_n == 0)
  26.                         return End();
  27.                 for (size_t i = 0; i < _tables.size(); i++)
  28.                 {
  29.                         Node* cur = _tables[i];
  30.                         if (cur)
  31.                         {
  32.                                 return ConstIterator(cur, this);
  33.                         }
  34.                 }
  35.                 return End();
  36.         }
  37.         ConstIterator End()const
  38.         {
  39.                 return ConstIterator(nullptr, this);
  40.         }
复制代码
如许底层就实现好了,接下来我们开始在上层做动作!
3、 容器的上层抽象与封装

3.1 unordered_set 的高效封装

unordered_set 是一种基于哈希表(哈希桶)实现的容器,它只存储 key 值,并通过哈希值实现快速查找。为了确保哈希表利用的高效性和数据的同等性,我们在筹划 unordered_set 时须要注意以下几个关键点:
底层筹划

  • 数据存储: unordered_set 只存储 key 值,而不是键值对,因此其存储的元素是不可修改的。为了包管哈希表的稳固性,key 值一旦插入就不可更改,这要求在底层存储时必须将 key 设置为 const 范例,确保数据的不可修改性。
  • 获取 key 的仿函数: 在 unordered_set 中,固然只存储 key 值,但我们须要从存储的元素中提取出 key 来举行哈希盘算。为此,我们利用 仿函数(比方 SetKeyOfT)来从存储的元素中提取 key。对于 unordered_set,存储的元素自己就是 key,因此仿函数的功能就是直接返回该元素。
  • 哈希函数: 为了在哈希表中定位元素的位置,unordered_set 须要提供一个哈希函数。这个哈希函数将 key 转换为哈希值,进而映射到哈希表的桶中。上层容器可以通过提供自界说的哈希函数来决定哈希盘算的方式,以确保元素可以大概匀称分布在哈希桶中。
  • 函数调用与封装: 在大多数环境下,unordered_set 中的常见利用(如插入、查找、删除等)会直接调用底层哈希表的相干函数。通过这种方式,unordered_set 可以大概有用地封装底层细节,提供高效的接供词外部利用。
迭代器筹划
迭代器是容器访问和利用元素的紧张工具。在 unordered_set 的筹划中,迭代器须要具备以下几个紧张特性:

  • 明确迭代器范例: 在实例化迭代器时,我们须要利用 typename 关键字明确指出 iterator 是一个范例,而不是一个变量或其他范例。这是由于编译器大概会将迭代器辨认为一个平常变量,从而导致编译错误。因此,利用 typename 可以确保编译器精确剖析迭代器范例。
  • 遍历逻辑: 由于 unordered_set 是基于哈希表实现的,元素的存储次序是无序的。因此,迭代器须要可以大概遍历哈希表中的全部桶和桶内的元素。迭代器的 ++ 运算符须要根据桶的遍历规则举行处置惩罚:

    • 假如当前桶中尚有元素,迭代器移动到下一个元素。
    • 假如当前桶已经遍历完,迭代器须要跳转到下一个非空桶,并从该桶的第一个元素开始继承遍历。
    • 假如没有更多的桶可以遍历,迭代器表现已经遍历完全部元素。

  • 根本迭代器利用: 迭代器须要支持标准的利用,如 !=(不即是)、==(即是)、*(解引用)等。通过这些利用,我们直接复用一下底层函数。
  1. template<class K, class Hash = HashFunc<K>>
  2. class unordered_set
  3. {
  4.         struct SetKeyOfT
  5.         {
  6.                 const K& operator()(const K& key)
  7.                 {
  8.                         return key;
  9.                 }
  10.         };
  11. public:
  12.         typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
  13.         typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;
  14.         iterator begin()
  15.         {
  16.                 return _ht.Begin();
  17.         }       
  18.         iterator end()
  19.         {
  20.                 return _ht.End();
  21.         }
  22.         const_iterator begin()const
  23.         {
  24.                 return _ht.Begin();
  25.         }
  26.         const_iterator end()const
  27.         {
  28.                 return _ht.End();
  29.         }
  30.        
  31.         pair<iterator,bool> insert(const K& key)
  32.         {
  33.                 return _ht.Insert(key);
  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();
  42.         }
  43. private:
  44.         hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht; //typedef 的参数 一定跟定义的成员变量一致 不可缺省
  45. };
复制代码
测试一下!!
  1. void test_set1()
  2. {
  3.         xc::unordered_set<string> S;
  4.         vector<string> arr = { "sort" , "hello" , "XXXCCC" , "Hi" };
  5.         for (auto e : arr)
  6.         {
  7.                 S.insert(e);
  8.         }
  9.         xc::unordered_set<string>::iterator it = S.begin();
  10.         cout << "-------while循环遍历--------" << endl;
  11.         while (it != S.end())
  12.         {
  13.                 //(*it)++;
  14.                 std::cout << *it << endl;
  15.                 ++it;
  16.         }
  17.         cout << "-------基于范围的for循环--------" << endl;
  18.         for (auto e : S)
  19.         {
  20.                 //e++;
  21.                 cout << e << endl;
  22.         }
  23.         cout << "-------查找"hello"--------" << endl;
  24.         cout << *(S.find("hello")) << endl;
  25. }
复制代码


3.2 unordered_map 的高效封装与扩展

unordered_map 是一种基于哈希表实现的容器,它存储的是 键值对 pair<key, value>,与 unordered_set 差别的是,unordered_map 须要同时管理 key 和 value,而且提供了通过 key 查找和修改对应 value 的本领。
底层筹划

  • 数据存储: unordered_map 储存的是 pair<key, value>,此中 key 是键,value 是值。由于 key 在哈希表中起到索引作用,因此必须包管 key 的不可修改性。这意味着 key 必须界说为 const key,确保其在插入后不会被改变,从而保持哈希值的稳固性。
    为了实现这一点,unordered_map 的内部实现通常会利用 pair<const key, value> 来存储数据。
  • 获取 key 的仿函数: unordered_map 须要可以大概从存储的 pair<key, value> 中提取出 key,以便举行哈希盘算。我们利用 仿函数 MapKeyOfT 来从 pair 范例中提取 key。这与 unordered_set 雷同,但差别的是,这里我们提取的是 pair 的第一个元素作为 key。
  • 哈希函数: 与 unordered_set 雷同,unordered_map 也须要提供哈希函数来盘算 key 的哈希值。哈希函数将 key 转换为哈希值,用于在哈希表中定位对应的桶。通过为 unordered_map 提供自界说的哈希函数,用户可以控制哈希的分布,以实现更匀称的元素分配。
  • 函数封装: unordered_map 的大部门利用(如插入、查找、删除等)会直接调用底层哈希表的相干函数。通过这种方式,容器的利用可以大概保持简便且高效,同时屏蔽了哈希表的复杂实现细节。
迭代器筹划
unordered_map 的迭代器筹划与 unordered_set 雷同,也须要具备以下特点:

  • 明确迭代器范例: 与 unordered_set 一样,在实例化 unordered_map 的迭代器时,我们须要利用 typename 关键字显式地声明 iterator 是一个范例,而不是变量或其他范例。这是为了克制编译器的歧义,确保迭代器可以大概正常工作。
  • 遍历逻辑: 由于 unordered_map 基于哈希表实现,元素的存储次序是无序的。因此,迭代器须要支持哈希表的桶遍历。与 unordered_set 雷同,unordered_map 迭代器在遍历时也须要处置惩罚桶内元素的次序和差别桶之间的跳转。

    • 桶内遍历: 假如当前桶尚有元素,迭代器移动到桶中的下一个元素。
    • 桶间遍历: 假如当前桶已遍历完,迭代器须要跳转到下一个非空桶,并从该桶的第一个元素开始继承遍历。

  • 根本迭代器利用: unordered_map 的迭代器须要支持常见的利用,如 !=(不即是)、==(即是)、*(解引用)等。这些利用可以复用使得可以大概方便地访问和修改容器中的 key-value 。
[ ] 利用符的实现
unordered_map 中的 [ ] 利用符是一个非常紧张且常用的功能。其根本运算规则如下:


  • 假如对应的 key 已经存在,[ ] 运算符会返回该 key 对应的 value。
  • 假如 key 不存在,则会举行插入,插入的 value 会设置为默认构造的初始值(如整数的默认值是 0,指针的默认值是 nullptr 等)。
为了实现这一功能,我们可以通过调用底层的 Insert 函数。Insert 函数会查抄是否已经存在雷同的 key,假如 key 已存在则不会插入重复数据,并返回对应的迭代器。通过 Insert 的方式,我们可以大概确保 unordered_map 中不会有重复的 key,同时当 key 不存在时可以大概自动举行插入利用。
  1.         template<class K, class V, class Hash = HashFunc<K>>
  2.         class unordered_map
  3.         {
  4.                 struct MapKeyOfT
  5.                 {
  6.                         const K& operator()(const pair<K, V>& kv)
  7.                         {
  8.                                 return kv.first;
  9.                         }
  10.                 };
  11.         public:
  12.                 typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
  13.                 typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;
  14.                 iterator begin()
  15.                 {
  16.                         return _ht.Begin();
  17.                 }
  18.                 iterator end()
  19.                 {
  20.                         return _ht.End();
  21.                 }
  22.                 const_iterator begin()const
  23.                 {
  24.                         return _ht.Begin();
  25.                 }
  26.                 const_iterator end()const
  27.                 {
  28.                         return _ht.End();
  29.                 }
  30.                 V& operator[](const K& key)
  31.                 {
  32.                         pair<iterator, bool>ret = insert({ key,V() });
  33.                         //return ret.first.operator->()->second;
  34.                         return ret.first->second; //省略了一个  ->
  35.                 }
  36.                 pair<iterator,bool> insert(const pair<K, V>& kv)
  37.                 {
  38.                         return _ht.Insert(kv);
  39.                 }
  40.                 iterator find(const K& key)
  41.                 {
  42.                         return _ht.Find(key);
  43.                 }
  44.                 bool erase(const K& key)
  45.                 {
  46.                         return _ht.Erase();
  47.                 }
  48.         private:
  49.                 hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
  50.         };
复制代码
测试一下:
  1. void test_map2()
  2. {
  3.         unordered_map<string, string> dict;
  4.         dict.insert({ "sort", "排序" });
  5.         dict.insert({ "left", "左边" });
  6.         dict.insert({ "right", "右边" });
  7.         dict["left"] = "左边,剩余";
  8.         dict["insert"] = "插⼊";
  9.         dict["string"];
  10.         unordered_map<string, string>::iterator it = dict.begin();
  11.         while (it != dict.end())
  12.         {
  13.                  //不能修改first,可以修改second
  14.                 //it->first += 'x';
  15.                  it->second += 'x';
  16.                 cout << it->first << ":" << it->second << endl;
  17.                 ++it;
  18.         }
  19.         cout << endl;
  20. }
复制代码


4、 口试题剖析与实践履历

在处置惩罚大规模数据时,尤其是面临凌驾100GB的日志日志文件或其他海量数据集,怎样高效地找到频仍出现的元素(如IP所在、整数等)成了一个关键标题。以下我们将探究几种常用的技能本领,团结现实标题,提出更加优化的办理方案。
标题一:怎样找到出现次数最多的IP所在?

   对于一个凌驾100GB的日志日志文件,此中记载了大量的IP所在,最直观的做法是遍历一遍文件,利用哈希表统计每个IP出现的次数。如许做的标题在于,哈希表的内存斲丧大概非常大,尤其是当日志日志文件体积巨大时,哈希表自己的开销大概导致无法处置惩罚整个数据集。为了应对这个挑衅,可以思量以下优化方案:
  优化方法:


  • 分治法: 将超大日志文件分割成多个小文件。每个小文件的巨细基于内存限定来分别,然后针对每个小文件利用哈希表统计IP出现次数。如许,每次处置惩罚的数据量不会凌驾内存限定,可以大概有用淘汰内存开销。
  • 哈希分区: 基于IP所在的哈希值将日志数据分布到多个小文件中。每个小文件独立处置惩罚,盘算IP所在的频率,末了将各个小文件的统计结果归并。这种方法有用地将数据分散到差别节点,淘汰了单一节点的内存压力。
  • 外部排序: 对于内存有限的环境,可以接纳外部排序算法。起首提取全部IP所在,将其排序,然后对每个IP的出现次数举行计数。利用外部排序将日志文件分批加载到内存中处置惩罚,末了归并计数结果,找出出现次数最多的IP所在。
标题二:怎样找到Top K的IP所在?

   当标题进一步扩展为找到Top K的IP所在时,我们不但须要统计每个IP的出现次数,还须要根据这些统计信息举行排序。思量到数据集的巨大,直接将全部结果加载到内存中举行排序不切现实。以下是更加优化的做法:
  优化方法:


  • 提取与排序: 利用Linux下令行工具(如awk、grep)提取IP所在,起首对这些所在举行排序。接着利用uniq -c下令对IP所在举行去重计数。末了,通过再次排序并利用head -n K来获取出现次数最多的Top K个IP所在。
    具体下令:
    1. awk '{print $1}' log_file | sort | uniq -c | sort -nr | head -n K
    复制代码

  • 外部排序与归并: 假如数据量非常巨大且内存不敷,接纳外部排序来处置惩罚:起首将日志数据分批次加载到内存中,举行排序和计数,然后徐徐归并这些中央结果,终极找出Top K的IP所在。
标题三:如那边理100亿个整数,找出只出现一次的整数?

   当我们须要处置惩罚超大规模的数据集,比方有100亿个整数,而且内存仅为1GB,探求只出现一次的整数成为一个挑衅。平常的计数方法不实用,由于它须要在内存中存储全部整数的计数信息,超出了内存承载本领。此时可以借助位图(Bitmap)等高效的数据布局举行处置惩罚。
  位图方法:


  • 初始化位图: 创建一个富足大的位图,用于标记每个整数的出现环境。位图是一个位数组,每个整数在位图中占用一个位置。若某整数第一次出现,将其对应的位设置为1;若该整数再次出现,则将对应的位设置为-1。终极,位图中标记为1的整数就是只出现一次的整数。
  • 高效存储: 由于位图是以位为单位存储数据,相较于传统的哈希表,它的内存利用更加紧凑,非常得当于内存受限的环境下举行处置惩罚。
标题四:怎样找到两个文件中100亿个整数的交集?

   劈面临两个文件中各有100亿个整数,且内存仅为1GB时,怎样高效地找到它们的交集是一个范例的外部数据处置惩罚标题。我们可以接纳以下两种优化方法:
  方法一:分治法与哈希分桶


  • 分治法: 将两个文件各自分割成多个小文件,巨细根据内存限定来决定。然后利用哈希表对每个小文件中的整数举行计数,找出交集。
  • 哈希分桶: 利用哈希函数将文件中的整数分布到多个桶中。对于每个桶,可以在内存中处置惩罚这部门数据,找出交集。末了将各个桶的结果归并,得到两个文件的交集。
方法二:外部排序


  • 排序: 分别对两个文件举行外部排序。由于内存有限,数据可以徐徐加载进内存举行排序,并将排序后的数据存储回磁盘。
  • 归并: 通过外部归并排序的方式,徐徐归并两个文件中的数据。通过一次遍历,找出两个文件中的交集。
标题五:如那边理出现次数不凌驾2次的整数?

   当我们须要在一个超大文件中找出全部出现次数不凌驾2次的整数,而且内存限定为1GB时,可以接纳以下方法:
  方法一:分治法 + 哈希表


  • 分治法: 将大文件分割成多个小文件,并根据内存限定徐徐处置惩罚每个小文件。利用哈希表统计每个整数的出现次数。
  • 过滤: 遍历每个哈希表,筛选出出现次数不凌驾2次的整数,并将其输出到结果文件中。
  • 归并结果: 将各个小文件的结果归并,终极得到出现次数不凌驾2次的整数。
方法二:哈希分桶


  • 哈希分桶: 利用哈希函数将文件中的整数分配到多个桶中。在每个桶中利用哈希表统计出现次数。
  • 过滤与归并: 对每个桶内的结果举行过滤,找出出现次数不凌驾2次的整数,然后将各个桶的结果归并。
Thanks谢谢阅读!!!

回复

使用道具 举报

登录后关闭弹窗

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