当前位置: 首页 > news >正文

C++哈希(链地址法)(二)详解

文章目录

  • 1.开放地址法
    • 1.1key不能取模的问题
      • 1.1.1将字符串转为整型
      • 1.1.2将日期类转为整型
  • 2.哈希函数
    • 2.1乘法散列法(了解)
    • 2.2全域散列法(了解)
  • 3.处理哈希冲突
    • 3.1线性探测(挨着找)
    • 3.2二次探测(跳跃着找)
    • 3.3双重散列(了解)
  • 4.链地址法
    • 4.1扩容
    • 4.2基本的框架
    • 4.3插入
    • 4.4查找
    • 4.5删除
  • 5.代码

1.开放地址法

1.1key不能取模的问题

当key是string/Date等类型时,key不能取模,那么我们需要给HashTable增加一个仿函数,这个仿函数支持把key转换成一个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数就用默认参数即可如果这个Key不能转换为整形,我们就需要自己实现一个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每个值都参与到计算中,让不同的key转换出的整形值不同。string做哈希表的key非常常见,所以我们可以考虑把string特化一下。

1.1.1将字符串转为整型

key如果是字符串,转为整形需要仿函数

// key / M , M哈希表的空间大小
size_t hash0 = hash(kv.first) % _tables.size();// 将key转为无符号的整形,因为key可能是负数
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};// 特化
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t sum = 0;for (auto& ch : s){sum += ch;sum *= 131;// *131为了避免哈希冲突,每次的key都不一样}return sum;}
};int main()
{const char* a[] = { "abcd","def","gca" };HashTable<string, string> ha;// 类型+()是匿名对象// 哈希冲突了cout << HashFunc<string>()("abcd") << endl;cout << HashFunc<string>()("aadd") << endl;cout << HashFunc<string>()("acbd") << endl;for (auto& ch : a){ha.Insert({ ch,ch });}return 0;
}

1.1.2将日期类转为整型

struct Date
{int _year;int _month;int _day;Date(int year = 1,int month = 1,int day = 1):_year(year),_month(month),_day(day){}bool operator==(const Date& d){return _year == d._year&&_month == d._month&&_day == d._day;}
};struct DateHashFunc
{size_t operator()(const Date& d){size_t hash = 0;hash += d._year;hash *= 131;hash += d._month;hash *= 131;hash += d._day;hash *= 131;return hash;}
};int main()
{// 将日期类转化为整型HashTable<Date, int, DateHashFunc> ht;ht.Insert({ { 2024,12,10 }, 1 });ht.Insert({ { 2024,10,12 }, 1 });return 0;
}

2.哈希函数

设计哈希函数为了减少冲突,让更多的位参与运算,不管使用%不太接近2的幂次方的质数,还是用位运算计算都是可以的

2.1乘法散列法(了解)

  1. 乘法散列法对哈希表大小M没有要求,他的大思路第一步:用关键字 Key 乘上常数 A (0<A<1),并抽
    取出 key * A 的小数部分。第二步:后再用M乘以key*A 的小数部分,再向下取整。
  2. h(key) = floor(M × ((A × key)%1.0)) ,其中floor表示对表达式进行下取整,A∈(0,1),这里最重要的是A的值应该如何设定,Knuth认为 A = ( 5 − 1)/2 = 0.6180339887… (黄金分割点])比较好。
  3. 乘法散列法对哈希表大小M是没有要求的,假设M为1024,key为1234,A = 0.6180339887, A * key
    = 762.6539420558,取小数部分为0.6539420558, M×((A×key)%1.0) = 0.6539420558*1024 =669.6366651392,那么h(1234) = 669。

2.2全域散列法(了解)

  1. 如果存在一个恶意的对手,他针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,比如,让所有关键字全部落入同一个位置中。这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决方法自然是见招拆招,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据。这种方法叫做全域散列。
  2. hab (key) = ((a × key + b)%P)%M ,P需要选⼀个足够大的质数,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了一个P*(P-1)组全域散列函数组。假设P=17,M=6,a = 3, b = 4, 则 h34 (8) = ((3 × 8 + 4)%17)%6 = 5 。
  3. 需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使用,后续增删查改都固定使用这个散列函数,否则每次哈希都是随机选一个散列函数,那么插入是一个散列函数,查找又是另一个散列函数,就会导致找不到插入的key了。

在这里插入图片描述

3.处理哈希冲突

3.1线性探测(挨着找)

缺点:堆积

3.2二次探测(跳跃着找)

缺点:无法充分利用位置
3.1和3.2上一篇博客详细说明了

3.3双重散列(了解)

缺点:虽然可以充分利用位置,但是还是要解决冲突的问题

  • h1 (key) = hash0 = key % M , hash0位置冲突了,则双重探测公式为:hc(key, i) = hashi =
    (hash0 + i ∗ h2 (key)) % M, i = {1, 2, 3, …, M}
  • 要求 h2 (key) < Mh2 (key) 和M互为质数,有两种简单的取值方法:
    1、当M为2整数幂时,h2 (key) 从[0,M-1]任选一个奇数;
    2、当M为质数时, h2 (key) = key % (M − 1) + 1
  • 反例:保证 h2 (key) 与M互质是因为根据固定的偏移量所寻址的所有位置将形成一个若最大公约数说无法充分利用整个散列表。举例来说,若初始探查位置为1,偏移量为3,整个散列表大小为12,那么所能寻址的位置为{1, 4, 7, 10},寻址个数为p = gcd(M, h1 (key)) > 1 ,那么所能寻址的位置的个数为 M/P < M ,使得对于一个关键字来
    12/gcd(12, 3) = 4
  • 下面演示 {19,30,52,74} 等这一组值映射到M=11的表中,设 h2 (key) = key%10 + 1
  • 本质是跳跃探测,减少冲突堆积
  • 双重散列就是让数据更加地分散,不容易产生哈希冲突

在这里插入图片描述

4.链地址法

开放地址法的问题是你占别人位置,别人来了又占其他人的位置,链地址法就不用占别人的位置,自己位置可以存多个位置,用了链表挂了多个数据

4.1扩容

  • 开放地址法的负载因子必须小于1,链地址法的负载因子没有这种规定,可以大于1,但是unordered_xxx中最大负载因子基本控制在1,大于1就会扩容。
    在这里插入图片描述

4.2基本的框架

namespace hash_bucket
{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,class V,class Hash = HashFunc<K>>class HashTable{typedef HashTable<K, V> Node;public:// 构造HashTable():_tables(__stl_next_prime(0)),_n(0){}private:vector<Node*> _tables;// 指针数组size_t _n;// 表示存了多少个数据};
}

4.3插入

头插,尾插都可以,这里用了头插
在这里插入图片描述

// 插入
bool Insert(const pair<K,V>& kv)
{// 负载因子 == 1时扩容if (_n == _tables.size()){// 这种方法每个节点都要拷贝,影响效率// 并且原数组释放完后,不会自动析构每个节点,因为是内置类型// 还要自己写析构函数,比较麻烦//HashTable<K, V> newht;//newht._tables.resize(_stl_next_prime(tables.size() + 1));////for(size_t i = 0;i < _tables.size();i++)//{//	// 旧表的数据扩容后可能不冲突,必须一个一个取//	Node* cur = _tables[i];//	while (cur)//	{//		newht.Insert(cur->_kv);//		cur = cur->_next;//	}//}//_tables.swap(newht._tables);vector<Node*> newTable(_tables.size() * 2);for(size_t i = 0;i < _tables.size();i++){// 表旧表中的数据插入到新表中Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 算cur在新表中的位置size_t hashi = cur->_kv.first % newTable.size();cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTable);}size_t hashi = kv.first % _tables.size();// 头插Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}int main()
{int a2[] = { 19,30,5,36,13,20,21,12,24,96 };hash_bucket::HashTable<int, int> ht;for (auto e : a2){ht.Insert({ e,e });}ht.Insert({ 100,100 });ht.Insert({ 200,200 });return 0;
}

4.4查找

// 查找
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;
}

4.5删除

删除分为三种情况:

  1. 头删,让下一个节点变为头节点
  2. 删除中间的节点,保留前一个节点的指针指向后一个节点的指针
  3. 尾删,让最后一个节点的前一个节点的指针指向空
  4. 2和3可以归为一类,删除中间的节点可以是前一个节点指向空
    在这里插入图片描述
// 删除
bool Erase(const K& key)
{size_t hashi = key % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){// 头删_tables[hashi] = cur->_next;}else{// 删除中间的节点prev->_next = cur->_next;}delete cur;--_n;return true;}else{prev = cur;cur = cur->_next;}}return false;
}

5.代码

namespace hash_bucket
{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,class V,class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:// 构造HashTable():_tables(__stl_next_prime(0)),_n(0){}// 析构~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){Hash hash;// 如果插入的值存在冗余了返回falseif (Find(kv.first)){return false;}// 负载因子 == 1时扩容if (_n == _tables.size()){// 这种方法每个节点都要拷贝,影响效率// 并且原数组释放完后,不会自动析构每个节点,因为是内置类型// 还要自己写析构函数,比较麻烦//HashTable<K, V> newht;//newht._tables.resize(_stl_next_prime(tables.size() + 1));////for(size_t i = 0;i < _tables.size();i++)//{//	// 旧表的数据扩容后可能不冲突,必须一个一个取//	Node* cur = _tables[i];//	while (cur)//	{//		newht.Insert(cur->_kv);//		cur = cur->_next;//	}//}//_tables.swap(newht._tables);vector<Node*> newTable(__stl_next_prime(_tables.size() + 1));for(size_t i = 0;i < _tables.size();i++){// 表旧表中的数据插入到新表中Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 算cur在新表中的位置size_t hashi = hash(cur->_kv.first) % newTable.size();cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTable);}size_t hashi = hash(kv.first) % _tables.size();// 头插Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}// 查找Node* Find(const K& key){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){size_t hashi = key % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){// 头删_tables[hashi] = cur->_next;}else{// 删除中间的节点prev->_next = cur->_next;}delete cur;--_n;return true;}else{prev = cur;cur = cur->_next;}}return false;}private:vector<Node*> _tables;// 指针数组size_t _n;// 表示存了多少个数据};
}

相关文章:

C++哈希(链地址法)(二)详解

文章目录 1.开放地址法1.1key不能取模的问题1.1.1将字符串转为整型1.1.2将日期类转为整型 2.哈希函数2.1乘法散列法&#xff08;了解&#xff09;2.2全域散列法&#xff08;了解&#xff09; 3.处理哈希冲突3.1线性探测&#xff08;挨着找&#xff09;3.2二次探测&#xff08;跳…...

IME关于输入法横屏全屏显示问题-Android14

IME关于输入法横屏全屏显示问题-Android14 1、输入法全屏模式updateFullscreenMode1.1 全屏模式判断1.2 全屏模式布局设置 2、应用侧关闭输入法全屏模式2.1 调用输入法的应用设置flag2.2 继承InputMethodService.java的输入法应用覆盖onEvaluateFullscreenMode方法 InputMethod…...

网络工程师 (11)软件生命周期与开发模型

一、软件生命周期 前言 软件生命周期&#xff0c;也称为软件开发周期或软件开发生命周期&#xff0c;是指从软件项目的启动到软件不再被使用为止的整个期间。这个过程可以细分为多个阶段&#xff0c;每个阶段都有其特定的目标、任务和产出物。 1. 问题定义与需求分析 问题定义…...

【人工智能】基于Python的机器翻译系统,从RNN到Transformer的演进与实现

《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 机器翻译(Machine Translation, MT)作为自然语言处理领域的重要应用之一,近年来受到了广泛的关注。在本篇文章中,我们将详细探讨如何使…...

网络工程师 (12)软件开发与测试

一、软件设计 &#xff08;一&#xff09;定义与目的 软件设计是从软件需求出发&#xff0c;设计软件的整体结构、功能模块、实现算法及编写代码的过程&#xff0c;旨在确定系统如何完成预定任务。其目标是确保目标系统能够抽象、普遍地完成预定任务&#xff0c;并为后续的软件…...

3.Spring-事务

一、隔离级别&#xff1a; 脏读&#xff1a; 一个事务访问到另外一个事务未提交的数据。 不可重复读&#xff1a; 事务内多次查询相同条件返回的结果不同。 幻读&#xff1a; 一个事务在前后两次查询同一个范围的时候&#xff0c;后一次查询看到了前一次查询没有看到的行。 二…...

Python字典详解:从入门到实践

Python字典详解&#xff1a;从入门到实践 字典&#xff08;Dictionary&#xff09;是Python中最重要且最常用的数据结构之一。本文将深入讲解字典的特性、操作方法和实际应用案例。 1. 字典简介 字典是可变的、无序的键值对集合&#xff0c;使用{}创建。每个元素由key: valu…...

91,【7】 攻防世界 web fileclude

进入靶场 <?php // 包含 flag.php 文件 include("flag.php");// 以高亮语法显示当前文件&#xff08;即包含这段代码的 PHP 文件&#xff09;的内容 // 方便查看当前代码结构和逻辑&#xff0c;常用于调试或给解题者提示代码信息 highlight_file(__FILE__);// 检…...

41【文件名的编码规则】

我们在学习的过程中&#xff0c;写出数据或读取数据时需要考虑编码类型 火山采用&#xff1a;UTF-16 易语言采用&#xff1a;GBK php采用&#xff1a;UTF-8 那么我们写出的文件名应该是何种编码的&#xff1f;比如火山程序向本地写出一个“测试.txt”&#xff0c;理论上这个“测…...

蓝桥杯备赛经验帖

蓝桥杯备赛经验帖 作者&#xff1a;blue 时间&#xff1a;2025.2.1 文章目录 蓝桥杯备赛经验帖1.为什么有这篇文章2.赛制3.比赛流程4.如何准备5.其他建议6.一些感悟 1.为什么有这篇文章 ​ 笔者近期发现&#xff0c;观看我写的两道第十五届蓝桥杯题解的人数逐渐增多&#xf…...

一文大白话讲清楚webpack基本使用——17——Tree Shaking

文章目录 一文大白话讲清楚webpack基本使用——17——Tree Shaking1. 建议按文章顺序从头看&#xff0c;一看到底&#xff0c;豁然开朗2. 啥叫Tree Shaking3. 什么是死代码&#xff0c;怎么来的3. Tree Shaking的流程3.1 标记3.2 利用Terser摇起来 4. 具体使用方式4.1 适用前提…...

【C++ 区间位运算】3209. 子数组按位与值为 K 的数目|2050

本文涉及知识点 位运算、状态压缩、枚举子集汇总 LeetCode3209. 子数组按位与值为 K 的数目 给你一个整数数组 nums 和一个整数 k &#xff0c;请你返回 nums 中有多少个子数组 满足&#xff1a;子数组中所有元素按位 AND 的结果为 k 。 示例 1&#xff1a; 输入&#xff1a…...

8 比例缩放(scale.rs)

scale.rs代码是几何变换库euclid中典型的数据结构和方法的例子&#xff0c;用于处理二维和三维空间中的缩放变换。 一、scale.rs文件源码 //! A type-checked scaling factor between units.use crate::num::One;use crate::approxord::{max, min}; use crate::{Box2D, Box3D…...

二分 机器人的跳跃问题

二段性:找到一个值&#xff0c;大于此值的时候都成立&#xff0c;小于的时候都不成立 更新的方式只有两种&#xff0c;左边的mid更新不需要1&#xff1b;右边的mid更新需要1 //对能量进行二分&#xff0c;确定能量的范围 //特判防止溢出int #include<bits/stdc.h> using…...

Hive:复杂数据类型之Map函数

Map函数 是Hive里面的一种复杂数据类型, 用于存储键值对集合。Map中的键和值可以是基础类型或复合类型&#xff0c;这使得Map在处理需要关联存储信息的数据时非常有用。 定义map时,需声明2个属性: key 和 value , map中是 key value 组成一个元素 key-value, key必须为原始类…...

R 字符串:深入理解与高效应用

R 字符串:深入理解与高效应用 引言 在R语言中,字符串是数据处理和编程中不可或缺的一部分。无论是数据清洗、数据转换还是数据分析,字符串的处理都是基础技能。本文将深入探讨R语言中的字符串概念,包括其基本操作、常见函数以及高效应用方法。 字符串基本概念 字符串定…...

设计模式Python版 桥接模式

文章目录 前言一、桥接模式二、桥接模式示例三、桥接模式与适配器模式的联用 前言 GOF设计模式分三大类&#xff1a; 创建型模式&#xff1a;关注对象的创建过程&#xff0c;包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式和建造者模式。结构型模式&…...

记5(一元逻辑回归+线性分类器+多元逻辑回归

目录 1、一元逻辑回归2、线性可分&线性不可分3、Iris数据集实现多元逻辑回归4、绘制分类图5、鸢尾花分类图6、多分类问题&#xff1a;&#xff08;softmax回归&#xff09;6.1、编码&#xff1a;自然顺序码、独热编码、独冷编码6.2、二/多分类问题&#xff1a;6.3、softmax…...

【Python】第七弹---Python基础进阶:深入字典操作与文件处理技巧

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【MySQL】【Python】 目录 1、字典 1.1、字典是什么 1.2、创建字典 1.3、查找 key 1.4、新增/修改元素 1.5、删除元素 1.6、遍历…...

Nginx 运维开发高频面试题详解

一、基础核心问题 原文链接&#xff1a;https://blog.csdn.net/weixin_51146329/article/details/142963853 1、什么是Nginx&#xff1f; Nginx 是一个高性能的 HTTP 和反向代理服务器&#xff0c;它以轻量级和高并发处理能力而闻名。Nginx 的反向代理功能允许它作为前端服务…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

Swagger和OpenApi的前世今生

Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章&#xff0c;二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑&#xff1a; &#x1f504; 一、起源与初创期&#xff1a;Swagger的诞生&#xff08;2010-2014&#xff09; 核心…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比

在机器学习的回归分析中&#xff0c;损失函数的选择对模型性能具有决定性影响。均方误差&#xff08;MSE&#xff09;作为经典的损失函数&#xff0c;在处理干净数据时表现优异&#xff0c;但在面对包含异常值的噪声数据时&#xff0c;其对大误差的二次惩罚机制往往导致模型参数…...

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要&#xff1a; 近期&#xff0c;在使用较新版本的OpenSSH客户端连接老旧SSH服务器时&#xff0c;会遇到 "no matching key exchange method found"​, "n…...

在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)

考察一般的三次多项式&#xff0c;以r为参数&#xff1a; p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]&#xff1b; 此多项式的根为&#xff1a; 尽管看起来这个多项式是特殊的&#xff0c;其实一般的三次多项式都是可以通过线性变换化为这个形式…...

【JavaSE】多线程基础学习笔记

多线程基础 -线程相关概念 程序&#xff08;Program&#xff09; 是为完成特定任务、用某种语言编写的一组指令的集合简单的说:就是我们写的代码 进程 进程是指运行中的程序&#xff0c;比如我们使用QQ&#xff0c;就启动了一个进程&#xff0c;操作系统就会为该进程分配内存…...

NPOI Excel用OLE对象的形式插入文件附件以及插入图片

static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...

省略号和可变参数模板

本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...

论文阅读:LLM4Drive: A Survey of Large Language Models for Autonomous Driving

地址&#xff1a;LLM4Drive: A Survey of Large Language Models for Autonomous Driving 摘要翻译 自动驾驶技术作为推动交通和城市出行变革的催化剂&#xff0c;正从基于规则的系统向数据驱动策略转变。传统的模块化系统受限于级联模块间的累积误差和缺乏灵活性的预设规则。…...