C++_哈希表

本篇文章是对C++学习的哈希表部分的学习分享

相信一定会对你有所帮助~

那咱们废话不多说,直接开始吧!


一、基础概念

1. 哈希核心思想:

  • 哈希函数的作用:通过此函数建立一个Key与存储位置之间的映射关系。
  • 理想目标:实现O(1)时间复杂度的查找

2.直接定址法

本质:⽤关键字计算出⼀个绝对位置或者相对位置

适用场景:Key 范围集中(如 [0,99]

二、关键问题与解决方案

1.哈希冲突:

  • 根本原因:不同 Key 映射到同一位置

  • 负载因子(Load Factor):α = N/M(N为已映射存储的值,M为哈希表的大小)

    • α↑ → 冲突概率↑,空间利用率↑

    • α↓ → 冲突概率↓,空间利用率↓

2. 哈希函数设计原则:

  • 目标:均匀分布、减少冲突

  • 除法散列法 / 除留余数法(重点)

    • h(key) = key % M

    • M 的选择:避免 2^n 或 10^n,因为 key%M 会仅保留 key 的最后 n 位(二进制或十进制),导致不同 key 可能映射到同一位置。例如:

M=16(2^4)时,63(00111111)和31(00011111)的后4位均为 1111,哈希值均为15。

M=100(10^2)时,112和12312的后两位均为12,哈希值相同。

  • 理论上建议选择远离 2^n 的质数作为哈希表大小 M,以减少冲突。但实践中可灵活优化,如 Java 的 HashMap 采用 2^16 作为 M,通过位运算((key ^ (key >> 16)) & (M-1))替代取模,既提升效率又让高位参与计算,分散哈希值。核心在于均匀分布,而非机械套用理论。

  • 其他方法(了解):乘法散列法、全域散列法

3. 非整数Key的处理

有些数据类型无法直接用整形的哈希函数,比如string字符串类型,这时我们便可以尝试将字符串转证书(BKDR哈希思路)

size_t hash = 0;
for (char c : str) {hash = hash * 131 + c;  // 质数 131 减少冲突
}

三、 冲突解决策略

1. 开放定址法

线性探测

  • 从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛ 到哈希表尾,则回绕到哈希表头的位置
  • 冲突后公式:hashi = (hash0 + i) % M
  • 缺点:易产生聚集(Clustering)

二次探测

  • 从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为 ⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表 尾的位置

  • 公式:hashi = (hash0 ± i²) % M
  • 缓解聚集,但可能错过空位
删除优化
  • 状态标记(EXIST EMPTY DELETE

  1. EXIST:当前槽位存储有效数据。

  2. EMPTY:槽位从未使用过,查找时可终止探测。

  3. DELETE:槽位数据已删除,但探测链不能中断(需继续向后查找)。

enum STATE
{EMPTY,DELETE,EXIST
};

扩容机制

1. 负载因子与扩容条件

  • 负载因子(Load Factor)α = 元素数量 / 哈希表大小

  • 扩容阈值:当 α ≥ 0.7 时扩容,以降低冲突概率。

  • 扩容倍数:通常扩容为原大小的 2倍

2. 质数大小的必要性
  • 理论要求:哈希表大小 M 应为质数,使 key % M 分布更均匀(减少聚集)。

  • 问题:若初始 M 是质数(如 7),2倍扩容后(14)不再是质数,可能引发更多冲突。

3.解决方案

  • SGI 版本的质数表:

预定义质数表:按近似2倍递增的质数序列扩容

//写出来的28个素数(每一个都差不多为前一个的两倍)
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
};//取素数的函数
inline unsigned long __stl_next_prime(unsigned long n)
{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);return pos == last ? *(last - 1) : *pos;
}
  • 扩容步骤

    1. 当前大小为 53(质数),负载因子 ≥0.7 时,从表中取下一个质数 97(≈53×1.8)。

    2. 重新哈希所有元素到新表。

  • 例子:在插入函数中,发现负载因子>=0.7后的操作:

  • bool Insert(const pair<K, V>& kv)
    {//Check(_tables);//如果负载因子 >=0.7 时就需要扩容了if ((double)_n / (double)_tables.size() >= 0.7){HashTable<K, V,Hash> newHT(__stl_next_prime(_tables.size()+1));for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);return true;}

    完整代码实现

  • #pragma once
    #include<iostream>
    #include<vector>
    using namespace std;//写出来的28个素数(每一个都差不多为前一个的两倍)
    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
    };//取素数的函数
    inline unsigned long __stl_next_prime(unsigned long n)
    {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);return pos == last ? *(last - 1) : *pos;
    }enum STATE
    {EMPTY,DELETE,EXIST
    };template<class K,class V>
    struct HashData
    {pair<K, V> _kv;STATE _state;
    };template<class K>
    struct HashFunc
    {size_t operator()(const K& key){return (size_t)key;}
    };//因为用string类型的数值做key的情况十分常见,但是在unordered_map中却没有在另外写一个仿函数
    //是因为直接将HashFunc 特化 了
    template<>
    struct HashFunc<string>
    {size_t operator()(const string& s){size_t hashi = 0;for (auto e : s){hashi += e;}return hashi;}
    };template<class K,class V,class Hash = HashFunc<K>>
    class HashTable
    {
    public://构造函数HashTable(size_t size = __stl_next_prime(0)):_n(0), _tables(size){}//将一些非整形的数值强转成整形一次方便映射关系的计算void Check(vector<HashData<K,V>>& table){double fuzai = _n / table.size();if (fuzai >= 0.7){cout << "负载过大" << endl;}else{cout << "负载正常" << endl;}}bool Insert(const pair<K, V>& kv){//Check(_tables);//如果负载因子 >=0.7 时就需要扩容了if ((double)_n / (double)_tables.size() >= 0.7){HashTable<K, V,Hash> newHT(__stl_next_prime(_tables.size()+1));for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);return true;}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();size_t hashi = hash0;size_t i = 1;//如果映射的位置已经被占用了while (_tables[hashi]._state == EXIST){hashi = (hashi + i) % _tables.size();++i;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}HashData<K,V>* Find(const K& key){Hash hs;size_t hash0 = hs(key) % _tables.size();size_t hashi = hash0;size_t i = 1;while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._kv.first == key && _tables[hashi]._state != DELETE){return &_tables[hashi];}hashi = (hashi + i) % _tables.size();++i;}cout << " 找不到找不到 " ;return nullptr;}bool Erase(const K& key	){HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;cout << "成功删除!" << endl;return true;}else{cout << "删除失败奥" << endl;return false;}}private:vector<HashData<K, V>> _tables;size_t _n;
    };
  • 2.链地址法

  • 核心思想:冲突位置挂链表(桶)

  • 扩容时机:负载因子 α ≥ 1(STL 风格)

  • 极端场景优化

    • 链表过长 → 转红黑树(Java 8 HashMap 策略)

  • 扩容技巧

    • 直接移动旧节点(避免重复创建):

// 旧节点重新映射到新表
cur->_next = newTable[hashi];
newTable[hashi] = cur;
  • 例子:在哈希桶版本的Insert函数中发现负载因子过大
  • bool Insert(const pair<K, V>& kv)
    {if (_n == _tables.size()){vector<Node*> newTables(__stl_next_prime(0), nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = cur->_kv.first % newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = kv.first % _tables.size();Node* newnode = new Node(kv);newnode->_next = _table[hashi];_tables[hashi] = newnode;++_n;return true;
    }

    完整代码实现

  • template<class K, class V>
    struct HashNode
    {pair<K, V> _kv;HashNode* _next;HashNode(const pair<K,V>& key):_kv(key),_next(nullptr){}
    };template<class K,class V>
    class HashTable
    {typedef HashNode<K, V> Node;
    public:HashTable(size_t size = __stl_next_prime(0)):_tables(size,nullptr){}~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;}}bool Insert(const pair<K, V>& kv){if (_n == _tables.size()){vector<Node*> newTables(__stl_next_prime(0), nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = cur->_kv.first % newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = kv.first % _tables.size();Node* newnode = new Node(kv);newnode->_next = _table[hashi];_tables[hashi] = newnode;++_n;return true;}Node* Find(const K& key){size_t hashi = 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) {size_t hashi = key % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){cur->_next = _tables[hashi];}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}return false;}
    private:vector<Node*> _tables;size_t _n = 0;
    };

    那么本次关于哈希表的知识分享就此结束了~

    非常感谢你能够看到这里~

    如果感觉对你有些许的帮助也请给我三连 这会给予我莫大的鼓舞!

    之后依旧会继续更新C++学习分享

    那么就让我们

    下次再见~

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

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

相关文章

Mac M芯片 RAG 极简流程 安装 ragflow + LM studio

本文基于 【【知识科普】【纯本地化搭建】【不本地也行】DeepSeek RAGFlow 构建个人知识库】 https://www.bilibili.com/video/BV1WiP2ezE5a/?share_sourcecopy_web&vd_source9a55f12dd64d8e30ab6c0efc62844343 1 .docker-compose yml文件修改,指定平台 platform: linux/…

Rsync+inotify+nfs实现数据实时备份方案

技术栈 NFS是 Network File System的简写&#xff0c;即网络文件系统。NFS的优点是内核直接支持&#xff0c;部署简单、运行稳定&#xff0c;协议简单、传输效率高。缺点是仅依靠IP地址或主机名来决定用户能否挂载共享目录&#xff0c;容易出现单点故障。 rsync是linux系统下的…

Vue ⑥-路由

单页应用程序 单页应用程序&#xff0c;即 Single-Page Application&#xff0c;简称 SPA&#xff0c;是一种使用 JavaScript、HTML 和 CSS 构建的 Web 应用程序。SPA 的核心是前端路由&#xff0c;它使得用户在访问网站时&#xff0c;只需加载一次页面&#xff0c;然后通过前…

Hadoop复习(九)

Azkaban工作流管理器 选择 问题 1 判断题 2 / 2 分 工作流是指具有依赖的一组job任务&#xff0c;被依赖的job任务最后执行 正确 错误 问题 2 判断题 2 / 2 分 Azkaban兼容任何版本的Hadoop 正确 错误 问题 3 判断题 2 / 2 分 独立服务器模式下&#xff0c;Azkab…

SpringMVC相关知识(二)

一.重定向和转发 1.ModelandView 设置ModelAndView对象 , 根据view的名称 , 和视图解析器跳到指定的页面 页面 : {视图解析器前缀} viewName {视图解析器后缀} 相关代码&#xff1a; <!-- 视图解析器 --> <bean class"org.springframework.web.servlet.vi…

std::ratio 简单使用举例

author: hjjdebug date: 2025年 06月 09日 星期一 14:28:40 CST descrip: std::ratio 简单使用举例 文章目录 1. 先看一个简单的例子 1/2/1/35/62 std::ratio 的手册页3. std::ratio_add 到底是什么呢&#xff1f;4. 代码注释5. 加深理解.6. 自定义的std::ratio 与 std::ratio_…

Docker 优势与缺点全面解析:容器技术的利与弊

在当今云计算、微服务、DevOps盛行的时代&#xff0c;Docker 几乎成了开发者、运维工程师的标配工具之一。自2013年诞生以来&#xff0c;Docker 以其轻量、快速、易移植的特点&#xff0c;彻底改变了应用的构建、交付与部署方式。 但任何技术都有两面性&#xff0c;Docker 也不…

大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计

随着大语言模型&#xff08;LLM&#xff09;参数规模的增长&#xff0c;推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长&#xff0c;而KV缓存的内存消耗可能高达数十GB&#xff08;例如Llama2-7B处理100K token时需50GB内存&a…

排序算法总结(C++)

目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指&#xff1a;同样大小的样本 **&#xff08;同样大小的数据&#xff09;**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…

使用Redis作为缓存优化ElasticSearch读写性能

在现代数据密集型应用中,ElasticSearch凭借其强大的全文搜索能力成为许多系统的首选搜索引擎。然而,随着数据量和查询量的增长,ElasticSearch的读写性能可能会成为瓶颈。本文将详细介绍如何使用Redis作为缓存层来显著提升ElasticSearch的读写性能,包括完整的架构设计、详细…

获取wordpress某个栏目的内容数量

获取wordpress某个栏目的内容数量 <?php // 将以下 8 改成你的分类 ID 即可echo get_category(8)->count;?> 在制作wordpress模板时&#xff0c;有时会需要调用某个分类目录下的所有内容数量&#xff0c;通过这段简洁的代码就可以实现。 给WordPress自定义字段加…

uniapp 安卓 APP 后台持续运行(保活)的尝试办法

在移动应用开发领域&#xff0c;安卓系统的后台管理机制较为复杂&#xff0c;应用在后台容易被系统回收&#xff0c;导致无法持续运行。对于使用 Uniapp 开发的安卓 APP 来说&#xff0c;实现后台持续运行&#xff08;保活&#xff09;是很多开发者面临的重要需求&#xff0c;比…

深度学习——知识提炼

第一部分&#xff1a;引言与背景——为什么需要知识提炼&#xff1f; 一、模型压缩的背景 随着深度学习的发展&#xff0c;模型变得越来越大&#xff08;如 ResNet152、BERT、ViT、GPT 等&#xff09;&#xff0c;其参数量动辄数亿甚至上百亿。这些大模型虽然性能强大&#x…

开源之夏·西安电子科技大学站精彩回顾:OpenTiny开源技术下沉校园,点燃高校开发者技术热情

开源之夏2025编程活动正在如火如荼的进行中&#xff0c;当前也迎来了报名的倒计时阶段&#xff0c;开源之夏组织方也通过高校行系列活动进入各大高校&#xff0c;帮助高校开发者科普开源文化、开源活动、开源技术。 6月4日 开源之夏携手多位开源技术大咖、经验型选手走进西安电…

时间复杂度和算法选择

数据范围 时间复杂度 算法选择 n \leq 30 指数级别 O(2^n) 深度优先搜索&#xff08;DFS&#xff09; 剪枝、状态压缩动态规划 n \leq 100 O(n^3) Floyd 算法、动态规划、高斯消元 n \leq 1000 O(n^2) 、 O(n^2 \log n) 动态规划、二分…

数据分析实战2(Tableau)

1、Tableau功能 数据赋能&#xff08;让业务一线也可以轻松使用最新数据&#xff09; 分析师可以直接将数据看板发布到线上自动更新看板自由下载数据线上修改图表邮箱发送数据设置数据预警 数据探索&#xff08;通过统计分析和数据可视化&#xff0c;从数据发现问题&#xf…

CentOS7_Linux下安装Docker和docker-compose

目录 环境要求安装步骤1、修改镜像源配置文件2、卸载旧版本 Docker&#xff08;如有&#xff09;3、安装依赖工具4、添加 Docker 官方仓库5、安装 Docker 引擎6、启动 Docker 并设置开机自启7、验证安装8、配置镜像加速器创建配置文件重启 Docker 生效 9、允许非 root 用户操作…

ubuntu中使用docker

上一篇我已经下载了一个ubuntu:20.04的镜像&#xff1b; 1. 查看所有镜像 sudo docker images 2. 基于本地存在的ubuntu:20.04镜像创建一个容器&#xff0c;容器的名为cppubuntu-1。创建的时候就会启动容器。 sudo docker run -itd --name cppubuntu-1 ubuntu:20.04 结果出…

均衡后的SNRSINR

本文主要摘自参考文献中的前两篇&#xff0c;相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程&#xff0c;其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt​ 根发送天线&#xff0c; n r n_r nr​ 根接收天线的 MIMO 系…

佰力博科技与您探讨热释电测量的几种方法

热释电的测量主要涉及热释电系数的测定&#xff0c;这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中&#xff0c;积分电荷法最为常用&#xff0c;其原理是通过测量在电容器上积累的热释电电荷&#xff0c;从而确定热释电系数…