「C++」哈希表的实现(unordered系底层)

在这里插入图片描述

💻文章目录

  • 📄前言
  • 哈希表概念
    • 哈希函数
  • 哈希冲突
    • 闭散列
    • 开散列
  • 📓总结


📄前言

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构,使其在查找上的时间复杂度几乎减低到了 O ( 1 ) O(1) O(1)

哈希表概念

顺序结构或者平衡树中,要查找一个元素,必须要经过关键码(查找的数值)的多次比较,顺序表和平衡树最佳的查找时间复杂度都为 O ( l o g 2 N ) O(log2_N) O(log2N)

哈希,是一种关键码与数值所一一映射的结构,如果能通过某种函数(HashFunc)使元素的存储位置和他的关键码创建一种映射关系,那么在查找时可以通过该函数快速的找到元素,而存储关键码和数值的顺序表就是哈希表。

哈希表的样例
在这里插入图片描述

哈希函数

哈希表是通过哈希函数构成的结构,其本质也是数组 。哈希方式中使用的函数也被成为哈希函数,使用哈系函数构成的结构称为哈希表。

常见的哈希函数

  1. 直接定址法–(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况
  2. 除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
    按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
  3. 平方取中法–(了解)
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
    再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

哈希冲突

哈希冲突指的是不同关键码通过哈希函数被分配到了同一个哈希地址,哈希冲突是无法避免的,解决冲突的两种常见办法是:闭散列开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去
。那如何寻找下一个空位置
呢?

  1. 线性探测
    比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
    因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

    • 空间标记
      采用线性探测时,为了识别哈希表的位置是否为空,可以给表中每个空间一个标记。

      // 哈希表每个空间给个标记
      // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
      enum State{EMPTY, EXIST, DELETE};
      
    • 插入

      • 通过哈希函数获取待插入元素在哈希表中的位置
      • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
        在这里插入图片描述
    • 删除
      采用闭散列删除时,采用伪删除法删除一个元素,即把需要删除元素的空间设为DELETE。

线性探测的实现

	template<class K, class V>class HashTable{enum State{ EMPTY, EXIST, DELETE };struct Elem	//哈希表存储的元素{std::pair<K, V> _kv;State _state;};public:HashTable(size_t capacity = 3): _ht(capacity), _size(0), _totalSize(0){for (size_t i = 0; i < capacity; ++i)_ht[i]._state = EMPTY;}	//初始化元素void print(){	//打印元素for(int i = 0; i < _ht.size(); ++i){	if(_ht[i]._state == EXIST)std::cout << _ht[i]._kv.first << ":" << _ht[i]._kv.second << std::endl;}}// 插入bool Insert(const std::pair<K, V>& val){if(Find(val.first) != -1)return false;CheckCapacity();	//检查是否需要扩容size_t hashi = HashFunc(val.first);while(_ht[hashi]._state == EXIST){	//存在冲突的情况,找到下一个非空hashi++;hashi %= _ht.size();}_ht[hashi]._kv = val;_ht[hashi]._state = EXIST;++_size;return true;}// 查找size_t Find(const K& key){size_t hashi = HashFunc(key);CheckCapacity();while(_ht[hashi]._state != EMPTY){if(_ht[hashi]._state == EXIST&& _ht[hashi]._kv.first == key)return hashi;hashi++;hashi %= _ht.size();}return -1;}// 删除bool Erase(const K& key){size_t hashi = Find(key);if(hashi == -1) return false;_ht[hashi]._state = DELETE;	//直接讲元素设为DELETE--_size;return true;}size_t Size()const{return _size;}bool Empty() const{return _size == 0;}void Swap(HashTable<K, V>& ht)  //交换节点{std::swap(_size, ht._size);std::swap(_totalSize, ht._totalSize);_ht.swap(ht._ht);}private:size_t HashFunc(const K& key){return key % _ht.capacity();}void CheckCapacity(){if(_size * 10 / _ht.size() >= 7){HashTable<K, V> newHT;newHT._ht.resize(_ht.size() * 2);for(size_t i = 0; i < _ht.size(); ++i){if(_ht[i]._state == EXIST)newHT.Insert(_ht[i]._kv);}_ht.swap(newHT._ht);}}private:std::vector<Elem> _ht;size_t _size;};
}

开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

在这里插入图片描述

开散列哈希表及其迭代器的声明

template <class T>
struct HashBucketNode
{HashBucketNode<T>* _next;	//下一个节点T _data;		//节点的数据HashBucketNode(const T& data):_data(data),_next(nullptr){}
};// 为了实现简单,在哈希桶的迭代器类中需要用到hashBucket本身,
// 因为迭代器中用到了哈希桶,所以得先声明。
template<class K, class T, class KeyOfValue, class HF>
class HashBucket;// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
template <class K, class V, class Ref, class Ptr, class KeyOfValue, class HF>
struct HBIterator 
{typedef HashBucket<K, V, KeyOfValue, HF> HBK;	typedef HashBucketNode<V> Node;		typedef HBIterator<K, V, Ref, Ptr, KeyOfValue, HF> Self;	typedef HBIterator<K, V, V&, V*, KeyOfValue, HF> iterator;	//这个是为了实现unordered_set所准备的Node* _node;             // 当前迭代器关联的节点const HBK* _pHt;         // 哈希桶--主要是为了找下一个空桶时候方便size_t _hashi;	//当前的未知HBIterator(const iterator& it):_node(it._node),_pHt(it._pHt),_hashi(it._hashi){}HBIterator(Node* pNode = nullptr, const HBK* pHt = nullptr, size_t hashi = -1):_node(pNode),_pHt(pHt),_hashi(hashi){}Self& operator++();	//C++ unordereded系不支持--操作Ref operator*();Ptr operator->();
}template <class K>
struct HashFuc	//将数据转换成int,方便哈希函数取余数
{size_t operator()(const K& key){return (size_t)key;}
};template <class K, class T, class KeyOfValue, class HF = HashFuc<K>>
class HashBucket	//哈系桶
{template<class Key, class Value, class Ref, class Ptr, class KeyOfT, class Hash>  //clang errorfriend struct HBIterator;	//迭代器需要使用到类的私有成员,所以将其设为友元类public:typedef HashBucketNode<T> Node;typedef HBIterator<K, T, T&, T*, KeyOfValue, HF> iterator;typedef HBIterator<K, T, const T&, const T*, KeyOfValue, HF> const_iterator;iterator find(const K& key);	//查找std::pair<iterator, bool> insert(const T& val);    //插入bool erase(const K& key);       //删除
private:void CheckCapacity();	//检查是否需要扩容
private:HF _hf;KeyOfValue _kot;std::vector<Node*> _ht;size_t _size;

迭代器的实现

Self& operator++()
{_node = _node->_next;	if(!_node)	//如果节点为空,在哈希表探索{while(!_node && _hashi < _pHt->_ht.size()){	//寻找下一个非空节点_node = _pHt->_ht[++_hashi];}}if(_hashi >= _pHt->_ht.size())_node = nullptr;return *this;
}Ref operator*()
{return _node->_data;
}Ptr operator->()
{return &operator*();
}

插入实现

iterator find(const K& key)
{size_t hashi = _hf(key) % _ht.size();Node* cur = _ht[hashi];while(cur){if(_kot(cur->_data) == key)return iterator(cur, this, hashi);cur = cur->_next;}return iterator(cur);
}std::pair<iterator, bool> insert(const T& val)    //插入
{iterator it = find(_kot(val));	//寻找位置if (it != end()){	//节点存在的情况return std::make_pair(it, false);}CheckCapacity();	//检查扩容size_t hashi = _hf(_kot(val)) % _ht.size();Node* node = new Node(val);node->_next = _ht[hashi];		//头插到所在位置_ht[hashi] = node;_size++;return std::make_pair(iterator(node, this, hashi), true);
}void CheckCapacity()
{if(_size == _ht.size()){std::vector<Node*> newHT;newHT.resize(_ht.size() * 2, nullptr);for(size_t i = 0; i < _ht.size(); ++i){Node* node = _ht[i];while(node){Node* next = node->_next;size_t hashi = _hf(_kot(node->_data)) % newHT.size();node->_next = newHT[hashi];newHT[hashi] = node;node = next;}_ht[i] = nullptr;}_ht.swap(newHT);}
}

删除

bool erase(const K& key)       //删除
{size_t hashi = _hf(key) % _ht.size();Node* cur = _ht[hashi];Node* prev = nullptr;while(cur) {if(_kot(cur->_data) == key){if(!prev){_ht[hashi] = cur->_next;}else {   prev->_next = cur->_next;}--_size;delete cur;return true;}prev = cur;cur = cur->_next;}return false;
}

📓总结

优点缺点
闭散列实现简单容易导致数据堆积
开散列存储开销减少如果数据过于集中,会导致查找性能上的损耗

📜博客主页:主页
📫我的专栏:C++
📱我的github:github

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

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

相关文章

企业ERP软件定制开发的重点|app小程序网站建设

企业ERP软件定制开发的重点|app小程序网站建设 随着企业信息化程度的不断提高&#xff0c;企业资源计划&#xff08;ERP&#xff09;软件成为了现代企业管理的重要工具。然而&#xff0c;由于不同企业的业务流程、组织结构和管理模式各异&#xff0c;现有的通用ERP软件无法完全…

基于若依的ruoyi-nbcio的flowable流程管理系统增加服务任务和我的抄送功能

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 1、增加一个状态字段 wf_copy增加下面两个字段 就用未读已读来区分 2、前端 api接口增加如下&#xff…

IDEA检查项目的jdk版本需要看的地方

IDEA检查项目的jdk版本需要看的地方 1、检查项目结构&#xff0c;如下图所示选择即可 选择了之后打开了如下界面&#xff1a; 下面的三张图全部都要检查选择jdk8的版本 2、进入设置&#xff0c;如下所示&#xff1a; 进入之后&#xff0c;根据下图&#xff0c;挨个选择&#xf…

「海蓝色」海关可视化监管平台,助力海关体系实现规范化程序管理

海关监管是国家对进出境货物、旅客和邮件进行检查和控制的重要机构&#xff0c;其职责是保障国家的安全和经济利益。海关监管的核心目标是防止非法进出境活动&#xff0c;包括走私、偷逃税款等行为。海关监管通过检查和核实货物的品质、数量和价值&#xff0c;确保货物符合相关…

SMART PLC编码器角度测量

编码器角度测量在倒立摆摆杆角度测量上的应用请参考下面文章链接: https://rxxw-control.blog.csdn.net/article/details/131443657https://rxxw-control.blog.csdn.net/article/details/131443657线性模式下编码器旋转圈数和单圈计数值测量可以查看下面文章: https://rxxw…

C语言实现植物大战僵尸(完整版)

实现这个游戏需要Easy_X 这个在我前面一篇C之番外篇爱心代码有程序教你怎么下载&#xff0c;大家可自行查看 然后就是需要植物大战僵尸的素材和音乐&#xff0c;需要的可以在评论区 首先是main.cpp //开发日志 //1导入素材 //2实现最开始的游戏场景 //3实现游戏顶部的工具栏…

关于pycharm代码误删,本地历史恢复

关于pycharm代码误删&#xff0c;本地历史恢复 1.工作中或多或少的出现代码误删&#xff0c;或者其他原因导致我们的辛辛苦苦写的码子没了&#xff0c;一定很着急。 2.我们点击工作目录文件夹或者是项目内鼠标右击&#xff0c;找到local history 然后选择show history,就可以…

Python3+RIDE+RobotFramework自动化测试框架搭建过程详解

一、Python安装 最新版Python下载地址&#xff1a;https://www.python.org/ 根据操作系统选择对应版本制品下载安装即可&#xff0c;本机用的是Windows x86-64 executable installer。 注意事项&#xff1a; 安装完成后检查下环境变量&#xff0c;默认会配置好&#xff0c;可…

算法初阶双指针+C语言期末考试之编程题加强训练

双指针 常⻅的双指针有两种形式&#xff0c;⼀种是对撞指针&#xff0c;⼀种是左右指针。 对撞指针&#xff1a;⼀般⽤于顺序结构中&#xff0c;也称左右指针。 • 对撞指针从两端向中间移动。⼀个指针从最左端开始&#xff0c;另⼀个从最右端开始&#xff0c;然后逐渐往中间逼…

【链表Linked List】力扣-2 两数相加

目录 题目描述 解题过程 题目描述 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 …

JVM GUI可视化监控及诊断工具

工具既述 使用命令行工具或组合能帮您获取目标Java应用性能相关的基础信息&#xff0c;但它们存在下列局限&#xff1a; 无法获取方法级别的分析数据&#xff0c;如方法间的调用关系、各方法的调用次数和调用时间等&#xff08;这对定位应用性能瓶颈至关重要&#xff09;。要…

决战排序之巅(一)

决战排序之巅 插入排序直接插入排序 void InsertSort(int* arr, int n)希尔排序 void ShellSort(int* arr, int n)测试插入排序测试函数 void verify(int* arr, int n)测试 InsertSort测试 ShellSort测试速度 InsertSort & ShellSort 选择排序直接选择排序 void SelectSort…

初试Jakarta EE项目 - Servlet + JSP

文章目录 一、Jakarta EE概述二、Servlet概述&#xff08;一&#xff09;Servlet的概念&#xff08;二&#xff09;Servlet的工作原理&#xff08;三&#xff09;Servlet的特点1、独立性2、灵活性3、生命周期管理 &#xff08;四&#xff09;Servlet的应用场景&#xff08;五&a…

从零开始,利用ChatGPT学会写作的完整指南

文章目录 前言了解ChatGPT访问OpenAI平台使用ChatGPT进行简单的对话定义写作主题逐步生成文章段落添加个性化和细节编辑和润色反复修改直至满意 图书推荐内容简介作者简介获取方式 前言 在数字时代&#xff0c;人工智能技术日益成熟&#xff0c;为我们提供了全新的学习和创作机…

Linux CentOS本地部署SQL Server数据库结合cpolar内网穿透实现公网访问

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、Cpolar杂谈 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. 安装sql server二. 局域网测试连接三. 安装cpolar内网穿透四. 将sqlserver映射…

AI报告专题:创造性和生成式人工智能

今天分享的AI系列深度研究报告&#xff1a;《AI报告专题&#xff1a;创造性和生成式人工智能》。 &#xff08;报告出品方&#xff1a;Capgemini&#xff09; 报告共计&#xff1a;64页 AI一代 生成式人工智能 (AI)正在迅速改变我们与技术的交互方式&#xff0c;使机器能够创…

基于ssm在线医疗服务系统论文

摘 要 网络技术和计算机技术发展至今&#xff0c;已经拥有了深厚的理论基础&#xff0c;并在现实中进行了充分运用&#xff0c;尤其是基于计算机运行的软件更是受到各界的关注。加上现在人们已经步入信息时代&#xff0c;所以对于信息的宣传和管理就很关键。因此医疗服务信息的…

Linux---逻辑卷管理

本章主要介绍逻辑卷的管理。 了解什么是逻辑卷创建和删除逻辑卷扩展逻辑卷缩小逻辑卷逻辑卷快照的使用 前面介绍了分区的使用&#xff0c;如果某个分区空间不够&#xff0c;想增加空间是非常困难的。所以&#xff0c;建议尽可能使用逻辑卷而非普通的分区&#xff0c;因为逻辑卷…

贪心算法及相关题目

贪心算法概念 贪心算法是指&#xff0c;在对问题求解时&#xff0c;总是做出在当前看来是最好的选择。也就是说&#xff0c;不从整体最优上加以考虑&#xff0c;算法得到的是在某种意义上的局部最优解 。 贪心算法性质&#xff08;判断是否可以使用贪心算法&#xff09; 1、贪…

el-menu标题过长显示不全问题处理

项目基于vue-element-admin 问题 期望 处理方式 \src\layout\components\Sidebar\index.vue 文件后添加CSS <style scped> /* 侧栏导航菜单经典 文字超长溢出问题 CSS折行 */ .el-submenu__title {display: flex;align-items: center; } .el-submenu__title span {white-…