C/C++ 比 ‘二叉树算法’ 效率更高的 ‘哈希表算法’

哈希表的简介

今天写下一篇哈希表算法,他的查找效率可是比二叉树还要高。

哈希表的运用场景还是挺多的,比如‘分布式文件系统存储引擎’、‘基因测试’等。

哈希表 又称 散列表,它是基于快速存取的角度设计的,也是一种典型的“空间换时间”的做法。

他主要是由五部分组成:

键(key)组员的编号 如, 1 、 5 、 19 。 。 。
值(value)组员的其它信息(包含 性别、年龄和战斗力等)
索引数组的下标(0,1,2,3,4) ,用以快速定位和检索数据
哈希桶保存索引的数组(链表或数组),数组成员为每一个索引值相同的多个元素
哈希函数将组员编号映射到索引上,采用求余法 ,如: 组员编号 19

哈希函数最常用的就是取余法了,当然也可以使用其他的方法,根据具体项目情况定夺。

解析:

  1. 键:他是唯一的,通过键可以快速找到其对应的值。
  2. 值:所存储的元素。
  3. 索引:可以通过索引找到在某个哈希桶里面,进而缩短了查询时间。
  4. 哈希桶:每个索引对应一个哈希桶,每个哈希桶里面存储n个值。
  5. 哈希函数:通过键除以哈希桶的数量取余,而得到索引。

如下图:
在这里插入图片描述

  1. 图中二十个数据,也就有对应的二十个键。
  2. 我们可以将其分成1 - 20 个哈希桶存储,这里就以5个桶为例。
  3. 0 - 19 是它的键,使用键除以哈希桶的数量取余,就可以得到键对应存储在哪个哈希桶里。
  4. 经过一轮的操作,就可以将全部数据存储进哈希桶里,形成一张完美的哈希表了。

在这里说明一下:为什么哈希桶的效率要比二叉树高?

以上表为例,假如我们需要查找键为3的数据,那么我们将13除以哈希桶的数量再取余,就可以得到对应哈希桶的索引,也就是下图这个桶(每一行代表一个哈希桶)。
在这里插入图片描述
可以看到我们一次就排除了4/5的数据,比二叉树一次只能排除一半的数据效率要高很多。
然后再从这个桶里面一个一个的比较就可以找到键为13的元素了。
当然,如果20个数据,我们使用二十个哈希桶来存储的话,只是需要一次比较就可以找到该值了,这就是上面所说的一种典型的“空间换时间”的做法。


算法实现

哈希表的定义

需要两个结构体:

  1. 哈希表所存储元素的结构体
  2. 哈希表的结构体

也需要一个默认的哈希桶的最大个数

#define DEFAULT_SIZE 16		// 默认最大哈希桶个数

// 哈希表的元素
typedef struct _LinkNode {
	struct _LinkNode *next;		// 下一个节点
	int key;					// 键值(唯一)
	void *data;					// 元素
}LinkNode;

// 两个类型一样,为了方便阅读代码,才分开两个
typedef LinkNode *Link;		// 链表
typedef LinkNode *Element;	// 节点


// 哈希表
typedef struct _HashTable {
	int tableSize;		// 哈希桶的个数(索引个数)
	Link *theLists;		// 存储所有哈希桶头节点的指针数组
}HashTable;

哈希表的初始化

哈希表使用最多的是使用链表的方式来存储数据。

所以初始化哈希表时,需要三个分配内存的步骤:

  1. 为整个哈希表分配内存;
    在这里插入图片描述
  2. 为存储哈希桶头部分配内存,通常为指针数组,且不存储元素(下表为了方便描述才…);
    在这里插入图片描述
  3. 为每个哈希桶的头节点分配内存。
    在这里插入图片描述

代码中都有详细注释:

// 初始化哈希表
HashTable *InitHashTable(int tableSize) {	// 参数一:哈希桶的数量(索引数量)
	HashTable *hTable = NULL;	// 定义哈希表对象返回

	// 检测哈希桶数量的合理性检查
	if (tableSize <= 0) {
		tableSize = DEFAULT_SIZE;	// 不合法将宏定义的默认值赋值给tableSize
	}

	// 哈希表分配内存
	hTable = (HashTable *)malloc(sizeof(HashTable));	// 相当于 hTable = new HashTable;
	if (!hTable) {
		cout << "HashTable 分配内容失败!" << endl;
		return NULL;
	}


	hTable->tableSize = tableSize;	// 赋值哈希桶数量

	//为 Hash 桶分配内存空间,其为一个指针数组
	hTable->theLists = (Link *)malloc(sizeof(Link) * tableSize);	// 相当于 hTable->theLists = new Link[tableSize];
	if (!hTable->theLists) {
		cout << "hTable->theLists 分配内存失败!" << endl;
		free(hTable);	// 释放哈希表的内存
		return NULL;
	}


	//为 Hash 桶对应的指针数组初始化链表节点
	for (int i = 0; i < tableSize; i++) {
		hTable->theLists[i] = (LinkNode *)malloc(sizeof(LinkNode));	// 相当于 hTable->theLists[i] = new LinkNode;
		if (!hTable->theLists[i]) {
			cout << "hTable->theLists[i] 分配内存失败!" << endl;
			free(hTable->theLists);	// 释放之前分配的所有内存
			free(hTable);			// 释放哈希表的内存
			return NULL;
		} else {
			// 将节点元素中的所有值都赋值0
			memset(hTable->theLists[i], 0, sizeof(LinkNode));
		}
	}


	return hTable;
}	

哈希表的查找

这时就需要使用到哈希函数了:

// 哈希函数,计算该键值对应的索引返回
int Hash(int key, int tableSize) {	// 参数一:键值;参数二:哈希桶的数量(索引数量)
	return (key % tableSize);	// 这里采用取余法
}

根据索引定位到具体的哈希桶,再通过链表的方式一个一个的遍历比较键,直到找到相同的键,将其对应的元素返回;当没有找到,就返回NULL。

// 从哈希表中根据键值查找元素
Element Find(HashTable *hTable, int key) {	// 参数一:哈希表;参数二:键值
	int i = 0;			// 保存键值对应的哈希桶的下标索引
	Link L = NULL;		// 保存对应哈希桶的头节点
	Element e = NULL;	// 用于循环遍历

	if (!hTable) {
		return NULL;
	}


	i = Hash(key, hTable->tableSize);	// 找到索引
	L = hTable->theLists[i];			// 将对应的哈希桶头节点赋值给L
	e = L->next;						// 从L的下一个节点开始查找

	// 结束条件:没有找到,返回NULL; 找到了,返回键值对应的节点
	while (e != NULL && e->key != key) {
		e = e->next;
	}

	return e;
}

哈希表的插入

  1. 先通过哈希表的查找函数进行键的配对,如果已经有相同的键了,根据具体项情况进行定夺,这里我就以输出一句话作为提示。
  2. 当哈希表中没有相同的键时,分配一个元素节点的内存,然后再根据键找到哈希桶的索引,再找到哈希桶,最后根据头插法进行插入(也可以使用尾插法等)。
// 哈希表插入元素,元素为键值对
void Insert(HashTable *hTable, int key, void *value) {	// 参数一:哈希表;参数二:键值;参数三:待插入的值
	int i = 0;				// 保存键值对应的哈希桶索引
	Link L = NULL;			// 保存键值对应的哈希桶的头节点
	Element e = NULL;		// 查找判断哈希表中是否已经存在相同的键值
	Element tem = NULL;		// new新对象插入
	

	if (!hTable || !value) {
		return;
	}

	// 查找判断哈希表中是否已经存在相同的键值
	e = Find(hTable, key);

	// 等于NULL说明没有哈希表中没有相同的键值,可以插入
	if (e == NULL) {

		// 为新插入的哈希元素分配内存
		tem = (Element)malloc(sizeof(LinkNode));		// 相当于 tem = new LinkNode;	
		if (!tem) {
			cout << "查找函数中的Element类型分配内存失败!" << endl;
			return;
		}

		// 保存值待插入
		tem->data = value;
		tem->key = key;

		i = Hash(key, hTable->tableSize);	// 获取索引
		L = hTable->theLists[i];			// 根据索引获取对应的哈希桶头节点

		// 使用头插法插入
		tem->next = L->next;
		L->next = tem;
	} else {
		cout << "插入失败,哈希表有重复的键值!" << endl;
	}
}	

哈希表的元素删除

  1. 由索引找到哈希桶,再有哈希桶的头节点开始遍历,一个变量保存对应键的元素,一个变量保存对应键的前一个元素。
  2. 进行第一个变量的判断,当他不等于NULL,说明哈希表中有该键值对应的元素。
  3. 将第二个变量的next指向待删除节点的next,就完成分离。最后free释放掉内存即可。
// 哈希表删除元素,元素为键值对
void Delete(HashTable *hTable, int key) {	// 参数一:哈希表;参数二:键值
	int i = 0;					// 保存键值对应的哈希桶索引
	Link L = NULL;				// 保存键值对应的哈希桶的头节点
	Element e = NULL;			// 保存待删除节点
	Element last = NULL;		// 保存待删除节点的前一个节点
	
	if (!hTable) {
		return;
	}

	i = Hash(key, hTable->tableSize);
	L = hTable->theLists[i];
	e = L->next;
	last = L;	

	while (e != NULL && e->key != key) {
		last = e;		// last永远指向待删除节点的前一个节点
		e = e->next;
	}

	//如果键值对存在
	if (e != NULL) {
		last->next = e->next;
		delete e;
	}
}	

哈希表的销毁

  1. 由for循环从索引0开始,将对应哈希桶里面的节点元素释放完后,再将哈希桶释放,然后索引值加一,直到释放完所有的哈希桶内存为止。
  2. 最后释放存储哈希桶头节点的指针数组内存和哈希表的内存。
// 销毁哈希表
void Destroy(HashTable *hTable) {
	Link L = NULL;			// 保存哈希桶头节点
	Element cur = NULL;		// 用于遍历与释放
	Element next = NULL;	// 用于辅助遍历与释放

	if (!hTable) {
		return;
	}

	// 从零开始遍历哈希桶,直到遍历完为止
	for (int i = 0; i < hTable->tableSize; i++) {
		L = hTable->theLists[i];	// 获取第i个哈希桶
		cur = L->next;				// 从哈希桶的下一个节点开始遍历

		while (cur != NULL) {
			next = cur->next;		// next指向当前节点的下一个节点
			free(cur);				// 释放当前节点
			cur = next;				// cur获取自己的下一个节点
		}

		free(L);					// 释放当前第i的哈希桶内存
	}

	free(hTable->theLists);			// 释放存储哈希桶头节点的内存
	free(hTable);					// 释放哈希表

	cout << "释放成功" << endl;
}


全部代码:

HashTable.h

#pragma once

#define DEFAULT_SIZE 16		// 默认最大哈希桶个数

// 哈希表的元素
typedef struct _LinkNode {
	struct _LinkNode *next;		// 下一个节点
	int key;					// 键值(唯一)
	void *data;					// 元素
}LinkNode;

// 两个类型一样,为了方便阅读代码,才分开两个
typedef LinkNode *Link;		// 链表
typedef LinkNode *Element;	// 节点


// 哈希表
typedef struct _HashTable {
	int tableSize;		// 哈希桶的个数(索引个数)
	Link *theLists;		// 存储所有哈希桶头节点的指针数组
}HashTable;



// 哈希函数,计算该键值对应的索引返回
int Hash(int key, int tableSize);	// 参数一:键值;参数二:哈希桶的数量(索引数量)


// 初始化哈希表
HashTable* InitHashTable(int tableSize);	// 参数一:哈希桶的数量(索引数量)


// 从哈希表中根据键值查找元素
Element Find(HashTable *hTable, int key);	// 参数一:哈希表;参数二:键值


// 哈希表插入元素,元素为键值对
void Insert(HashTable *hTable, int key, void *value);	// 参数一:哈希表;参数二:键值;参数三:待插入的值


// 哈希表删除元素,元素为键值对
void Delete(HashTable *hTable, int key);	// 参数一:哈希表;参数二:键值


// 销毁哈希表
void Destroy(HashTable* hTable);	// 参数一:哈希表


/*哈希表元素中提取数据*/
void *Retrieve(Element e);

HashTable.cpp

#include <iostream>
#include <Windows.h>
#include "HashTable.h"

using namespace std;

// 哈希函数,计算该键值对应的索引返回
int Hash(int key, int tableSize) {	// 参数一:键值;参数二:哈希桶的数量(索引数量)
	return (key % tableSize);	// 这里采用取余法
}



// 初始化哈希表
HashTable *InitHashTable(int tableSize) {	// 参数一:哈希桶的数量(索引数量)
	HashTable *hTable = NULL;	// 定义哈希表对象返回

	// 检测哈希桶数量的合理性检查
	if (tableSize <= 0) {
		tableSize = DEFAULT_SIZE;	// 不合法将宏定义的默认值赋值给tableSize
	}

	// 哈希表分配内存
	hTable = (HashTable *)malloc(sizeof(HashTable));	// 相当于 hTable = new HashTable;
	if (!hTable) {
		cout << "HashTable 分配内容失败!" << endl;
		return NULL;
	}


	hTable->tableSize = tableSize;	// 赋值哈希桶数量

	//为 Hash 桶分配内存空间,其为一个指针数组
	hTable->theLists = (Link *)malloc(sizeof(Link) * tableSize);	// 相当于 hTable->theLists = new Link[tableSize];
	if (!hTable->theLists) {
		cout << "hTable->theLists 分配内存失败!" << endl;
		free(hTable);	// 释放哈希表的内存
		return NULL;
	}


	//为 Hash 桶对应的指针数组初始化链表节点
	for (int i = 0; i < tableSize; i++) {
		hTable->theLists[i] = (LinkNode *)malloc(sizeof(LinkNode));	// 相当于 hTable->theLists[i] = new LinkNode;
		if (!hTable->theLists[i]) {
			cout << "hTable->theLists[i] 分配内存失败!" << endl;
			free(hTable->theLists);
			free(hTable);
			return NULL;
		} else {
			// 将节点元素中的所有值都赋值0
			memset(hTable->theLists[i], 0, sizeof(LinkNode));
		}
	}


	return hTable;
}	



// 从哈希表中根据键值查找元素
Element Find(HashTable *hTable, int key) {	// 参数一:哈希表;参数二:键值
	int i = 0;			// 保存键值对应的哈希桶的下标索引
	Link L = NULL;		// 保存对应哈希桶的头节点
	Element e = NULL;	// 用于循环遍历

	if (!hTable) {
		return NULL;
	}


	i = Hash(key, hTable->tableSize);	// 找到索引
	L = hTable->theLists[i];			// 将对应的哈希桶头节点赋值给L
	e = L->next;						// 从L的下一个节点开始查找

	// 结束条件:没有找到,返回NULL; 找到了,返回键值对应的节点
	while (e != NULL && e->key != key) {
		e = e->next;
	}

	return e;
}



// 哈希表插入元素,元素为键值对
void Insert(HashTable *hTable, int key, void *value) {	// 参数一:哈希表;参数二:键值;参数三:待插入的值
	int i = 0;				// 保存键值对应的哈希桶索引
	Link L = NULL;			// 保存键值对应的哈希桶的头节点
	Element e = NULL;		// 查找判断哈希表中是否已经存在相同的键值
	Element tem = NULL;		// new新对象插入
	

	if (!hTable || !value) {
		return;
	}

	// 查找判断哈希表中是否已经存在相同的键值
	e = Find(hTable, key);

	// 等于NULL说明没有哈希表中没有相同的键值,可以插入
	if (e == NULL) {

		// 为新插入的哈希元素分配内存
		tem = (Element)malloc(sizeof(LinkNode));		// 相当于 tem = new LinkNode;	
		if (!tem) {
			cout << "查找函数中的Element类型分配内存失败!" << endl;
			return;
		}

		// 保存值待插入
		tem->data = value;
		tem->key = key;

		i = Hash(key, hTable->tableSize);	// 获取索引
		L = hTable->theLists[i];			// 根据索引获取对应的哈希桶头节点

		// 使用头插法插入
		tem->next = L->next;
		L->next = tem;
	} else {
		cout << "插入失败,哈希表有重复的键值!" << endl;
	}
}	



// 哈希表删除元素,元素为键值对
void Delete(HashTable *hTable, int key) {	// 参数一:哈希表;参数二:键值
	int i = 0;					// 保存键值对应的哈希桶索引
	Link L = NULL;				// 保存键值对应的哈希桶的头节点
	Element e = NULL;			// 保存待删除节点
	Element last = NULL;		// 保存待删除节点的前一个节点
	
	if (!hTable) {
		return;
	}

	i = Hash(key, hTable->tableSize);
	L = hTable->theLists[i];
	e = L->next;
	last = L;	

	while (e != NULL && e->key != key) {
		last = e;		// last永远指向待删除节点的前一个节点
		e = e->next;
	}

	//如果键值对存在
	if (e != NULL) {
		last->next = e->next;
		delete e;
	}
}	


// 销毁哈希表
void Destroy(HashTable *hTable) {
	Link L = NULL;			// 保存哈希桶头节点
	Element cur = NULL;		// 用于遍历与释放
	Element next = NULL;	// 用于辅助遍历与释放

	if (!hTable) {
		return;
	}

	// 从零开始遍历哈希桶,直到遍历完为止
	for (int i = 0; i < hTable->tableSize; i++) {
		L = hTable->theLists[i];	// 获取第i个哈希桶
		cur = L->next;				// 从哈希桶的下一个节点开始遍历

		while (cur != NULL) {
			next = cur->next;		// next指向当前节点的下一个节点
			free(cur);				// 释放当前节点
			cur = next;				// cur获取自己的下一个节点
		}

		free(L);					// 释放当前第i的哈希桶内存
	}

	free(hTable->theLists);			// 释放存储哈希桶头节点的内存
	free(hTable);					// 释放哈希表

	cout << "释放成功" << endl;
}


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



int main(void) {
	//char *elem[] = { "迪丽热巴", "古力娜扎", "马尔扎哈" };
	char elem[][10] = { "迪丽热巴", "古力娜扎", "马尔扎哈" };
	HashTable *hTable = NULL;

	hTable = InitHashTable(16);

	// 默认第零个不存储元素
	Insert(hTable, 1, elem[0]);
	Insert(hTable, 2, elem[1]);
	Insert(hTable, 3, elem[2]);

	Delete(hTable, 1);


	for (int i = 0; i < 4; i++) {
		Element e = Find(hTable, i);
		if (e) {
			cout << (const char*)Retrieve(e) << endl;
		} else {
			cout << "没有找到!" << endl;
		}
		
	}

	Destroy(hTable);


	system("pause");
	return 0;
}

运行截图:
在这里插入图片描述
因为键值为零不存储元素,而且键值为一的元素已经被删除,所以运行结果才会由两个没有找到。这是正确的。



总结:
我个人感觉,只需要将哈希表的初始化搞懂了,他是如何分配内存的,是什么顺序分配内存,分配什么类型的内存。这些搞懂后,后面的查找、插入等,都是很简单的了。
笔者也是在哈希表的初始化那里卡了很久,一直没有搞懂他的分配内存,所以在哪里卡了很久,最总也还是吃透了。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cpp_learners

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值