【C++】哈希表的改造——unordered_map和unordered_set的模拟实现(上)

简介: 【C++】哈希表的改造——unordered_map和unordered_set的模拟实现(上)

1. unordered系列的容器封装


在C++11中,增加了unordered系列的容器,其底层就是哈希原理。在之前的博客内容中,我们实现了哈希的代码部分,包括闭散列和开散列两种。由于闭散列的局限性,所以C++11标准库是采用开散列的方式封装了unordered系列容器,接下来我们将使用之前实现的**哈希桶(开散列)**代码对unordered系列的容器进行改写与封装。


1.1 改造1:模版参数类型的改造

1.1.1 HashNode改造

//改造前
template<class K, class V>
struct HashNode
{
    std::pair<K, V> _kv;
    HashNode* _next;
    HashNode(const std::pair<K, V>& kv)
        :_kv(kv)
          ,_next(nullptr)
    {}
};


由于unordered_set是K模型,而unordered_map是KV模型,为了让底层的哈希桶能够同时支持两种不同的模型,所以这里需要对哈希节点进行改造,改造后的结果如下:

//改造后
template<class T>//将模板参数变成T,对于K模型来说,传入的类型就是<Key,Key>键值对,对于KV模型来说,传入的就是<Key,Value>键值对
struct HashNode
{
    T _data;
    HashNode* _next;
    HashNode(const T& data)
      :_data(data)
        ,_next(nullptr)
      {}
};


1.1.2 HashTable改造

//改造前
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<K, V> Node;
public:
    bool Insert(const std::pair<K,V>& kv);
    bool Erase(const K& key);
    Node* Find(const K& key);
private:
    std::vector<Node*> _tables;
    size_t _n = 0;
}


这里由于节点的模板类型已经更改,所以之前的KV也需要进行更改,同时传入仿函数KeyOfT用来从键值对T中提取出key。

//修改后
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{
    typedef HashNode<T> Node;//这里节点传入的类型是T
public:
    HashTable()
        :_n(0)
    {
    _tables.resize(10);
    }
    bool Insert(const T& data);
    bool Erase(const K& key);
    Node* Find(const K& key);
private:
    std::vector<Node*> _tables;
    size_t _n = 0;
};


1.2 改造2:迭代器的增加与封装

在之前使用红黑树封装map&set的时候已经说明,我们对于map&set的迭代器的处理,使用的是红黑树实现的迭代器。所以这里同样的,使用的是哈希桶封装的迭代器,那么我们首先就改写哈希桶

1.2.1 迭代器类的实现

和红黑树的迭代器一样,原生指针不能支持迭代器行为,所以我们需要自己手动实现一个迭代器类,然后进行一些运算符重载。

在迭代器类的实现中,最重要的一个运算符就是++运算符的重载。

template<class _K, class _T, class _KeyOfT, class _Hash = HashFunc<_K>>
class HashTable;//由于在迭代器类里面使用了HashTable,同时在HashTable中也使用了迭代器类,所以需要进行提前声明类模板
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
struct __HTIterator
{
    typedef HashTable<K,  T, KeyOfT, Hash> HT;
    typedef __HTIterator<K,T,KeyOfT,Hash> Self;
    typedef HashNode<T> Node;
    Node* _node;
    HT* _ht;
    __HTIterator(Node* node, HT* ht)
        :_node(node)
        ,_ht(ht)
    {}
    //这里的思路是按照table的顺序遍历哈希桶,在哈希桶里面单向遍历桶内的每个数据
    Self& operator++()
    {
        if(_node->_next)//当前桶还有元素
        {
            _node = _node->_next;
        }
        else//当前桶走完了,找下一个桶
        {
            size_t hashi = Hash()(KeyOfT()(_node->_data)) % _ht->_tables.size();//找到当前桶的哈希地址
            //找到下一个有元素的哈希桶对应的哈希地址,并将其第一个元素赋值给node
            ++hashi;
            while(hashi < _ht->_tables.size())
            {
                if(_ht->_tables[hashi])
                {
                    _node = _ht->_tables[hashi];
                    break;
                }
                else
                {
                    ++hashi;
                }
            }
            //后面没有桶了
            if(hashi == _ht->_tables.size())
            _node = nullptr;
        }
        return *this;
    }
    //其他的重载在之前的文章中有详细说明,这里道理是一样的,就不再赘述了
    T& operator*()
    {
        return _node->_data;
    }
    T* operator->()
    {
        return &_node->_data;
    }
    bool operator!=(const Self& s)
    {
        return _node != s._node;
    }
    bool operator== (const Self& s)
    {
        return _node==s._node;
    }
};


注:

类模板的声明方法:template<class type_name> class class_name;

同时,这里需要调用HashTable中的私有成员,所以需要给出友元类。

友元类的声明方法:template<class type_name> friend class class_name

这里有一个点需要注意:

由于在clang下,使用同一个模版参数名会出现报错:Declaration of '模版参数名' shadows template parameter,所以本次实现时在迭代器类的模版参数前加上_以示区分(详细代码见后文)。


1.2.2 迭代器的封装

实现了迭代器类之后,就可以在HashTable中封装迭代器接口

typedef __HTIterator<K, T, KeyOfT, Hash> iterator;
//迭代器
iterator begin()
{
    //遍历,找到第一个有元素的桶
    for(size_t i = 0; i < _tables.size(); ++i)
    {
        if(_tables[i])
        {
            return iterator(_tables[i], this);
        }
    }
    return iterator(nullptr, this);
}
//这里采用空指针当作最后一个节点的下一个元素指针
iterator end()
{
    return iterator(nullptr, this);
}


1.3 改造3:insert的改写封装

//改造前
bool Insert(const std::pair<K,V>& kv)
{
    if(Find(kv.first))
        return false;
    if(_n == _tables.size())
    {
        std::vector<Node*> newTables;
        newTables.resize(2* _tables.size(), nullptr);
        for(size_t i = 0; i < _tables.size(); ++i)
        {
            Node* cur = _tables[i];
            while(cur)
            {
                Node* next = cur->_next;
                size_t hashi = Hash()(cur->_kv.first) % newTables.size();
                cur->_next = newTables[hashi];
                newTables[hashi] = cur;
                cur = next;
            }
            _tables[i] = nullptr;
        }
        _tables.swap(newTables);
    }
    size_t hashi = Hash()(kv.first) % _tables.size();
    Node* newnode = new Node(kv);
    newnode->_next = _tables[hashi];
    _tables[hashi] = newnode;
    ++_n;
    return true;
}


为了实现[]重载,所以这里和之前的map&set一样,需要对底层数据结构的insert进行改写,这里需要让返回值变成一个pair,其中的第一个成员是迭代器,第二个成员是原来的bool类型,所以代码如下

//改造后代码
std::pair<iterator, bool> Insert(const T& data)//这里将返回值修改为std::pair类型
{
    iterator it = Find(KeyOfT()(data));//使用仿函数从data中提取到key
    if(it != end())
        return make_pair(it, false);//如果当前对应的key存在,那么就返回当前key的迭代器类型和false组成的pair
    if(_n == _tables.size())
    {
        std::vector<Node*> newTables;
        newTables.resize(2* _tables.size(), nullptr);
        for(size_t i = 0; i < _tables.size(); ++i)
        {
            Node* cur = _tables[i];
            while(cur)
            {
                Node* next = cur->_next;
                size_t hashi = Hash()(KeyOfT()(cur->_data)) % newTables.size();//使用仿函数从data中提取到key
                cur->_next = newTables[hashi];
                newTables[hashi] = cur;
                cur = next;
            }
            _tables[i] = nullptr;
        }
        _tables.swap(newTables);
    }
    size_t hashi = Hash()(KeyOfT()(data)) % _tables.size();//使用仿函数从data中提取到key
    Node* newnode = new Node(data);
    newnode->_next = _tables[hashi];
    _tables[hashi] = newnode;
    ++_n;
    return make_pair(iterator(newnode, this), true);//使用newnode和this构造一个迭代器,将这个迭代器和插入情况构造成一个pair返回。
}


1.4 析构函数的实现

由于在构造或者插入的过程中使用new创建节点了,所以需要对new的资源进行手动释放,所以为了避免内存泄漏,需要进行析构函数的实现。

析构函数的实现原理就是:遍历哈希桶,依次释放节点

所以代码就显而易见了:

~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;//将当前桶的指针置空,防止出现野指针的情况
    }
}


相关文章
|
1月前
|
存储 JavaScript Java
(Python基础)新时代语言!一起学习Python吧!(四):dict字典和set类型;切片类型、列表生成式;map和reduce迭代器;filter过滤函数、sorted排序函数;lambda函数
dict字典 Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。 我们可以通过声明JS对象一样的方式声明dict
122 1
|
4月前
|
存储 缓存 JavaScript
Set和Map有什么区别?
Set和Map有什么区别?
341 1
|
24天前
|
存储 算法 容器
set_map的实现+set/map加持秒杀高频算法题锻炼算法思维
`set`基于红黑树实现,支持有序存储、自动去重,增删查效率为O(logN)。通过仿函数可自定义排序规则,配合空间配置器灵活管理内存。不支持修改元素值,迭代器失效需注意。`multiset`允许重复元素。常用于去重、排序及查找场景。
|
6月前
|
存储 监控 算法
基于 C++ 哈希表算法实现局域网监控电脑屏幕的数据加速机制研究
企业网络安全与办公管理需求日益复杂的学术语境下,局域网监控电脑屏幕作为保障信息安全、规范员工操作的重要手段,已然成为网络安全领域的关键研究对象。其作用类似网络空间中的 “电子眼”,实时捕获每台电脑屏幕上的操作动态。然而,面对海量监控数据,实现高效数据存储与快速检索,已成为提升监控系统性能的核心挑战。本文聚焦于 C++ 语言中的哈希表算法,深入探究其如何成为局域网监控电脑屏幕数据处理的 “加速引擎”,并通过详尽的代码示例,展现其强大功能与应用价值。
147 2
|
5月前
|
存储 JavaScript 前端开发
for...of循环在遍历Set和Map时的注意事项有哪些?
for...of循环在遍历Set和Map时的注意事项有哪些?
297 121
|
8月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
214 2
|
8月前
|
存储 算法 数据处理
公司局域网管理中的哈希表查找优化 C++ 算法探究
在数字化办公环境中,公司局域网管理至关重要。哈希表作为一种高效的数据结构,通过哈希函数将关键值(如IP地址、账号)映射到数组索引,实现快速的插入、删除与查找操作。例如,在员工登录验证和设备信息管理中,哈希表能显著提升效率,避免传统线性查找的低效问题。本文以C++为例,展示了哈希表在局域网管理中的具体应用,包括设备MAC地址与IP分配的存储与查询,并探讨了优化哈希函数和扩容策略,确保网络管理高效准确。
|
4月前
|
存储 算法 PHP
数组去重性能优化:为什么Set和Object哈希表的效率最高
在处理数组去重问题时,使用 `Set` 和 `Object` 哈希表是高效的解决方案。它们基于哈希表实现,插入和查找操作的时间复杂度为 `O(1)`,相比传统嵌套循环的 `O(n²)` 方法性能优势显著。`Set` 能保持元素插入顺序,适用于需要顺序的场景;`Object` 则通过键的唯一性实现去重,适合无需顺序的场景。两者均能在大规模数据中实现高效的去重操作,是数组去重最优选择。
|
5月前
|
存储 缓存 Java
c++的哈希表、哈希桶的介绍与实现
这一篇文章大致实现详细介绍什么是哈希,然后再介绍什么是哈希表,怎么代码实现哈希表,然后再介绍哈希桶,怎么代码实现哈希桶,最后再介绍他俩有什么细节上的差别,与代码的一些细节优化。
117 0

热门文章

最新文章