C++ unordered_map和unordered_set的使用,哈希表的实现

文章目录

  • unordered_map,unorder_set和map ,set的差异
  • 哈希表的实现
    • 概念
    • 直接定址法
    • 哈希冲突
      • 哈希冲突举个例子
    • 负载因子
    • 将关键字转为整数
    • 哈希函数
      • 除法散列法/除留余数法
    • 哈希冲突的解决方法
      • 开放定址法
        • 线性探测
        • 二次探测
    • 开放定址法代码实现
  • 哈希表的代码

unordered_map,unorder_set和map ,set的差异

unordered_map,unordered_set在功能方面和map,set基本一致
unordered_map和unordered_set底层是哈希表,遍历无序,查找删除修改的效率为O(1)
map和set底层是红黑树,遍历有序,查找删除修改的效率为O(logN)

  1. 功能:迭代器之间也有差异,set是双向迭代器,unordered_set是单向迭代器,set底层是红黑树,走中序是有序的,所以迭代器的遍历是有序+去重unordered_set底层是哈希表,迭代器遍历是无序+去重
  2. 效率:unordered_set和set性能方面也有差异,set是O(logN),unordered_set是O(1)
  3. 使用要求:对key的要求不同,set要求key支持小于的比较,unordered_set要求key转为整形且要支持等于的比较
  4. unordered_set,unordered_map和map,set要求基本一致
  5. unordered_multimap和unordered_multiset支持键值冗余(key冗余)
  6. 效率上无序的比有序的总体上要好很多,但是在排有序的数的时候,有序的删除,插入比无序的效率高,但是查找效率还是无序的效率高
    在这里插入图片描述
void set_test1()
{// 迭代器遍历set<int> s = { 3,1,6,7,8,2};set<int>::iterator it = s.begin();while (it != s.end()){cout << *it << " ";++it;}// 3 1 6 7 8 2cout << endl;
}int main()
{set_test1();return 0;
}

哈希表的实现

概念

  • 哈希表也叫散列表,散列有散乱排列的意思。通过关键字Key跟存储位置建立一个映射关系

直接定址法

  • 适合关键字的范围比较集中的,直接定址法是非常高效的方法。比如给’a’ - 'z’的小写字母为范围,通过直接映射或相对映射关键字的值就是存储位置的下标,下面也有一道题是这样的。只适合整形,字符串,浮点数都不行

字符串中的第一个唯一字符

class Solution 
{
public:int firstUniqChar(string s) {int hash[26] = {0};for(auto ch : s){hash[ch - 'a']++;}    for(int i = 0;i < s.size();i++){if(hash[s[i] - 'a'] == 1)return i;}return -1;}
};

哈希冲突

  • 直接定址法的缺点非常明显,当关键字比较分散时,会浪费空间甚至空间会不够用。当有N个值要映射到M个空间中(M >= N),就要借助哈希函数,关键字key要放到数组的h(key)的位置,h(key)计算的值要在[0,M)中。
  • 哈希冲突也叫哈希碰撞,就是两个不同的key映射到同一个位置了,我们要设计一个哈希函数来减少哈希冲突,在实际问题中哈希冲突是不可避免的

哈希冲突举个例子

N == 100 M == 200,N个数要存到M个空间中
除法散列法:h(key) = key % M
哈希冲突:3 % 200 = 3,203 % 200 = 3

负载因子

哈希表中已经存了N个值,哈希表的大小为M,负载因子 = N/M,负载因子也叫装载因子/载荷因子,负载因子越大,哈希冲突越高,空间利用率越高,相反就越低。

将关键字转为整数

比如是浮点数转为整数或者有符号的整数(负数)要转为正数,字符串转为整数。

哈希函数

一个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却很难做到,但是我们要尽量往这个方向去考量设计。

除法散列法/除留余数法

  • 除法散列表是最常用的,哈希表的大小为M,通过key除以M的余数作为映射的下标,h(key) = key % M
  • 使用这个方法时要注意M不要为2的幂,10的幂等。如果是2^X,那么key%2 ^ X,相当于保留key的后X位,那么key的后X位相同的值就哈希冲突了
  • 比如63,31,如果M是16,2 ^ 4,计算出的哈希值都是15,00111111为63,00011111为31,后4位都是1111,所以会哈希冲突。10进制的,比如123和345123,如果M = 10 ^ 3,保留后三位,都是123,也哈希冲突。
  • 当使用除留余数法时,建议M取不太接近2的整数次幂的一个质数
  • %其实用到了除,除的效率比位运算的效率更低,所以Java中用了位运算

Java中用了2的整数次幂作为哈希表的大小M
在这里插入图片描述

哈希冲突的解决方法

开放定址法

  • 开放定址法,哈希表是空的,把所有元素放到哈希表中,当关键字key用哈希函数找到了冲突位置,就按照某种规则去找一个没有存储数据的位置进行储存。这里有三种规则:线性探测,二次探测,双重探测
线性探测

现在给一组数据:
{19,30,5,36,13,20,21,12} 等这一组值映射到M=11的表中

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,
h(13) = 2,h(20) = 9,h(21) =10,h(12) = 1
在这里插入图片描述
上面这组数据在8位置就冲突了

  • 线性探测:从发生冲突的位置依次往后探测,直到找到一个没有存储数据的位置为止,如果走到哈希尾都没有找到,则回绕到哈希头继续往后找
  • h(key) = hash0 = key % M , hash0位置冲突了,则线性探测公式为:hc(key, i) = hashi = (hash0 + i) % M, i = {1, 2, 3, …, M − 1},因为负载因子必须小于1(因为要留一个位置不放数据,不然会出问题),则最多探测M-1次,一定能找到一个存储key的位置。
  • 连续冲突:hash0位置连续冲突,(你占我的位置,我占你的位置)这种现象叫做 群集/堆积,二次探测可以改善这样的问题
二次探测

二次探测和线性探测非常类似
如果往左走回绕到哈希表尾,用减,比如3-9,到5的位置停下,-6 + M,M == 11, -6 + M = 5
在这里插入图片描述

// 二次探测
int flag = 1;
while (_tables[hashi]._state == EXIST)
{// 存在进行二次探测,删除和空就退出hashi = (hash0 + i*i*flag) % _tables.size();if (hashi < 0)hashi += _tables.size();if (flag == 1){flag = -1;}else{flag = 1;++i;}
}

开放定址法代码实现

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,
h(13) = 2,h(20) = 9,h(21) =10,h(12) = 1
蓝色的四个位置连续冲突在这里插入图片描述
查找的原则:遇到删除,存在才继续找,遇到空就结束查找
状态标识:存在,空和删除,空和删除可以放值,空就线性探测
要注意给每个存储值的位置加一个状态标识,否则删除一些值以后,会影响后面冲突的值的查找。
如下图,我们删除30,会导致查找20失败,当我们给每个位置加⼀个状态标识{EXIST,EMPTY,DELETE} ,删除30就可以不用删除值,而是把状态改为 DELETE ,那么查找20时是遇到 EMPTY 才能停止,就可以找到20。

哈希表的代码

#pragma once#include<vector>// 枚举状态
enum State
{EXIST,EMPTY,DELETE
};template<class K,class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};template<class K,class V>
class HashTable
{
public:// 构造HashTable()// 会取到53:_tables(__stl_next_prime(0)),// 11是数据个数()_n(0){}// 为了解决M是质数的问题inline unsigned long __stl_next_prime(unsigned long n){// Note: assumes long is at least 32 bits.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};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);// lower_bound取表中大于等于first的数return pos == last ? *(last - 1) : *pos;}// 插入bool Insert(const pair<K, V>& kv){// 不支持冗余if (Find(kv.first)){return false;}// 负载因子大于等于0.7就扩容if (_n * 10 / _tables.size() >= 7){// 扩容//vector<HashTable<K, V>> newtables(_tables.size()*2);//for (auto& data : _tables)//{//	// 把旧表的数据映射到新表//	// 不能直接拷贝,因为映射关系还是原来的(会乱)//	if (data._state == EMPTY)//	{//		size_t hash0 = data._kv.first % newtables.size();//		// ...//	}//}//_tables.swap(newtables);// 上面的方法代码过于冗余// 把新表和旧表交换HashTable<K, V> newht;// newht._tables.resize(_table.size() * 2);newht._tables.resize(__stl_next_prime(_table.size()));for (auto& data : _tables){// 把旧表的数据映射到新表if (data._state == EMPTY){// 相当于递归newht.Insert(data._kv);}}// 函数结束,newht销毁,数据交换给旧表_tables.swap(newht._tables);}// key / M , M哈希表的空间大小size_t hash0 = kv.first % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state == EXIST){// 存在进行线性探测,删除和空就退出hashi = (hash0 + i) % _tables.size();++i;}// 二次探测//int flag = 1;//while (_tables[hashi]._state == EXIST)//{//	// 存在进行二次探测,删除和空就退出//	hashi = (hash0 + i*i*flag) % _tables.size();//	if (hashi < 0)//		hashi += _tables.size();//	if (flag == 1)//	{//		flag = -1;//	}//	else//	{//		flag = 1;//		++i;//	}//}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}// 查找HashData<K, V>* Find(const K& key){size_t hash0 = key % _tables.size();size_t hashi = hash0;size_t i = 1;// Find等于空就找不到了while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST&& _tables[hashi]._kv.first == key){return &_tables[hashi];}// 存在进行线性探测,删除和空就退出hashi = (hash0 + i) % _tables.size();++i;}return nullptr;}// 删除bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (key){ret->_state = DELETE;return true;}else{return false;}}private:vector<HashData<K, V>> _tables;size_t _n;// 记录哈希表中的数据个数
};#include"HashTable.h"int main()
{int a[] = { 19,30,5,36,13,20,21,12 };HashTable<int, int> ha;for (auto& e : a){ha.Insert({ e,e });}ha.Erase(20);if (ha.Find(30)){cout << "找到了" << endl;}if (ha.Find(20)){cout << "找到了" << endl;}else{cout << "没有找到" << endl;}return 0;
}

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

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

相关文章

c#使用log4Net配置日志文件

1.# 写一个通用类 LogHelper using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using log4net;namespace WindowsFormsApplication22 {public class LogHelper{static ILog mylog LogManager.GetLogge…

WebSocket 详解:全双工通信的实现与应用

目录 一、什么是 WebSocket&#xff1f;&#xff08;简介&#xff09; 二、为什么需要 WebSocket&#xff1f; 三、HTTP 与 WebSocket 的区别 WebSocket 的劣势 WebSocket 的常见应用场景 WebSocket 握手过程 WebSocket 事件处理和生命周期 一、什么是 WebSocket&#xf…

Qt Ribbon使用实例

采用SARibbon创建简单的ribbon界面 实例代码如下所示&#xff1a; 1、头文件&#xff1a; #pragma once #include <SARibbonBar.h> #include "SARibbonMainWindow.h" class QTextEdit; class SAProjectDemo1 : public SARibbonMainWindow { Q_OBJECT pub…

认识小程序的基本组成结构

1.基本组成结构 2.页面的组成部分 3.json配置文件 4.app.json文件(全局配置文件&#xff09; 5.project.config.json文件 6.sitemap.json文件 7.页面的.json配置文件 通过window节点可以控制小程序的外观

JVM--类加载器

概念 类加载器&#xff1a;只参与加载过程中的字节码获取并加载到内存中的部分&#xff1b;java虚拟机提供给应用程序去实现获取类和接口字节码数据的一种技术&#xff0c;也就是说java虚拟机是允许程序员写代码去获取字节码信息 类加载是加载的第一步&#xff0c;主要有以下三…

51单片机开发:定时器中断

目标&#xff1a;利用定时器中断&#xff0c;每隔1s开启/熄灭LED1灯。 外部中断结构图如下图所示&#xff0c;要使用定时器中断T0&#xff0c;须开启TE0、ET0。&#xff1a; 系统中断号如下图所示&#xff1a;定时器0的中断号为1。 定时器0的工作方式1原理图如下图所示&#x…

Greenplum临时表未清除导致库龄过高处理

1.问题 Greenplum集群segment后台日志报错 2.回收库龄 master上执行 vacuumdb -F -d cxy vacuumdb -F -d template1 vacuumdb -F -d rptdb 3.回收完成后检查 仍然发现segment还是有库龄报警警告信息发出 4.检查 4.1 在master上检查库年龄 SELECT datname, datfrozen…

小程序-视图与逻辑

前言 1. 声明式导航 open-type"switchTab"如果没有写这个&#xff0c;因为是tabBar所以写这个&#xff0c;就无法跳转。路径开始也必须为斜线 open-type"navigate"这个可以不写 现在开始实现后退的效果 现在我们就在list页面里面实现后退 2.编程式导航…

Kotlin开发(六):Kotlin 数据类,密封类与枚举类

引言 想象一下&#xff0c;你是个 Kotlin 开发者&#xff0c;敲着代码忽然发现业务代码中需要一堆冗长的 POJO 类来传递数据。烦得很&#xff1f;别急&#xff0c;Kotlin 贴心的 数据类 能帮你自动生成 equals、hashCode&#xff0c;直接省时省力&#xff01;再想想需要多种状…

games101-作业2

图形管线 Vertex Processing 对顶点进行加工&#xff0c;使其变换到屏幕空间坐标。 Triangle Processing 将加工后的顶点组装成三角形&#xff0c;用于下一步的光栅化。 void rst::rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, col_buf_id col_buffer, Pr…

Baklib引领企业内容中台建设的新思路与应用案例

内容概要 在数字化转型的浪潮中&#xff0c;内容中台的概念逐渐成为企业实现高效运营的重要基础。内容中台不仅是信息资产的集中管理平台&#xff0c;更是企业在应对快速变化市场需求时的一种敏捷响应机制。通过搭建内容中台&#xff0c;企业能够有效整合各类资源&#xff0c;…

准备知识——旋转机械的频率和振动基础

旋转频率&#xff0c;也称为转速或旋转速率&#xff08;符号ν&#xff0c;小写希腊字母nu&#xff0c;也作n&#xff09;&#xff0c;是物体绕轴旋转的频率。其国际单位制单位是秒的倒数(s −1 )&#xff1b;其他常见测量单位包括赫兹(Hz)、每秒周期数(cps) 和每分钟转数(rpm)…

Java 大视界 -- Java 大数据在生物信息学中的应用与挑战(67)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

【NLP251】NLP RNN 系列网络

NLP251 系列主要记录从NLP基础网络结构到知识图谱的学习 &#xff11;.原理及网络结构 &#xff11;.&#xff11;&#xff32;&#xff2e;&#xff2e; 在Yoshua Bengio论文中( http://proceedings.mlr.press/v28/pascanu13.pdf )证明了梯度求导的一部分环节是一个指数模型…

Unbutu虚拟机+eclipse+CDT编译调试环境搭建

问题1: 安装CDT&#xff0c;直接Help->eclipse Market space-> 搜cdt , install&#xff0c;等待重启即可. 问题2&#xff1a;C变量不识别vector ’could not be resolved 这是库的头文件没加好&#xff0c;右键Properties->C Build->Enviroment&#xff0c;增加…

关于opencv环境搭建问题:由于找不到opencv_worldXXX.dll,无法执行代码,重新安装程序可能会解决此问题

方法一&#xff1a;利用复制黏贴方法 打开opencv文件夹目录找到\opencv\build\x64\vc15\bin 复制该目录下所有文件&#xff0c;找到C:\Windows\System32文件夹&#xff08;注意一定是C盘&#xff09;黏贴至该文件夹重新打开VS。 方法二&#xff1a;直接配置环境 打开opencv文…

OpenEuler学习笔记(十五):在OpenEuler上搭建Java运行环境

一、在OpenEuler上搭建Java运行环境 在OpenEuler上搭建Java运行环境可以通过以下几种常见方式&#xff0c;下面分别介绍基于包管理器安装OpenJDK和手动安装Oracle JDK的步骤。 使用包管理器安装OpenJDK OpenJDK是Java开发工具包的开源实现&#xff0c;在OpenEuler上可以方便…

Flutter_学习记录_基本组件的使用记录

1.TextWidge的常用属性 1.1TextAlign: 文本对齐属性 常用的样式有&#xff1a; TextAlign.center 居中TextAlign.left 左对齐TextAlign.right 有对齐 使用案例&#xff1a; body: Center(child: Text(开启 TextWidget 的旅程吧&#xff0c;珠珠, 开启 TextWidget 的旅程吧&a…

Java面试题2025-并发编程进阶(线程池和并发容器类)

线程池 一、什么是线程池 为什么要使用线程池 在开发中&#xff0c;为了提升效率的操作&#xff0c;我们需要将一些业务采用多线程的方式去执行。 比如有一个比较大的任务&#xff0c;可以将任务分成几块&#xff0c;分别交给几个线程去执行&#xff0c;最终做一个汇总就可…

算法基础学习——二分查找(附带Java模板)

有单调性的数列一定可以使用二分&#xff0c;没有单调性的题目也可能可以使用二分&#xff1b; &#xff08;一&#xff09;整数二分 二分的本质&#xff1a; 在某个整数区间内&#xff0c;存在某种性质使得区间内左半边的数都不满足该性质&#xff1b;而右半边的数都满足该性…