【C++】哈希

在这里插入图片描述

🚀write in front🚀
📜所属专栏: C++学习
🛰️博客主页:睿睿的博客主页
🛰️代码仓库:🎉VS2022_C语言仓库
🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我你们将会看到更多的优质内容!!

在这里插入图片描述

文章目录

  • 前言
  • 一.哈希的引入
  • 二.哈希的概念
    • 1.暴力查找:
    • 2.二分查找
    • 3.二叉搜索树:
    • 4.哈希:
  • 三.哈希函数:
    • 1.哈希函数设计原则
    • 2.常见的哈希函数:
  • 四.哈希冲突:
  • 五.解决哈希冲突的两种方案:
    • 闭散列——开放定址法
      • 插入:
      • 删除:
      • 查找:
      • 负载因子:
      • 字符串怎么使用哈希:
      • 扩容操作:
      • 取余的数值:
      • 代码实现:
    • 拉链法(哈希桶):
      • 插入:
      • 删除:
      • 查找:
      • 扩容:
      • 代码实现
  • 六.开散列与闭散列比较
  • 总结

前言

  在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同。

一.哈希的引入

  在C++11里面的unordered_set,unordered_map,unordered_multiset等这四个容器也是叫map,set,所以在使用上基本都是类似的。但是如果大家遍历容器时就会发现,红黑树实现的是有序的,unordered 的是无序的,这就是因为其底层的哈希决定的。

二.哈希的概念

我们对元素进行搜索有几种方式:

1.暴力查找:

直接遍历元素,时间复杂度为O(N)
在这里插入图片描述

2.二分查找

时间复杂度为O(logN)
在这里插入图片描述

但是二分查找有2个弊端:

  • 必须为有序
  • 增删查改不方便O(N)

这两个弊端导致二分查找只是一个理想的查找方式,并不是很现实

3.二叉搜索树:

增删查改的效率都是O(logN),总体性能都很不错
在这里插入图片描述

4.哈希:

  哈希,提供了一种与这些完全不同的存储和查找方式,即将存储的值和存储的位置建立出一个对应的函数关系。在我们平时的生活里面,我们的通讯录联系人就是哈希的一种,每个人的名字对应了一个位置,A的对应在A的位置,B的就对应在B的位置:
在这里插入图片描述
其实我们早就学过了哈希,是在计数排序的时候,我们先开一个空间(为待排序数组的最大值和最小值之差),此时,数组里面的每一个值都对应了一个她自己的位置,这就是一种哈希。

在这里插入图片描述
那么,到底什么是哈希呢?为什么要创造哈希呢?

  顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。

  理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素
     根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素
     对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

那么我们通常怎么构建这种映射关系呢?
举个🌰:
以数组{10000000000000001,8,6,3}为例,假设hashi = key % 6 ,那则有如下对应关系:
在这里插入图片描述
这里如果我们还用计数排序那种方式,就会很浪费空间,所以我们使用这种映射方式就可以减少很多不必要的开销。

三.哈希函数:

上面我们的%的那个操作就是一种哈希函数,叫做除留余数法。

1.哈希函数设计原则

哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须是[0,m-1]之间哈希函数计算出来的地址能均匀分布在整个空间中哈希函数较为简单,能在较短时间内计算出结构。

2.常见的哈希函数:

直接定址法(值的范围集中)

取某个线性函数作为散列的地址:Hash(key) = A*key + B

除留余数法(值的范围分散)

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数, 按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

四.哈希冲突:

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) ==
Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
比如:1%10=1,101%10=1,此时他们的位置就会发生冲突。

五.解决哈希冲突的两种方案:

闭散列——开放定址法

插入:

  通过哈希函数获取待插入元素在哈希表中的位置如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。说白了就是如果你的位置被占了,你就去占下一个人的位置

在这里插入图片描述

删除:

  采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素,对于每一个插入的值都给他们一个状态:

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

查找:

给每一个位置一个状态值也正好解决了查找的问题,查找一个元素的停止条件是查找到空的地方。如果我们直接删掉了,那个地方为空就会影响查找。

负载因子:

哈希表定义了一个载荷因子:α = 填入表中元素个数 / 哈希表的长度

如果负载因子设计的大,那么哈希冲突的概率就越大(空间利用率高)

如果负载因子设计的小,那么哈希冲突的概率就越小(空间利用率低)

对于开放定址法,经过测算,负载因子应该控制在0.7 ~ 0.8,下面代码实现采用0.7。

字符串怎么使用哈希:

  说到这里就不得不说到仿函数了,由于我们传进来的类型不知道是什么类型,比如负数,字符串等。所以我们写一个仿函数来对不同类型的数值映射一个值来。以字符串为例:

对于一个字符串,我们要先对应一个整数,然后才能对应他的存储位置。

扩容操作:

  上面我们说到,当扩容因子到达0.7的时候要扩容,扩容的目的其实就是让查找的时候更容易找到空的位置,但是扩容的时候每个元素映射的位置肯定要变(数值的长度变了,取余的数值变了),所以扩容的时候还要重写遍历一遍。

取余的数值:

  取余的数值一定不是capacity(),这个问题我一开始还想了很久,因为我们在模拟实现的时候我以为new了那么大的空间,肯定就可以用啊,但是实际上vector底层是虽然你开了那么多空间,但是你只能访问和修改size里面的东西,重载的[]会对你访问的位置进行检查,超出size直接报错了。就是我开了那么大的空间,但是不给你用。所以这里是模上size()

代码实现:

#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>
struct DefaultHashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//这里巧用了模板的实例化相关的知识。
template<>
struct DefaultHashFunc<string>
{size_t operator()(const string& str){// BKDR//这里的操作会使不同字符串的数值差距变大size_t hash = 0;for (auto ch : str){hash *= 131;hash += ch;}return hash;}
};template<class K,class V, class HashFunc = DefaultHashFunc<K>>
//这里就使用了仿函数来调用
class HashTable
{
public:HashTable(){_table.resize(10);}bool Insert(const pair<K, V>& kv){// 扩容//if ((double)_n / (double)_table.size() >= 0.7)if (_n*10 / _table.size() >= 7){size_t newSize = _table.size() * 2;// 遍历旧表,重新映射到新表HashTable<K, V, HashFunc> newHT;newHT._table.resize(newSize);// 遍历旧表的数据插入到新表即可for (size_t i = 0; i < _table.size(); i++){if (_table[i]._state == EXIST){newHT.Insert(_table[i]._kv);}}_table.swap(newHT._table);}// 线性探测HashFunc hf;size_t hashi = hf(kv.first) % _table.size();while (_table[hashi]._state == EXIST){++hashi;hashi %= _table.size();}_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_n;return true;}HashData<const K, V>* Find(const K& key){// 线性探测HashFunc hf;size_t hashi = hf(key) % _table.size();while (_table[hashi]._state != EMPTY){//只有值相同且状态为存在的时候才算找到if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key){return (HashData<const K, V>*)&_table[hashi];}++hashi;hashi %= _table.size();}return nullptr;}// 按需编译bool Erase(const K& key){HashData<const K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_n;return true;}--_n;return false;}private:vector<HashData<K, V>> _table;size_t _n = 0; // 存储有效数据的个数,便于计算负载因子
};

线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”。关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

拉链法(哈希桶):

开放定址法的缺陷就是冲突会相互影响。而哈希桶的做法是,设置一个指针数组,如果发现冲突,则内部消化
在这里插入图片描述
这里其实还是很好理解的,就是在vector里面存单链表。也就是插入了一堆桶。
在这里插入图片描述

插入:

插入操作就是通过数值找到位置,然后头插到那个位置的单链表中

删除:

链表的删除

查找:

链表的查找

扩容:

如果我们不扩容,一直插入,某些桶就会越来越长,效率就下降了。因此这里的负载因子可以适当放大一点,一般负载因子控制在1,平均下来每个桶都有数据。

对于扩容操作,因为这里我们存的是链表,是需要释放空间的,所以在扩容的时候可以将结点移到另外一个数组里面,这样就就不用创造新结点,也不用释放空间了,对那些结点继续使用。

代码实现

template<class K, class V, class HashFunc = DefaultHashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_table.resize(10, nullptr);}~HashTable(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}bool Insert(const pair<K, V>& kv){if(Find(kv.first)){return false;}HashFunc hf;// 负载因子到1就扩容if (_n == _table.size()){// 16:03继续size_t newSize = _table.size()*2;vector<Node*> newTable;newTable.resize(newSize, nullptr);// 遍历旧表,顺手牵羊,把节点牵下来挂到新表for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;// 头插到新表size_t hashi = hf(cur->_kv.first) % newSize;cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTable);}size_t hashi = hf(kv.first) % _table.size();// 头插Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;++_n;return true;}Node* Find(const K& key){HashFunc hf;size_t hashi = hf(key) % _table.size();Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& key){HashFunc hf;size_t hashi = hf(key) % _table.size();Node* prev = nullptr;Node* cur = _table[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}--_n;delete cur;	return true;}prev = cur;cur = cur->_next;}return false;}void Print(){for (size_t i = 0; i < _table.size(); i++){printf("[%d]->", i);Node* cur = _table[i];while (cur){cout << cur->_kv.first <<":"<< cur->_kv.second<< "->";cur = cur->_next;}printf("NULL\n");}cout << endl;}private:vector<Node*> _table; // 指针数组size_t _n = 0; // 存储了多少个有效数据};

六.开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。

但是事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间

总结

  因为哈希这里还是比较好理解的,只要自己写一写就很好实现,所以只分析了几点易错点和重点。下次我们就开始封装了!!!
  更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

在这里插入图片描述

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

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

相关文章

rust cfg的使用

前提是一个crate倒入另一个crate。 先看结构 test_lib目录结构 这与另一个crate处于同一个目录,所以另一crate倒入的时候在Cargo.toml中使用如下语句。 test_lib = {path = "../test_lib" }先在test_lib/src/abc/abc.rs中添加没有cfg的两个函数做测试。 pub fn…

ViewModifier/视图修饰符, ButtonStyle/按钮样式 的使用

1. ViewModifier 视图修饰符 1.1 创建默认按钮视图修饰符 ViewModifierBootcamp.swift import SwiftUI/// 默认按钮修饰符 struct DefaultButtonViewModifier: ViewModifier{let bcakgroundColor: Colorfunc body(content: Content) -> some View {content.foregroundColor…

C/C++之自定义类型(结构体,位段,联合体,枚举)详解

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言程序设计————KTV C语言小游戏 C语言进阶 C语言刷题 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力&#xff0c;一起奔赴大厂。 目录 个人主页&#xff1a;点我进入主页 …

MySQL 安装+启动+报错的解决方案

目录 一、安装准备 1.1 下载 1.2 版本说明 二、安装步骤 2.1 解压缩 2.2 配置环境变量 2.3 配置文件 2.4 安装 2.5 启动/停止服务 三、使用说明 3.1 用户名密码登录 3.1 设置用户名密码 四、卸载步骤 4.1 卸载服务 五、安装问题 六、启动问题 6.1 提示【服务无…

web基础和http协议

1.DNS解析的三种方式 DNS解析&#xff1a; 网站都是域名&#xff1a;dns解析的作用是把域名解析成ip地址 迭代&#xff1a;从跟域名到二级域 返回用户的过程&#xff1a;递归---运营商--本地hosts---用户 三种方式&#xff1a; /etc/hosts 本地解析&#xff0c;速度最快&…

岩土工程安全监测无线振弦采集仪在无线组网的关键要点

岩土工程安全监测无线振弦采集仪在无线组网的关键要点 岩土工程是一种奇特而又极其重要的工程。它涉及到土地、岩石、气候等等因素&#xff0c;需要重视安全因素。而无线振弦采集仪作为一种常用的监测设备&#xff0c;可以采集岩土工程中的振动数据&#xff0c;从而确保工程的…

智慧安防AI视频智能分析云平台EasyCVR加密机授权小tips

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、…

Transformer模型 | Python实现基于LSTM与Transfomer的股票预测模型(pytorch)

文章目录 效果一览文章概述LSTM模型原理时间序列模型从RNN到LSTMLSTM预测股票模型实现结语程序设计参考资料效果一览 文章概述 基于LSTM与Transfomer的股票预测模型 股票行情是引导交易市场变化的一大重要因素,若能够掌握股票行情的走势,则对于个人和企业的投资都有巨大的帮…

【AntDesign】多环境配置和启动

环境分类&#xff0c;可以分为 本地环境、测试环境、生产环境等&#xff0c;通过对不同环境配置内容&#xff0c;来实现对不同环境做不同的事情。 AntDesign 项目&#xff0c;通过 config.xxx.ts 添加不同的后缀来区分配置文件&#xff0c;启动时候通过后缀启动即可。 config…

【RabbitMQ 实战】10 消息持久化和存储原理

一、持久化 1.1 持久化对象 rabbitmq的持久化分为三个部分&#xff1a; 交换器的持久化。队列的持久化。消息的持久化。 1.1.1 交换器持久化 交换器的持久化是通过在声明交换器时&#xff0c; 指定Durability参数为durable实现的。若交换器不设置持久化&#xff0c;在rabb…

c语言:通讯录管理系统(文件版本)

前言&#xff1a;在大多数高校内&#xff0c;都是通过设计一个通讯录管理系统来作为c语言课程设计&#xff0c;通过一个具体的系统设计将我们学习过的结构体和函数等知识糅合起来&#xff0c;可以很好的锻炼学生的编程思维&#xff0c;本文旨在为通讯录管理系统的设计提供思路和…

将nginx注册为Windows系统服务

文章目录 1、使用nssm小工具2、使用winsw小工具2.1、下载2.2、用法2.3、重命名2.4、创建配置文件2.4.1、xml文件2.4.2、config文件&#xff08;该文件可省略&#xff09; 2.5、最终文件2.6、安装与卸载 1、使用nssm小工具 该方法最简单 首先&#xff0c;下载nssm小工具&#…

HTML5+CSSDAY4综合案例一--热词

样式展示图&#xff1a; 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>热词…

docker 基本操作

一、docker 概述 Docker是一个开源的应用容器引擎&#xff0c;基于go语言开发并遵循了apache2.0协议开源。 Docker是在Linux容器里运行应用的开源工具&#xff0c;是一种轻量级的“虚拟机”。 Docker 的容器技术可以在一台主机上轻松为任何应用创建一个轻量级的、可移植的、自…

HomeView/主页 的实现

1. 创建数据模型 1.1 创建货币模型 CoinModel.swift import Foundation// GoinGecko API info /*URL:https://api.coingecko.com/api/v3/coins/markets?vs_currencyusd&ordermarket_cap_desc&per_page250&page1&sparklinetrue&price_change_percentage24…

机器人制作开源方案 | 齿轮传动轴偏心轮摇杆简易四足

1. 功能描述 齿轮传动轴偏心轮摇杆简易四足机器人是一种基于齿轮传动和偏心轮摇杆原理的简易四足机器人。它的设计原理通常如下&#xff1a; ① 齿轮传动&#xff1a;通过不同大小的齿轮传动&#xff0c;实现机器人四条腿的运动。通常采用轮式齿轮传动或者行星齿轮传动&#xf…

Blender:使用立方体制作动漫头像

好久没水文章 排名都掉到1w外了 ~_~ 学习一下blender&#xff0c;看能不能学习一点曲面变形的思路 一些快捷键 ctrl 空格&#xff1a;区域最大化&#xff0c;就是全屏 ctrl alt 空格&#xff1a;也是区域最大化 shift b&#xff1a;框选区域然后最大化显示该范围 shift 空…

3.springcloudalibaba gateway项目搭建

文章目录 前言一、搭建gateway项目1.1 pom配置1.2 新增配置如下 二、新增server服务2.1 pom配置2.2新增测试接口如下 三、测试验证3.1 分别启动两个服务&#xff0c;查看nacos是否注册成功3.2 测试 总结 前言 前面已经完成了springcloudalibaba项目搭建&#xff0c;接下来搭建…

AI如何帮助Salesforce从业者找工作?

在当今竞争激烈的就业市场中&#xff0c;找到满意的工作是一项艰巨的任务。成千上万的候选人竞争一个岗位&#xff0c;你需要利用一切优势从求职大军中脱颖而出。 这就是AI的用武之地&#xff0c;特别是像ChatGPT这样的人工智能工具&#xff0c;可以成为你的秘密武器。本篇文章…

【Windows】RPC调用过程实例详解

概述&#xff1a;windows 创建 RPC调用过程实例详解 参考文章&#xff1a;Remote procedure call (RPC)&#xff08;远程过程调用 (RPC)&#xff09; - Win32 apps | Microsoft Learn 文章目录 0x01、生成 UUID 和模版(IDL)文件0x02、添加 acf 文件0x03、编译 idl 文件0x04、客…