用同一个哈希表实现unordered_map和unordered_set(C++实现)【STL】

简介: 用同一个哈希表实现unordered_map和unordered_set(C++实现)【STL】

1. 模板参数控制

我们知道,unordered_set和unordered_map与set和map是一样的,前者不是真正的键值对,它的value值和key值相同;后者是真正的键值对。STL非常注重代码的复用,它们在底层使用了同一棵红黑树模板实现,这也是此文要用同一个哈希表实现unordered_set和unordered_map的原因。如果各自拥有一个哈希表,set和unordered_set只要一个key值即可。

1.1 容器模板参数

unordered_set

template<class K>
class unordered_set
{
public:
    // ..
private:
    HashTable<K, K> _ht;
};

unordered_map

template<class K, class V>
class unordered_map
{
public:
    //...
private:
    HashTable<K, pair<K, V>> _ht;
};

1.2 结点类的定义

同一个哈希表实现unordered_set和unordered_map,就需要控制底层的哈希表的模板参数。由于原先实现哈希表时默认是以<key,value>作为键值对的,而哈希表在底层是不知道上层是unordered_set还是unordered_map,所以为了区分两者键值对的第二个模板参数,将哈希表中的第二个模板参数从V改成T

  • 当上层是unordered_set时,T和K相同;
  • 当上层是unordered_map时,T就是value。

同时,原先的键值对也要改成模板参数T,由于这个T可能是key,也有可能是<key,value>键值对,所以将原来结点类中的_kv(键值对)改成_data,以保存数据。

template<class T>
struct HashNode
{
    T _data;                    // 保存数据
    HashNode<T>* _next;         // 后继指针
    HashNode(const T& data)     // 结点构造函数
        :_data(data)
        , _next(nullptr)
    {}
};

1.3 仿函数获取键值

上面的操作将unordered_set和unordered_map的键值对是用一个T类型的_data保存的,由于哈希函数要根据键值key计算,所以要取出_data中的key值。在这里可以使用一个仿函数获取,仿函数其实就是一个结构体中重载的operator()

template<class K, class V>
class unordered_map
{
    struct MapKeyOfT
    {
        const K& operator()(const pair<K, V>& kv)
        {
            return kv.first;
        }
    };
public:
    // ...
private:
    HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};
template<class K>
class unordered_set
{
    struct SetKeyOfT
    {
        const K& operator()(const K& key)
        {
            return key;
        }
    };
public:
    // ...
private:
    HashTable<K, K, SetKeyOfT> _ht;
};

因此,在哈希表中的参数列表要新增一个仿函数:

template<class K, class T, class KeyOfT>
class HashTable
{
    // ... 
};

2. 字符串哈希函数

在使用map和set时,大多数情况key值都是字符串类型,unordered_set和unordered_map也是一样的,但是string虽然是常用的类型,但是它无法直接取模,这也是哈希常见的问题。为此,有许多大佬研究了这个问题。

Kernighan和Dennis在《The C programming language》中提出的BKDRHash 算法,是一个高效且实用的哈希函数,它对每次累加后的结果再乘以素数131。

至于为什么是131,这是一个数学问题,但实际上也有其他类似的哈希算法,只是将131改成了其他数字,大多数是素数。

template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
};
// 模板的特化
template<>
struct Hash<string>
{
  size_t operator()(const string& s)
  {
    size_t value = 0;
    for (auto ch : s)
    {
      value = value * 131 + ch;
    }
    return value;
  }
};

这种专门针对某种特定类型而写的模板,是模板的特化。

注意:

模板的特化必须在原有模板的基础上特化的。

所以哈希表的模板参数还要增加一个:

template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{
    // ...
};

3. 哈希表默认成员函数

3.1 默认构造函数

当实例化一个哈希表对象时,哈希表的两个成员会被这样初始化:

  • _table调用vector的默认构造函数初始化;
  • _size被默认值0初始化。
vector<Node*> _table;       // 哈希表
size_t _size = 0;           // 有效元素个数

但是拷贝构造函数和默认构造函数是二选一的,所以需要使用 default 关键字显示指定生成默认构造函数。

//构造函数
HashTable() = default; //显示指定生成默认构造函数

3.2 拷贝构造函数

深拷贝实现拷贝构造函数,新哈希表和旧哈希表中的结点的地址都是相同的。

步骤:

  1. 将新哈希表的大小risize到旧哈希表大小;
  2. 将旧哈希表的每个桶的结点全部拷贝到新的哈希表中;
  3. 更新新哈希表的_size
//拷贝构造函数
HashTable(const HashTable& ht)
{
    _table.resize(ht._table.size());
    for (size_t i = 0; i < ht._table.size(); i++)
    {
        if (ht._table[i])
        {
            Node* cur = ht._table[i];
            while (cur)
            {
                Node* copy = new Node(cur->_data);
                copy->_next = _table[i];
                _table[i] = copy;
                cur = cur->_next;
            }
        }
    }
    _size = ht._n;
}

3.3 赋值运算符重载函数

赋值运算符重载函数的本质就是拷贝构造函数:

在参数部分构造一个哈希表,然后将新旧哈希表的地址和有效数据_size交换,为了=能连续赋值,最后返回当前对象的this指针。这里巧妙使用了参数部分构造新对象和函数结束时调用对应的析构函数。

//赋值运算符重载函数
HashTable& operator=(HashTable ht)  // 这里调用了构造函数
{
    _table.swap(ht._table);
    swap(_size, ht._size);  // 交换数据
    return *this;  
} // 调用析构函数

3.4 析构函数

由于每个哈希桶中的结点都是new出来的,所以要遍历哈希表,将每个哈希桶中的所有结点delete。

// 析构函数
~HashTable()
{
    for (size_t i = 0; i < _table.size(); i++)
    {
        Node* cur = _table[i];
        while (cur)
        {
            Node* next = cur->_next;
            free(cur);
            cur = next;
        }
        _table[i] = nullptr;
    }
}

4. 正向迭代器

由于上层容器有++和–等操作,所以要先在哈希表中增加迭代器,以供上层封装。STL中实现了双向迭代器,但这里只实现了正向迭代器。

4.1 定义迭代器结构体

迭代器实际上就是对结点的封装,它本质上是结点的地址,是指针。由于不同的运算符重载函数的返回值可能不同,类型有引用、指针和迭代器本身,所以在迭代器内部typedef一下。

// 前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
//正向迭代器
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
struct __HTIterator
{
    typedef HashNode<T> Node;                         // 哈希结点类型
    typedef HashTable<K, T, KeyOfT, Hash> HT;         // 哈希表类型
    typedef __HTIterator<K, T, KeyOfT, Hash> Self;    // 正向迭代器类型
    Node* _node;            // 结点指针
    HT* _pht;               // 哈希表的地址
    // ...
};

注意

这里将哈希表的结构体和迭代器的结构体放在了同一个头文件中,其原因是模板不支持分离编译,为了结构清晰,将迭代器放在哈希表之前,而迭代器也需要使用到哈希表的定义,所以需要前置声明,就像声明函数一样。

4.2 构造函数

由于需要++操作,所以必须要保存哈希表的地址(稍后就会知道)。

// 构造函数
__HTIterator(Node* node, HT*, pht)
    :_node(node)
        , _pht(pht)
    {}

4.3 operator*()和operator->()

对结点进行*解引用操作,就是取出结点中保存的value值。

->成员访问操作符就是取出结点中数据的地址。

因此需要区分它们的返回值。

T& operator*()
{
    return _node->_data;
}
T* operator->()
{
    return &_node_ -> _data;
}

4.4 operator==()和operator!=()

判断结点是否相等,只需要判断结点的地址是否相同即可。

bool operator!=(const Self& s) const
{
    return _node != s._node;
}
bool operator==(const Self& s) const
{
    return _node == s._node;
}

4.5 operator++()

运算符++的重载,可以分为两种情况:

  • 如果当前在桶中:找链表的下一个结点;
  • 如果当前在桶的最后一个元素:找下一个非空的桶:
  • 通过哈希函数计算当前桶的下标值;
  • 然后对算出来的下标值++,得到下一个桶的位置;
  • 如果下一个桶为空,则重复上述操作;
  • 找到非空桶,更新当前结点的值;
  • 走到最后一个位置也没找到非空桶,返回nullptr。
// 前置++
Self& operator++()
{
    if (_node->_next)                   // 当前结点不是哈希桶的最后一个结点
    {
        _node = _node->_next;           // 迭代
    }
    else                                // 当前结点是哈希桶的最后一个结点
    {
        KeyOfT kot;
        Hash hash;
        size_t index = hash(kot(_node->_data)) % _pht->_table.size(); // 得到当前哈希桶的下标
        index++;                                                      // 下标+1
        while (index < _pht->_table.size())                           // 遍历哈希表中所有哈希桶
        {
            if (_pht->_table[index])                                  // 当前哈希桶不为空
            {
                _node = _pht->_table[index];
                return *this;                                         // 更新结点指针并返回
            }
            index++;                                                  // 当前哈希表为空,往后找
        }
        _node = nullptr;                                              // 上一个是空桶
    }
    return *this;
}

5. 哈希表正向迭代器

5.1 定义迭代器

写好正向迭代器以后,可以在哈希表的内部封装迭代器了。在此之前,需要在哈希表的内部进行以下操作:

  1. 为了能方便地像使用STL中的iterator类型一样,将带有模板参数的迭代器typedef为iterator
    注意:为了方便哈希表外部使用iterator类型,要在哈希表的public区域定义。
  2. 除此之外,由于正向迭代器中++操作符重载函数使用了哈希表中的_table,而它是哈希表类的私有成员,所以要将正向迭代器结构体作为哈希表类的友元。
  3. 将哈希表类中的Find函数的返回值类型改为由结点指针和哈希表地址构造的正向迭代器。将Insert函数的返回值类型改成由正向迭代器和布尔类型构成的键值对。
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<T> Node;
    // 将正向迭代器类声明为哈希表类的友元
    template<class K, class T, class KeyOfT, class Hash>
    friend struct __HTIterator;
public:
    typedef __HTIterator<K, T, KeyOfT, Hash> iterator;          // 定义迭代器
    iterator begin()
    {
        size_t i = 0;
        while (i < _table.size())                           // 遍历哈希表中所有哈希桶
        {
            if (_table[i])                                  // 当前哈希桶不为空
            {
                return iterator(_table[i], this);           // 返回桶地址和表地址构造的迭代器
            }
            i++;                                            // 下一个桶
        }
        return end();
    }
    iterator end()
    {
        return iterator(nullptr, this);                     // 返回nullptr和表地址构造的迭代器
    }
    // ...
private:
    vector<Node*> _table;       // 哈希表
    size_t _size = 0;           // 有效元素个数
};

6. 实现unordered_set和unordered_map

实现unordered_set和unordered_map,只要调用哈希表开放的接口即可。

6.1 unordered_set

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
    struct SetKeyOfT
    {
        const K& operator()(const K& key)
        {
            return key;
        }
    };
public:
    typedef typename HashTable<K, K, SetKeyOfT>::iterator iterator; // 定义迭代器    
    iterator begin()
    {
        return _ht.begin();
    }
    iterator end()
    {
        return _ht.end();
    }
    // 插入函数
    pair<iterator, bool> insert(const K& key)
    {
        return _ht.Insert(key);
    }
    // 删除函数
    void erase(const K& key)
    {
        _ht.Erase(key);
    }
    // 查找函数
    iterator find(const K& key)
    {
        return _ht.Find(key);
    }
private:
    HashTable<K, K, SetKeyOfT> _ht;
};

6.2 unordered_map

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
    struct MapKeyOfT
    {
        const K& operator()(const pair<K, V>& kv)
        {
            return kv.first;
        }
    };
public:
  typedef typename HashTable<K, pair<K, V>, MapKeyOfT>::iterator iterator;
  iterator begin()
  {
    return _ht.begin();
  }
  iterator end()
  {
    return _ht.end();
  }
  //插入函数
  pair<iterator, bool> insert(const pair<K, V>& kv)
  {
    return _ht.Insert(kv);
  }
  //删除函数
  void erase(const K& key)
  {
    _ht.Erase(key);
  }
  //查找函数
  iterator find(const K& key)
  {
    return _ht.Find(key);
  }
  //赋值运算符重载
  V& operator[](const K& key)
  {
    pair<iterator, bool> ret = insert(make_pair(key, V()));
    iterator it = ret.first;
    return it->second;
  }
private:
    HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};

源代码

目录
相关文章
|
5月前
|
存储 监控 算法
基于 C++ 哈希表算法实现局域网监控电脑屏幕的数据加速机制研究
企业网络安全与办公管理需求日益复杂的学术语境下,局域网监控电脑屏幕作为保障信息安全、规范员工操作的重要手段,已然成为网络安全领域的关键研究对象。其作用类似网络空间中的 “电子眼”,实时捕获每台电脑屏幕上的操作动态。然而,面对海量监控数据,实现高效数据存储与快速检索,已成为提升监控系统性能的核心挑战。本文聚焦于 C++ 语言中的哈希表算法,深入探究其如何成为局域网监控电脑屏幕数据处理的 “加速引擎”,并通过详尽的代码示例,展现其强大功能与应用价值。
116 2
|
7月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
181 2
|
7月前
|
存储 算法 数据处理
公司局域网管理中的哈希表查找优化 C++ 算法探究
在数字化办公环境中,公司局域网管理至关重要。哈希表作为一种高效的数据结构,通过哈希函数将关键值(如IP地址、账号)映射到数组索引,实现快速的插入、删除与查找操作。例如,在员工登录验证和设备信息管理中,哈希表能显著提升效率,避免传统线性查找的低效问题。本文以C++为例,展示了哈希表在局域网管理中的具体应用,包括设备MAC地址与IP分配的存储与查询,并探讨了优化哈希函数和扩容策略,确保网络管理高效准确。
|
7月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
352 73
|
4月前
|
存储 缓存 Java
c++的哈希表、哈希桶的介绍与实现
这一篇文章大致实现详细介绍什么是哈希,然后再介绍什么是哈希表,怎么代码实现哈希表,然后再介绍哈希桶,怎么代码实现哈希桶,最后再介绍他俩有什么细节上的差别,与代码的一些细节优化。
87 0
|
4月前
|
存储 C++ 容器
unordered_set、unordered_multiset、unordered_map、unordered_multimap的介绍及使用
unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。在unordered_set中,元素的值同时也是唯一地标识它的key。在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。它的迭代器至少是前向迭代器。前向迭代器的特性。
186 0
|
6月前
|
存储 监控 算法
基于 C++ 哈希表算法的局域网如何监控电脑技术解析
当代数字化办公与生活环境中,局域网的广泛应用极大地提升了信息交互的效率与便捷性。然而,出于网络安全管理、资源合理分配以及合规性要求等多方面的考量,对局域网内计算机进行有效监控成为一项至关重要的任务。实现局域网内计算机监控,涉及多种数据结构与算法的运用。本文聚焦于 C++ 编程语言中的哈希表算法,深入探讨其在局域网计算机监控场景中的应用,并通过详尽的代码示例进行阐释。
122 4
|
7月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
288 3
|
8月前
|
编译器 容器
哈希表模拟封装unordered_map和unordered_set
哈希表模拟封装unordered_map和unordered_set
|
4月前
|
安全 Java 数据库连接
让我们讲解一下 Map 集合遍历的方式
我是小假 期待与你的下一次相遇 ~
133 43