[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.hqwc.cn/news/191338.html

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

相关文章

向pycdc项目提的一个pr

向pycdc项目提的一个pr 前言 pycdc这个项目&#xff0c;我之前一直有在关注&#xff0c;之前使用他反编译python3.10项目&#xff0c;之前使用的 uncompyle6无法反编译pyhton3.10生成的pyc文件&#xff0c;但是pycdc可以&#xff0c;但是反编译效果感觉不如uncompyle6。但是版…

Docker入门学习笔记

学习笔记网址推送&#xff1a;wDocker 10分钟快速入门_哔哩哔哩_bilibili docker是用来解决什么问题的&#xff1f; 例如当你在本地主机写了个web应用&#xff0c;而你打算将该应用发送给其他客户端进行案例测试和运行&#xff0c;若是传统做法&#xff0c;就比较复杂&#xf…

基于MS16F3211芯片的触摸控制灯的状态变化和亮度控制(11.17,PWM控制与状态切换)

1.今天做了什么 2.过程思路 看了两天文档才慢慢看懂&#xff0c;有点满了 现在接着前一天的思路&#xff0c;可以通过代码来控制pwm的占空比。我这里采用的是TP0定时器 初步控制pwm的占空比 void LED_PWM_OPEN(void) {//占空比 PWM1-Y-PB2PWM1DH 0X0F;PWM1DL 0X00; //占…

基于 gin + websocket 即时通讯项目 (一、项目初始化)

基于 gin websocket 即时通讯项目 1、安装环境与初始化 搜索各种包官网 https://pkg.go.dev/ 1.1 安装 grom go get -u gorm.io/grom 1.2 安装 MySQL 驱动 go get -u gorm.io/driver/sqlite go get -u gorm.io/driver/mysql 1.3 安装 gin go get -u github.com/gin-gonic/gi…

​软考-高级-系统架构设计师教程(清华第2版)【第12章 信息系统架构设计理论与实践(P420~465)-思维导图】​

软考-高级-系统架构设计师教程&#xff08;清华第2版&#xff09;【第12章 信息系统架构设计理论与实践&#xff08;P420~465&#xff09;-思维导图】 课本里章节里所有蓝色字体的思维导图

代码随想录算法训练营第三十九天【动态规划part02】 | 62.不同路径、63. 不同路径 II

62.不同路径 题目链接&#xff1a; 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 求解思路&#xff1a; 动规五部曲 确定dp数组及其下标含义&#xff1a;dp[i][j] 表示从&#xff08;0,0&#xff09;出发&#xff0c;到&#xff08;i,j&#x…

课程设计(毕业设计)—基于机器学习(CNN+opencv+python)的车牌识别—(可远程调试)计算机专业课程设计(毕业设计)

基于机器学习(CNNopencvpython)的车牌识别 下载本文机器学习(CNNopencvpython)的车牌识别系统完整的代码和参考报告链接&#xff08;或者可以联系博主koukou(壹壹23七2五六98)&#xff0c;获取源码和报告&#xff09;https://download.csdn.net/download/shooter7/88548767此处…

腾讯云服务器租用价格,腾讯云服务器价格流量怎么算?

首先&#xff0c;让我们来看看腾讯云服务器租用价格。根据您的需求不同&#xff0c;腾讯云提供了多种不同的配置选项&#xff0c;从轻量级应用服务器到高性能的GPU服务器&#xff0c;都可以满足您的需求。以下是一些常见的腾讯云服务器租用价格&#xff1a; 一、腾讯云服务器租…

代码随想录 Day49 单调栈01 LeetCode LeetCodeT739每日温度 T496 下一个最大元素I

前言 折磨的死去活来的动态规划终于结束啦,今天秋秋给大家带来两题非常经典的单调栈问题,可能你不清楚单调栈是什么,可以用来解决什么问题,今天我们就来一步一步的逐渐了解单调栈,到能够灵活使用单调栈.注意以下讲解中&#xff0c;顺序的描述为 从栈头到栈底的顺序 什么时候用单…

解决:虚拟机远程连接失败

问题 使用FinalShell远程连接虚拟机的时候连接不上 发现 虚拟机用的VMware&#xff0c;Linux发行版是CentOs 7&#xff0c;发现在虚拟机中使用ping www.baidu.com是成功的&#xff0c;但是使用FinalShell远程连接不上虚拟机&#xff0c;本地网络也ping不通虚拟机&#xff0c…

STM32 HAL库函数HAL_SPI_Receive_IT和HAL_SPI_Receive的区别

背景 前段时间开发一个按键板驱动&#xff0c;该板用的STM32F103系列单片机&#xff0c;前任工程师用STM32CubeMX生成的工程&#xff0c;里面全是HAL库调用&#xff0c;我接手后&#xff0c;学习了下HAL库的用法&#xff0c;踩坑不少&#xff0c;特别是带IT后缀的函数&#xf…

Typecho用宝塔面板建站(保姆级教程)

提前准备&#xff1a; 1 已备案域名 注意:在腾讯云备案的域名部署阿里云服务器的话还需要在阿里云备案&#xff0c;反之亦然 2 服务器 服务器操作系统设置为windows 服务器实例设置&#xff1a;依次开放8888/888/443/3000-4000/21/22端口 个人用的阿里云&#xff0c;到安全组配…