[C++][数据结构][哈希表]详细讲解

目录

  • 1.哈希概念
  • 2.哈希冲突
  • 3.哈希函数
  • 4.哈希冲突解决
  • 5.闭散列
    • 1.何时扩容?如何扩容?
    • 2.线性探测
    • 3.二次探测
  • 6.开散列(哈希桶)
    • 1.概念
    • 2.开散列增容
    • 3.开散列思考
      • 只能存储key为整形的元素,其他类型怎么解决?
      • 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
    • 4.开散列与闭散列比较


1.哈希概念

  • 顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O(logN),搜索的效率取决于搜索过程中元素的比较次数
  • 理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
  • 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素
  • 当向该结构中:
    • 插入元素
      • 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
    • 搜索元素
      • 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
    • 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

2.哈希冲突

  • 对于两个数据元素的关键字k_i和k_j(i != j),有k_i != k_j,但有:Hash(k_i) == Hash(k_j)
    • 即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为 哈希冲突 或 哈希碰撞
  • 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

3.哈希函数

  • 引起哈希冲突的一个原因可能是:哈希函数设计不够合理
  • 哈希函数设计原则:
    • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
    • 哈希函数计算出来的地址能均匀分布在整个空间中
    • 哈希函数应该比较简单
  • 常见哈希函数:
    • 直接定址法 – (常用) --> 不存在哈希冲突
      • 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
      • 优点:简单、均匀
      • 缺点:需要事先知道关键字的分布情况
      • 使用场景:适合查找比较小且连续的情况
    • 除留余数法(常用) --> 存在哈希冲突,重点解决哈希冲突
      • 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数
      • 按照哈希函数:Hash(key) = key% p (p<=m),将关键码转换成哈希地址
    • 平方取中法 – (了解)
      • 假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
      • 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
      • 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
    • 折叠法 – (了解)
      • 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址
      • 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
    • 随机数法 – (了解)
      • 选择一个随机函数,取关键字的随机函数值为它的哈希地址
      • 即H(key) = random(key),其中 random为随机数函数
    • 数学分析法 – (了解) – 懒得介绍
  • 注意哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

4.哈希冲突解决

  • 解决哈希冲突两种常见的方法是:闭散列开散列

5.闭散列

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

1.何时扩容?如何扩容?

  • 散列表的载荷因子定义为:α = 填入表中的元素个数 / 散列表的长度
    • α越大,表中元素越多,产生冲突概率越大
    • α越小,表明元素越少,产生冲突概率越小
    • 一般不要超过0.7~0.8
  • 什么时候扩容? --> 负载因子到一个基准值就扩容
    • 基准值越大,冲突越多,效率越低,空间利用率越高
    • 基准值越小,冲突越少,效率越高,空间利用率越低

2.线性探测

  • 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

    • 插入
      • 通过哈希函数获取待插入元素在哈希表中的位置

      • 如果该位置中没有元素则直接插入新元素

      • 如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

        请添加图片描述

  • 删除

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

3.二次探测

  • 线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找
  • 因此二次探测为了避免该问题,找下一个空位置的方法为:
    • H_i = (H_0 + i^2 ) % m 或者 H_i = (H_0 - i^2 ) % m (i = 1,2,3**…)**
    • H_0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小
  • 研究表明:
    • 表的长度为质数表载荷因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次
    • 因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容
  • 因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
enum State
{EMPTY,EXIST,DELETE
};template <class K, class V>struct HashData{pair<K, V> _kv;State _state = EMPTY;};template <class K>struct HashFunc{size_t operator()(const K &key){return (size_t)key;}};template <> // 特化
struct HashFunc<string>
{size_t operator()(const string &key){size_t val = 0;for (auto &ch : key){val *= 131; // BKDRval += ch;}return val;}
};template <class K, class V, class Hash = HashFunc<K>> // Hash允许用户自己提供HashFuncclass HashTable{public:bool Insert(const pair<K, V> &kv){if (Find(kv.first)) // 元素已存在则不插入{return false;}// 负载因子到了就扩容if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 将载荷因子α定为 0.7{size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;HashTable<K, V> newHT; // 构建一个新的HashTable对象,来进行映射逻辑newHT._tables.resize(newsize);// 旧表的数据映射到新表for (auto &e : _tables){if (e._state == EXIST) // 状态为存在则进行映射{newHT.Insert(e._kv);}}_tables.swap(newHT._tables);}// 线性探测Hash hash;size_t hashi = hash(kv.first) % _tables.size(); // 哈希地址计算while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size(); // 防止已经遍历完vector,若遍历完,则回头}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_size;// 二次探测// Hash hash;// size_t start = hash(kv.first) % _tables.size();// size_t i = 0;// size_t hashi = start;// while (_tables[hashi]._state == EXIST)//{//  ++i;//  hashi = start + i * i; // 二次探测的哈希地址跳跃//  hashi %= _tables.size(); // 防止已经遍历完vector,若遍历完,则回头// }//_tables[hashi]._kv = kv;//_tables[hashi]._state = EXIST;//++_size;return true;}HashData<K, V> *Find(const K &key){if (_tables.size() == 0){return nullptr;}Hash hash;size_t hashi = hash(key) % _tables.size();while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key){return &_tables[hashi];}++hashi;hashi %= _tables.size();}return nullptr;}bool Erase(const K &key){HashData<K, V> *ret = Find(key);if (ret){ret->_state = DELETE; // 标记删除即可--_size;return true;}else{return false;}}private:vector<HashData<K, V>> _tables;size_t _size = 0; // 存储有效数据的个数};

6.开散列(哈希桶)

1.概念

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

  • 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

2.开散列增容

  • 桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?
  • 开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容

3.开散列思考

  • 只能存储key为整形的元素,其他类型怎么解决?

  • 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为整形的方法

    • 利用仿函数
  • 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?

4.开散列与闭散列比较

  • 应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销
  • 事实上:
    • 由于开放定址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7
    • 而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间
template <class K, class V>struct HashNode{pair<K, V> _kv;HashNode<K, V> *_next;HashNode(const pair<K, V> &kv): _kv(kv), _next(nullptr){}};template <class K>struct HashFunc{size_t operator()(const K &key){return (size_t)key;}};template <> // 特化
struct HashFunc<string>
{size_t operator()(const string &key){size_t val = 0;for (auto &ch : key){val *= 131; // BKDRval += ch;}return val;}
};template <class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:~HashTable(){for (size_t i = 0; i < _tables.size(); ++i){Node *cur = _tables[i];while (cur){Node *next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}// vector本身不需要手动析构,析构函数会去自动调用所有成员变量的析构函数}inline size_t __stl_next_prime(size_t n) // STL中素数空间优化{static const size_t __stl_num_primes = 28;static const size_t __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};for (size_t i = 0; i < __stl_num_primes; ++i){if (__stl_prime_list[i] > n){return __stl_prime_list[i];}}return -1;}bool Insert(const pair<K, V> &kv){// 去重if (Find(kv.first)){return false;}Hash hash;// 负载因子到1就扩容if (_size == _tables.size()){// size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node *> newTables;// newTables.resize(newsize, nullptr);newTables.resize(__stl_next_prime(_tables.size()), nullptr);// 旧表中节点移动映射到新表for (size_t i = 0; i < _tables.size(); ++i){Node *cur = _tables[i];while (cur){Node *next = cur->_next;size_t hashi = hash(cur->_kv.first) % newTables.size();cur->_next = newTables[hashi]; // 头插逻辑newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}// 头插size_t hashi = hash(kv.first) % _tables.size();Node *newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_size;return true;}Node *Find(const K &key){if (_tables.size() == 0){return nullptr;}Hash hash;size_t hashi = hash(key) % _tables.size();Node *cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K &key){if (_tables.size() == 0){return true;}Hash hash;size_t hashi = hash(key) % _tables.size();Node *prev = nullptr;Node *cur = _tables[hashi];while (cur){if (cur->_kv.first == key){// 1.头删// 2.中间删if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_size;return true;}prev = cur;cur = cur->_next;}return false;}size_t Size(){return _size;}// 表的长度size_t TablesSize(){return _tables.size();}// 桶的个数size_t BucketNum(){size_t num = 0;for (auto &hashNode : _tables){if (hashNode){++num;}}return num;}size_t MaxBucketLength(){size_t maxLen = 0;for (auto &hashNode : _tables){size_t len = 0;Node *cur = hashNode;while (cur){++len;cur = cur->_next;}if (len > maxLen){maxLen = len;}}return maxLen;}private:vector<Node *> _tables;size_t _size = 0; // 存储有效数据个数};

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

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

相关文章

一季度直播6000场,同比增长60%,遥望科技透露重要信息

6月17日&#xff0c;经由深圳证券交易所许可&#xff0c;遥望科技&#xff08;股票代码&#xff1a;002291&#xff09;正式对《年报问询函》进行公开回复&#xff0c;就经营的多个维度做出解释和回应。 在回复中&#xff0c;遥望科技预测2024年毛利率为14.4%&#xff0c;相比…

【CSS in Depth2精译】1.1.1 样式表来源

您添加到网页的样式表并非浏览器呈现样式的唯一来源。样式表有三种不同的类型或来源。您添加到页面的样式称为 作者样式&#xff08;author styles&#xff09;&#xff1b;此外还有 用户样式&#xff08;user styles&#xff09;&#xff0c;即终端用户设置的自定义样式&#…

configure: error: library ‘crypto‘ is required for OpenSSL

1、执行命令&#xff1a;./configure --prefix/opt/app/postgresql --with-openssl 报错&#xff1a; 2、解决办法 执行命令&#xff1a;yum install openssl-devel 重新执行 ./configure --prefix/opt/app/postgresql --with-openssl

充电学习—3、Uevent机制和其在android层的实现

sysfs 是 Linux userspace 和 kernel 进行交互的一个媒介。通过 sysfs&#xff0c;userspace 可以主动去读写 kernel 的一些数据&#xff0c;同样的&#xff0c; kernel 也可以主动将一些“变化”告知给 userspace。也就是说&#xff0c;通过sysfs&#xff0c;userspace 和 ker…

探索序列到序列模型:了解编码器和解码器架构的强大功能

目录 一、说明 二、什么是顺序数据&#xff1f; 三、编码器解码器架构的高级概述&#xff1a; 3.1 编码器和解码器架构的简要概述&#xff1a; 3.2 训练机制&#xff1a;编码器和解码器架构中的前向和后向传播&#xff1a; 四、编码器解码器架构的改进&#xff1a; 4.1.…

一道session文件包含题

目录 环境说明 session文件包含getshell 审计源码 session包含 base64在session中的解码分析 题目&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1Q0BN08b8gWiVE4tOnirpTA?pwdcate 提取码&#xff1a;cate 环境说明 这里我用的是linux&#xff0c;也可以用p…

【论文阅读】-- DeepVisualInsight: 深度分类训练时空因果关系的时间旅行可视化

中文标题 摘要引言动机举例相关工作时间旅行可视化的属性符号定义邻居保护属性边界距离保持属性逆投影保持属性暂时保存属性 方法 δ \delta δ-边界估计(k)-BAVR综合体建设逆投影保持时间连续性 评估案例分析结论参考文献 摘要 了解深度学习模型的预测在训练过程中是如何形成…

[WTL/Win32]_[中级]_[MVP架构在实际项目中应用的地方]

场景 在开发Windows和macOS的界面软件时&#xff0c;Windows用的是WTL/Win32技术&#xff0c;而macOS用的是Cocoa技术。而两种技术的本地语言一个主打是C,另一个却是Object-c。界面软件的源码随着项目功能增多而增多&#xff0c;这就会给同步Windows和macOS的功能造成很大负担…

Linux-远程访问及控制

一、SSH远程管理 SSH&#xff08;Secure Shell&#xff09;是一种安全通道协议&#xff0c;主要用来实现字符界面的远程登录、远程复制等功能。SSH 协议对通信双方的数据传输进行了加密处理&#xff0c;其中包括用户登录时输入的用户口令。与早期的 Telent&#xff08;远程登录…

【Spine学习11】之 战士攻击动作 思路总结(手动调整贝塞尔曲线实现前快后慢)

拿到一份psd文件先观察检查一下图片顺序有没有问题&#xff0c; 重点看一下人物的腿部分层&#xff0c;&#xff08;如果是大小腿分开画的就网格可打可不打&#xff0c;如果是连在一起画的&#xff0c;那必须打网格&#xff09; 拿着剑的时候剑和手的层级有没有错位&#xff0c…

HCS-华为云Stack-容器网络

HCS-华为云Stack-容器网络 容器隧道overlay VPC网络

第〇篇:深入Docker的世界系列博客介绍

深入Docker的世界系列博客介绍 欢迎来到“深入Docker的世界”系列博客&#xff0c;这是一次旨在全面探索Docker容器化技术的冒险之旅。从基础原理到高级应用&#xff0c;再到实践案例分析&#xff0c;我们将深入挖掘Docker的每一个角落&#xff0c;帮助你不仅掌握这项技术的实…

FreeRtos-09事件组的使用

1. 事件组的理论讲解 事件组:就是通过一个整数的bit位来代表一个事件,几个事件的or和and的结果是输出 #define configUSE_16_BIT_TICKS 0 //configUSE_16_BIT_TICKS用1表示16位,用0表示32位 1.1 事件组适用于哪些场景 某个事件若干个事件中的某个事件若干个事件中的所有事…

第10章 文件和异常

第10章 文件和异常 10.1 从文件中读取数据10.1.1 读取整个文件10.1.2 文件路径10.1.3 逐行读取10.1.4 创建一个包含文件各行内容的列表10.1.5 使用文件的内容10.1.6 包含一百万位的大型文件10.1.7 圆周率值中包含你的生日吗 10.2 写入文件10.2.1 写入文件10.2.2 写入多行10.2.3…

MyBatisPlus基础学习

一、简介 二、集成MP 三、入门HelloWorld 四、条件构造器EntityWrapper 五、ActiveRecord(活动记录 ) 六、代码生成器 七、插件扩展 八、自定义全局操作 九、公共字段自动填充 十、Oracle主键Sequence 十一、Idea快速开发插件 十二、mybatis-plus实践及架构原理

C#聊天室客户端完整③

窗体 进入聊天室界面(panel里面,label,textbox,button): 聊天界面(flowLayoutPanel(聊天面板))&#xff1a; 文档大纲(panel设置顶层(登录界面),聊天界面在底层) 步骤&#xff1a;设置进入聊天室→输入聊天→右边自己发送的消息→左边别人发的消息 MyClient.cs(进入聊天室类) …

如何利用TikTok矩阵源码实现自动定时发布和高效多账号管理

在如今社交媒体的盛行下&#xff0c;TikTok已成为全球范围内最受欢迎的短视频平台之一。对于那些希望提高效率的内容创作者而言&#xff0c;手动发布和管理多个TikTok账号可能会是一项繁琐且耗时的任务。幸运的是&#xff0c;通过利用TikTok矩阵源码&#xff0c;我们可以实现自…

Linux C语言:字符串处理函数

一、字符串函数 1、C库中实现了很多字符串处理函数 #include <string.h> ① 求字符串长度的函数strlen② 字符串拷贝函数strcpy③ 字符串连接函数strcat④ 字符串比较函数strcmp 2、字符串长度函数strlen 格式&#xff1a;strlen(字符数组)功能&#xff1a;计算字符串…

【Python】已解决报错:AttributeError: module ‘json‘ has no attribute ‘loads‘解决办法

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。 &#x1f913; 同时欢迎大家关注其他专栏&#xff0c;我将分享Web前后端开发、人工智能、机器学习、深…

1)Java项目笔记搭建系统梳理相关知识

目录 前言项目结构Java部分Spring整合部分SpringBoot整合部分 模块说明规划 小结javarabbitmqmybatisspring最后推荐几本工具书 前言 工作有年头了&#xff0c;学到了很多技术&#xff0c;收获了很多。但是对与工作相关的专业技能知识的掌握杂而乱&#xff0c;不够全面系统。因…