C++数据结构:散列表简单实现(hash表)

news2025/1/12 13:34:05

文章目录

  • 前言
  • 一、设计思想
  • 二、实现步骤
    • 1、定义节点
    • 2、定义Hash表类
  • 三、数据示例
  • 总结


前言

散列表是一种常用的数据结构,它可以快速地存储和查找数据。散列表的基本思想是,将数据的关键字映射到一个有限的地址空间中,然后在该地址空间中存储数据。这样,当需要查找某个数据时,只需要计算其关键字的映射地址,然后在该地址处访问数据,从而实现高效的查找操作。

一、设计思想

散列表的核心是散列函数,也称为哈希函数。散列函数的作用是,将任意长度的关键字转换为一个固定长度的地址,这个地址就是散列值或哈希值。散列函数应该满足以下几个要求:

  • 一致性:相同的关键字应该产生相同的散列值。

  • 高效性:散列函数的计算应该简单快速,不占用过多的时间和空间。

  • 均匀性:散列函数应该尽可能地将关键字均匀地分布在地址空间中,避免产生冲突。冲突是指不同的关键字映射到相同的地址的情况,这是不可避免的。因此,散列表还需要一个解决冲突的方法。常用的解决冲突的方法有两种:

  • 开放寻址法:当发生冲突时,按照某种规则在地址空间中寻找下一个空闲的位置,直到找到或者遍历完整个地址空间为止。这种方法需要保证地址空间有足够的容量,否则会导致插入失败或者查找效率降低。

  • 链地址法:当发生冲突时,将具有相同地址的数据组织成一个链表,每个地址处只存储链表的头指针。这种方法不受地址空间大小的限制,但是需要额外的空间存储链表节点。

下面我们用C++实现一个简单的散列表,采用除留余数法作为散列函数,采用链地址法作为解决冲突的方法。除留余数法是指,将关键字除以一个素数p,然后取余数作为散列值。我们假设关键字是整数类型,素数p是10(也可以不是素数,但因为不是素数能被更多的数整除,冲突概率大,所以文中用了1,3,7,11做演示),地址空间大小也是10。
在这里插入图片描述

二、实现步骤

首先,我们定义一个链表节点类Node,用来存储数据和指向下一个节点的指针:

1、定义节点

这是一个链表的节点定义,它的作用是存储同一hash值(也叫散列值)的数据,所以它有next指向下一个节点,从设计思想来看,hash函数设计得越好,这个单向链表越短,那么效率就越高。最差情况当然就是所有的数据经过hash函数计算后的值都一样,那就成了一个单向链表,这种情况是一定要避免的。

class Node{
	public:
		int key;        //键
		int value;      //值
		Node* next;            //指向下一个结点的指针
		Node(int k, int v){    //构造函数
			key = k;
			value = v;
			next = nullptr;
		}
};

2、定义Hash表类

然后我们定义一个散列表类,用来管理结点和提供基本的操作:
int hash(int key),这个就是散列函数,它只简单计算了余数。对于散列函数来说,应在hash值尽量分布均匀的基础上设计简单点,不然把大把时间都花在计算hash值上了。这里是以整数作为例子的,所以直接模运算了,实际泛型编程要考虑其它类型数据的hash值计算。

class HashTable{
	private:
		int capacity;   //数组的容量
		int size;       //当前存储的键值对的数量
		Node** array;   //指向数组的指针
		int hash(int key){    //散列函数,简单地取模
			return key % capacity;
		}
		
	public:
		HashTable(int c){   //构造函数,初始化数组和其他变量
			capacity = c;
			size = 0;
			array = new Node*[capacity];
			for (int i = 0; i < capacity; i++){
				array[i] = nullptr;
			}
		}
		
		~HashTable(){       //析构函数,释放内存
			for (int i = 0; i < capacity; i++){
				Node* cur = array[i];
				while (cur != nullptr){
					Node* next = cur->next;
					delete cur;
					cur = next;
				}
			}
			delete[] array;
		}

以上代码定义了散列表,就是一个构造函数和析构函数,比较简单。有一定C++语言基础的都能看明白,既然来看这篇文章了,肯定是有C++基础的。

Node** array; //指向数组的指针 这是一个二级指针,指向一个存放指针的数组。这个数组的下标就是我们hash计算的目标。这个数组中存放了每个链表的链表头节点。

		
	    void insert(int key, int value){      //插入操作
	        int index = hash(key);           //计算键对应的位置
	        Node* cur = array[index];        //获取该位置的链表头结点
	        while (cur != nullptr){         //遍历链表,查找是否已经存在相同的键
	            if (cur->key == key){       //如果存在,更新值并返回
	                cur->value = value;
	                return;
	            }
	            cur = cur->next;
	        }
	        //如果不存在,创建新的结点,并插入到链表头部
	        Node* newNode = new Node(key, value);
	        newNode->next = array[index];
	        array[index] = newNode;
	        size++;          //更新键值对数量
	    }
    
	    int get(int key){          //查找操作
	        int index = hash(key);    //计算键对应的位置
	        Node* cur = array[index];  //获取该位置的链表头结点
	        while (cur != nullptr){      //遍历链表,查找是否存在相同的键
	            if (cur->key == key){    //如果存在,返回值
	                return cur->value;
	            }
	            cur = cur->next;
	        }
	        //如果不存在,返回-1表示错误
	        return -1;
	    }

以上是插入、取值的操作,具体看注释就明白实现过程了。
以下是删除键值对的方法,因为实际存储数据是单向链表,所以删除操作时要记录上一个节点的指针,略微麻烦一点。

	    void remove(int key){      //删除操作
	        int index = hash(key);    //计算键对应的位置
	        Node* cur = array[index];   //获取该位置的链表头结点
	        Node* prev = nullptr;       //记录前一个结点的指针
	        while (cur != nullptr) {    //遍历链表,查找是否存在相同的键
	            if (cur->key == key){      //如果存在,删除结点,并更新链表和数组
	                if (prev == nullptr){     //如果是头结点,直接更新数组
	                    array[index] = cur->next;
	                } else {                 //如果不是头结点,更新前一个结点的指针
	                prev->next = cur->next;
	                }
	                delete cur;       //释放内存
	                size--;           //更新键值对数量
	                return;
	            }
	            prev = cur;         //更新前一个结点的指针
	            cur = cur->next;       //移动到下一个结点
	        }
	        //如果不存在,不做任何操作
	    }
};

以上是具体功能实现的函数,和前面链表的功能有很多相似之处。加上代码的详细注释,相信是很容易看明白的。流程很简单,根据传入的 key ,用 hash 函数计算得到一个值,因为是模运算,这个值肯定在0到9之间(这里以10为例,事实上也可以是任何其它正整数),就以这个余数作为arrary数组的下标,这个数组中存储的又是一个单向链表的头节点。我们就到这个链表中去具体匹配是哪个节点进行增加删除工作。

三、数据示例

以上就是我们用C++实现的散列表类。我们可以用以下代码测试它的功能:

#include <iostream>
using namespace std;

int main(){
    HashTable ht(13); // 创建一个容量为10的散列表对象

	ht.insert(1, 10); // 插入一些键值对
	ht.insert(7, 20);
	ht.insert(3, 30);
	ht.insert(11, 110);

	cout << ht.get(1) << endl; // 查找一些键,并打印结果
    cout << ht.get(7) << endl;
	cout << ht.get(3) << endl;
	cout << ht.get(11) << endl;

	ht.remove(2); // 删除一个键

	cout << ht.get(2) << endl; // 再次查找该键,并打印结果

	return 0;
}

输出结果如下:

10
20
30
110
-1

根据程序设计,要求“键”和“值”都是整数。所以我们传入的参数是两个整数。ht.insert(1, 10); // 插入一些键值对,这里的参数1是作为key的,参数10是作为value的。散列表还有个不正规的名字叫“键值对”。它是根据“键”去确定“值”存储位置的,当然也根据“键”去取“值”。设计优秀的散列表存取速度是很快的。它和数据库的设计思想是有相通之处的。所以在示例中,和图上画的一样,“key”:1 和“key”:11 都是存在下标为1的数组指针指向的链表,根本不存在下标为11的情况。而ht.get(2)hash计算的结果是2,数组下标2是空指针,所以输出是-1。


总结

可以看到,我们实现的散列表可以正确地执行插入、查找和删除操作。当然,这只是一个简单的示例,实际上还有很多细节和优化可以考虑。例如,如何选择合适的散列函数和容量?如何动态地调整容量?如何处理负数或其他类型的键?如何提高代码的可读性和可维护性?等等。这些问题都可以作为进一步学习和探索的方向。

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

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

相关文章

Nacos源码-从Demo出发研究事件驱动与观察者模式的应用

在我们分析 Nacos 源码时&#xff0c;会看见大量的事件发布的动作&#xff0c;不管是客户端注册/下线、服务改变、服务订阅等等都是利用了事件发布。 下面我在自己的项目中&#xff0c;引入Nacos的依赖进行一个简单的demo的演示&#xff0c;我个人认为其和spring容器的listene…

Koa学习1:初始化项目

前言 作为前端开发者&#xff0c;最适合我们的后端就是node了&#xff0c;node的框架挺多的。选择Koa是因为国内用的挺多的、关于这方面的教程也很多、而且比较适合小项目。 学习教程是&#xff1a;【杰哥课堂】-项目实战-NodeKoa2从零搭建通用API服务 写这些文章&#xff0…

K8s in Action 阅读笔记——【5】Services: enabling clients to discover and talk to pods

K8s in Action 阅读笔记——【5】Services: enabling clients to discover and talk to pods 你已了解Pod以及如何通过ReplicaSets等资源部署它们以确保持续运行。虽然某些Pod可以独立完成工作&#xff0c;但现今许多应用程序需要响应外部请求。例如&#xff0c;在微服务的情况…

在Python中载入大量图片型数据集,与matlab结合使用时,如何解决RAM的占用爆炸性增长的问题

在Python中载入大量图片时&#xff0c;由于每张图片都会被转换成Numpy数组并存储在内存中&#xff0c;因此可能会导致RAM的占用爆炸性增长。为了减少RAM的使用&#xff0c;可以考虑采用以下方法&#xff1a; Python和Matlab结合使用。首先&#xff0c;可以使用Python的Pillow库…

【Linux】遇事不决,可先点灯,LED驱动的进化之路---1

【Linux】遇事不决&#xff0c;可先点灯&#xff0c;LED驱动的进化之路---1 前言&#xff1a; 一、最简单的LED驱动程序 1.1 字符设备驱动程序框架 1.2 程序实战 1.2.1 驱动程序&#xff08;led_drive_simple.c&#xff09; 1.2.2 应用程序&#xff08;led_test_simple.c…

C#,码海拾贝(25)——求解“三对角线方程组”的“追赶法”之C#源代码,《C#数值计算算法编程》源代码升级改进版

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 求解线性方程组的类 LEquations /// 原作 周长发 /// 改编 深度混淆 /// </summary> public static partial class LEquations { /// <summary> /…

Apache Kafka - 理解Kafka内部原理

文章目录 Kafka的实现机制1. 集群成员关系&#xff1a;2. 控制器*&#xff1a;3. Kafka的复制&#xff1a;4. 请求处理&#xff1a;5. 物理存储&#xff1a; 导图 Kafka的实现机制 作为Kafka专家&#xff0c;我很高兴为您深入解释Kafka的实现机制。我将从以下几个方面对Kafka进…

ARM体系结构与异常处理

目录 一、ARM体系架构 1、ARM公司概述 ARM的含义 ARM公司 2.ARM产品系列 3.指令、指令集 指令 指令集 ARM指令集 ARM指令集 Thumb指令集 &#xff08;属于ARM指令集&#xff09; 4.编译原理 5.ARM数据类型 字节序 大端对齐 小端对齐 …

VTK安装和运行

创建日期: 2019-04-02 09:19:00 开始 学习资源 官方网站&#xff1a;https://vtk.org/ GitHub&#xff1a;https://github.com/Kitware/VTK 官方教程&#xff1a;https://vtk.org/Wiki/VTK/Tutorials 官方文档&#xff1a;https://vtk.org/documentation/ 用户手册&#…

RocketMQ 学习教程——(一)安装 RocketMQ

文章目录 RocketMQ 安装下载安装上传服务器配置环境变量修改 runserver.sh修改 runbroker.sh修改 broker.conf启动 安装 RocketMQ 控制台安装Linux 防火墙命令 Docker 安装 RocketMQ拉取镜像启动 NameServer 服务启动 Broker 服务启动控制台 RocketMQ 官网&#xff1a; http://…

​【编写UI自动化测试集】Appium+Python+Unittest+HTMLRunner​

简介 获取AppPackage和AppActivity 定位UI控件的工具 脚本结构 PageObject分层管理 HTMLTestRunner生成测试报告 启动appium server服务 以python文件模式执行脚本生成测试报告 下载与安装 下载需要自动化测试的App并安装到手机 获取AppPackage和AppActivity 方法一 有源码的…

算法11.从暴力递归到动态规划4

算法|11.从暴力递归到动态规划4 1.最长公共子序列 题意&#xff1a;给定两个字符串str1和str2&#xff0c;返回这两个字符串的最长公共子序列长度 比如 &#xff1a; str1 “a12b3c456d”,str2 “1ef23ghi4j56k” 最长公共子序列是“123456”&#xff0c;所以返回长度6 解…

【PowerShell】PowerShell 7.1 之后版本的安装

当前以下操作系统支持PowerShell 7.1 版本的安装,非Windows 系统支持的版本和要求有一定的限制。 Windows 8.1/10 (including ARM64)Windows Server 2012 R2, 2016, 2019, and Semi-Annual Channel (SAC)Ubuntu 16.04/18.04/20.04 (including ARM64)Ubuntu 19.10 (via Snap pa…

图的邻接矩阵表示

设图有n个顶点&#xff0c;则邻接矩阵是一个n*n的方阵&#xff1b;若2个顶点之间有边&#xff0c;则方阵对应位置的值为1&#xff0c;否则为0&#xff1b; 看几个例子&#xff1b; 此图的邻接矩阵是 0 1 1 1 1 0 1 0 1 1 0 1 1 0…

学习 xss+csrf 组合拳

目录 1.xss基础铺垫 1.1反射型xss 1.2存储型xss 1.3基于DOM的xss 1.4xss漏洞的危害 1.5xss漏洞的黑盒测试 1.6xss漏洞的白盒测试 2.csrf基础铺垫 2.1csrf攻击原理 2.2csrf攻击防护 3.应用案例 3.1存储型xsscsrf组合拳 3.2csrfselfxss组合拳 1.xss基础铺垫 跨站脚…

线程和进程

进程和线程的区别(超详细) 与进程不同的是同类的多个线程共享进程的堆和方法区资源&#xff0c;但每个线程有自己的程序计数器、虚拟机栈和本地方法栈&#xff0c;所以系统在产生一个线程&#xff0c;或是在各个线程之间作切换工作时&#xff0c;负担要比进程小得多&#xff0…

【架构】常见技术点--服务治理

导读&#xff1a;收集常见架构技术点&#xff0c;作为项目经理了解这些知识点以及解决具体场景是很有必要的。技术要服务业务&#xff0c;技术跟业务具体结合才能发挥技术的价值。 目录 1. 微服务 2. 服务发现 3. 流量削峰 4. 版本兼容 5. 过载保护 6. 服务熔断 7. 服务…

微服务之流量控制

Informal Essay By English I have been thinking about a question recently, what is the end of coding? 参考书籍&#xff1a; “凤凰架构” 流量控制 任何一个系统的运算、存储、网络资源都不是无限的&#xff0c;当系统资源不足以支撑外部超过预期的突发流量时&…

数字信号处理8:利用Python进行数字信号处理基础

我前两天买了本MATLAB信号处理&#xff0c;但是很无语&#xff0c;感觉自己对MATLAB的语法很陌生&#xff0c;看了半天也觉得自己写不出来&#xff0c;所以就对着MATLAB自己去写用Python进行的数字信号处理基础&#xff0c;我写了两天左右&#xff0c;基本上把matlab书上的代码…

【数据结构】轻松掌握二叉树的基本操作及查找技巧

二叉树的基本操作 ​ 在学习二叉树的基本操作前&#xff0c;需先要创建一棵二叉树&#xff0c;然后才能学习其相关的基本操作。由于现在大家对二 叉树结构掌握还不够深入&#xff0c;为了降低学习成本&#xff0c;此处手动快速创建一棵简单的二叉树&#xff0c;快速进入二叉树操…