哈希表的简介
今天写下一篇哈希表算法,他的查找效率可是比二叉树还要高。
哈希表的运用场景还是挺多的,比如‘分布式文件系统存储引擎’、‘基因测试’等。
哈希表 又称 散列表,它是基于快速存取的角度设计的,也是一种典型的“空间换时间”的做法。
他主要是由五部分组成:
– | – |
---|---|
键(key) | 组员的编号 如, 1 、 5 、 19 。 。 。 |
值(value) | 组员的其它信息(包含 性别、年龄和战斗力等) |
索引 | 数组的下标(0,1,2,3,4) ,用以快速定位和检索数据 |
哈希桶 | 保存索引的数组(链表或数组),数组成员为每一个索引值相同的多个元素 |
哈希函数 | 将组员编号映射到索引上,采用求余法 ,如: 组员编号 19 |
哈希函数最常用的就是取余法了,当然也可以使用其他的方法,根据具体项目情况定夺。
解析:
- 键:他是唯一的,通过键可以快速找到其对应的值。
- 值:所存储的元素。
- 索引:可以通过索引找到在某个哈希桶里面,进而缩短了查询时间。
- 哈希桶:每个索引对应一个哈希桶,每个哈希桶里面存储n个值。
- 哈希函数:通过键除以哈希桶的数量取余,而得到索引。
如下图:
- 图中二十个数据,也就有对应的二十个键。
- 我们可以将其分成1 - 20 个哈希桶存储,这里就以5个桶为例。
- 0 - 19 是它的键,使用键除以哈希桶的数量取余,就可以得到键对应存储在哪个哈希桶里。
- 经过一轮的操作,就可以将全部数据存储进哈希桶里,形成一张完美的哈希表了。
在这里说明一下:为什么哈希桶的效率要比二叉树高?
以上表为例,假如我们需要查找键为3的数据,那么我们将13除以哈希桶的数量再取余,就可以得到对应哈希桶的索引,也就是下图这个桶(每一行代表一个哈希桶)。
可以看到我们一次就排除了4/5的数据,比二叉树一次只能排除一半的数据效率要高很多。
然后再从这个桶里面一个一个的比较就可以找到键为13的元素了。
当然,如果20个数据,我们使用二十个哈希桶来存储的话,只是需要一次比较就可以找到该值了,这就是上面所说的一种典型的“空间换时间”的做法。
算法实现
哈希表的定义
需要两个结构体:
- 哈希表所存储元素的结构体
- 哈希表的结构体
也需要一个默认的哈希桶的最大个数
#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;
哈希表的初始化
哈希表使用最多的是使用链表的方式来存储数据。
所以初始化哈希表时,需要三个分配内存的步骤:
- 为整个哈希表分配内存;
- 为存储哈希桶头部分配内存,通常为指针数组,且不存储元素(下表为了方便描述才…);
- 为每个哈希桶的头节点分配内存。
代码中都有详细注释:
// 初始化哈希表
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;
}
哈希表的插入
- 先通过哈希表的查找函数进行键的配对,如果已经有相同的键了,根据具体项情况进行定夺,这里我就以输出一句话作为提示。
- 当哈希表中没有相同的键时,分配一个元素节点的内存,然后再根据键找到哈希桶的索引,再找到哈希桶,最后根据头插法进行插入(也可以使用尾插法等)。
// 哈希表插入元素,元素为键值对
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;
}
}
哈希表的元素删除
- 由索引找到哈希桶,再有哈希桶的头节点开始遍历,一个变量保存对应键的元素,一个变量保存对应键的前一个元素。
- 进行第一个变量的判断,当他不等于NULL,说明哈希表中有该键值对应的元素。
- 将第二个变量的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;
}
}
哈希表的销毁
- 由for循环从索引0开始,将对应哈希桶里面的节点元素释放完后,再将哈希桶释放,然后索引值加一,直到释放完所有的哈希桶内存为止。
- 最后释放存储哈希桶头节点的指针数组内存和哈希表的内存。
// 销毁哈希表
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;
}
运行截图:
因为键值为零不存储元素,而且键值为一的元素已经被删除,所以运行结果才会由两个没有找到。这是正确的。
总结:
我个人感觉,只需要将哈希表的初始化搞懂了,他是如何分配内存的,是什么顺序分配内存,分配什么类型的内存。这些搞懂后,后面的查找、插入等,都是很简单的了。
笔者也是在哈希表的初始化那里卡了很久,一直没有搞懂他的分配内存,所以在哪里卡了很久,最总也还是吃透了。