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

news2024/12/29 11:08:57

目录

一、前言

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
{
public:
	typedef HashNode<K, V> Node;
	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.coloradmin.cn/o/1697926.html

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

相关文章

0成本的副业兼职,虚拟资源项目,1单利润49,操作简单变现快

最新刷某音时&#xff0c;我意外地发现了一位同行&#xff0c;他正在出售一份某音运营和直播的资料。然而&#xff0c;他销售这份资料的方式非常独特。他将这些所谓的某音运营资料全部打印出来。 周周近财&#xff1a;让网络小白少花冤枉钱&#xff0c;赚取第一桶金 每个视频的…

GESP等级大纲

CCF编程能力等级认证概述 CCF编程能力等级认证&#xff08;GESP&#xff09;为青少年计算机和编程学习者提供学业能力验证的规则和平台。GESP覆盖中小学阶段&#xff0c;符合年龄条件的青少年均可参加认证。C & Python编程测试划分为一至八级&#xff0c;通过设定不同等级…

CAD二次开发(2)-将直线对象添加到CAD图形文件

1. 准备工作 创建一个类库项目&#xff0c;如下&#xff1a; 2. 分析Line对象 Line类的初始化方法和参数 using Autodesk.AutoCAD.DatabaseServices; Line line new Line();Line 继承Curve 继承Entity 继承DBObject 继承Drawable 继承RXObject 初始化方法有两个&#xf…

探索微软Edge开发者工具:优化前端开发的艺术与科学

探索微软Edge开发者工具&#xff1a;优化前端开发的艺术与科学 引言&#xff1a;Edge开发者工具概览一、基础操作&#xff1a;步入DevTools的大门1.1 启动与界面布局1.2 快速导航与定制 二、元素审查与样式调整2.1 精准元素选取2.2 实时CSS编辑2.3 自动完成与内联文档 三、Java…

代码随想录|Day56|动态规划 part16|● 583. 两个字符串的删除操作 ● 72. 编辑距离

583. 两个字符串的删除操作 class Solution: def minDistance(self, word1: str, word2: str) -> int: dp [[0] * (len(word2) 1) for _ in range(len(word1) 1)] for i in range(len(word1) 1): dp[i][0] i for j in range(len(word2) 1): dp[0][j] j for i in rang…

js 生成二维码

第一种&#xff0c;需要先下载包 npm install qrcode <template><div><h2>二维码</h2><img :src"qrCodeImage" alt""></div> </template><script> import QRCode from qrcode export default {dat…

【OpenGL实践12】关于缓存区Framebuffer的运用

文章目录 一、说明二、帧缓冲区三、创建新的帧缓冲区四、附属装饰4.1 纹理图像4.2 渲染缓冲区对象图像 五、使用帧缓冲区5.1 后期处理5.2 更改代码 六、后期处理效果6.1 色彩处理6.2 模糊6.3 Sobel算子 七、结论练习 一、说明 关于FrameBuffer的使用&#xff0c;是OpenGL的高级…

spark实战:实现分区内求最大值,分区间求和以及获取日志文件固定日期的请求路径

spark实战&#xff1a;实现分区内求最大值&#xff0c;分区间求和以及获取日志文件固定日期的请求路径 Apache Spark是一个广泛使用的开源大数据处理框架&#xff0c;以其快速、易用和灵活的特点而受到开发者的青睐。在本文中&#xff0c;我们将通过两个具体的编程任务来展示S…

spiderfoot一键扫描IP信息(KALI工具系列九)

目录 1、KALI LINUX简介 2、spiderfoot工具简介 3、在KALI中使用spiderfoot 3.1 目标主机IP&#xff08;win&#xff09; 3.2 KALI的IP 4、命令示例 4.1 web访问 4.2 扫描并进行DNS解析 4.3 全面扫描 5、总结 1、KALI LINUX简介 Kali Linux 是一个功能强大、多才多…

Spring Boot集成testcontainers快速入门Demo

1.什么是testcontainers&#xff1f; Testcontainers 是一个用于创建临时 Docker 容器进行单元测试的 Java 库。当我们想要避免使用实际服务器进行测试时&#xff0c;它非常有用。&#xff0c;官网介绍称支持50多种组件。​ 应用场景 数据访问层集成测试&#xff1a; 使用My…

掌握ASPICE标准:汽车软件测试工程师的专业发展路径

掌握ASPICE标准&#xff1a;汽车软件测试工程师的专业发展路径 文&#xff1a;领测老贺 随着新能源汽车在中国的蓬勃发展&#xff0c;智能驾驶技术的兴起&#xff0c;汽车测试工程师的角色变得愈发关键。这一变革带来了前所未有的挑战和机遇&#xff0c;要求测试工程师不仅要具…

Matlab中函数或变量 ‘eeglab‘ 无法识别

EEGLAB 没有安装或添加到 MATLAB 路径中&#xff1a; 确保已经安装了 EEGLAB&#xff0c;并且将其添加到 MATLAB 的路径中。您可以通过在 MATLAB 命令窗口中运行 which eeglab 来检查是否能够找到 EEGLAB。 EEGLAB 函数路径设置错误&#xff1a; 如果已经安装了 EEGLAB&#x…

Mac | Mac 移动硬盘无法分区问题

现象问题 电脑配置&#xff1a;MacBook Pro M1&#xff0c;系统 Sonoma Mac 系统新升级了 Sonoma&#xff0c;结果出现各种问题。外接屏幕居然不能旋转 90 &#xff0c;查了一下是Sonoma系统导致的&#xff0c;以及莫名发热的问题。想着要么回退一下系统算了&#xff0c;于是网…

Sql Sever删除数据库时提示数据库正在被使用,解决办法

报错解释&#xff1a; 当您尝试删除SQL Server中的某个对象&#xff08;如数据库、表等&#xff09;时&#xff0c;如果有程序或进程正在使用该对象&#xff0c;您可能会收到一个错误信息&#xff0c;提示该对象正被使用。这通常是因为还有一个或多个数据库连接仍然保持着对该…

使用libtorch加载YOLOv8生成的torchscript文件进行目标检测

在网上下载了60多幅包含西瓜和冬瓜的图像组成melon数据集&#xff0c;使用 LabelMe 工具进行标注&#xff0c;然后使用 labelme2yolov8 脚本将json文件转换成YOLOv8支持的.txt文件&#xff0c;并自动生成YOLOv8支持的目录结构&#xff0c;包括melon.yaml文件&#xff0c;其内容…

网络通信(二)

UDP通信 特点&#xff1a;无连不是先接、不可靠通信 不事先建立连接&#xff1b;发送端每次把要发送的数据&#xff08;限制在64KB内&#xff09;、接收端IP、等信息封装成一个数据包&#xff0c;发出去就不管了 java提供了一个java.net.DatagramSocket类来实现UDP通信 Dat…

20.SkyWalking

一.简介 SkyWalking用于应用性能监控、分布式链路跟踪、诊断&#xff1a; 参考连接如下&#xff1a; https://github.com/apache/skywalking https://skywalking.apache.org/docs/ 二.示例 通过官网连接进入下载页面&#xff1a;https://archive.apache.org/dist/skywalkin…

普通人转行程序员,最大的困难是找不到就业方向

来百度APP畅享高清图片 大家好&#xff0c;这里是程序员晚枫&#xff0c;小破站也叫这个名。 我自己是法学院毕业后&#xff0c;通过2年的努力才转行程序员成功的。[吃瓜R] 我发现对于一个外行来说&#xff0c;找不到一个适合自己的方向&#xff0c;光靠努力在一个新的行业里…

美团Java社招面试题真题,最新面试题

如何处理Java中的内存泄露&#xff1f; 1、识别泄露&#xff1a; 使用内存分析工具&#xff08;如Eclipse Memory Analyzer Tool、VisualVM&#xff09;来识别内存泄露的源头。 2、代码审查&#xff1a; 定期进行代码审查&#xff0c;关注静态集合类属性和监听器注册等常见内…

Leetcode算法题笔记(3)

目录 矩阵101. 生命游戏解法一解法二 栈102. 移掉 K 位数字解法一 103. 去除重复字母解法一 矩阵 101. 生命游戏 根据 百度百科 &#xff0c; 生命游戏 &#xff0c;简称为 生命 &#xff0c;是英国数学家约翰何顿康威在 1970 年发明的细胞自动机。 给定一个包含 m n 个格子…