哈希表----数据结构

news2024/11/25 12:34:21

引入

如果你是一个队伍的队长,现在有 24 个队员,需要将他们分成 6 组,你会怎么分?其实有一种方法是让所有人排成一排,然后从队头开始报数,报的数字就是编号。当所有人都报完数后,这 24 人也被分为了 6 组,看下方。

(你可能会让 1~4 号为第一组,5~8 号为第二组……但是这样有新成员加入时,就很难决定新成员的去向)

编号除以 6 能被整除的为第一组: 6        12        18        24

编号除以 6 余数是 1 的为第二组:1         7         13        19

编号除以 6 余数是 2 的为第三组:2         8         14        20

编号除以 6 余数是 3 的为第四组:3         9         15        21

编号除以 6 余数是 4 的为第五组:4         10       16        22

编号除以 6 余数是 5 的为第六组:5         11       17        23

OK呀,也是分好了。通过这种方式划分小组,无论是往小组中添加成员,还是快速确定成员的小组都非常方便,例如新加一个队员编号为 25 号,就能够很从容地让他加入到第二组。这种编号方式就是高效的散列,或者称为“哈希”,所以我们经常听说的哈希表也叫做散列表。(哈希就是Hash英文的音译,而Hash的意思是散列)

以上的过程是通过把关键码值 key (编号) 映射到表中的一个位置(数组的下标)来访问记录,从而加快了查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

上面的例子中每个人的编号都是一个关键码值 key ,例如 17 通过映射函数(编号%6 )就能得到一个位置 5 ,就能在数组里下标为 5 的位置找到第六组的所有成员,从而快速找到 17 所在的位置,如下图。

到这里你可以想象一下,一个 key 经过了加工得到了所在的地址,知道地址就可以快速访问所在的地方,就比如你知道一个学生的学号,你通过一系列操作你可以得知那个学生所在的宿舍楼,甚至知道所在的宿舍,从而去那个宿舍交流一下。

哈希表概念

散列表-哈希表 它是基于快速存取的角度设计的,“以空间换时间”。

为什么哈希表很快呢?例如在上面的例子中,问你队伍中存在编号为 17 的队员吗?如果你一个一个遍历你要遍历 24次,不能排除任何一个数据。假设数组有序,你使用二分查找一次也只能排除一半的数据,但是你使用哈希表你可以一次排除六分之五的数据,只需要到第六组中去遍历了,假如小组数量变多是不是效率更高了。

键(key):组员的编号,如:1、15、36……每个编号都是独一无二的,具有唯一性,为了快速访问到某一个组员。

值(value):组员存储的信息,可以是一个整型,可以是一个结构体、也可以是一个类。

索引:用 key 映射到数组的下标(0,1,2,3,4,5)用来快速定位并检索数据

哈希桶:用来保存索引的数组(或链表)存放的成员为索引值相同的组员

映射函数:将文件编号映射到索引上,采用求余法。如文件编号 17 % 5,得到索引 2 

哈希表的实现

哈希表的数据结构定义

我的哈希桶的实现方式是链表。

#define DEFAULT_SIZE 16 //默认的哈希表大小

typedef struct _ListNode //哈希链表
{
	void* date;		//值,指向保存的数据
	int key;		//键
	struct _ListNode* next;
}ListNode;

typedef ListNode* List;
typedef ListNode* Element; 

typedef struct _HashTable
{
	int TableSize;
	List* lists;    //二级指针,指向指针数组,指针数组里的元素是一级指针,指向了哈希桶
}HashTable;

 

我们是用指向HashTable的指针,访问lists二级指针,lists[i]【(*lists + i)】都是指向了ListNode的指针,用lists[i]去访问哈希桶,如果不太明白可以看看哈希表的初始化。

哈希函数

其实哈希函数的参数不仅仅只有整型,例如参数可以是一个字符串,将字符串的首字符的ASCII码返回也是一个函数。只需要对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址就可以了。

如图:

int Hash(int key, int TableSize)
{
	//根据 key 得到索引,也就得到了哈希桶的位置
	return (key % TableSize);
}

初始化哈希表

HashTable* initHash(int TableSize)
{
	if (TableSize <= 0) //哈希桶的数量不能小于 1
	{
		TableSize = DEFAULT_SIZE; //使用默认的大小
	}

	HashTable* Hash = NULL; //指向哈希表结构体
	Hash = (HashTable * ) malloc(sizeof(HashTable));

	if (Hash == NULL) //防御性编程
	{
		cout << "哈希表分配内存失败" << endl;
		return NULL;
	}

	//为二级指针分配内存,指向指针数组,每一个指针指向一个哈希桶
	Hash->lists = (List*)malloc(sizeof(List) * TableSize);

	if (Hash->lists == NULL) //防御性编程
	{
		cout << "哈希表分配内存失败" << endl;
		free(Hash); // 运行到这一步,说明Hash不为空,别忘记释放
		return NULL;
	}

	for (int i = 0; i < TableSize; i++)
	{
		Hash->lists[i] = (ListNode*)malloc(sizeof(ListNode));

		if (Hash->lists[i] == NULL) //防御性编程
		{
            for (int j = 0; j < i; j++)
            {
	            free(Hash->lists[j]);
            }
			free(Hash->lists);
			free(Hash);
			return NULL;
		}
		else
		{
			memset(Hash->lists[i], 0, sizeof(ListNode)); //将指向的节点初始化,全部变成0
		}
	}

	return Hash;
}

查找这个键在哈希表中是否存在

Element Find(HashTable* hash, int key)
{
	int i = 0;
	List list = NULL;
	Element e = NULL; //Element 和 List 本质一样

	i = Hash(key, hash->TableSize); //确定哈希桶位置
	list = hash->lists[i];			//指向哈希桶
	e = list->next;		
	while (e != NULL && e->key != key)
	{
		e = e->next;
	}

	return e; //存在就返回这个节点,不存在就返回 NULL
}

哈希表插入元素

//哈希表插入元素,键key和值(*date)
void insertHash(HashTable* hash, int key, void* date) 
{
	Element e = NULL , temp = NULL; //temp为指向新加节点
	List lists = NULL;
	e = Find(hash, key);

	if (e == NULL)
	{
		temp = (ListNode*)malloc(sizeof(ListNode));
		if (temp == NULL) //防御性编程
		{
			cout << "为新加节点分配内存失败" << endl;
			return;
		}
		lists = hash->lists[Hash(key, hash->TableSize)]; //指向新加节点所在的哈希桶

		//采用前插法,插入节点
		temp->date = date;
		temp->key = key;
		temp->next = lists->next;
		lists->next = temp;


	}
	else
	{
		cout << "此键已经存在于哈希表" << endl;
	}
}

哈希表删除元素

void deleteHash(HashTable* hash, int key)
{
	int i = 0;

	i = Hash(key, hash->TableSize);
	Element e = NULL,last = NULL;

	List  l = NULL;
	l = hash->lists[i];

	last = l;
	e = l->next;

	while (e != NULL && e->key != key)
	{
		last = e; //last保存着要删除的节点的上一个节点
		e = e->next;
	}
	
	if (e != NULL) //存在这个元素
	{
		last->next = e->next;
		free(e);
	}
	else
	{
		;
	}
}

哈希表元素中提取数据

void* getDate(Element e)
{
    return e ? e->date : NULL;
}

销毁哈希表

void destoryHash(HashTable* hash)
{
	int i = 0;
	
	List l = NULL;
	Element tmp = NULL,next = NULL;
	for (int i = 0; i < hash->TableSize; i++)
	{
		l = hash->lists[i];

		tmp = l->next; 
		while (tmp != NULL)
		{
			next = tmp->next;
			free(tmp);
			tmp = next;
		}
		free(l);
	}
	free(hash->lists);
	free(hash);
}

全部代码

#define _CRT_SECURE_NO_WARNINGS 1


#include <iostream>
using namespace std;


#define DEFAULT_SIZE 16 //默认的哈希表大小

typedef struct _ListNode //哈希链表
{
	void* date;		//值,指向保存的数据
	int key;		//键
	struct _ListNode* next;
}ListNode;

typedef ListNode* List;
typedef ListNode* Element;

typedef struct _HashTable
{
	int TableSize;
	List* lists;
}HashTable;


//哈希函数
int Hash(int key, int TableSize)
{
	//根据 key 得到索引,也就得到了哈希桶的位置
	return (key % TableSize);
}

//初始化哈希表
HashTable* initHash(int TableSize)
{
	if (TableSize <= 0) //哈希桶的数量不能小于 1
	{
		TableSize = DEFAULT_SIZE; //使用默认的大小
	}

	HashTable* Hash = NULL; //指向哈希表
	Hash = (HashTable * ) malloc(sizeof(HashTable));

	if (Hash == NULL) 
	{
		cout << "哈希表分配内存失败" << endl;
		return NULL;
	}

	//为哈希桶分配内存,为指针数组,每一个指针指向一个哈希桶
	Hash->lists = (List*)malloc(sizeof(ListNode*) * TableSize);

	if (Hash->lists == NULL) //防御性编程
	{
		cout << "哈希表分配内存失败" << endl;
		free(Hash); // 运行到这一步,说明Hash不为空,别忘记释放
		return NULL;
	}

	for (int i = 0; i < TableSize; i++)
	{
		Hash->lists[i] = (ListNode*)malloc(sizeof(ListNode));

		if (Hash->lists[i] == NULL) //防御性编程
		{
            for (int j = 0; j < i; j++)
            {
	            free(Hash->lists[j]);
            }
			free(Hash->lists);
			free(Hash);
			return NULL;
		}
		else
		{
			memset(Hash->lists[i], 0, sizeof(ListNode)); //将指向的节点初始化
		}
	}

	return Hash;
}

//查找这个键在哈希表中是否存在
Element Find(HashTable* hash, int key)
{
	int i = 0;
	List list = NULL;
	Element e = NULL;

	i = Hash(key, hash->TableSize); //确定哈希桶位置
	list = hash->lists[i];			//指向哈希桶
	e = list->next;		
	while (e != NULL && e->key != key)
	{
		e = e->next;
	}

	return e; //存在就返回这个节点,不存在就返回 NULL
}

//哈希表插入元素,键key和值(*date)
void insertHash(HashTable* hash, int key, void* date) 
{
	Element e = NULL , temp = NULL; //temp为指向新加节点
	List lists = NULL;
	e = Find(hash, key);

	if (e == NULL)
	{
		temp = (ListNode*)malloc(sizeof(ListNode));
		if (temp == NULL) //防御性编程
		{
			cout << "为新加节点分配内存失败" << endl;
			return;
		}
		lists = hash->lists[Hash(key, hash->TableSize)]; //指向新加节点所在的哈希桶

		//采用前插法,插入节点
		temp->date = date;
		temp->key = key;
		temp->next = lists->next;
		lists->next = temp;


	}
	else
	{
		cout << "此键已经存在于哈希表" << endl;
	}
}

//哈希表删除元素
void deleteHash(HashTable* hash, int key)
{
	int i = 0;

	i = Hash(key, hash->TableSize);
	Element e = NULL,last = NULL;

	List  l = NULL;
	l = hash->lists[i];

	last = l;
	e = l->next;

	while (e != NULL && e->key != key)
	{
		last = e; //last保存着要删除的节点的上一个节点
		e = e->next;
	}
	
	if (e != NULL) //存在这个元素
	{
		last->next = e->next;
		free(e);
	}
	else
	{
		;
	}
}

//哈希表元素中提取数据
void* getDate(Element e)
{
	return e ? e->date : NULL;
}

//销毁哈希表
void destoryHash(HashTable* hash)
{
	int i = 0;
	
	List l = NULL;
	Element tmp = NULL,next = NULL;
	for (int i = 0; i < hash->TableSize; i++)
	{
		l = hash->lists[i];

		tmp = l->next; 
		while (tmp != NULL)
		{
			next = tmp->next;
			free(tmp);
			tmp = next;
		}
		free(l);
	}
	free(hash->lists);
	free(hash);
}
int main(void)
{
	const char* elems[] = { "苍老师","一花老师","天老师" };

	HashTable *hash = NULL;
	hash = initHash(31);


	insertHash(hash, 1, (void*)elems[0]);
	insertHash(hash, 2, (void*)elems[1]);
	insertHash(hash, 3, (void*)elems[2]);
	deleteHash(hash, 3);

	for (int i = 0; i < 3; i++)
	{
		Element e = Find(hash, i + 1);
		if (e)
		{
			cout << (const char *)getDate(e) << endl;
		}
		else
		{
			cout << "键值为" << i + 1 << "的元素不存在" << endl;
		}
	}

	return 0;
}

如果不太理解的话,可以多看看结构体的定义和初始化,谢谢你看到这里!

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

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

相关文章

【Python环境管理工具】Anaconda安装及使用教程

Anaconda安装及使用教程 1 Anaconda简介2 Anaconda下载及安装2.1 下载及安装2.2 手动配置环境变量&#xff08;重点&#xff09;2.3 测试Anaconda环境是否配置成功 3 Anaconda使用教程3.1 Anaconda Prompt环境管理的常用命令3.1.1 检查conda3.1.2 管理运行环境 4 Pycharm与Anac…

C++ STL 迭代器失效

一、学习资料 STL迭代器的使用 二、vector容器获取值是下标法和at()的区别 vector<int> vA; int array[]{0,1,2,3,4}; vA.assign(array,array5); cout<<vA[6]<<endl; cout<<va.at(6)<<endl;如上述代码&#xff0c;当使用vA[6]的方式出现访问越…

EASYX播放音频文件

添加winmm.lib的依赖 选中链接器中的输入选项&#xff1a;添加附加依赖项winmm.lib并且应用即可 添加音频相关代码 #include <easyx.h> #include <stdio.h> #include <math.h> // 宏定义 #define WINDOW_WIDTH 800 #define WINDOW_HEIGHT 600 #define MAX_…

K8s学习笔记——资源组件篇

引言 前一篇文章我们介绍了K8s的概念理解和常用命令&#xff0c;这篇我们重点介绍K8s的资源组件和相关配置使用。 1. Node & Pod Node: 是 Pod 真正运行的主机&#xff0c;可以是物理机&#xff0c;也可以是虚拟机。为了管理 Pod&#xff0c;每个 Node 节点上至少要运行…

剖析 Tomcat 线程池与 JDK 线程池的区别和联系

文章目录 引言JDK 线程池Tomcat 线程池Tomcat 连接器Tomcat 的 ExecutorTomcat 线程池配置 区别和联系区别联系 结论 &#x1f389;欢迎来到Java面试技巧专栏~剖析 Tomcat 线程池与 JDK 线程池的区别和联系 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a…

qt报错permission denied

写fk项目的时候&#xff0c;报这个错&#xff0c;然后网上查&#xff0c;说的是因为之前运行的qt进程没有关闭&#xff0c;然后我在任务管理器上查看&#xff0c;却没有看见有我正在运行的qt程序&#xff0c;我再出现清除 qmake也不可以&#xff0c;然后我再去删除out目录下的所…

软件提示msvcp110.dll丢失的5个修复方法,快速解决dll丢失问题

你是否曾遇到过msvcp110.dll文件丢失的情况&#xff1f;msvcp110.dll是Microsoft Visual C 2012运行时库中的一个动态链接库文件&#xff0c;它是Visual Studio 2012编译的程序所必需的。它包含了许多C标准库函数的实现&#xff0c;如字符串处理、数学运算等。当我们运行一个程…

C# 文件 文件夹 解除占用

文件/文件夹 解除占用或直接删除。 编程语言&#xff1a;C# 这个就不用过多功能描述了。 注册windows 文件/文件夹 右键菜单。 文件夹解除占用&#xff1a;遍历文件夹所有文件&#xff0c;判断是否被占用&#xff0c;先解除文件占用&#xff0c;后解除文件夹占用&#xff0…

Pikachu(一)

暴力破解 Burte Force&#xff08;暴力破解&#xff09;概述 “暴力破解”是一攻击手段&#xff0c;在web攻击中&#xff0c;一般会使用这种手段对应用系统的认证信息进行获取。 其过程就是使用大量的认证信息在认证接口进行尝试登录&#xff0c;直到得到正确的结果。 为了提高…

uni-app学习笔记

目录 一、前期准备 1、项目认识 2、pages.json基本配置 3、创建页面 二、tabBar 1、获取图标 2、代码配置 三、基础认识 1、页面生命周期 2、App.vue应用生命周期 四、基础组件 1、scroll-view可滚动视图区域 2、提示框 3、swiper滑块视图容器 4、form表单组件 一…

第 370 周赛 100112. 平衡子序列的最大和(困难,离散化,权值树状数组)

太难了&#xff0c;看答案理解了半天 题目的要求可以理解为 nums[ij] - ij > nums[ii] - ii &#xff0c;所以问题化为求序列 bi nums[i] - i 的非递减子序列的最大元素和需要前置知识&#xff0c;离散化&#xff0c;树状数组离散化&#xff1a;将分布大却数量少(即稀疏)的…

【嵌入式框架】搭建调试输出、建立时间系统

一、Zorb简介 Zorb Framework是一个基于面向对象的思想来搭建一个轻量级的嵌入式框架。 搭建Zorb Framework的目的是为在不能运行Linux的芯片上快速开发应用&#xff0c;不用反复造轮子。 Zorb Framework的初步设计功能有&#xff1a; 1、时间系统功能zf_time 2、环形缓冲…

微信小程序获取openid

1.需要小程序中调用 wx.login获取临时code值&#xff08;每次获取的code值只能用一次&#xff09; wx.login({success (res) {console.log(res)} }) 打印结果为&#xff1a; 2.调用微信提供的apid接口&#xff0c;获取openid&#xff0c;入参需要三个参数&#xff1a;AppID(小…

SPSS单因素方差分析

前言&#xff1a; 本专栏参考教材为《SPSS22.0从入门到精通》&#xff0c;由于软件版本原因&#xff0c;部分内容有所改变&#xff0c;为适应软件版本的变化&#xff0c;特此创作此专栏便于大家学习。本专栏使用软件为&#xff1a;SPSS25.0 本专栏所有的数据文件请点击此链接下…

Python 用户输入和字符串格式化指南

Python 允许用户输入数据。这意味着我们可以向用户询问输入。在 Python 3.6 中&#xff0c;使用 input() 方法来获取用户输入。在 Python 2.7 中&#xff0c;使用 raw_input() 方法来获取用户输入。以下示例要求用户输入用户名&#xff0c;并在输入用户名后将其打印在屏幕上&am…

Prometheus接入AlterManager配置企业微信告警(基于K8S环境部署)

文章目录 一、创建企业微信机器人二、配置AlterManager告警发送至企业微信三、Prometheus接入AlterManager配置四、部署PrometheusAlterManager(放到一个Pod中)五、测试告警 注意&#xff1a;请基于 PrometheusGrafana监控K8S集群(基于K8S环境部署)文章之上做本次实验。 一、创…

IDEA快捷键总结+常识积累

&#xff08;一&#xff09;常用快捷键总结 以下快捷键输入完成后按Tab键即可。 1、输入main public static void main(String[] args) {}2、输入sout System.out.println();3、输入fori for (int i 0; i < ; i) {}4、输入foreach&#xff08;增强for循环快捷键&#x…

CMake引用QT、CMake构建一个转换为3d tile的开源代码

在CMake里单独运行一下 find_package(Qt5 REQUIRED COMPONENTS Core Xml Test) ,Core Xml Test 这三个是需要的qt组件; 情况如下;提示找不到QT; 根据资料,cmake引用qt需要3-4个方面, 首先Qt包含三个编译工具:moc、uic、rcc, moc:元对象编译器(Meta O…

翻页电子杂志制作功略,快收藏,保管好用!

翻页电子杂志&#xff0c;我相信这对大家很熟悉吧&#xff0c;大家也都经常看电子杂志吧。它和我们的生活紧密相关&#xff0c;也极大地改变了我们的阅读方式。听到这“翻页电子杂志”&#xff0c;是不是觉得制作起来肯定很难很复杂&#xff0c;需要专业的人才能制作呢&#xf…

2023 年最值得推荐的 10 款 iPhone 数据恢复软件

iPhone 从来都不是一个简单的打电话电话。它就像一台微型电脑&#xff0c;让我们互相联系、拍照、拍视频、发邮件、看文档、看书。然而&#xff0c;随着它成为日常生活的必需品&#xff0c;我们总是容易因各种原因丢失数据&#xff0c;如删除、恢复出厂设置、iOS 错误、文件同步…