C++数据结构——哈希桶HashBucket

目录

一、前言

1.1 闭散列

1.2 开散列

1.3 string 与 非 string 

二、哈希桶的构成

2.1 哈希桶的节点

2.2 哈希桶类

三、 Insert 函数

3.1 无需扩容时

3.2 扩容

复用 Insert:

逐个插入:

优缺点比对:

第一种写法优点

第一种写法缺点

第二种写法优点

第二种写法缺点

3.3 完整代码

四、 Erase 函数

4.1 析构函数

4.2 Erase 函数

五、 Find 函数

六、完整代码 


一、前言

上一篇文章讲的哈希表,属于闭散列。可以解决哈希冲突有两种方式:闭散列和开散列。现在要学习的哈希桶就是开散列。

1.1 闭散列

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

1.2 开散列

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

下面则是即将学习的哈希桶的简易图:

类似于一个数组中存了若干个链表头,每个头所代表的链表成为一个桶。

1.3 string 与 非 string 

在上一篇博客的最后:哈希表HashTable-CSDN博客
探讨过当 Key 为负数、浮点数、字符串...时,类函数逻辑中有关 Key 取模的问题,当 Key 为负数、浮点数、字符、字符串时,显然这几个内置类型无法完成取模的操作,这时就用到了仿函数,这里不再多说,直接来看仿函数的代码,下面会直接使用仿函数来完成 HashBucket

template<class T>
class HashFunc<>
{size_t operator()(const T& Key){return (size_t)Key;}
}
template<>
class HashFunc<string>
{size_t operator()(const string& Key){size_t hash = 0;for (auto ch : Key){hash *= 131;hash += ch;}return hash;}
}

二、哈希桶的构成

2.1 哈希桶的节点

由上图就可以看出来,每个结点必要存一个 pair 和一个指向下一个节点的指针 _next。

template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode* _next;
}

2.2 哈希桶类

哈希桶类的构成和哈希表类似,都是一个由一个 vector 存放每个节点,但是这里与 HashTable 不同的是需要存放的是节点的指针。还有一个 _n 代表有效数据的个数:

template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{typedef HashNode Node;
private:vector<Node*> _bucket;size_t _n;
};

三、 Insert 函数

3.1 无需扩容时

下面要介绍的是不需要扩容时的插入逻辑:

此时只需要使用 Key 模数组的大小来计算出该节点需要连接在 vector 上的位置,然后使用 new 得到储存 kv 的新节点,当 new 一个新节点时,节点的构造函数必不可少,下面先来看一下单个节点的构造函数以及类的构造函数:

template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode* _next;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{    typedef HashNode<K, V> Node;HashBucket(){_bucket.resize(10, nullptr);_n = 0;}
private:vector<Node*> _bucket;size_t _n;
}

此时需要思考的是,既然 vector 每个节点都要存放一个链表,那么链表头插还是尾插的效率更高呢?

显然是头插,所以这个新结点就需要以类似头插的方式添加到 vector 的这个位置上,

bool Insert(const pair<K, V>& kv)
{Hash hs;size_t index = hs(kv.first) % _bucket.size(); Node* newnode = new Node(kv);newnode->_next = _bucket[index];_buckte[index] = newnode;++_n;return true;
}

3.2 扩容

这里的载荷因子可以直接设为1,至于载荷因子是什么,可以查看上一篇博客哈希表HashTable-CSDN博客,在扩容中的何时扩容标题下,介绍了载荷因子的概念。

在扩容中,既可以使用 HashTable 中类似的写法直接复用 Insert ,也可以直接挨个让节点插入,下面先介绍每种方式,再进行优缺点的处理:

复用 Insert:

bool Insert(const pair<K, V>& kv)
{Hash hs;if (_n == _bucket.size()){HashBucket newHB = new HashBucket;newHB._bucket.resize(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while(cur){Node* next = cur->_next;  // 保存下一个节点指针newHB.Insert(cur->_kv);   // 插入当前节点的键值对到新哈希表cur = next;               // 移动到下一个节点}_bucket[i] = nullptr;}_bucket.swap(newHB);}
}

逐个插入:

bool Insert(const pair<K, V>& kv)
{Hash hs;if (_n == _bucket.size()){vector<Node*> newBucket(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;size_t index = hs(cur->_kv.first) % newBucket.size();cur->_next = newBucket[index];newBucket[index] = cur;cur = next;}_bucket[i] = nullptr;}_bucket.swap(newBucket);}
}

优缺点比对:

第一种写法优点
  1. 代码复用:通过调用 newHB.Insert(cur->_kv) 来重新插入节点,重用了 Insert 方法,减少了代码重复。
  2. 逻辑清晰:将旧节点迁移到新桶中,然后交换桶,逻辑分离清晰。
第一种写法缺点
  1. 性能:因为每次扩容时调用 Insert,可能会多次计算哈希值和处理冲突,性能可能稍差。
第二种写法优点
  1. 性能:直接处理节点迁移,无需调用 Insert 方法,减少了函数调用和重复计算,提高了性能。
  2. 直接操作:直接操作指针,代码简洁,性能高效。
第二种写法缺点
  1. 代码重复:需要手动处理节点迁移逻辑,代码重复。
  2. 复杂性:直接操作指针可能增加代码的复杂性,增加错误的可能性。

3.3 完整代码

    bool Insert(const pair<K, V>& kv){if (Find(kv.first)) return false;Hash hs;if (_n == _bucket.size()){HashBucket newHB;newHB._bucket.resize(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;  // 保存下一个节点指针newHB.Insert(cur->_kv);   // 插入当前节点的键值对到新哈希表cur = next;               // 移动到下一个节点}_bucket[i] = nullptr;}_bucket.swap(newHB._bucket);}size_t index = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);newnode->_next = _bucket[index];_bucket[index] = newnode;++_n;return true;}bool Insert(const pair<K, V>& kv){if (Find(kv.first)) return false;Hash hs;if (_n == _bucket.size()){vector<Node*> newBucket(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;size_t index = hs(cur->_kv.first) % newBucket.size();cur->_next = newBucket[index];newBucket[index] = cur;cur = next;}_bucket[i] = nullptr;}_bucket.swap(newBucket);}size_t index = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);newnode->_next = _bucket[index];_bucket[index] = newnode;++_n;return true;}

四、 Erase 函数

4.1 析构函数

根据 Insert 函数中,可以得知, HashBucket 的每个节点都是 new 出来的,那删除的时候就要使用 delete ,又因为每个节点都是自定义类型,所以要为 HashBucket 写一个析构函数。
对类的析构就是遍历 vector 的每个节点,再从每个节点遍历每个链表,以此遍历全部节点。

    ~HashBucket(){for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_bucket[i] = nullptr;}}

4.2 Erase 函数

下面介绍一下Erase函数的步骤:

  1. 计算哈希值:使用哈希函数 hs 计算给定键 Key 的哈希值,并确定它在桶中的索引 index

  2. 遍历链表:从索引 index 开始,遍历链表中的每个节点。

  3. 查找节点:检查当前节点的键是否等于 Key

    • 如果找到匹配节点:
      • 如果该节点是链表的第一个节点,将桶的头指针 _bucket[index] 指向下一个节点。
      • 否则,将前一个节点的 _next 指针指向当前节点的下一个节点。
      • 删除当前节点 cur,释放内存。
      • 返回 true,表示删除成功。
    • 如果没有找到匹配节点,继续遍历链表,更新 prevcur
  4. 返回结果:如果遍历完整个链表未找到匹配节点,返回 false,表示删除失败。

    bool Erase(const K& Key){Hash hs;size_t index = hs(Key) % _bucket.size();Node* cur = _bucket[index];Node* prev = nullptr;while (cur){if (cur->_kv.first == Key){//删除的是第一个节点if (prev == nullptr){_bucket[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}

五、 Find 函数

这里的 Find 函数与 HashTable 中的 Find 函数逻辑略有不同,因为这里如果发送哈希冲突,那么存储的位置还是 vector 中取模后的位置,不需要像 HashTable 那样进行线性探测,只需要取一个指针挨个遍历位于 _bucket[index] 链表上的节点即可。

    Node* Find(const K& Key){if (_bucket.empty()) return nullptr;Hash hs;size_t index = hs(Key) % _bucket.size();Node* cur = _bucket[index];while (cur){if (cur->_kv.first == Key)return cur;else cur = cur->_next;}return nullptr;}

写完 Find 后,还可以进一步改进 Insert 函数,在插入时可以先进行查找,如果存在,那就插入失败;如果不存在,再继续插入。这样避免了哈希桶中数据冗余的结果。

六、完整代码 

#pragma once
#include <iostream>
#include <vector>
#include <string>using namespace std;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 hash = 0;for (auto ch : Key){hash *= 131;hash += ch;}return hash;}
};template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode* _next;HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr){}
};template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{typedef HashNode<K, V> Node;
public:HashBucket(){_bucket.resize(10, nullptr);_n = 0;}~HashBucket(){for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_bucket[i] = nullptr;}}bool Insert(const pair<K, V>& kv){if (Find(kv.first)) return false;Hash hs;if (_n == _bucket.size()){vector<Node*> newBucket(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;size_t index = hs(cur->_kv.first) % newBucket.size();cur->_next = newBucket[index];newBucket[index] = cur;cur = next;}_bucket[i] = nullptr;}_bucket.swap(newBucket);}size_t index = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);newnode->_next = _bucket[index];_bucket[index] = newnode;++_n;return true;}bool Insert(const pair<K, V>& kv){if (Find(kv.first)) return false;Hash hs;if (_n == _bucket.size()){HashBucket newHB;newHB._bucket.resize(_bucket.size() * 2, nullptr);for (size_t i = 0; i < _bucket.size(); i++){Node* cur = _bucket[i];while (cur){Node* next = cur->_next;  // 保存下一个节点指针newHB.Insert(cur->_kv);   // 插入当前节点的键值对到新哈希表cur = next;               // 移动到下一个节点}_bucket[i] = nullptr;}_bucket.swap(newHB._bucket);}size_t index = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);newnode->_next = _bucket[index];_bucket[index] = newnode;++_n;return true;}bool Erase(const K& Key){Hash hs;size_t index = hs(Key) % _bucket.size();Node* cur = _bucket[index];Node* prev = nullptr;while (cur){if (cur->_kv.first == Key){//删除的是第一个节点if (prev == nullptr){_bucket[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}Node* Find(const K& Key){if (_bucket.empty()) return nullptr;Hash hs;size_t index = hs(Key) % _bucket.size();Node* cur = _bucket[index];while (cur){if (cur->_kv.first == Key)return cur;else cur = cur->_next;}return nullptr;}
private:vector<Node*> _bucket;size_t _n;
};
void TestHB1()
{int ret[] = {5, 15, 3, 12, 13, 31};HashBucket<int, int> hb;for (auto e : ret){hb.Insert(make_pair(e, e));}cout << hb.Erase(0) << endl;cout << hb.Erase(5) << endl;
}
void TestHB2()
{HashBucket<string, int> hb;hb.Insert(make_pair("sort", 1));hb.Insert(make_pair("left", 1));hb.Insert(make_pair("insert", 1));}

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

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

相关文章

gfast:基于全新Go Frame 2.3+Vue3+Element Plus构建的全栈前后端分离管理系统

gfast&#xff1a;基于全新Go Frame 2.3Vue3Element Plus构建的全栈前后端分离管理系统 随着信息技术的飞速发展和数字化转型的深入&#xff0c;后台管理系统在企业信息化建设中扮演着越来越重要的角色。为了满足市场对于高效、灵活、安全后台管理系统的需求&#xff0c;gfast应…

OpenUI 可视化 AI:打造令人惊艳的前端设计!

https://openui.fly.dev/ai/new 可视化UI的新时代&#xff1a;通过人工智能生成前端代码 许久未更新, 前端时间在逛github&#xff0c;发现一个挺有的意思项目&#xff0c;通过口语化方式生成前端UI页面&#xff0c;能够直观的看到效果&#xff0c;下面来给大家演示下 在现代…

SAP FS00如何导出会计总账科目表

输入T-code : S_ALR_87012333 根据‘FS00’中找到的总账科目&#xff0c;进行筛选执行 点击左上角的列表菜单&#xff0c;选择‘电子表格’导出即可

echarts-地图

使用地图的三种的方式&#xff1a; 注册地图(用json或svg,注册为地图)&#xff0c;然后使用map地图使用geo坐标系&#xff0c;地图注册后不是直接使用&#xff0c;而是注册为坐标系。直接使用百度地图、高德地图&#xff0c;使用百度地图或高德地图作为坐标系。 用json或svg注…

GpuMall智算云:meta-llama/llama3/Llama3-8B-Instruct-WebUI

LLaMA 模型的第三代&#xff0c;是 LLaMA 2 的一个更大和更强的版本。LLaMA 3 拥有 35 亿个参数&#xff0c;训练在更大的文本数据集上GpuMall智算云 | 省钱、好用、弹性。租GPU就上GpuMall,面向AI开发者的GPU云平台 Llama 3 的推出标志着 Meta 基于 Llama 2 架构推出了四个新…

Qt pro工程文件编写汇总(区分debug和release、32位和64位的方法,编译输出目录等)

前言&#xff1a; 从事qt开发已经好几年了&#xff0c;但有关pro编写的一些细节问题一直没有一个很好的梳理汇总——因为实际工作开发中&#xff0c;往往只需要编译特定版本的软件&#xff08;例如32位release版本&#xff09;&#xff0c;项目创建好后并设置好编译路径&#x…

ML307R OpenCPU GPIO使用

一、GPIO使用流程图 二、函数介绍 三、GPIO 点亮LED 四、代码下载地址 一、GPIO使用流程图 这个图是官网找到的&#xff0c;ML307R GPIO引脚电平默认为1.8V&#xff0c;需注意和外部电路的电平匹配&#xff0c;具体可参考《ML307R_硬件设计手册_OpenCPU版本适用.pdf》中的描…

零基础PHP入门(一)选择IDE和配置环境

配置环境 官网下载安装包&#xff0c;windows https://windows.php.net/download#php-8.3 我是下载的最新版&#xff0c;也可以切换其他版本 https://windows.php.net/downloads/releases/archives/ 下载好压缩文件后&#xff0c;双击解压到一个目录 D:\soft\php 复制ph…

成都爱尔眼科医院《中、欧国际近视手术大数据白皮书2.0》解读会圆满举行

2024年5月12日&#xff0c;爱尔眼科联合中国健康促进基金会健康传播与促进专项基金、新华社新媒体中心与中南大学爱尔眼科研究院、爱尔数字眼科研究所重磅发布《中、欧国际近视手术大数据白皮书2.0》。这是继2021、2022年在国内相继发布《国人近视手术白皮书》、《2022中、欧近…

模型蒸馏笔记

文章目录 一、什么是模型蒸馏二、如何蒸馏三、实践四、参考文献 一、什么是模型蒸馏 Hinton在NIPS2014提出了知识蒸馏&#xff08;Knowledge Distillation&#xff09;的概念&#xff0c;旨在把一个大模型或者多个模型ensemble学到的知识迁移到另一个轻量级单模型上&#xff0…

【SpringBoot】SpringBoot中防止接口重复提交(单机环境和分布式环境)

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 目录 &#x1f33c;前言 &#x1f512;单机环境下防止接口重复提交 &#x1f4d5;导入依赖 &#x1f4c2;项目结构 &#x1f680;创建自定义注解 ✈创建AOP切面 &#x1f697;创建Conotroller &#x1f4bb;分布…

构建高效的在线培训机构CRM应用架构实践

在当今数字化时代&#xff0c;在线培训已成为教育行业的重要趋势之一。为了提供更好的学习体验和管理服务&#xff0c;在线培训机构需要构建高效的CRM&#xff08;Customer Relationship Management&#xff09;应用架构。本文将探讨在线培训机构CRM应用架构的设计与实践。 一、…

绿色智能:AI机器学习在环境保护中的深度应用与实践案例

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

在vps的centos系统中用Python和青龙检测网页更新

环境&#xff1a;vps&#xff0c;centos7&#xff0c;python3.8.10&#xff0c;青龙面板&#xff08;用宝塔安装&#xff09; 任务&#xff1a;用python代码&#xff0c;监控一个网站页面是否有更新&#xff08;新帖子&#xff09;&#xff0c;若有&#xff0c;则提醒&#xf…

【数据结构】二叉树的认识与实现

目录 二叉树的概念&#xff1a; 二叉树的应用与实现&#xff1a; 二叉树实现接口&#xff1a; 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树 二叉树节点个数​编辑 二叉树叶子节点个数 二叉树第k层节点个数 二叉树查找值为x的节点​编辑 二叉树前序遍…

XSS+CSRF攻击

一、前言 在DVWA靶场的XSS攻击下结合CSRF攻击完成修改密码 也就是在具有XSS漏洞的情况下实施CSRF攻击 二、实验 环境配置与上一篇博客一致&#xff0c;有兴趣可以参考CSRF跨站请求伪造实战-CSDN博客 首先登录DVWA&#xff0c;打开XSS模块 name随便输入&#xff0c;message…

HQL面试题练习 —— 合并数据

题目来源&#xff1a;京东 目录 1 题目2 建表语句3 题解 1 题目 已知有数据 A 如下&#xff0c;请分别根据 A 生成 B 和 C。 数据A ------------ | id | name | ------------ | 1 | aa | | 2 | aa | | 3 | aa | | 4 | d | | 5 | c | | 6 | aa…

Android 使用 ActivityResultLauncher 申请权限

前面介绍了 Android 运行时权限。 其中&#xff0c;申请权限的步骤有些繁琐&#xff0c;需要用到&#xff1a;ActivityCompat.requestPermissions 函数和 onRequestPermissionsResult 回调函数&#xff0c;今天就借助 ActivityResultLauncher 来简化书写。 步骤1&#xff1a;创…

基于FPGA的VGA协议实现

文章目录 一、VGA介绍1.1 VGA原理1.2VGA电路 二、配置三、实现3.1 字符显示3.2图片显示 四、代码4.1.vga驱动模块4.2数据模块4.3按键消抖模块4.4顶层模块4.5TCL引脚绑定 参考 一、VGA介绍 1.1 VGA原理 VGA接口 最主要的几根线&#xff1a; VGA其实就是相当于一块芯片&#…

gcc g++不同版本切换命令

sudo update-alternatives --config g sudo update-alternatives --config gcc ubuntu20.04 切换 gcc/g 版本_ubuntu降低g版本-CSDN博客