C++实现unordered_map和unordered_set

news2024/11/15 7:06:14

1. 结构示意

2. 模版参数控制

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

如果各自拥有一个哈希表,set和unordered_set只要一个key值即可。

2.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;
};

2.2 结点类定义

我们将设计一个底层哈希表类,通过模板参数 T 来区分 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)
    {}
};

2.3 仿函数获取键值

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

unordered_set 

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

 unordered_map

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 T, class KeyOfT>
class HashTable
{
    // ... 
};

3. 哈希函数

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

提出的BKDRHash 算法,是一个高效且实用的哈希函数,之前的文章中提到过,实现起来是:

数字类型 

// 哈希函数
template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        return static_cast<size_t>(key);
    }
};

字符串类型 (BKDRHash算法)

// 模板的特化
template<>
struct HashFunc<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
{
    // ...
};

4. 哈希表默认成员

4.1 默认构造函数

这里的 _table 是一个 std::vector,在构造函数中使用 resize(size, nullptr) 来初始化,确保 _table 的大小为 size,并且每个桶初始化为 nullptr

// 构造函数,初始化桶大小为 size
HashTable(size_t size = 10) {
    _table.resize(size, nullptr);
}

4.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;
}

4.3 赋值运算符重载

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

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

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

4.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;
    }
}

接下来是对数据的插入查找删除的操作,显然直接对 Node* 的++或--操作不能正常遍历到数据,所以就需要对结点指针的迭代器封装

5. 正向迭代器

STL中实现了双向迭代器,但这里只实现了正向迭代器。

在哈希表的情况下,实际上不支持有效的双向迭代器操作,因为哈希表的设计通常不支持向前遍历。

5.1 定义迭代器结构体

由于不同的运算符重载函数的返回值可能不同,类型有引用、指针和迭代器本身,所以在迭代器内部typedef一下。

// 前置声明
template<class K, class T, class KeyOfT, class Hash>  // __HTIterator类需要知道HashTable类的存在,以便使用它作为成员类型HT
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;               // 哈希表的地址

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

    // 解引用运算符重载
    T& operator*()
    {
        return _node->_data;
    }

    // 箭头运算符重载
    T* operator->()
    {
        return &_node->_data;
    }

    // 比较运算符重载
    bool operator!=(const Self& s) const
    {
        return _node != s._node;
    }

    // ==运算符重载
    bool operator==(const Self& s) const
    {
        return _node == s._node;
    }

    // 前置++
    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;
    }
};

6. 操作函数

6.1 插入

如果元素已存在,返回迭代器指向该元素和 false。否则,插入新元素并返回指向该元素的迭代器和 true。

pair<typename HashTable::iterator, bool> Insert(const T& data)
{
    KeyOfT kot;
    Hash hash;
    size_t index = hash(kot(data)) % _table.size(); // 计算哈希值并获取桶索引

    Node* current = _table[index];
    while (current)
    {
        if (kot(current->_data) == kot(data)) // 检查元素是否已存在
        {
            return { iterator(current, this), false }; // 元素已存在
        }
        current = current->_next;
    }

    // 元素不存在,插入新元素
    Node* newNode = new Node(data);
    newNode->_next = _table[index];
    _table[index] = newNode;
    ++_size;

    return { iterator(newNode, this), true };
}

6.2 查找

若找到元素,返回指向该元素的迭代器;若未找到元素,返回 end() 迭代器。

typename HashTable::iterator Find(const K& key)
{
    Hash hash;
    KeyOfT kot;
    size_t index = hash(key) % _table.size(); // 计算哈希值并获取桶索引

    Node* current = _table[index];
    while (current)
    {
        if (kot(current->_data) == key) // 找到元素
        {
            return iterator(current, this);
        }
        current = current->_next;
    }

    return end(); // 未找到元素
}

6.3 删除  

若元素存在,则删除该元素;若元素不存在,则不做任何操作。 

void Erase(const K& key)
{
    Hash hash;
    KeyOfT kot;
    size_t index = hash(key) % _table.size(); // 计算哈希值并获取桶索引

    Node* current = _table[index];
    Node* prev = nullptr;
    while (current)
    {
        if (kot(current->_data) == key) // 找到要删除的元素
        {
            if (prev)
            {
                prev->_next = current->_next;
            }
            else
            {
                _table[index] = current->_next;
            }
            delete current;
            --_size;
            return;
        }
        prev = current;
        current = current->_next;
    }
}

6. 实现unordered_set和unordered_map

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;
};

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;
};

用一个哈希表来实现 unordered_set 和 unordered_map 到此结束🌹

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

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

相关文章

floodfill算法(一)

目录 一、图像渲染 1. 题目链接&#xff1a;733. 图像渲染 2. 题目描述&#xff1a; 3. 解法 &#x1f334;算法思路&#xff1a; &#x1f334;算法代码&#xff1a; 二、岛屿数量 1. 题目链接&#xff1a;200. 岛屿数量 2. 题目描述&#xff1a; 3. 解法 &#x1f…

FinLex An effective use of word embeddings for financial lexicon generation

FinLex: An effective use of word embeddings for financial lexicon generation 论文阅读 文章目录 FinLex: An effective use of word embeddings for financial lexicon generation 论文阅读 AbstractMethodology具体词表例子LM 词列表与 FinLex 词列表 词列表在分类任务中…

网络安全学习(二)Netdiscover

Netdiscover是一款网络扫描工具&#xff08;kali中有这个工具&#xff09;&#xff0c;它可以扫描本地网络主机&#xff0c;它借助ARP协议实施主机发现&#xff0c;长这个样子。 点击打开&#xff0c;进入命令行。 首先查看本机的IP地址&#xff0c;命令为ifconfig&#xff0c…

TAG:BladeLLM 的纯异步推理架构

作者&#xff1a;张子鹏 PAI引擎团队 随着 GQA/MLA/MoE 等模型结构不断发展&#xff0c;大语言模型的推理逐步解除了显存限制&#xff0c;逐渐向着高并发、高吞吐的方向发展。推理引擎的运行时开销也变得不可忽视。主流 LLM 推理框架的运行时开销大致来自&#xff1a; Python …

【MySQL学习】基础指令全解:构建你的数据库技能

&#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;MySQL学习 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f49e; &#x1f49e; &#x1f49e; 引言 下面的操作都是在windows 的…

AT89C51 Intel HEX手工结构分析 反汇编工具

在不查询格式情况下分析确定 Intel HEX 格式 Hex文件内容 :0300000002090BE7 :0C090B00787FE4F6D8FD7581080208F63C :01091700419E :1008F60078087C007D007BFF7A0979177E007F01EE :050906001208D080FE84 :10080000E709F608DFFA8046E709F208DFFA803EDA :1008100088828C83E709F0…

C++基础面试题 | C++中的构造函数可以是虚函数吗? C++中的析构函数一定要是虚函数吗?

文章目录 问题一&#xff1a;在C中&#xff0c;构造函数不能是虚函数。问题二&#xff1a;析构函数不一定需要声明为虚函数&#xff0c;但在多态环境下&#xff0c;建议一定将其声明为虚函数。示例虚函数总结 问题一&#xff1a;在C中&#xff0c;构造函数不能是虚函数。 这是…

PMP--一模--解题--81-90

文章目录 4.整合管理81、 [单选] 一位先前不活跃的干系人参与程度突然增加&#xff0c;这种意外的参与导致了一些变更请求。项目经理应该做什么&#xff1f; 4.整合管理82、 [单选] 公司的新产品系列将在两个月内发布&#xff0c;95%的项目任务均已完成。但是&#xff0c;管理层…

二分算法——优选算法

个人主页&#xff1a;敲上瘾-CSDN博客 个人专栏&#xff1a;游戏、数据结构、c语言基础、c学习、算法 本章我们来学习的是二分查找算法&#xff0c;二分算法的应用非常广泛&#xff0c;不仅限于数组查找&#xff0c;还可以用于解决各种搜索问题、查找极值问题等。在数据结构和算…

无人机飞手培训机构组建及市场分析

飞手培训机构是专门为培养无人机飞行员&#xff08;飞手&#xff09;而设立的教育机构。这些机构通过提供专业的培训课程&#xff0c;帮助学员掌握无人机飞行技术、了解相关法规、提升实战能力&#xff0c;并最终获得相关证书&#xff0c;以便在航拍摄影、农业植保、物流配送、…

MS SQL Server 实战 排查多列之间的值是否重复

目录 需求 范例运行环境 数据样本设计 功能实现 上传EXCEL文件到数据库 SQL语句 小结 需求 在日常的应用中&#xff0c;排查列重复记录是经常遇到的一个问题&#xff0c;但某些需求下&#xff0c;需要我们排查一组列之间是否有重复值的情况。比如我们有一组题库数据&…

django实现开发、测试、生产环境配置区分

文章目录 一、为什么要区分开发 (dev)、测试 (test) 和生产 (prod) 环境二、django项目如何通过配置实现环境配置的区分1、针对不同的环境创建不同的设置文件settings.py2、在设置文件中根据需要进行配置区分3、根据不同的环境运行使用不同的设置文件 任何实际的软件项目中都要…

使用Python打造全自动wx好友添加器:批量操作,高效省时的社交神器

在现代的数字营销和社交扩展中&#xff0c;自动化操作可以显著提高效率。尤其是wx这种广泛使用的即时通讯工具&#xff0c;很多用户有批量添加好友的需求&#xff0c;但手动操作费时费力。本教程将详细介绍如何使用 Python 开发一个自动化工具&#xff0c;帮助你批量添加wx好友…

宏任务和微任务+超全面试真题(持续更新ing

概念 微任务和宏任务是在异步编程中经常使用的概念&#xff0c;用于管理任务的执行顺序和优先级。 宏任务&#xff1a;setTimeout, setInterval&#xff0c;I/O 操作和 UI 渲染等。微任务&#xff1a; Promise 回调、async/await等 微任务通常比宏任务具有更高的优先级。 执…

C#基础(11)函数重载

前言 前面我们已经完成了ref和out补充知识点的学习&#xff0c;以及函数参数相关的学习&#xff0c;今天便再次为函数补充一个知识点&#xff1a;函数重载。 函数重载是指在同一个作用域中&#xff0c;可以有多个同名函数&#xff0c;但参数列表不同。它的发展可以追溯到早期…

【Chrome】开发一个Chrome扩展以及常见问题的解决方案

前言 本文介绍开发chrome扩展很重要的几种操作&#xff0c;如&#xff1a;操作网页dom、发送请求、渲染弹层、不同沙盒环境的通信方式、扩展与网页的通信方式、遇到iframe时的操作等。最终会提供一个简单的案例&#xff0c;其中涵盖了上述操作。 还有一些本人相关文章&#x…

HashMap 详解

哈希表 哈希表又叫散列表&#xff0c;或者映射、字典都是指哈希表&#xff0c;哈希表是通过关键码映射到数组的某个位置来访问的数据结构&#xff0c;实现这个映射的函数就是哈希函数&#xff0c;哈希表结合了数组和链表的优点&#xff0c;查找和插入操作的时间复杂度都是O(1)。…

MySQL篇(高级字符串函数/正则表达式)(持续更新迭代)

目录 讲点一&#xff1a;高级字符串函数 一、简介 二、常见字符串函数 1. CONCAT() 2. SUBSTRING() 3. LENGTH() 4. REPLACE() 5. TRIM() 6. UPPER() 7. LOWER() 8. LEFT() 9. RIGHT() 10. INSTR() 11. LENTH(str) 讲点二&#xff1a;正则表达式 一、简介 二、…

AIGC实战——多模态模型Flamingo

AIGC实战——多模态模型Flamingo 0. 前言1. Flamingo 架构2. 视觉编码器3. Perceiver 重采样器4. 语言模型5. FIamingo 应用小结系列链接 0. 前言 我们已经学习了文本生成图像模型 DALL.E 2&#xff0c;在本节中&#xff0c;我们将探索另一种多模态模型 Flamingo&#xff0c;它…

学习使用在windows系统上安装nodejs以及环境配置图文教程整理

学习使用在windows系统上安装nodejs以及环境配置图文教程整理 Node.js 介绍Node.js 安装1、Node.js下载2、Node.js安装3、Node.js测试4、Node.js安装目录5、Node.js环境变量配置6、配置镜像站&#xff0c;提升速度7、检查镜像站配置8、测试环境变量是否生效9、安装cnpm Node.js…