【哈希】闭散列的线性探测和开散列的哈希桶解决哈希冲突(C++两种方法模拟实现哈希表)(2)

图片名称
🎉博主首页: 有趣的中国人

🎉专栏首页: C++进阶

🎉其它专栏: C++初阶 | Linux | 初阶数据结构

在这里插入图片描述

小伙伴们大家好,本片文章将会讲解 哈希函数与哈希 之 哈希桶解决哈希冲突 的相关内容。

如果看到最后您觉得这篇文章写得不错,有所收获,麻烦点赞👍、收藏🌟、留下评论📝。您的支持是我最大的动力,让我们一起努力,共同成长!

🎉系列文章: 1. 闭散列的线性探测实现哈希表

文章目录

  • `0. 前言`
  • `1. 何为开散列`
    • ==<font color = blue><b>🎧1.1 开散列的概念🎧==
    • ==<font color = blue><b>🎧1.2 开散列哈希表图示🎧==
  • `2. 开散列哈希表的实现`
    • ==<font color = blue><b>🎧2.1 开散列哈希表的结构🎧==
    • ==<font color = blue><b>🎧2.2 哈希桶插入Insert🎧==
    • ==<font color = blue><b>🎧2.3 哈希桶查找Find🎧==
    • ==<font color = blue><b>🎧2.4 哈希桶删除Erase🎧==
  • `3. 字符串哈希与仿函数`
  • `4.哈希桶实现哈希表完整代码`



0. 前言


在上一篇文章中我们详细描述了如何用 开放寻址法(闭散列)的线性探测 的方法来实现哈希表。此篇文章我们将用 开散列的哈希桶 来实现哈希表。




1. 何为开散列


🎧1.1 开散列的概念🎧


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

🎧1.2 开散列哈希表图示🎧


在这里插入图片描述

插入元素44

在这里插入图片描述

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。




2. 开散列哈希表的实现


🎧2.1 开散列哈希表的结构🎧


很明显,这个哈希表中存储了一个指针数组,我们可以用vector来实现,数组中的每个位置存储了一个节点类型的指针每个节点相当于是链表的一个节点,即:节点中有一个链表类型的指针,还有一个存放值的位置。

哈希节点和哈希表结构代码:

// 定义节点类型
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 HashFunc = HashFunc<K>>
class HashTable
{
public:typedef HashNode<K, V> Node;HashTable(size_t n = 10){_tables.resize(n);}
private:// 指针数组vector<Node*> _tables;// 存储的元素个数size_t _n = 0;
};

🎧2.2 哈希桶插入Insert🎧


插入元素的思路:

  1. 利用 哈希函数 计算出 要插入的值应该存放在哪个桶里面
  2. 之后在对应的桶中进行链表的头插:
    • 首先new一个哈希表的节点newnode
    • newnode->_next= _tables[i]
    • 再让newnode当作头:_tables[i] = newnode
  3. ++_n

关于哈希桶的扩容:

在线性探测中,当负载因子 load_factor 0.75 0.75 0.75 左右的时候就要进行扩容,但是在哈希桶中,我们可以适当让负载因子大一点,在STL库中,哈希桶的扩容是当负载因子等于 1 1 1 的时候进行扩容,即: n = = t a b l e . s i z e ( ) n == table.size() n==table.size()

注意:哈希桶中的负载因子是可以大于1的,因为一个桶中可能存储的不止一个值。


扩容思路1:

我们可以继续利用在线性探测的扩容思路:

  1. 新定义一个HashTable的对象newht,表的容量还是两倍;
  2. 遍历原始的HashTable中的vector _tables
    • 如果_tables[i]不为空,那么就调用newht.Insert()函数;
      • 定义一个节点类型的指针Node* cur = _tables[i]
      • 调用newht.Insert(cur->_kv);
      • 再让cur = cur->_next
    • 如果_tables[i]为空,就让i++
  3. 直到 i == _tables.size(),则newht插入完成;
  4. 最后两个_tables进行交换:_tables.swap(newht._tables)

但是这样扩容虽然可以,但是会很麻烦,因为:

  1. 由于每个哈希节点是new出来的,因此不能直接使用vector的析构函数,要自己写一个析构函数,不然会有内存泄漏;
  2. 每次调用newht.Insert()的时候都会重新new一个节点,原始的节点都会被释放,因此这样操作就会很麻烦编译器。

扩容代码(version1):

// 手动进行析构
~HashTable()
{for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;delete cur;cur = next;}}
}// 扩容代码
if (_n == _tables.size())
{// 方法1:新定义一个对象size_t newsize = 2 * _tables.size();HashTable<K, V> newht(newsize);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;newht.Insert(cur->_kv);cur = next;}}_tables.swap(newht._tables);
}

扩容思路2:

  1. 定义一个新表vector newtables,表的容量还是两倍;
  2. 遍历旧表,如果当前位置不为空,在新表中进行插入,思路如下:
    • 定义一个哈希节点指针Node* cur = _tables[i]
    • 通过cur->_kv.first 和 哈希函数 计算出 应该插入到新表的哪个桶中(hashi);
    • 由于插入之后会找不到下一个节点的位置,所以应该再定义一个Node* next = cur->next
    • 在新表中头插cur,还是同样的思路:
      • cur->_next = newtables[hashi]cur的下一个指向原始的头节点);
      • 接着让 newtables[hashi] = cur(让cur当头);
      • 插入完成让cur = next
      • 直到cur == nullptr,说明此桶中的节点都在新表中插入完成;
    • 让旧表中的_tables[i] = nullptr; (这部也可以不做,因为表不会调用析构函数,但是最好还是置空一下)
  3. 如果当前位置为空,则i++
  4. 直到 i == _tables.size(),说明此表的所有元素在新表中插入完成;
  5. 最后两表进行交换:_tables.swap(newtables)

扩容代码(version2):

if (_n == _tables.size())
{vector<Node*> newtable;// 两倍的旧表容量size_t newsize = 2 * _tables.size();newtable.resize(newsize);for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];Node* next = nullptr;while (cur){// 记录下一个位置next = cur->_next;// 计算在新表中的位置size_t hashi = cur->_kv.first % newtable.size();// cur的下一个位置指向原来的头cur->_next = newtable[hashi];// cur当头newtable[hashi] = cur;// 更新cur的位置cur = next;}// 旧表置空_tables[i] = nullptr;}_tables.swap(newtable);
}

完整的插入逻辑代码:

bool Insert(const pair<K, V>& kv)
{// 这边就是上一篇文章的仿函数HashFunc hf;// 查找思路待会实现if (Find(kv.first)){return false;}// 判断负载因子扩容// 负载因子为1扩容if (_n == _tables.size()){// 方法1:新定义一个对象/*size_t newsize = 2 * _tables.size();HashTable<K, V> newht(newsize);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;newht.Insert(cur->_kv);cur = next;}}_tables.swap(newht._tables);*/// 方法2:新定义一个表vector<Node*> newtable;size_t newsize = 2 * _tables.size();newtable.resize(newsize);for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;size_t hashi = hf(cur->_kv.first) % newtable.size();cur->_next = newtable[hashi];newtable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtable);}size_t hashi = hf(kv.first) % _tables.size();Node* newnode = new Node(kv);// 头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}

🎧2.3 哈希桶查找Find🎧


查找实现思路如下:

  1. 根据 key 和 哈希函数计算出对应的桶(hashi);
  2. 在此桶中进行寻找:
    • 定义一个哈希节点类型的指针Node* cur = _tables[hashi]
    • 一直向后寻找,直到找到或者 cur == nullptr(没有此元素)。
    • 找到返回此位置的指针,找不到返回空。

完整的查找逻辑代码:

Node* Find(const K& key)
{HashFunc hf;// 根据 `key` 和 哈希函数计算出对应的桶(`hashi`)size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}else{cur = cur->_next;}}return nullptr;
}

🎧2.4 哈希桶删除Erase🎧


删除实现思路如下:

  1. 根据 key 和 哈希函数计算出对应的桶(hashi);
  2. 在此桶中进行查找,这里要考虑要删除的节点的前一个节点是否为空;
  3. 如果前一个节点不为空,直接让prev->_next = cur->_next
  4. 如果前一个节点为空,就让 _tables[i] = cur->_next
  5. delete cur; cur = nullptr;
  6. 如果一直到 cur == nullptr 最后都未曾找到,则返回false
  7. 最后 --_n

完整的删除逻辑代码:

bool Erase(const K& key)
{HashFunc hf;//  根据 `key` 和 哈希函数计算出对应的桶(`hashi`);size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){// 如果前一个节点为空,就让 `_tables[i] = cur->_next`;if (prev == nullptr){_tables[hashi] = cur->_next;}// 如果前一个节点为空,就让 `_tables[i] = cur->_next`else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;
}		



3. 字符串哈希与仿函数


字符串哈希我们上一篇文章讲过::

  1. 当我们插入数字的类型,例如:double、float、int、 char、unsigned用的是一种类型的哈希函数
  2. 当我们插入字符串类型string的时候用的是另一种类型的哈希函数
  3. 🔎遇到这种情况的时候我们一般用仿函数来解决问题!!!🔍

因此我们要加一个仿函数的模板参数:class HashFunc

对于数字类型的仿函数代码:

template<class K>
struct Hash
{size_t operator()(const K& key){// 强转即可return (size_t)key;}
};

对于string类型的仿函数代码:

这里先写一下,待会再细谈:

struct StringFunc
{size_t operator()(const string& str){size_t ret = 0;for (auto& e : str){ret *= 131;ret += e;}return ret;}
};

由于string类型的哈希我们经常用,因此可以用模板的特化,并将此模板用缺省参数的形式传递,这样我们就不用在每次用的时候传入仿函数了。

template<class K>
struct Hash
{size_t operator()(const K& key){return (size_t)key;}
};template<>
struct Hash<string>
{size_t operator()(const string& str){size_t ret = 0;for (auto& e : str){ret *= 131;ret += e;}return ret;}
};




4.哈希桶实现哈希表完整代码



🎧有需要的小伙伴自取哈,博主已经检测过了,无bug🎧

🎨博主gitee链接: Jason-of-carriben 哈希桶实现哈希表完整代码

在这里插入图片描述

#pragma once
#include <iostream>
#include <vector>
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& str){size_t hash_value = 0;for (auto& e : str){hash_value = hash_value * 131 + e;}return hash_value;}
};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 HashFunc = HashFunc<K>>class HashTable{public:typedef HashNode<K, V> Node;HashTable(size_t n = 10){_tables.resize(n);}~HashTable(){for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;delete cur;cur = next;}}}bool Insert(const pair<K, V>& kv){HashFunc hf;if (Find(kv.first)){return false;}// 判断负载因子扩容// 负载因子为1扩容if (_n == _tables.size()){// 方法1:新定义一个对象/*size_t newsize = 2 * _tables.size();HashTable<K, V> newht(newsize);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;newht.Insert(cur->_kv);cur = next;}}_tables.swap(newht._tables);*/// 方法2:新定义一个表vector<Node*> newtable;size_t newsize = 2 * _tables.size();newtable.resize(newsize);for (size_t i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;size_t hashi = hf(cur->_kv.first) % newtable.size();cur->_next = newtable[hashi];newtable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtable);}size_t hashi = hf(kv.first) % _tables.size();Node* newnode = new Node(kv);// 头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}Node* Find(const K& key){HashFunc hf;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}else{cur = cur->_next;}}return nullptr;/*for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];Node* next = nullptr;while (cur){next = cur->_next;if (cur->_kv.first == key){return cur;}else{cur = next;}}}return nullptr;*/}bool Erase(const K& key){HashFunc hf;size_t hashi = hf(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;return true;}else{prev = cur;cur = cur->_next;}}return false;//for (size_t i = 0; i < _tables.size(); ++i)//{//	Node* prev = nullptr;//	Node* cur = _tables[i];//	//Node* next = nullptr;//	while (cur)//	{//		if (cur->_kv.first == key)//		{//			if (prev == nullptr)//			{//				_tables[i] = cur->_next;//			}//			else//			{//				prev->_next = cur->_next;//			}//			delete cur;//			return true;//		}//		else//		{//			prev = cur;//			cur = cur->_next;//		}//	}//}//return false;}private:vector<Node*> _tables;size_t _n = 0;};void HashTest1(){int a[] = { 10001,11,55,24,19,12,31,93,67,26 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(32, 32));//ht.Insert(make_pair(32, 32));ht.Erase(31);ht.Erase(10001);}void HashTest2(){string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };HashTable<string, string> countMap;for (auto& e : arr){countMap.Insert(make_pair(e, e));}}
}

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

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

相关文章

2024年5月份架构师考试真题完整版

截至2024-5-28 19:24:14已全部收录完成 共75到选择题&#xff0c;5道案例题&#xff0c;4道论文题。题目顺序不分先后。 全网最全的2024年5月份架构师考试真题回忆版&#xff0c;包含答案和解析。 群友 疯狂程序员 花落无声 半夏 鲁迅-三战老兵(预备役) 本次必成 锦鲤附体 2024…

超详细介绍基于微调 Faster R-CNN实现海上航拍图像中检测小物体-含源码与数据集免费下载

在航拍图像中检测小物体,尤其是对于海上救援等关键应用而言,是一项独特的挑战。及时检测水中的人可能意味着生死之间的差别。我们的研究重点是微调 Faster R-CNN(一种强大的两阶段物体检测器),以满足这一重要需求。 我们研究的核心是SeaDroneSee 数据集,这是一组重要的图…

618数码产品怎么选?四大必看推荐,自费无广测评

6.18盛宴即将开启&#xff0c;你是否已摩拳擦掌&#xff0c;准备在电商海洋中乘风破浪&#xff1f;然而&#xff0c;在繁多的商品和错综复杂的优惠面前&#xff0c;你是否感到些许迷茫&#xff1f;团团这位网购小能手&#xff0c;特地为大家梳理了一份精选购物清单。这些宝贝不…

sectigo企业通配符证书1590元13个月

通配符SSL证书可以同时保护多个域名站点&#xff0c;简化了用户管理SSL证书的流程&#xff0c;降低了管理成本&#xff0c;因此&#xff0c;不管是个人开发者还是企事业单位开发者都愿意尝试这款SSL证书产品。今天就随SSL盾小编了解Sectigo旗下的企业通配符SSL证书。 1.Sectig…

如何创建一个vue项目?详细教程,如何创建第一个vue项目?

已经安装node.js在自己找的到的地方新建一个文件夹用于存放项目&#xff0c;记住文件夹的存放路径&#xff0c;以我为例&#xff0c;我的文件夹路径为D:\tydic 打开cmd命令窗口&#xff0c;进入刚刚的新建文件夹 切换硬盘&#xff1a; D: 进入文件夹&#xff1a;cd tydic 使…

迈向F5G-A,开启全光万兆新时代——南通移动完成全市首个50G-PON技术验证

近日&#xff0c;南通移动在崇川区完成全市首个50G-PON万兆技术现网验证&#xff0c;标志着南通成为首批具备F5G-A(The 5th GenerationFixed Network-advanced)的万兆光网城市&#xff0c;使其成为网速最快、覆盖最全、时延最低的城市之一。 作为全光万兆的关键技术&#xff0c…

ScrumMaster认证机构及CSM、PSM、RSM价值比较

企业现有的经营管理模式和传统的瀑布式交付模式&#xff0c;已经不能适应快速变化的市场响应和客户需求&#xff0c;现代的敏捷工作方式在过去数年涌现&#xff0c;比如Scrum&#xff0c;XP&#xff0c;看板&#xff0c;DevOps等敏捷方法&#xff0c;近十年Scrum在国内企业中备…

近200个幼儿启蒙简笔画含步骤图含音频

其实早就想搞一下简笔画的相关数据&#xff0c;但奈何几乎所有简笔画类的数据中的图片都有大大的水印&#xff0c;今天正好遇到一个启蒙简笔画的数据&#xff0c;好的地方是&#xff1a;简笔画步骤都是4步&#xff0c;步骤都有说明&#xff0c;步骤图片没有水印&#xff0c;每个…

一款拥有15000+POC漏洞扫描工具

1 工具介绍 0x01 免责声明 请勿使用本文中所提供的任何技术信息或代码工具进行非法测试和违法行为。若使用者利用本文中技术信息或代码工具对任何计算机系统造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责。本文所提供的技术信息或代码工具仅供于学习&am…

深入分析 Android Activity (十)

文章目录 深入分析 Android Activity (十)1. Activity 的资源管理1.1 使用资源 ID 访问资源1.2 Drawable 资源1.3 使用 TypedArray 管理资源1.4 使用资源配置 2. Activity 的数据存储2.1 SharedPreferences2.2 文件存储2.3 SQLite 数据库2.4 ContentProvider 3. Activity 的性能…

一书读懂Python全栈安全,剑指网络空间安全

写在前面 通过阅读《Python全栈安全/网络空间安全丛书》&#xff0c;您将能够全面而深入地理解Python全栈安全的广阔领域&#xff0c;从基础概念到高级应用无一遗漏。本书不仅详细解析了Python在网络安全、后端开发、数据分析及自动化等全栈领域的安全实践&#xff0c;还紧密贴…

力扣刷题---409. 最长回文串【简单】

题目描述 给定一个包含大写字母和小写字母的字符串 s &#xff0c;返回 通过这些字母构造成的 最长的回文串 。 在构造过程中&#xff0c;请注意 区分大小写 。比如 “Aa” 不能当做一个回文字符串。 示例 1: 输入:s “abccccdd” 输出:7 解释: 我们可以构造的最长的回文串…

JVM的垃圾回收机制--GC

垃圾回收机制&#xff0c;是java提供的对于内存自动回收的机制。java不需要像C/C那样手动free()释放内存空间&#xff0c;而是在JVM中封装好了。垃圾回收机制&#xff0c;不是java独创的&#xff0c;现在应该是主流编程语言的标配。GC需要消耗额外的系统资源&#xff0c;而且存…

Codeforces Round 946 (Div. 3) A~G

A.Phone Desktop (枚举) 题意&#xff1a; 小 A A A的手机有一个桌面&#xff08;或称启动器&#xff09;。桌面可以由多个屏幕组成。每个屏幕表示为大小为 5 3 5 \times 3 53 的网格&#xff0c;即五行三列。 有 x x x 个应用程序的图标大小为 1 1 1 \times 1 11 个单…

学前基础知识

1、Java版本&#xff1a; 1995年发布第一个版本&#xff0c;创始人gosling。 可知&#xff0c; JAVA8 和 JAVA11 为长期版本&#xff0c;其他均非长期版本&#xff0c;因此主流都在用 JAVA8 或 JAVA11。 2、Java技术体系平台&#xff1a; 3、Java重要特点 ①Java语言是面向对象…

【IDEA】Redis可视化神器

在开发过程中&#xff0c;为了方便地管理 Redis 数据库&#xff0c;我们可能会使用一些数据库可视化插件。这些插件通常可以帮助你在 IDE 中直观地查看和管理 Redis 数据库&#xff0c;包括查看键值对、执行命令、监视数据库活动等。 IDEA作为IDE界的Jenkins&#xff0c;本身自…

游戏联运的挑战与核心关键点

​游戏联运一个看似充满机遇与挑战的行业&#xff0c;吸引了很多创业者的加入。然而&#xff0c;真正踏入这个行业后&#xff0c;许多人会发现&#xff0c;手游代理并非想象中的那么简单。今天&#xff0c;溪谷软件就来和大家聊聊游戏联运是怎么做的&#xff0c;需要注意什么。…

HTTP请求拦截器链

文章目录 HTTP请求拦截器链需求定义写一个Controller方法接口写三个http请求拦截器把拦截器加入到配置中&#xff0c;并且配置拦截规则在postman里面发送请求&#xff0c;看下测试结果是否正确 HTTP请求拦截器链 需求定义 我们写一个包含三个HTTP请求拦截器的拦截器链&#x…

MongoDB数据库(10亿条数据)清理策略: 自动化过期数据删除实战

1、引言 随着应用程序和业务数据的持续增长&#xff0c;有效地管理数据库存储空间成为维护系统性能的关键。在MongoDB这类NoSQL数据库中&#xff0c;定期清理过期数据变得尤为重要&#xff0c;这不仅能释放宝贵的存储资源&#xff0c;还能优化查询性能&#xff0c;确保数据库运…

PS:电子书App自动截图后合成一个PDF文档

说明&#xff1a;有的电子书App不能下载到本地&#xff0c;通过自动截图后合成一个PDF文档来解决&#xff01; 一、自动截图App 1.安装”免ROOT自动化助手“ 2.创建一个任务 3.编辑任务&#xff1a;根据电子书的操作顺序制定&#xff0c;400次就是书籍页数&#xff08;次数一…