[C国演义] 哈希的使用和开闭散列的模拟实现

哈希的使用和开闭散列的模拟实现

  • 1. 使用
    • 1.1 unordered_map的接口
    • 1.2 unordered_set的接口
  • 2. 哈希底层
    • 2.1 概念
    • 2.2 解决哈希冲突
  • 3. 实现
    • 3.1 开放寻址法
    • 3.2 拉链法

1. 使用

1.1 unordered_map的接口

  1. 构造
void test1()
{// 空的unordered_map对象unordered_map<int, int> m1(10);cout << "桶的实际个数->" << m1.bucket_count() << endl;// 用列表初始化来进行初始化unordered_map<int, int> m2{ {1,1}, {2,2}, {3,3} };cout << "列表初始化-> " << endl;for (const auto& e : m2){cout << e.first << " " << e.second << endl;}cout << endl;// 迭代器区间初始化unordered_map<int, int> m3(m2.begin(), m2.end());cout << "迭代器区间初始化-> " << endl;for (const auto& e : m2){cout << e.first << " " << e.second << endl;}cout << endl;
}int main()
{test1();return 0;
}

运行结果 :

桶的实际个数->16
列表初始化->
1 1
2 2
3 3迭代器区间初始化->
1 1
2 2
3 3
  1. 容量
  2. 元素访问
void test2()
{unordered_map<string, int> mat{ {"小呆呆", 1}, {"波比", 2} };cout << "初始化-> " << endl;for (const auto& e : mat){cout << e.first << " " << e.second << endl;}cout << endl;mat["猪猪侠"];cout << "插入功能-> " << endl;for (const auto& e : mat){cout << e.first << " " << e.second << endl;}cout << endl;mat["波比"] = 5;cout << "修改功能-> " << endl;for (const auto& e : mat){cout << e.first << " " << e.second << endl;}cout << endl;mat["超人强"] = 6;cout << "插入 + 修改功能-> " << endl;for (const auto& e : mat){cout << e.first << " " << e.second << endl;}cout << endl;
}int main()
{test2();return 0;
}

运行结果 :

初始化->
小呆呆 1
波比 2插入功能->
小呆呆 1
猪猪侠 0
波比 2修改功能->
小呆呆 1
猪猪侠 0
波比 5插入 + 修改功能->
小呆呆 1
猪猪侠 0
波比 5
超人强 6
  1. 查询
void test3()
{unordered_map<string, int> mat{ {"小呆呆", 1}, {"波比", 2} };mat["猪猪侠"] = 5;mat["猪猪侠"] = 7;size_t cnt = mat.count("猪猪侠");cout << cnt << endl;auto it = mat.find("小呆呆");if (it != mat.end()){cout << it->first << " is " << it->second << endl;}else{cout << "查无元素! " << endl;}cout << endl;auto git = mat.find("超人强");if (git != mat.end()){cout << git->first << " is " << git->second << endl;}else{cout << "查无元素! " << endl;}}int main()
{test3();return 0;
}

运行结果 :

1
小呆呆 is 1查无元素!
  1. 修改
void test4()
{unordered_map<string, int> mat1{ {"小呆呆", 1}, {"波比", 2} };// 插入mat1.insert({ "小呆呆", 5 });mat1.insert({ "超人强", 6 });for (const auto& e : mat1){cout << e.first << " " << e.second << endl;}cout << endl;// 删除mat1.erase("波比");mat1.erase("猪猪侠");for (const auto& e : mat1){cout << e.first << " " << e.second << endl;}cout << endl;// 交换unordered_map<string, int> mat2{ {"迪迦",1}, {"戴拿",2}};mat1.swap(mat2);cout << "交换后的mat1-> " << endl;for (const auto& e : mat1){cout << e.first << " is " << e.second << endl;}cout << endl;cout << "交换后的mat2-> " << endl;for (const auto& e : mat2){cout << e.first << " is " << e.second << endl;}
}int main()
{test4();return 0;
}

运行结果 :

小呆呆 1
波比 2
超人强 6小呆呆 1
超人强 6交换后的mat1->
迪迦 is 1
戴拿 is 2交换后的mat2->
小呆呆 is 1
超人强 is 6
  1. 桶操作
void test5()
{unordered_map<string, int> mat1{ {"小呆呆", 1}, {"波比", 2}, {"迪迦", 3} };// 桶的个数cout << mat1.bucket_count() << endl << endl;// 每个桶的有效个数for (int i = 0; i < mat1.bucket_count(); i++){printf("[%d] -> %d\n", i, mat1.bucket_size(i));}cout << endl;// 各个key所在的桶for (const auto& e : mat1){cout << mat1.bucket(e.first) << endl;}
}int main()
{test5();return 0;
}

运行结果 :

8[0] -> 1
[1] -> 0
[2] -> 1
[3] -> 0
[4] -> 0
[5] -> 0
[6] -> 1
[7] -> 02
6
0

1.2 unordered_set的接口

unordered_set 和 unordered_map的接口大致一样, 但是没有 operator [ ]

2. 哈希底层

2.1 概念

unordered_set 和 unordered_map的效率高的原因 ⇒ 底层是哈希结构

  • 理想中的搜索方法 :
    顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
    理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
    如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素.

使元素的存储位置和关键码建立一 一映射的关系, 这个方法称为 哈希(散列)方法,
通过一个函数使得存储位置和关键码建立一 一映射的关系, 这个函数称为 哈希函数,
最终形成的结构, 称为 哈希(散列)表, HashTable
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为 哈希冲突或哈希碰撞

  • 先浅浅地看一下哈希结构, 来理解一下概念

  • 常见的哈希函数
    哈希冲突的一个重要原因就是 哈希函数设置的不好
    那么, 我们来了解一下最常见的两个哈希函数

    1. 直接寻址法 : 一个key对应一个位置
      前提 : 知道数据集合的大小 和 分布情况
      适合场景: 数据量小且均匀

    2. 除留余数法 : 准备一个基准值去估计数据量的多少, 设为m, 采用 hash(key) = key % m的方法去建立元素和下标的一 一 映射关系

  • 采用直接寻址法 — — 数据量小 且 集中
    字符串中第一个唯一字符

class Solution {
public:int firstUniqChar(string s) {// <s中的每个字符, 个数>int hash[26] = {0};// 映射for(auto e : s){hash[e-'a']++;}// 查找for(int i = 0; i < s.size(); i++){if(hash[s[i] - 'a'] == 1){return i;}}return -1;}
};
  • 采用除留余数法 — — 任何场景下都可
    下面的 哈希冲突解决 和 实现 都是采用的除留余数法

  • 哈希函数设置的越巧妙, 哈希冲突就越低, 但是 哈希冲突无法避免

2.2 解决哈希冲突

解决哈希冲突主要有两种方法 : 闭散列 和 开散列

  1. 闭散列
    闭散列, 也叫 开放寻址法,
    思路是 : 当冲突发生时, 必然有空位置, 那么把冲突的元素放到 "下一个空位置" 即可!
  • 插入逻辑

  • 查找逻辑

    • 这从另一方面也体现了 哈希表的有效数据不应该占比太大 ⇒ 否则就是遍历这个哈希结构, O(N)
      但是 也不能占比太少 ⇒ 浪费空间
      一般, 控制 有效个数 / 哈希结构的大小 在 [0.7, 0.8]的范围内是比较合理的
  • 删除逻辑
    首先, 能确定的是不能直接把这个位置去掉

    那么该位置要进行保留, 那么 值该怎么处理呢 ?
    改为 0, -1 … … 等无意义的数值?
    其实这些都是不行的, 你怎么知道你修改后的数据是无意义的呢 ⇒ 能确定的是 该位置的值也要进行保留
    该位置要进行保留, 值也要进行保留 && 不能影响后面的查找逻辑 那么该怎么把它删掉呢? ⇒ 引入每个下标的状态 : 删除状态, 空白状态, 存在状态

  • 由于删除逻辑而导致新的插入逻辑

  • 由于删除逻辑而导致新的查找逻辑

  1. 开散列
    开散列, 又叫作 拉链法
    上面的 开放地址法 解决哈希冲突的办法是 将经过哈希函数处理过的 相同的key, "延后落座"
    拉链法的解决思路是 将经过哈希函数处理的 相同的key 放到一个单链表中, 然后将每一个单链表的头结点放到一个数组里面. 本质是一个 指针数组
  • 这里的插入删除, 查找逻辑就是在 key那个桶进行单链表操作

🗨️ 有同学就会说, 这不是单链表操作吗, 不过如此!

  • 我们可以控制 有效数据个数 / 桶的大小 = 1 ⇒ 平均下来就是一个桶一个数据

3. 实现

这里都先实现 数据位pair<K, V>类型的

3.1 开放寻址法

  1. STATE类型
enum STATE
{EXIT,DELETE,EMPTY
};
  1. HashData类
template<class K, class V>
struct HashData
{public:HashData(){}HashData(const pair<K, V>& kv):_data(kv){}public:pair<K, V> _data;STATE _st = EMPTY;
};
  1. Hash类
template<class K, class V, class Com = DEFAULT<K>>
class hash
{public:hash(){// 1. 先给4个空间// 2. size 和 capacity一样大_table.resize(4);}bool insert(const pair<K, V>& kv){// 扩容逻辑if ((double)_sz / _table.size() >= 0.7){size_t newsize = _table.size() * 2;hash<K, V> new_ht;new_ht._table.resize(newsize);// 挪动数据for (size_t i = 0; i < _table.size(); i++){// 不用挪动删除状态的值if (_table[i]._st == EXIT){new_ht.insert(_table[i]._data);}}std::swap(*this, new_ht);}// 线性探测for (const auto& e : _table){if (kv.first == e._data.first){return false;}}size_t hashi = com(kv.first) % _table.size();while (_table[hashi]._st == EXIT){++hashi;hashi %= _table.size();}_sz++;_table[hashi] = kv;_table[hashi]._st = EXIT;return true;}// 返回有key,// 不允许用户在外面更改key,// 所以返回<const K, V>*HashData<const K, V>* find(const K& key){size_t hashi = com(key) % _table.size();while (_table[hashi]._st != EMPTY){if (_table[hashi]._st == EXIT &&  _table[hashi]._data.first == key){return (HashData<const K, V>*)&_table[hashi];}hashi++;hashi %= _table.size();}return nullptr;}bool erase(const K& key){// 复用findHashData<const K, V>* res = find(key);if (res){res->_st = DELETE;_sz--;return true;}else{return false;}//for (auto e : _table)//{//	if (e._data.first == key)//	{//		e._st = DELETE;//		_sz--;//		return true;//	}//}//return false;}private:vector<HashData<K, V>> _table;size_t _sz = 0;Com com;
};
  1. DEFAULT — 通过仿函数来解决 字符串 不能进行 % 的问题
// 通过仿函数来解决 字符串 不能进行 % 
template<class K>
struct DEFAULT
{size_t operator()(const K& key){return (size_t)key;}
};// 模版的特化 -- 全特化
// 解决 字符串问题
template<>
struct DEFAULT<string>
{size_t operator()(const string& key){int res = 0;for (auto e : key){res += e * 131;}return res;}
};

3.2 拉链法

  1. HashData类
template<class K, class V>
struct HashData
{
public:
HashData(const pair<K, V>& kv):_data(kv)
{}public:
pair<K, V> _data;
HashData<K, V>* _next;
};
  1. Hash类
template<class K, class V, class Com = DEFAULT<K>>
class hash
{typedef HashData<const K, V> Node;
public:hash(){_table.resize(4, nullptr);}Node* find(const K& key){size_t hashi = com(key) % _table.size();Node* cur = _table[hashi];while (cur){if (com(cur->_data.first) == com(key)){return cur;}cur = cur->_next;}return nullptr;}bool insert(const pair<K, V>& kv){Node* res = find(kv.first);if (res){return false;}// 扩容逻辑if (_sz == _table.size()){vector<Node*> new_table;new_table.resize(_table.size() * 2, nullptr);for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];// 顺手牵走这个桶的内容while (cur){// 提前保存 next, 后面会改变的Node* next = cur->_next;size_t hashi = com(cur->_data.first) % new_table.size();// 先让cur链接上新表中该桶的内容cur->_next = new_table[hashi];// 再让cur成为新表中该桶的头节点new_table[hashi] = cur;cur = next;}}_table.swap(new_table);}// 插入逻辑size_t hashi = com(kv.first) % _table.size();Node* newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;++_sz;return true;}bool erase(const K& key){Node* res = find(key);if (res == nullptr){return false;}else{size_t hashi = com(key) % _table.size();Node* cur = _table[hashi];Node* prev = nullptr;while (cur){if (cur->_data.first == key){if (prev == nullptr){_table[hashi] = cur->_next;}else{prev->_next = cur->_next;}}prev = cur;cur = cur->_next;}--_sz;delete cur;}return true;}void print(){for (int i = 0; i < _table.size(); i++){Node* cur = _table[i];printf("[%d]->", i);while (cur){printf("%d", cur->_data.first);cur = cur->_next;}cout << "NULL" << endl;}cout << endl;}private:vector<Node*> _table;size_t _sz = 0;Com com;
};
  1. DEFAUL — 通过仿函数来解决 字符串 不能 % 的问题
// 通过仿函数来解决 字符串 不能进行 % 
template<class K>
struct DEFAULT
{size_t operator()(const K& key){return (size_t)key;}
};// 模版的特化 -- 全特化
// 解决 字符串问题
template<>
struct DEFAULT<string>
{size_t operator()(const string& key){int res = 0;for (auto e : key){res += e * 131;}return res;}
};

无心买酒谒青春,对镜空嗟白发新。
花下少年应笑我,垂垂羸马访高人。
— — 岳飞 <过张溪赠张完>

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

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

相关文章

动态头像如何制作?这个方法请收藏

照片是记录生活的一种方式&#xff0c;但是静态图片有时候不能够完全表达我们的情感。而动态的图片能够让图片以更生动的方式来展示我们的想象力和内心情感。那么&#xff0c;大家知道动态图片制作的方法有哪些吗&#xff1f;使用gif动画制作&#xff08;https://www.gif.cn/&a…

React项目首页中用canvas实现星空

文章目录 前言代码使用后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;前端系列文章 &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误&#xff0c;感谢大家…

机器学习笔记 - 隐马尔可夫模型的简述

隐马尔可夫模型是一个并不复杂的数学模型,到目前为止,它一直被认为是解决大多数自然语言处理问题最为快速、有效的方法。它成功地解决了复杂的语音识别、机器翻译等问题。看完这些复杂的问题是如何通过简单的模型得到描述和解决,我们会由衷地感叹数学模型之妙。 人类信息交流…

数据结构与算法设计分析——常用搜索算法

目录 一、穷举搜索二、图的遍历算法&#xff08;一&#xff09;深度优先搜索&#xff08;DFS&#xff09;&#xff08;二&#xff09;广度优先搜索&#xff08;BFS&#xff09; 三、回溯法&#xff08;一&#xff09;回溯法的定义&#xff08;二&#xff09;回溯法的应用 四、分…

EfficientNet:通过模型效率彻底改变深度学习

一、介绍 EfficientNet 是深度学习领域的里程碑&#xff0c;代表了神经网络架构方法的范式转变。EfficientNet 由 Google Research 的 Mingxing Tan 和 Quoc V. Le 开发&#xff0c;在不影响性能的情况下满足了对计算高效模型不断增长的需求。本文深入探讨了 EfficientNet 背后…

百度搜索智能化算力调控分配方法

作者 | 泰来 导读 随着近年深度学习技术的发展&#xff0c;搜索算法复杂度不断上升&#xff0c;算力供给需求出现了爆发式的增长。伴随着AI技术逐步走到深水区&#xff0c;算法红利在逐步消失&#xff0c;边际效应日益显著&#xff0c;算力效能的提升尤为重要&#xff0c;同时随…

视频修复软件 Aiseesoft Video Repair mac中文版功能

AIseesoft Video RepAIr mac是一款专业的视频修复软件&#xff0c;主要用于修复损坏或无法播放的视频文件。AIseesoft Video RepAIr是一个功能强大的程序,可以帮助恢复丢失或损坏的数据的视频。只要您以相同的格式提供示例视频,并在功能强大的技术的支持下,只需单击几下即可收获…

智能配电系统解决方案

智能配电系统解决方案是一种集成了先进技术和智能化功能的配电系统&#xff0c;它能够提高电力系统的效率、可靠性和安全性。力安科技智能配电系统解决方案依托电易云-智慧电力物联网&#xff0c;具体实施的方案如下&#xff1a; 智能化设备和传感器&#xff1a;采用智能化的开…

「Java开发指南」如何在Spring中使用JAX-WS注释器?

本文将指导您如何使用JAX-WS注释器从Spring服务生成JAX-WS Web服务&#xff0c;在本教程中&#xff0c;您将学习如何&#xff1a; 为Spring服务启用JAX-WS部署应用程序并测试服务 所有与Spring scaffolding相关的任务都需要MyEclipse Spring或Bling授权。 MyEclipse v2023.1…

MapApp 地图应用

1. 简述 1.1 重点 1&#xff09;更好地理解 MVVM 架构 2&#xff09;更轻松地使用 SwiftUI 框架、对齐、动画和转换 1.2 资源下载地址: Swiftful-Thinking:https://www.swiftful-thinking.com/downloads 1.3 项目结构图: 1.4 图片、颜色资源文件图: 1.5 启动图片配置图: 2. Mo…

前端JS 使用input完成文件上传操作,并对文件进行类型转换

使用input实现文件上传 // 定义一个用于文件上传的按钮<input type"file" name"upload1" />// accept属性用于定义允许上传的文件类型&#xff0c; onchange用于绑定文件上传之后的相应函数<input type"file" name"upload2"…

2013年11月10日 Go生态洞察:Go语言四周年回顾

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

招聘小程序源码 人才招聘网源码

招聘小程序源码 人才招聘网源码 求职招聘小程序源码系统是一种基于微信小程序的招聘平台&#xff0c;它可以帮助企业和求职者快速、方便地进行招聘和求职操作。 该系统通常包括以下功能模块&#xff1a; 用户注册和登录&#xff1a;用户可以通过微信小程序注册和登录&#…

如何利用ChatGPT撰写学术论文?

在阅读全文前请注意&#xff0c;本文是利用ChatGPT“辅助完成”而不是“帮写”学术论文&#xff0c;请一定要注意学术规范&#xff01; 本文我将介绍如何使用清晰准确的“指令”让ChatGPT帮助我们在论文写作上提高效率&#xff0c;希望通过本文的指导&#xff0c;读者能够充分…

uniapp app tabbar 页面默认隐藏

1.在page.json 中找到tabbar visible 默认为true,设为false则是不显示 uni.setTabBarItem({ index: 1, //列表索引 visible:true //显示或隐藏 })

PyTorch

正常界面 创建环境 conda create -n env_test python3.6进入环境 conda activate env_testpycharm中&#xff0c;创建项目&#xff0c;选择环境

LeetCode(26)判断子序列【双指针】【简单】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 判断子序列 1.题目 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串。&#xff08;…

【Java】恺撒密码,stream流,方法引用

文章目录 一、题目二、题解2.1、写法12.2、写法2&#xff0c;stream流2.3、写法3&#xff0c;方法引用 一、题目 二、题解 2.1、写法1 普通写法, 遍历每个字符进行加密 public static void main1 (String[] args) {Scanner sc new Scanner(System.in);String strs sc.nextL…

Linux 进程管理 实时调度类及SMP和NUMA

文章目录 一、 实时调度类分析1.1 实时调度实体sched_rt_entity数据结构1.2 实时调度类rt_sched_class数据结构1.3 实时调度类功能函数 二、SMP和NUMA2.1 SMP&#xff08;多对称处理器结构&#xff0c;UMA&#xff09;2.2 NUMA&#xff08;非一致内存访问结构&#xff09;2.3 C…

【Linux系统化学习】进程的父子关系 | fork 进程

个人主页点击直达&#xff1a;小白不是程序媛 Linux专栏&#xff1a;Linux系统化学习 目录 前言&#xff1a; 父子进程 父子进程的引入 查看父子进程 查询进程的动态目录 更改进程的工作目录 fork创建进程 fork的引入 fork的使用 fork的原理 fork如何实现的&#…