『 C++ - STL 』unordered_xxx系列关联式容器及其封装(万字)

news2024/10/6 16:23:24

文章目录

    • 🎡 unordered系列关联式容器
    • 🎡 哈希表的改造
      • 🎢 节点的设置与总体框架
      • 🎢 迭代器的封装
        • 🎠 迭代器的框架
        • 🎠 operator++()运算符重载
        • 🎠 其余成员函数/运算符重载
      • 🎢 迭代器begin()与end()
      • 🎢 Insert插入函数
      • 🎢 Find查找函数
      • 🎢 修改后哈希表整体代码(供参考)
    • 🎡 使用哈希表封装unordered_set
      • 🎢 基本框架
      • 🎢 setKeyOfT的实现
      • 🎢 unordered_set代码整体(供参考)
    • 🎡 使用哈希表封装unordered_map
      • 🎢 基本框架
      • 🎢 mapKeyOfT的实现
      • 🎢 operator[]的重载实现
      • 🎢 unordered_map代码整体(供参考)
    • 🎡 总结


🎡 unordered系列关联式容器

请添加图片描述

在之前的博客中有提到C++中存在一些列的关联式容器,例如mapset,其底层采用红黑树进行实现;

但除了mapset以外,还存在着对应的unordered_mapunordered_set关联式容器,当然也存在着对应的multi版本(unordered_multimapunordered_multiset);

mapset不同,unordered_mapunordered_set的底层实现为哈希表;

unordered系列关联式容器在C++11标准中被首次引入,与有序容器不同,unordered系列的关联式容器并不会去维护数据中各个元素的顺序,由于该系列容器为哈希表实现,所以整体的存储顺序是由哈希函数决定的,可以在一些不需要对数据进行排序的场景下提供更快的性能;

  • 时间复杂度

    • unordered系列关联式容器

      插入、删除和查找操作的平均时间复杂度为O(1),即常数时间。

      在极少数情况下,可能会出现O(N)的性能退化,特别是当哈希冲突频繁发生时。

    • 普通关联式容器

      插入、删除和查找操作的平均时间复杂度为O(log N),其中N是元素的数量。

      这是因为普通关联式容器使用树结构(通常是红黑树)来维护有序性。

  • 存储方式

    • unordered系列关联式容器

      使用哈希表来存储元素,哈希函数决定了元素的存储位置,不维护有序性。

      元素的存储顺序是无序的。

    • 普通关联式容器

      使用二叉树(通常是红黑树)来存储元素,维护元素的有序性。

      元素的存储顺序是按键排序的。

  • 稳定性

    • unordered系列关联式容器

      插入元素不会导致重新哈希或重新排列容器中的元素。

      因此,unordered系列容器的迭代器相对来说更稳定,即在插入操作后,现有的迭代器仍然有效。

      但需要注意,当容器的装载因子(load factor)过高时,可能需要扩展容器的大小,这可能会导致重新哈希,但这种情况相对较少。

    • 普通关联式容器

      插入或删除元素可能导致树的重新平衡,或者树结构的改变。

      这可能会使得现有的迭代器失效,因为树的结构发生变化。

      不过,由于树的平衡调整相对较少,这种情况的发生频率通常较低。

本篇文章重点以使用哈希表来封装unordered_xxx系列关联式容器为主题,包括哈希表中对于迭代器的实现;


🎡 哈希表的改造

请添加图片描述

首先在上篇博客『 C++ - Hash 』闭散列与开散列哈希表详解及其实现 ( 万字 )-CSDN博客 中提到了哈希表的实现;

在该篇文章当中有关哈希表的代码是一个pair容器来实现其Key Value的模型;

但实际上在STL的源码当中,哈希表的实现框架与红黑树的实现框架相同,因为其也要结合且兼顾unordered_mapunordered_set两个容器,即一个哈希表封装成两个容器,而在上一篇文章中的哈希表为pair,不符合STL的源码框架;

故在进行封装前应先对哈希表进行改造,将其能够像map,set容器的红黑树底层一样;

那么大体构造上即相同;


🎢 节点的设置与总体框架

请添加图片描述

在上一篇文章中节点的设置为一个pair容器;

template <class K, class V>
struct HashNode {
  // 哈希表的节点设置
  typedef HashNode<K, V> Node;
  Node* _next = nullptr;
  std::pair<K, V> _kv;

  HashNode(const std::pair<K, V> kv) : _kv(kv) {}
};

而真正需要使用泛型的特性使其能够兼顾两个容器那么必须将模板参数变为一个,且模板参数的传入为unordered_mapunordered_set两个容器;

  • unordered_map

    当容器为unordered_map时节点中模板参数传来的类型为std::pair<const K , V>;

  • unordered_set

    当容器为unordered_set时节点中模板参数传来的类型为const K;

template <class T>
struct HashNode {
  T _data;
  HashNode<T>* _next;

  HashNode(const T& data) : _data(data), _next(nullptr) {}
};

而至于大体框架而言变化并不大,在原本的哈希表当中实现的是KV模型;

template <class K, class V, class Hash = HashFunc<K>>
class HashTable {
 public:
  typedef HashNode<K, V> Node;
    //...
 private:
  std::vector<Node*> _hashtable;  // 哈希表整体构造
  size_t _n = 0;                  // 负载因子
};

为了实现确保泛型,其模板参数应该更改为KT模型,其中K为键Key,T则为data,即传入的数据类型,根据实现的容器进行传参;

  • unordered_map

    当容器为unordered_mapKpairkey的键,一般用于查找或者遍历,而T则为真正存储的类型;

  • unordered_set

    当容器为unordered_setKkey键,用于查找或遍历,而T则为真正存储的类型;

在真正的实现当中unordered_set容器为了妥协能够与unordered_map容器共同使用一个哈希表,其将会在封装时传入一个冗余的参数;

template <class K /*键*/, class T /*数据类型(pair或者K)*/,
          class KeyOfT /*仿函数 用于取出data中的Key*/,
          class Hash = HashFunc<K> /*仿函数 用于将数据进行size_t化*/>
class HashTable {
  friend struct __Hash_Iterator<
      K, T, T&, T*, KeyOfT, Hash>;  // 避免友元声明时出现的"模板参数阴影"问题

 public:
  typedef __Hash_Iterator<K, T, T&, T*, KeyOfT, Hash> iterator;
  typedef __Hash_Iterator<K, T, const T&, const T*, KeyOfT, Hash>
      const_iterator;

  typedef HashNode<T> Node;
    //...
 protected:
 private:
  size_t _n = 0;  // 负载因子
  std::vector<Node*> _hashTable;
};

从该段代码当中可以看出实际上在修改后的哈希表的总体框架当中的模板参数甚至还多了一个模板参数;

这是因为与mapset相同;

由于是模板,所以不能确保所传进来的参数是什么,但是却都要对应的Key键;

所以在封装的过程中两个容器应该再传入一个仿函数,尤其是unordered_map,使其能够通过仿函数来取到pair容器中的Key键;

对应的仿函数与文章『 C++ - STL』map与set的封装 ( 万字 )-CSDN博客 中的仿函数相同,当然也将在下文中再次进行阐述;

代码段中所出现的对迭代器进行typedef从而能够更轻松的修改其他的代码内容;

其迭代器的实现将在下文进行阐述;


🎢 迭代器的封装

请添加图片描述

在上篇文章的哈希表实现当中并未对迭代器进行封装,该篇文章将对迭代器进行实现从而能够更好的使用哈希表能够封装两个容器;

哈希表的迭代器实际上是一个单向的迭代器,其在迭代期间不能使用类似于--的反向迭代,这也是在开始时拉链法的哈希表中桶下挂的是单链表而不是双向链表的原因之一;

那么既然是单向迭代器,其最重点的思考为:

  • 实现迭代器时其需要的具体参数是什么?

    从该点中可以想到,迭代器实际上是一个一个节点进行遍历,本质上是指针的运动;

    而为了实现迭代器的功能,其必须对指针进行封装,所以节点的指针至关重要;

    而我们实现的哈希表使用的是一个拉链法,即哈希桶的办法,那么在迭代过程中应该分为两种情况:

    1. 当前迭代器位置的下一个位置处于vector容器当中;
    2. 当前迭代器位置的下一个位置处于桶下的链表当中;

    当下一个位置是vector容器中时,则可以继续遍历vector容器,说明表的本体也至关重要;

    而下一个位置是链表时只需要对节点指向其_next指针即可;

    综上所述,重点需要的参数为节点指针哈希表本体;

  • 如何使其进行operator++()使其从当前迭代器位置移动至下一个位置?

    这个问题实际上考虑的是迭代器如何进行遍历,在上个问题的回答即阐明了如何对迭代器进行迭代;


🎠 迭代器的框架

请添加图片描述

template <
    class K /*键值*/, class T /*数据*/, class Ref /*引用 用于 opoerator* */,
    class Ptr /*指针 用于operator->*/, class KeyOfT,
    class
    Hash /*仿函数 该模板参数以及前一个模板参数KeyOfT都是用于实例化哈希表*/>
        
struct __Hash_Iterator {
  friend class HashTable<K,T,KeyOfT,Hash>;

  // 单向迭代器 只需要实现++
  typedef HashTable<K, T, KeyOfT, Hash> HsTable;
  typedef __Hash_Iterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
  typedef __Hash_Iterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
  typedef HashNode<T> Node;
    
      // 两个构造函数
  __Hash_Iterator(Node* node, HsTable* ht) : _Ht(ht), _node(node){}
  __Hash_Iterator(const Iterator& it) : _Ht(it._Ht), _node(it._node) {}

  
  HsTable* _Ht;// 需要哈希表本体
  Node* _node;  // 节点指针
};

在该段代码中可以发现这里采用了一个友元声明;

 friend class HashTable<K,T,KeyOfT,Hash>;

实际上友元声明的原因是确保在迭代器中能够正常的去访问哈希表中的私有成员;

当然这里还需要考虑一个声明顺序的原因,若你的哈希表定义处于迭代器的后面,你应该在迭代器定义前加上哈希表的前置声明,确保迭代器能够正确访问哈希表;

其中的Ref ,Ptr等模板参数在之前的文章与该代码段中都存在相应的注释,这里不再进行过多赘述;

而在代码段当中出现的构造函数一个为构造函数一个为拷贝构造函数;

拷贝构造函数能够通过普通版本的迭代器从而实现出const版本的迭代器;


🎠 operator++()运算符重载

请添加图片描述

对于该操作符的重载迭代思路可以参考上文中出现的迭代器的封装中所引出的问题答案之一;

// operator++();
  Self& operator++() {
    if (_node->_next != nullptr) {
      _node = _node->_next;
    } else {
      // 找下一个不为空的桶
      KeyOfT kot;
      Hash hash;
      // 算出我当前的桶位置
      size_t hashi = hash(kot(_node->_data)) % _Ht->_hashTable.size();
      ++hashi;
      while (hashi < _Ht->_hashTable.size()) {
        if (_Ht->_hashTable[hashi]) {
          _node = _Ht->_hashTable[hashi];
          break;
        } else {
          ++hashi;
        }
      } 
      // 没有找到不为空的桶
      if (hashi == _Ht->_hashTable.size()) {
        _node = nullptr;
      }
    }

    return *this;
  }


🎠 其余成员函数/运算符重载

请添加图片描述

其余成员函数于运算符重载主要着重为:

  • Ret operator*()
  • Ptr operator->()
  • bool operator==(const Iterator& it)
  • bool operator!=(const Iterator& it)
  // operator*();
  Ref operator*() { return _node->_data; }

  // operator->();
  Ptr operator->() { return &_node->_data; }

  // operator==();
  bool operator==(const Iterator& it) { return it._node == _node; }

  // operator!=();
  bool operator!=(const Iterator& it) { return it._node != _node; }


🎢 迭代器begin()与end()

请添加图片描述

在实现迭代器后即可以考虑如何找到迭代器的begin()位置与end()位置;

对于begin()位置而言,只需要对vector容器进行遍历,若是容器中的任意位置存在非空(nullptr)节点,则表示其begin()位置在该处;

而对于end()函数而言,只需要利用nullptr空指针构造迭代器即可;

对于const版本的迭代器也是如此;

  iterator begin() {  // 普通版本迭代器
    Node* cur = nullptr;
    size_t i = 0;
    for (; i < _hashTable.size(); ++i) {
      if (_hashTable[i]) {
        cur = _hashTable[i];
        break;
      }
    }
    if (cur) {
      return iterator(cur, this);
    }
    return iterator(nullptr, this);
  }

//------------------------------

  iterator end() { return iterator(nullptr, this); }  // 普通版本迭代器

//------------------------------

  const_iterator begin() const {  // const版本迭代器
    size_t i = 0;
    Node* cur = nullptr;
    for (; i < _hashTable.size(); ++i) {
      if (_hashTable[i]) {
        cur = _hashTable[i];
        break;
      }
    }
    if (cur) {
      return iterator(cur, this);
    }
    return iterator(nullptr, this);
  }

//------------------------------

  const_iterator end() const {
    return iterator(nullptr, this);
  }  // const版本迭代器

当然这里的返回值为迭代器或者是const版本的迭代器,在使用前应该对其进行typedef使得在哈希表中能够自由使用迭代器(下文内容也是如此);


🎢 Insert插入函数

请添加图片描述

对于Insert()函数而言,unordered系列容器中的insert()函数的返回值为std::pair<iterator,bool>类型的返回值;

而其insert()函数为调用哈希表中的Insert()函数;

在迭代器实现之后,更改后的Insert()函数也可以按照对应的返回值进行返回从而达到容器可以复用的效果;

std::pair<iterator, bool> Insert(const T& data) {

    KeyOfT kot;
    Hash hash;

    iterator isIn = Find(kot(data));//判断该数据是否存在 若是存在则不进行插入
    if (isIn != end()){
      return  std ::make_pair(iterator(nullptr, this), false);
    }

      if (_hashTable.size() == _n) {  // 负载因子 == 1 需要进行扩容
        size_t newSize = GetNextPrime(_hashTable.size());  // 获取新的节点大小
        std::vector<Node*> newTable(newSize,nullptr);

        for (size_t i = 0; i < _hashTable.size(); ++i) {
          Node* cur = _hashTable[i];
          while (cur) {
            Node* next = cur->_next;

            size_t hashi = hash(kot(cur->_data)) % newTable.size();
            cur->_next = newTable[hashi];
            newTable[hashi] = cur;
            cur = next;
          }
          _hashTable[i] = nullptr;
        }
        _hashTable.swap(newTable);
      }

    size_t hashi = hash(kot(data)) % _hashTable.size();
    Node* newnode = new Node(data);
    newnode->_next = _hashTable[hashi];
    _hashTable[hashi] = newnode;
    _n++;
    return std ::make_pair(iterator(newnode, this), true);
  }

在大体的思路上基本不变,唯一多了一个就是KeyOfT仿函数从而可以从T类型数据中取到其对应的Key;


🎢 Find查找函数

请添加图片描述

对于Find()而言,与Insert()函数相同,只需要更改其返回值为iterator类型即可;

iterator Find(const K& key) {
    Hash hash;
    KeyOfT kot;

    if (_hashTable.size() == 0) return end();
    size_t hashi = hash(key) % _hashTable.size();
    Node* cur = _hashTable[hashi];
    while (cur) {
      if (kot(cur->_data) == key) {
        return iterator(cur, this);
      }
      cur = cur->_next;
    }
    return end();
  }

🎢 修改后哈希表整体代码(供参考)

请添加图片描述

对于哈希表的改造在成员函数方面的改造着重对Insert()Find()进行阐述;

其他函数的改动微乎其微,至多只是加上了KeyOfT仿函数使其能够产生泛型属性,因此不进行赘述;

#pragma once

#include <iostream>
#include <string>
#include <vector>

namespace MyHash {
template <class T>
struct HashNode {
  T _data;
  HashNode<T>* _next;

  HashNode(const T& data) : _data(data), _next(nullptr) {}
};

template <class K>
struct HashFunc { /*仿函数 用于将数据进行size_t化*/
  size_t operator()(const K& key) { return key; }
};

template <>
struct HashFunc<std::string> { /*仿函数 用于将数据进行size_t化*/
  // 该处的仿函数需要使用字符串哈希的方式来确认整个string字符串的ASCII大小
  size_t operator()(const std::string& str) {
    size_t hash = 0;
    for (auto ch : str) {
      hash += ch;
      hash *= 31;
    }
    return hash;
  }
};

template <class K, class T, class KeyOfT, class Hash>
class HashTable;  // 前置声明


template <
    class K /*键值*/, class T /*数据*/, class Ref /*引用 用于 opoerator* */,
    class Ptr /*指针 用于operator->*/, class KeyOfT,
    class
    Hash /*仿函数 该模板参数以及前一个模板参数KeyOfT都是用于实例化哈希表*/>

struct __Hash_Iterator {
  friend class HashTable<K,T,KeyOfT,Hash>;

  // 单向迭代器 只需要实现++
  typedef HashTable<K, T, KeyOfT, Hash> HsTable;
  typedef __Hash_Iterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
  typedef __Hash_Iterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
  typedef HashNode<T> Node;



  // 两个构造函数
  __Hash_Iterator(Node* node, HsTable* ht) : _Ht(ht), _node(node){}
  __Hash_Iterator(const Iterator& it) : _Ht(it._Ht), _node(it._node) {}

  // operator++();
  Self& operator++() {
    if (_node->_next != nullptr) {
      _node = _node->_next;
    } else {
      // 找下一个不为空的桶
      KeyOfT kot;
      Hash hash;
      // 算出我当前的桶位置
      size_t hashi = hash(kot(_node->_data)) % _Ht->_hashTable.size();
      ++hashi;
      while (hashi < _Ht->_hashTable.size()) {
        if (_Ht->_hashTable[hashi]) {
          _node = _Ht->_hashTable[hashi];
          break;
        } else {
          ++hashi;
        }
      }

      // 没有找到不为空的桶
      if (hashi == _Ht->_hashTable.size()) {
        _node = nullptr;
      }
    }

    return *this;
  }

  // operator*();
  Ref operator*() { return _node->_data; }

  // operator->();
  Ptr operator->() { return &_node->_data; }

  // operator==();
  bool operator==(const Iterator& it) { return it._node == _node; }

  // operator!=();
  bool operator!=(const Iterator& it) { return it._node != _node; }

  // 需要哈希表本体
  HsTable* _Ht;
  Node* _node;  // 节点指针
};

template <class K /*键值*/, class T /*数据类型(pair或者K)*/,
          class KeyOfT /*仿函数 用于取出data中的Key*/,
          class Hash = HashFunc<K> /*仿函数 用于将数据进行size_t化*/>
class HashTable {
  friend struct __Hash_Iterator<
      K, T, T&, T*, KeyOfT, Hash>;  // 避免友元声明时出现的"模板参数阴影"问题

 public:
  typedef __Hash_Iterator<K, T, T&, T*, KeyOfT, Hash> iterator;
  typedef __Hash_Iterator<K, T, const T&, const T*, KeyOfT, Hash>
      const_iterator;

  typedef HashNode<T> Node;
  iterator begin() {  // 普通版本迭代器
    Node* cur = nullptr;
    size_t i = 0;
    for (; i < _hashTable.size(); ++i) {
      if (_hashTable[i]) {
        cur = _hashTable[i];
        break;
      }
    }
    if (cur) {
      return iterator(cur, this);
    }
    return iterator(nullptr, this);
  }

  ~HashTable() {
    Node* cur = nullptr;
    for (size_t i = 0; i < _hashTable.size(); ++i) {
      if (_hashTable[i]) {
        cur = _hashTable[i];
        Node* next = cur->_next;
        while (cur) {
          delete cur;
          cur = next;
        }
      }
    }
  }

  iterator end() { return iterator(nullptr, this); }  // 普通版本迭代器

  const_iterator begin() const {  // const版本迭代器
    size_t i = 0;
    Node* cur = nullptr;
    for (; i < _hashTable.size(); ++i) {
      if (_hashTable[i]) {
        cur = _hashTable[i];
        break;
      }
    }
    if (cur) {
      return iterator(cur, this);
    }
    return iterator(nullptr, this);
  }

  const_iterator end() const {
    return iterator(nullptr, this);
  }  // const版本迭代器

  std::pair<iterator, bool> Insert(const T& data) {

    KeyOfT kot;
    Hash hash;

    iterator isIn = Find(kot(data));//判断该数据是否存在 若是存在则不进行插入
    if (isIn != end()){
      return  std ::make_pair(iterator(nullptr, this), false);
    }

      if (_hashTable.size() == _n) {  // 负载因子 == 1 需要进行扩容
        size_t newSize = GetNextPrime(_hashTable.size());  // 获取新的节点大小
        std::vector<Node*> newTable(newSize,nullptr);

        for (size_t i = 0; i < _hashTable.size(); ++i) {
          Node* cur = _hashTable[i];
          while (cur) {
            Node* next = cur->_next;

            size_t hashi = hash(kot(cur->_data)) % newTable.size();
            cur->_next = newTable[hashi];
            newTable[hashi] = cur;
            cur = next;
          }
          _hashTable[i] = nullptr;
        }
        _hashTable.swap(newTable);
      }

    size_t hashi = hash(kot(data)) % _hashTable.size();
    Node* newnode = new Node(data);
    newnode->_next = _hashTable[hashi];
    _hashTable[hashi] = newnode;
    _n++;
    return std ::make_pair(iterator(newnode, this), true);
  }

  iterator Find(const K& key) {
    Hash hash;
    KeyOfT kot;

    if (_hashTable.size() == 0) return end();
    size_t hashi = hash(key) % _hashTable.size();
    Node* cur = _hashTable[hashi];
    while (cur) {
      if (kot(cur->_data) == key) {
        return iterator(cur, this);
      }
      cur = cur->_next;
    }
    return end();
  }

  bool Erase(const K& key) {
    Hash hash;
    KeyOfT kot;

    size_t hashi = hash(key) % _hashTable.size();

    Node* prev = nullptr;
    Node* cur = _hashTable[hashi];

    while(cur){
        if(kot(cur->_data) == key){
            if(prev == nullptr){
              _hashTable[hashi] = cur->_next;
            }
            else{
              prev->_next = cur->_next;
            }
            --_n;
            delete cur;
            return true;
        }
        else{
          prev = cur;
          cur = cur->_next;
        }
    }
    return false;
  }

  size_t GetNextPrime(size_t prime) {
    static const int __stl_num_primes = 28;
    static const unsigned long __stl_prime_list[__stl_num_primes] = {
        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};

    size_t i = 0;
    for (; i < __stl_num_primes; ++i) {
      if (__stl_prime_list[i] > prime) {
        return __stl_prime_list[i];
      }
    }
    return __stl_prime_list[i];
  }

 protected:
 private:
  size_t _n = 0;  // 负载因子
  std::vector<Node*> _hashTable;
};

🎡 使用哈希表封装unordered_set

请添加图片描述

对于哈希表封装unordered_xxx系列容器的大部分操作与红黑树封装map,set基本相同;

即在容器当中调用数据结构(红黑树,哈希表)中的成员函数即可,迭代器也是调用数据结构中的迭代器即可;


🎢 基本框架

请添加图片描述

template <class K, class Hash = HashFunc<K>>
class unordered_set {
 public:
  typedef typename MyHash::HashTable<K, K, setKeyOfT, Hash>::iterator iterator;
  typedef typename MyHash::HashTable<K, K, setKeyOfT, Hash>::iterator
      const_iterator;
 protected:
 private:
  HashTable<K, K, setKeyOfT, Hash> _Ht;
};

在代码段中可以看出,在设置模板参数时其中出现了一个在哈希表中实现的Hash仿函数;

这个仿函数实际上在容器的实现当中并不起到很大的作用,只是单纯的在进行传参时所用到;

这里在对迭代器进行typedef时还使用了typename,这里的typename的用途是告知正在typedef是一个类型;

同时这里可以发现在上文中提到的unordered_set在实现当中妥协unordered_map而传入的冗余模板参数,即多传了一个K;


🎢 setKeyOfT的实现

请添加图片描述

在上文当中提到,哈希表的实现当中的模板参数中存在一个KeyOfT的仿函数,这个仿函数实际上是用来取出类型中的Key;

而这个仿函数是由容器传至哈希表中,所以定义是在容器当中;

这个仿函数可以直接以内部类的形式进行定义;

set容器当中,由于set容器的传参只有一个Key,所以只需要在仿函数中直接返回key即可;

   struct setKeyOfT {
    const K& operator()(const K& key) { return key; }
  };

由于是内部类,所以其可以直接访问其外部的模板参数,没必要再次定义模板参数;


🎢 unordered_set代码整体(供参考)

请添加图片描述

对于其他成员函数而言只需要直接调用哈希表中的成员函数即可;

#include <iostream>

#include "Hash.h"

template <class K, class Hash = HashFunc<K>>
class unordered_set {
 public:
  struct setKeyOfT {
    const K& operator()(const K& key) { return key; }
  };
  typedef typename MyHash::HashTable<K, K, setKeyOfT, Hash>::iterator iterator;
  typedef typename MyHash::HashTable<K, K, setKeyOfT, Hash>::iterator
      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(); }

  std::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); }

 protected:
 private:
  HashTable<K, K, setKeyOfT, Hash> _Ht;
};

🎡 使用哈希表封装unordered_map

请添加图片描述

在上文当中已经提到了封装unordered_xxx系列容器的总体思路,在此不进行赘述;


🎢 基本框架

请添加图片描述

template <class K,class V, class Hash = HashFunc<K>>
class unordered_map {
 public:
  typedef typename MyHash::HashTable<K, std::pair<const K, V>, mapKeyOfT,
                                     Hash>::iterator iterator;
  typedef typename MyHash::HashTable<K, std::pair<const K, V>, mapKeyOfT,
                                     Hash>::iterator const_iterator;
    
 protected:
 private:
  HashTable<K,std::pair< const K, V>, mapKeyOfT, Hash> _Ht;
};

在该段代码中可以看出与unordered_set实现的框架只有在模板参数的传递中出现差异,其他大体相同;


🎢 mapKeyOfT的实现

请添加图片描述

由于unordered_map中所传递的类型是一个std::pair<const K,V>类型,所以显得其KeyOfT仿函数尤为重要;

需要使用仿函数在pair中找到其key;

  struct mapKeyOfT {
    const K& operator()(const std::pair<K,V>& data) { return data.first; }
  };

unordered_set相同也是一个内置类以至可以复用相同用的模板参数;


🎢 operator[]的重载实现

请添加图片描述

unordered_set不同的一点是unordered_map支持使用[]对数据进行访问;

其中[]的功能在unordered_map中既能实现查找插入,也能实现对其V值进行查找即修改;

  • [K]存在

    若是[K]存在则不进行插入,并返回对应的V值;

  • [K]不存在

    若是[K]不存在,则进行插入,并返回对应的V值;

  V& operator[](const K& key) {
    std::pair<iterator,bool> ret = insert(std::make_pair(key, V()));
    // ret.first 是 iterator 类型
    return ret.first->second;  // 访问 iterator 指向的 HashNode 的 _data 成员的 second 部分
  }

只需要通过返回的迭代器去访问其类型(pair)中的second即可;


🎢 unordered_map代码整体(供参考)

请添加图片描述

template <class K,class V, class Hash = HashFunc<K>>
class unordered_map {
 public:
  struct mapKeyOfT {
    const K& operator()(const std::pair<K,V>& data) { return data.first; }
  };
  typedef typename MyHash::HashTable<K, std::pair<const K, V>, mapKeyOfT,
                                     Hash>::iterator iterator;
  typedef typename MyHash::HashTable<K, std::pair<const K, V>, mapKeyOfT,
                                     Hash>::iterator 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(); }

  std::pair<iterator, bool> insert(const std::pair<K, V>& data) {
    return _Ht.Insert(data);
  }

  iterator find(const K& key) { return _Ht.Find(key); }

  bool erase(const K& key) { return _Ht.Erase(key); }

  V& operator[](const K& key) {
    std::pair<iterator,bool> ret = insert(std::make_pair(key, V()));
    // ret.first 是 iterator 类型
    return ret.first->second;  // 访问 iterator 指向的 HashNode 的 _data 成员的 second 部分
  }

 protected:
 private:
  HashTable<K,std::pair< const K, V>, mapKeyOfT, Hash> _Ht;
};

🎡 总结

请添加图片描述

在对unordered_xxx系列关联式容器进行封装时重点除了上述以外还应该检查在封装中出现的类型转换问题;

尤其是对const非const的转换进行检查,避免在封装中出现因为权限缩小问题所引发的报错;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1438250.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【iOS ARKit】人形提取

为解决人形分离和深度估计问题&#xff0c;ARKit 新增加了 Segmentation Buffer&#xff08;人体分隔缓冲区&#xff09;和Estimated Depth Data Buffer&#xff08;深度估计缓冲区&#xff09;两个缓冲区。人体分隔缓冲区作用类似于图形渲染管线中的 Stencil Buffer&#xff0…

数据结构第九天(堆排序)

目录 前言 概述 源码&#xff1a; 主函数&#xff1a; 运行结果&#xff1a; 其他 前言 哈哈&#xff0c;这个堆排序算法很久之前就已经敲过一遍了&#xff0c;时间一久&#xff0c;思路有点淡忘。今天重新看过一遍之后&#xff0c;又亲自撸代码&#xff0c;幸运的是&am…

python毕设选题 - 基于时间序列的股票预测于分析

文章目录 1 简介2 时间序列的由来2.1 四种模型的名称&#xff1a; 3 数据预览4 理论公式4.1 协方差4.2 相关系数4.3 scikit-learn计算相关性 5 金融数据的时序分析5.1 数据概况5.2 序列变化情况计算 最后 1 简介 Hi&#xff0c;大家好&#xff0c;今天向大家介绍一个大数据项目…

IEC 104电力规约详细解读(三) - 遥信

1.功能简述 遥信&#xff0c;、即状态量&#xff0c;是为了将断路器、隔离开关、中央信号等位置信号上送到监控后台的信息。遥信信息包括&#xff1a;反应电网运行拓扑方式的位置信息。如断路器状态、隔离开关状态&#xff1b;反应一次二次设备工作状况的运行信息&#xff0c;如…

位运算:进制

4982. 进制 - AcWing题库 给定两个整数 a,b 请你计算&#xff0c;在 [a,b] 范围内有多少个整数满足其二进制表示恰好有一个 0。 不考虑前导 0。 例如&#xff0c;当 a5,b10 时&#xff0c;[5,10]范围内的所有整数及其二进制表示如下&#xff1a; 可以看出&#xff0c;只有 5 和…

Python HTTP隧道在远程通信中的应用:穿越网络的“魔法门”

在这个数字化时代&#xff0c;远程通信就像是我们日常生活中的“魔法门”&#xff0c;让我们可以随时随地与远方的朋友、同事或服务器进行交流。而在这扇“魔法门”的背后&#xff0c;Python HTTP隧道技术发挥着举足轻重的作用。 想象一下&#xff0c;你坐在家里的沙发上&…

多维时序 | MATLAB实现基于CNN-LSSVM卷积神经网络-最小二乘支持向量机多变量时间序列预测

多维时序 | MATLAB实现基于CNN-LSSVM卷积神经网络-最小二乘支持向量机多变量时间序列预测 目录 多维时序 | MATLAB实现基于CNN-LSSVM卷积神经网络-最小二乘支持向量机多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.MATLAB实现基于CNN-LSSVM卷积神经…

【知识图谱--第二讲知识图谱的表示】

知识图谱的表示 知识表示Knowledge Representation 知识表示方法知识图谱的符号表示基于图的知识表示与建模简单图建模-最简单的无向图有向标记图OWL与Ontology 知识图谱的向量表示 知识表示 Knowledge Representation 知识表示&#xff08;KR&#xff09;就是用易于计算机处…

【C语言】socket函数

一、socket函数函数的原型 int socket(int domain, int type, int protocol); 其中&#xff1a; domain参数指定套接字应该使用的协议族&#xff08;例如&#xff0c;AF_INET表示IPv4协议族&#xff09;。type参数指定套接字类型&#xff08;例如&#xff0c;SOCK_STREAM表示…

老是抓不准现货白银实时报价怎么办?

现货白银的实时报价是不断变动的&#xff0c;投资者要了解当下的现货白银实时走势&#xff0c;并且依靠对实时报价的分析预判未来的趋势&#xff0c;这是不容易的&#xff0c;但是不是不能做到呢&#xff1f;也不是。因为市场不是横盘就是趋势&#xff0c;只要有趋势&#xff0…

机器学习--K近邻算法,以及python中通过Scikit-learn库实现K近邻算法API使用技巧

文章目录 1.K-近邻算法思想2.K-近邻算法(KNN)概念3.电影类型分析4.KNN算法流程总结5.k近邻算法api初步使用机器学习库scikit-learn1 Scikit-learn工具介绍2.安装3.Scikit-learn包含的内容4.K-近邻算法API5.案例5.1 步骤分析5.2 代码过程 1.K-近邻算法思想 假如你有一天来到北京…

Django前后端分离之后端实践2

小实践&#xff1a;实现用户登录、注销及ORM管理功能、事务开启小实践 models.py class Books(models.Model):id models.CharField(primary_keyTrue,max_length20,verbose_name"图书ID")name models.CharField(max_length20,verbose_name图书名称)status models…

Spring如何扫描自定义的注解?

目录 一、Spring框架介绍 二、什么是自定义注解 三、如何扫描自定义的注解 一、Spring框架介绍 Spring框架是一个开源的Java应用程序框架&#xff0c;它提供了一种全面的编程和配置模型&#xff0c;用于构建现代化的企业级应用程序。Spring框架的核心原则是依赖注入&#x…

零基础学Python之面向对象

1.面向对象编程简介 &#xff08;1&#xff09;什么是面向对象 面向对象程序设计(Object Oriented Programming)作为一种新方法&#xff0c;其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。模型是用来反映现实世界中事物特征的。任何一个模型都不可能反映客观事…

Java实现批量视频抽帧2.0

继上个版本 对其进行略微升级 &#x1f913; 上个版本仅对一个视频进行抽帧处理 此版本可对一个文件夹内的全部视频进行抽帧并对应的文件夹进行帧图片的保存 1️⃣配置pom.xml &#xff08;保持上次不变&#xff09; <dependencies><dependency><grou…

推理系统学习笔记

一些学习资料 最近对MLsys比较感兴趣&#xff0c;遂找些资料开始学习一下 https://fazzie-key.cool/2023/02/21/MLsys/https://qiankunli.github.io/2023/12/16/llm_inference.htmlhttps://dlsyscourse.orghttps://github.com/chenzomi12/DeepLearningSystem/tree/main/04Infe…

数智文旅:智慧文旅中的数字化转型

在数字化浪潮席卷全球的今天&#xff0c;旅游业作为传统服务业的代表&#xff0c;正面临着前所未有的转型压力与机遇。智慧文旅&#xff0c;作为旅游业与数字技术深度融合的产物&#xff0c;不仅标志着旅游业进入了全新的发展阶段&#xff0c;更预示着未来旅游业将朝着更加智能…

QAnything之BCEmbedding技术路线

QAnything和BCEmbedding简介 QAnything[github]是网易有道开源的检索增强生成式应用&#xff08;RAG&#xff09;项目&#xff0c;在有道许多商业产品实践中已经积累丰富的经验&#xff0c;比如有道速读和有道翻译。QAnything是一个支持任意格式文件或数据库的本地知识库问答系…

【开源】JAVA+Vue+SpringBoot实现公司货物订单管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 客户管理模块2.2 商品维护模块2.3 供应商管理模块2.4 订单管理模块 三、系统展示四、核心代码4.1 查询供应商信息4.2 新增商品信息4.3 查询客户信息4.4 新增订单信息4.5 添加跟进子订单 五、免责说明 一、摘要 1.1 项目…

Tauri 的基本使用笔记

文章目录 前言如何将 Tauri 集成到前端项目?进程间通信&#xff08;命令&#xff09;const invoke window.__TAURI__.invoke; 进程间通信&#xff08;事件&#xff09;前端 ⇒ RustRust ⇒ 前端我的疑问 开发时的一些技巧用代码打开前端的开发者工具让 Tauri 不要监听文件Rus…