C++【初识哈希】

✨个人主页: 北 海
🎉所属专栏: C++修行之路
🎃操作环境: Visual Studio 2019 版本 16.11.17

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、哈希思想
    • 2、哈希函数
      • 2.1、哈希函数的设计原则
      • 2.2、常见的哈希函数
    • 3、哈希冲突
      • 3.1、冲突的原因
      • 3.2、解决方法
    • 4、unordered_set / unordered_map
      • 4.1、使用
      • 4.2、与 set/map 的区别
      • 4.3、性能对比
  • 🌆总结


🌇前言

哈希(Hash)是一个广泛的概念,其中包括哈希表、哈希冲突、哈希函数等,核心为 元素(键值)存储位置(哈希值) 之间的映射关系,哈希值 可以通过各种哈希函数进行计算,需要尽量确保 “唯一性”,避免冲突,除此之外,哈希函数还可用于 区块链 中,计算 区块头(Head)中的信息,本文将带你认识哈希,学习其中的各种知识

图示


🏙️正文

1、哈希思想

哈希(Hash) 是一种 映射 思想,规定存在一个唯一的 哈希值键值 之前建立 映射 关系,由这种思想而构成的数据结构称为 哈希表(散列表)

图示

图示

哈希表中的数据查找时间可以做到 O(1)

这是非常高效的,比 AVL树 还要快

哈希表插入数据查找数据 的步骤如下:

  • 插入数据:根据当前待插入的元素的键值,计算出哈希值,并存入相应的位置中
  • 查找数据:根据待查找元素的键值,计算出哈希值,判断对应的位置中存储的值是否与 键值 相等

比如在 数组 中利用哈希思想,构建哈希表,存储数据:549273855

假设此时 数组 的大小 capacity8哈希函数 计算哈希值:HashI = key % capacity

数据存储如下:

图示

显然,这个哈希表并没有把所有位置都填满,数据分布无序且分散

因此,哈希表 又称为 散列表


2、哈希函数

元素对应的存储位置(哈希值)需要通过 哈希函数 进行计算,哈希函数 并非固定不变,可以根据需求自行设计

2.1、哈希函数的设计原则

在进行 映射 时,要尽量确保 唯一性,尽量让每一个元素都有自己的 映射 位置,这样在查找时,才能快速定位 元素

哈希函数 的设计原则如下:

  1. 哈希函数的定义域必须包括需要存储的全部键值,且如果哈希表允许有 m 个地址,其值域为 [0, m-1]
  2. 哈希函数计算出来的哈希值能均匀分布在整个哈希表中
  3. 哈希函数应该尽可能简单、实用

哈希函数 的设计没必要动用太多数学高阶知识,要确保 实用性

2.2、常见的哈希函数

哈希函数 的发展已经有很多年历史了,在前辈的实践之下,留下了这些常见的 哈希函数

1、直接定址法(常用)

函数原型:HashI = A * key + B

  • 优点:简单、均匀
  • 缺点:需要提前知道键值的分布情况
  • 适用场景:范围比较集中,每个数据分配一个唯一位置

2、除留余数法(常用)

假设 哈希表 的大小为 m

函数原型:HashI = key % p (p < m)

  • 优点:简单易用,性能均衡
  • 缺点:容易出现哈希冲突,需要借助特定方法解决
  • 适用场景:范围不集中,分布分散的数据

3、平方取中法(了解)

函数原型:HashI = mid(key * key)

  • 适用场景:不知道键值的分布,而位数又不是很大的情况

假设键值为 1234,对其进行平方后得到 1522756,取其中间的三位数 227 作为 哈希值

4、折叠法(了解)

折叠法是将键值从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按 哈希表 表长,取后几位作为散列地址

  • 适用场景:事先不需要知道键值的分布,且键值位数比较多

假设键值为 85673113,分为三部分 85673113,求和:1600,根据表长(假设为 100),哈希值 就是 600

5、随机数法(了解)

选择一个随机函数,取键值的随机函数值为它的 哈希值

函数原型:HashI = rand(key)
其中 rand 为随机数函数

  • 适用场景:键值长度不等时

哈希函数 还有很多很多种,最终目的都是为了计算出重复度低的 哈希值

最常用的是 直接定址法除留余数法


3、哈希冲突

哈希冲突(哈希碰撞) 是面试中的常客,可以通过一个 哈希冲突 间接引出 哈希 中的其他知识点

3.1、冲突的原因

哈希值键值 通过 哈希函数 计算得出的 位置标识符,难以避免重复问题

比如在上面的 哈希表 中,存入元素 20哈希值 HashI = 20 % 8 = 4,此时 4 位置中并没有元素,可以直接存入

图示

但是如果继续插入元素 36哈希值 HashI = 36 % 8 = 4,此时 4 位置处已经有元素了,无法继续存入,此时就发生了 哈希冲突

图示

不同的 哈希函数 引发 哈希冲突 的概率不同,但最终都会面临 哈希冲突 这个问题,因此需要解决一些方法,解决哈希冲突

3.2、解决方法

主要的解决方法有两种:闭散列开散列

闭散列(开放定址法)

规定:当哈希表中存储的数据量 与 哈希表的容量 比值(负载因子)过大时,扩大哈希表的容量,并重新进行映射

因为有 负载因子 的存在,所以 哈希表是一定有剩余空间的

当发生 哈希冲突 时,从冲突位置向后探测,直到找到可用位置

图示

像这种线性探测(暴力探测)可以解决 哈希冲突 问题,但会带来新的问题:踩踏

踩踏:元素的存储位置被别人占用了,于是也只能被迫线性探测,引发连锁反应,插入、查找都会越来越慢

哈希冲突 越多,效率 越低

优化方案:二次探测,每次向后探测 i ^ 2 步,尽量减少踩踏

尽管如此,闭散列 的实际效果 不尽人意,因为其本质上就是一个 零和游戏,实际中还是 开散列 用的更多一些

开散列(链地址法、开链法、哈希桶)

所谓 开散列 就在原 存储位置 处带上一个 单链表,如果发生 哈希冲突,就将 冲突的值依次挂载即可

因此也叫做 链地址法、开链法、哈希桶

开散列 中不需要 负载因子,如果每个位置都被存满了,直接扩容就好了,当然扩容后也需要重新建立映射关系

图示

开散列 中进行查找时,需要先根据 哈希值 找到对应位置,并在 单链表 中进行遍历

一般情况下,单链表的长度不会太长的,因为扩容后,整体长度会降低

如果 单链表 真的过长了(几十个节点),我们还可以将其转为 红黑树,此时效率依旧非常高

图示
图示
图片出自:2021dragon

值得一提的是 哈希表(开散列法)最快时间复杂度为 O(N),平均是 O(1)

哈希表(开散列法)快排 一样很特殊,时间复杂度不看最坏的,看 平均时间复杂度,因为 最快的情况几乎不可能出现

以上就是解决 哈希冲突 的两种方法,后面在模拟实现 哈希表 时会详细讲解


4、unordered_set / unordered_map

哈希表 最xx的地方在于 查找速度非常快

快过红黑树!

因此在 C++11 标准中,利用 哈希表 作为底层结构,重写了 set / map,就是 unordered_set / unordered_map

图示
图片出自:C++新特性之三:标准库中的新增容器

4.1、使用

哈希表 版的 unordered_set / unordered_map红黑树 版的 set / map 在功能上 没有差别

可以直接无缝衔接

关于 setmap 的使用 详见:C++【set 和 map 学习及使用】

unordered_set 的使用

#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;int main()
{vector<int> arr = { 7,3,6,9,3,1,6,2 };unordered_set<int> s1(arr.begin(), arr.end());//迭代器遍历cout << "迭代器遍历结果: ";unordered_set<int>::iterator it = s1.begin();while (it != s1.end()){cout << *it << " ";++it;}cout << endl;//判空、求大小cout << "===================" << endl;cout << "empty(): " << s1.empty() << endl;cout << "size(): " << s1.size() << endl;cout << "max_size(): " << s1.max_size() << endl;//插入元素cout << "===================" << endl;cout << "insert(5): ";s1.insert(5);for (auto e : s1) cout << e << " ";cout << endl;//删除元素cout << "===================" << endl;cout << "erase(6): ";s1.erase(6);for (auto e : s1) cout << e << " ";cout << endl;//交换、查找、清理cout << "===================" << endl;unordered_set<int> s2(arr.begin() + 5, arr.end());s1.swap(s2);cout << "s1: ";for (auto e : s1) cout << e << " ";cout << endl;cout << "s2: ";for (auto e : s2) cout << e << " ";cout << endl;cout << "s1.find(9): ";cout << (s1.find(9) != s1.end()) << endl;cout << "s2.clear(): " << endl;s2.clear();cout << "s1: ";for (auto e : s1) cout << e << " ";cout << endl;cout << "s2: ";for (auto e : s2) cout << e << " ";cout << endl;return 0;
}

图示

unordered_map 的使用

#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;int main()
{vector<pair<string, int>> arr{ make_pair("z", 122), make_pair("a", 97),  make_pair("K", 75), make_pair("h", 104), make_pair("B", 66) };unordered_map<string, int> m1(arr.begin(), arr.end());//迭代器遍历cout << "迭代器遍历结果: ";unordered_map<string, int>::iterator it = m1.begin();while (it != m1.end()){cout << "<" << it->first << ":" << it->second << "> ";++it;}cout << endl;//判空、求大小、解引用cout << "===================" << endl;cout << "empty(): " << m1.empty() << endl;cout << "size(): " << m1.size() << endl;cout << "max_size(): " << m1.max_size() << endl;cout << "m1[""a""]: " << m1["a"] << endl;//插入元素cout << "===================" << endl;cout << "insert(""a"", 5): ";m1.insert(make_pair("a", 5));for (auto e : m1) cout << "<" << e.first << ":" << e.second << "> ";cout << endl;//删除元素cout << "===================" << endl;cout << "erase(""a""): ";m1.erase("a");for (auto e : m1) cout << "<" << e.first << ":" << e.second << "> ";cout << endl;//交换、查找、清理cout << "===================" << endl;unordered_map<string, int> m2(arr.begin() + 2, arr.end());m1.swap(m2);cout << "m1.swap(m2)" << endl;cout << "m1: ";for (auto e : m1) cout << "<" << e.first << ":" << e.second << "> ";cout << endl;cout << "m2: ";for (auto e : m2) cout << "<" << e.first << ":" << e.second << "> ";cout << endl;cout << "m1.find(""B""): ";cout << (m1.find("B") != m1.end()) << endl;cout << "m2.clear()" << endl;m2.clear();cout << "m1: ";for (auto e : m1) cout << "<" << e.first << ":" << e.second << "> ";cout << endl;cout << "m2: " << endl;for (auto e : m2) cout << "<" << e.first << ":" << e.second << "> ";cout << endl;return 0;
}

图示

照搬 set / map 使用的代码,可以无缝衔接,所以说正常使用就好了,无非就是名字长一些

C++ 为了确保向前兼容性,无法修改原来的名字,因此只能加上 unordered 以示区分
相比之下,Java 中两个不同版本的 set / map 就非常容易区分

  • 红黑树版:TreeSet / TreeMap
  • 哈希表版:HashSet / HashMap

注意: unordered_set / unordered_map 默认不允许出现键值冗余,如果相要存储重复的数据,可以使用 unordered_multiset / unordered_multimap

4.2、与 set/map 的区别

哈希表 版 与 红黑树 版的主要区别有两个

  • 迭代器:哈希表版 是单向迭代器,红黑树版 是双向迭代器
  • 遍历结果:哈希表版 无序,红黑树 有序

因为 unordered_set单向迭代器,自然无法适配 反向迭代器

结果

两种不同底层数据结构的遍历结果:

#include <iostream>
#include <vector>
#include <set>
#include <unordered_set>using namespace std;int main()
{vector<int> v = { 2,3,45,2,345,231,21,543,121,34 };set<int> TreeSet(v.begin(), v.end());unordered_set<int> HashSet(v.begin(), v.end());cout << "TreeSet: ";for (auto e : TreeSet)cout << e << " ";cout << endl << endl;cout << "HashSet: ";for (auto e : HashSet)cout << e << " ";cout << endl;return 0;
}

图示

显然,哈希表 实现的 unordered_set / unordered_map 遍历结果为 无序

哈希表 究竟比 红黑树 强多少?

4.3、性能对比

下面是性能测试代码,包含 大量重复、部分重复、完全有序 三组测试用例,分别从 插入、查找、删除 三个维度进行对比

注:测试性能用的是 Release 版,这里的基础数据量为 100 w

#include <iostream>
#include <vector>
#include <set>
#include <unordered_set>using namespace std;int main()
{const size_t N = 1000000;unordered_set<int> us;set<int> s;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){//v.push_back(rand());	//大量重复//v.push_back(rand()+i);	//部分重复//v.push_back(i);	//完全有序}size_t begin1 = clock();for (auto e : v){s.insert(e);}size_t end1 = clock();cout << "set insert:" << end1 - begin1 << endl;size_t begin2 = clock();for (auto e : v){us.insert(e);}size_t end2 = clock();cout << "unordered_set insert:" << end2 - begin2 << endl;size_t begin3 = clock();for (auto e : v){s.find(e);}size_t end3 = clock();cout << "set find:" << end3 - begin3 << endl;size_t begin4 = clock();for (auto e : v){us.find(e);}size_t end4 = clock();cout << "unordered_set find:" << end4 - begin4 << endl << endl;cout << s.size() << endl;cout << us.size() << endl << endl;;size_t begin5 = clock();for (auto e : v){s.erase(e);}size_t end5 = clock();cout << "set erase:" << end5 - begin5 << endl;size_t begin6 = clock();for (auto e : v){us.erase(e);}size_t end6 = clock();cout << "unordered_set erase:" << end6 - begin6 << endl << endl;return 0;
}

插入大量重复数据

图示

插入数据 大量重复 ---- 结果:

  • 插入:哈希 比 红黑 快 88%
  • 查找:哈希 比 红黑 快 100%
  • 删除:哈希 比 红黑 快 37%

插入部分重复数据

图示

插入数据 部分重复 ---- 结果:

  • 插入:哈希 与 红黑 差不多
  • 查找:哈希 比 红黑 快 100%
  • 删除:哈希 比 红黑 快 41%

插入大量重复数据

图示

插入数据 完全有序 ---- 结果:

  • 插入:哈希 比 红黑 慢 52%
  • 查找:哈希 比 红黑 快 100%
  • 删除:哈希 比 红黑 慢 58%

总的来说,在数据 随机 的情况下,哈希各方面都比红黑强,在数据 有序 的情况下,红黑更胜一筹

单就 查找 这一个方面来说:哈希 一骑绝尘,远远的将红黑甩在了身后


🌆总结

以上就是本次关于 C++【初识哈希】的全部内容了,在本文中,我们主要学习了哈希的相关知识,包括哈希思想、哈希函数、哈希冲突及其解决方法,最后还学习了 C++11 中基于哈希表的新容器,见识了哈希表查找的快,不是一般的快。在下一篇文章中,我们将会对哈希表进行模拟实现,同时也会用一张哈希表同时封装实现 unordered_setunordered_map


星辰大海

相关文章推荐

C++ 进阶知识

C++【一棵红黑树封装 set 和 map】

C++【红黑树】

C++【AVL树】

C++【set 和 map 学习及使用】

C++【二叉搜索树】

C++【多态】

C++【继承】

STL 之 泛型思想

C++【模板进阶】

C++【模板初阶】

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

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

相关文章

云原生——什么是云原生?

❄️作者介绍&#xff1a;奇妙的大歪❄️ &#x1f380;个人名言&#xff1a;但行前路&#xff0c;不负韶华&#xff01;&#x1f380; &#x1f43d;个人简介&#xff1a;云计算网络运维专业人员&#x1f43d; 前言 伴随云计算的滚滚浪潮&#xff0c;云原生(CloudNative…

kubernetes核心概念 controller

kubernetes核心概念 Controller 一、pod控制器controller 1.1 Controller作用及分类 controller用于控制pod 参考: https://kubernetes.io/zh/docs/concepts/workloads/controllers/ 控制器主要分为: Deployments 部署无状态应用&#xff0c;控制pod升级,回退ReplicaSet 副…

ROS自带OpenCV和本地OpenCV版本冲突问题解决

1、报错信息 首先catkin_make编译功能包没有任何问题&#xff0c;100%生成目标文件&#xff0c;但是报了警告&#xff1a;库文件libmyslam.so需要的是libopencv_core.so.3.4&#xff0c;可能会与libopencv_core.so.3.2冲突。根据工程经验&#xff0c;警告不用管&#xff0c;直…

从浏览器输入url到页面加载(六)前端必须了解的路由器和光纤小知识

前言 上一章我们说到了数据包在网线中的故事&#xff0c;说到了双绞线&#xff0c;还说到了麻花。这一章继续沿着这条线路往下走&#xff0c;说一些和cdn以及路由器相关&#xff0c;运营商以及光纤相关的小知识&#xff0c;前端同学应该了解一下的 目录 前言 1. CDN和路由器…

Vue中watch与computed区别

<body><div id"root">姓&#xff1a;<input type"text" v-model"firstName"><br/><br/>名&#xff1a;<input type"text" v-model"lastName"><br/><br/>全名&#xff1a;&…

Spring:Bean

Bean 概述配置方式自动装配继承与依赖作用域外部属性文件的使用 概述 Spring 容器负责管理依赖注入&#xff0c;它将被管理的对象都称为 bean 。我们通过 xml 文件配置方式进行对 bean 的声明和管理。 写法如下&#xff1a; <beans><bean id"bean的唯一标识符…

EasyExcel实现execl导入导出

引言 在实际开发中&#xff0c;处理 Excel 文件是一个常见的需求。EasyExcel 是一个基于 Java 的开源库&#xff0c;提供了简单易用的 API&#xff0c;可以方便地读取和写入 Excel 文件。本文将介绍如何使用 EasyExcel 实现 Excel 导入功能&#xff0c;以及一些相关的技巧和注…

qt for android 开发之tcp通讯

简介 通过TCP使PC和android手机相互通讯。 准备 使用QT的网络模块 QT core gui networkgreaterThan(QT_MAJOR_VERSION, 4): QT widgets.h 定义TCP接口 #include <QTcpSocket>class MainWindow : public QMainWindow {Q_OBJECTpublic:explicit MainWindow(Q…

计算机中CPU、内存、缓存的关系

CPU&#xff08;Central Processing Unit&#xff0c;中央处理器&#xff09; 内存&#xff08;Random Access Memory&#xff0c;随机存取存储器&#xff09; 缓存&#xff08;Cache&#xff09; CPU、内存和缓存之间有着密切的关系&#xff0c;它们共同构成了计算机系统的核…

网络安全进阶学习第五课——文件上传漏洞

文章目录 一、常见文件上传点二、任意文件上传漏洞三、任意文件上传危害四、webshell五、上传木马所需条件六、木马上传流程七、上传绕过1、绕过JS验证1&#xff09;Burpsuite剔除响应JS。2&#xff09;浏览器审计工具剔除JS 2、绕过MIME-Type验证1&#xff09;利用抓包工具&am…

Pytroch本地安装方法

1 查看电脑安装的cuda版本 nvidia-smi这里的红圈标注的是cuda 最高版本&#xff0c;虚拟环境中安装&#xff0c;只要不超过12.0就可以 第二步 去官网看torch, torchvision等版本的匹配关系 https://pytorch.org/get-started/previous-versions/ 比如这里&#xff1a; &…

uniapp项目 封装一个饼图组件 并且修改显示项的排列方式

需求如下: 真实数据渲染后的完成效果如下: 记录一下代码: <template><view><view style"height: 600rpx;"><l-echart ref"chart" finished"init"></l-echart></view></view> </template><…