哈希表
(Hash Table),它通过哈希函数将键值映射到特定的数组索引,从而实现高效的查找、插入和删除操作。其核心思想是将数据直接存储到具有固定大小的数组中,通过哈希函数计算出每个数据的存储位置。
主要特性
-
哈希函数:哈希函数用于将输入的键(通常是字符串或数字)转换为数组中的索引。理想的哈希函数应尽量避免不同的键映射到同一索引(称为冲突)。
-
冲突处理:
- 开放地址法:在发生冲突时,通过线性探查、二次探查或双重哈希等方式,寻找数组中下一个空闲位置。
- 链地址法:在每个哈希表的索引处,维护一个链表或其他容器来存储所有具有相同哈希值的元素。
-
时间复杂度:
- 查找、插入和删除的平均时间复杂度为 O(1)O(1)O(1),但在最坏情况下(大量冲突)可能退化为 O(n)O(n)O(n)。
-
负载因子:负载因子是哈希表中存储的元素数量与哈希表大小的比值。当负载因子过大时,哈希表的性能会下降,通常通过扩展哈希表(重新哈希)来解决。
哈希表的应用
- 数据库索引
- 缓存(如 LRU Cache)
- 唯一性检测(如查找重复项)
- 字典/映射实现
优缺点
-
优点:
- 查找、插入和删除的平均时间复杂度为常数时间 O(1)O(1)O(1)。
- 适合快速查找和存储大量数据。
-
缺点:
- 当哈希函数设计不当或负载因子过大时,性能会急剧下降。
- 存在内存浪费,特别是在使用开放地址法时,需要额外的存储空间。
哈希冲突
发生在两个不同的键通过哈希函数映射到相同的数组索引时。为了解决哈希冲突,常见的解决办法主要有以下两种:开放地址法和链地址法,每种方法又有不同的变种和策略。
1. 链地址法(Separate Chaining)
链地址法是最常见的哈希冲突解决方案。它通过在哈希表的每个索引处维护一个链表(或其他容器),当多个键被映射到同一索引时,直接将它们放入这个链表中。
- 优点:
- 简单直观,容易实现。
- 不受表大小限制,可以处理超过哈希表容量的数据。
- 缺点:
- 如果冲突过多,链表长度变长,查找效率会退化到 O(n)O(n)O(n)。
- 改进方法:
- 使用自平衡二叉搜索树或跳表代替链表,从而在冲突严重时保持较好的性能。
2. 开放地址法(Open Addressing)
开放地址法通过在哈希表中寻找下一个可用的空闲位置来存储冲突的元素。它不使用外部链表,而是在数组内部解决冲突。
2.1 线性探查(Linear Probing)
当发生冲突时,按照线性顺序(即每次向前移动一格)依次检查下一个位置,直到找到一个空闲的槽位。
-
公式:
h(k, i) = (h(k) + i) % m
,其中i
表示冲突次数,m
为哈希表大小。 -
优点:
- 实现简单。
- 连续数据的访问具有较高的缓存命中率。
-
缺点:
-
容易产生主堆积现象(primary clustering):即多个连续的空位被占用后,新的元素很容易探查到这些连续区域,进一步加剧冲突。
-
算法时间复杂度
执行这个算法所花时间的度量
将数据量增长和时间增长用函数表示出来,这个函数就叫做时间复杂度。
一般用大O表示法:O(n)-----时间复杂度是关于数据n的一个函数
随着n的增加,时间复杂度增长较慢的算法时间复杂度低
时间复杂度的计算规则
1,用常数1 取代运行时间中的所有加法常数
2,在修改后的运行函数中,只保留最高阶项。
3,如果最高阶存在且系数不是1,则去除这个项相乘的常数。
哈希表相关操作的函数接口
#ifndef __HASH_H__ // 防止头文件重复包含,定义唯一的头文件保护符。
#define __HASH_H__
#include <head.h> // 包含标准或自定义的头文件,用于提供头文件的依赖。
#define HASH_SIZE 27 // 定义哈希表的大小,这里使用 27 个槽位(26 个字母 + 1 个非字母字符槽位)。
/**
* @brief 定义存储的数据类型,每个哈希节点存储一个用户的姓名和电话。
*/
typedef struct per
{
char name[64]; // 用户的姓名,最多 64 个字符。
char tel[32]; // 用户的电话号码,最多 32 个字符。
} HsDatetype;
/**
* @brief 定义哈希表节点,每个节点包含用户数据和指向下一个节点的指针(用于解决冲突时的链表)。
*/
typedef struct hashnode
{
HsDatetype data; // 该节点存储的用户数据(姓名和电话)。
struct hashnode *pnext; // 指向下一个节点的指针,用于处理哈希冲突(链表法)。
} Hsnode_t;
/**
* @brief 计算哈希值的函数,根据输入字符返回对应的哈希表索引。
* @param key 输入的字符(通常是姓名的首字母)。
* @return 返回计算得到的哈希表索引。
*/
int hashfuction(char key);
/**
* @brief 向哈希表中插入一个数据。
* @param data 要插入的用户数据(包含姓名和电话号码)。
* @return 插入成功返回 0,失败返回 -1。
*/
int insert_hatable(HsDatetype data);
/**
* @brief 遍历哈希表,输出所有存储的数据。
* @return 成功返回 0。
*/
int traverse_table();
/**
* @brief 查找哈希表中是否存在指定名字的用户数据。
* @param name 要查找的名字。
* @return 如果找到,返回指向该节点的指针;如果未找到,返回 NULL。
*/
Hsnode_t *fine_table(char *name);
/**
* @brief 删除哈希表中指定名字的用户数据,并将删除的数据存储到指定指针中。
* @param name 要删除的名字。
* @param data 用于存储删除的数据的指针。
* @return 成功删除返回 1,未找到返回 0。
*/
int delete_hatable(char *name, HsDatetype *data);
/**
* @brief 删除哈希表中的所有数据,释放内存。
* @return 成功返回 0。
*/
int delete_table();
#endif // __HASH_H__ 结束头文件保护符。
函数详细部分
#include "hash.h" // 包含哈希表相关的头文件,定义了数据类型和常量(如 HASH_SIZE)。
// 定义哈希表为全局变量,每个位置存储指向链表头节点的指针,初始化为NULL。
Hsnode_t *hashtable[HASH_SIZE] = {NULL};
/**
* @brief 哈希函数,将字符转换为哈希表的索引。
* @param key 输入的字符(通常是姓名的首字母)。
* @return 返回该字符在哈希表中的索引。
*/
int hashfuction(char key)
{
// 如果是小写字母,将其转换为从 0 开始的索引 ('a' -> 0, 'b' -> 1, ...)。
if(key >= 'a' && key <= 'z')
{
return key - 'a';
}
// 如果是大写字母,也转换为从 0 开始的索引 ('A' -> 0, 'B' -> 1, ...)。
else if(key >= 'A' && key <= 'Z')
{
return key - 'A';
}
// 如果不是字母字符,则返回哈希表的最后一个位置。
else
{
return HASH_SIZE - 1;
}
}
/**
* @brief 向哈希表中插入数据。
* @param data 要插入的用户数据(包含姓名和电话信息)。
* @return 成功返回 0,失败返回 -1。
*/
int insert_hatable(HsDatetype data)
{
// 根据名字的第一个字符计算哈希值,得到存储位置。
int addr = hashfuction(data.name[0]);
// 为新节点分配内存空间,存储数据。
Hsnode_t *pnode = (Hsnode_t*)malloc(sizeof(Hsnode_t));
if(NULL == pnode) // 如果内存分配失败,打印错误并返回 -1。
{
perror("malloc fail\n");
return -1;
}
// 初始化新节点的指针和数据。
pnode->pnext = NULL;
pnode->data = data;
// 如果当前哈希表位置为空,将新节点直接插入此处。
if(hashtable[addr] == NULL)
{
hashtable[addr] = pnode;
return 0;
}
// 如果哈希表位置已有节点,则需要按字母顺序插入到链表中。
Hsnode_t *p = hashtable[addr];
// 如果新节点应该插入到链表头部(字母顺序更小),则将其作为新的头节点。
if(strcmp(p->data.name, data.name) >= 0)
{
pnode->pnext = p;
hashtable[addr] = pnode;
return 0;
}
// 否则,找到链表中的正确位置,保持字母顺序。
while(p->pnext != NULL && strcmp(p->pnext->data.name, data.name) < 0)
{
p = p->pnext;
}
// 将新节点插入链表中,维护链表顺序。
pnode->pnext = p->pnext;
p->pnext = pnode;
return 0;
}
/**
* @brief 遍历哈希表,打印所有存储的用户信息。
* @return 成功返回 0。
*/
int traverse_table()
{
printf("\n");
// 遍历哈希表的每一个槽位。
for(int i = 0; i < HASH_SIZE; ++i)
{
if(hashtable[i] == NULL) // 如果当前位置为空,跳过。
{
continue;
}
Hsnode_t *p = hashtable[i];
printf("%c \n", i + 'a'); // 打印当前槽位对应的字母。
// 遍历链表,打印每个节点的用户数据(姓名和电话)。
while(p != NULL)
{
printf("%s %s \n", p->data.name, p->data.tel);
p = p->pnext;
}
printf("\n");
}
return 0;
}
/**
* @brief 查找哈希表中是否存在指定名字的用户信息。
* @param name 要查找的名字。
* @return 返回指向找到节点的指针,未找到返回 NULL。
*/
Hsnode_t *fine_table(char *name)
{
// 根据名字的第一个字符计算哈希地址。
int addr = hashfuction(name[0]);
// 遍历哈希表中对应的链表,寻找匹配的名字。
Hsnode_t *p = hashtable[addr];
while(p != NULL)
{
// 如果找到名字匹配的节点,返回该节点。
if(!strcmp(p->data.name, name))
{
return p;
}
p = p->pnext;
}
return NULL; // 未找到返回 NULL。
}
/**
* @brief 删除哈希表中指定名字的用户数据。
* @param name 要删除的名字。
* @param data 保存被删除的节点数据(输出参数)。
* @return 成功返回 1,未找到返回 0。
*/
int delete_hatable(char *name, HsDatetype *data)
{
// 根据名字的第一个字符计算哈希地址。
int addr = hashfuction(name[0]);
Hsnode_t *p = hashtable[addr];
if(hashtable[addr] == NULL) // 如果当前哈希表位置为空,返回 0。
{
return 0;
}
// 如果第一个节点就是要删除的节点,直接删除它。
if(!strcmp(p->data.name, name))
{
*data = p->data; // 保存删除的节点数据。
hashtable[addr] = p->pnext; // 更新哈希表头指针。
Hsnode_t *q = p;
free(q); // 释放节点内存。
return 0;
}
// 否则,遍历链表,查找要删除的节点。
while(p->pnext != NULL)
{
if(!strcmp(p->pnext->data.name, name))
{
*data = p->pnext->data; // 保存删除的节点数据。
Hsnode_t *q = p->pnext;
p->pnext = p->pnext->pnext; // 更新链表指针。
free(q); // 释放节点内存。
return 1;
}
p = p->pnext;
}
return 0; // 如果未找到节点,返回 0。
}
/**
* @brief 删除整个哈希表中的所有节点,释放所有内存。
* @return 成功返回 0。
*/
int delete_table()
{
// 遍历哈希表的每一个槽位。
for(int i = 0; i < HASH_SIZE; ++i)
{
if(hashtable[i] == NULL) // 如果当前槽位为空,跳过。
{
continue;
}
Hsnode_t *p = hashtable[i];
// 释放该槽位下链表中的所有节点。
while(p != NULL)
{
hashtable[i] = p->pnext; // 更新链表指针。
Hsnode_t *q = p;
p = p->pnext;
free(q); // 释放节点内存。
}
}
return 0;
}
函数验证
#include "hash.h" // 包含哈希表相关的头文件,提供数据结构和函数声明。
/**
* @brief 主函数,程序入口。
* @param argc 命令行参数的数量。
* @param argv 命令行参数的列表。
* @return 程序的退出状态码。
*/
int main(int argc, char *argv[])
{
// 初始化多个用户数据,包括姓名和电话号码,使用结构体数组存储。
HsDatetype pers[] = {{"zhansan", "110"}, {"lisi", "120"},
{"wangwu", "119"}, {"longjunlin", "114"},
{"maqi", "10086"}, {"waa", "156"}};
// 将每个用户数据插入哈希表。
insert_hatable(pers[0]);
insert_hatable(pers[1]);
insert_hatable(pers[2]);
insert_hatable(pers[3]);
insert_hatable(pers[4]);
insert_hatable(pers[5]);
// 遍历并打印当前哈希表中的所有数据。
traverse_table();
printf("\n**************\n");
// 查找哈希表中是否有 "wangwu" 的数据。
Hsnode_t *p = fine_table("wangwu");
if(p != NULL) // 如果找到,则打印该用户的信息。
{
printf("%s %s\n", p->data.name, p->data.tel);
}
printf("\n**************\n");
// 删除哈希表中的 "longjunlin" 数据,并将删除的节点数据保存到 `data` 中。
HsDatetype data;
int ret = delete_hatable("longjunlin", &data);
if(1 == ret) // 如果成功删除,打印删除的用户信息。
{
printf("%s %s\n", data.name, data.tel);
}
printf("\n**************\n");
// 再次遍历并打印当前哈希表中的所有数据,显示删除后的结果。
traverse_table();
// 删除整个哈希表,释放所有节点的内存。
delete_table();
return 0; // 程序正常退出。
}
运行结果
a
waa 156
l
lisi 120
longjunlin 114
m
maqi 10086
w
wangwu 119
z
zhansan 110