在C语言中,并没有标准库直接支持哈希表(hash table)数据结构,但有一些非常流行的第三方库可以实现这一功能,其中uthash
是一个非常流行且易于使用的哈希表库。它实现了常见的hash操作函数,例如查找、插入、删除等待。该套开源代码采用宏的方式实现hash函数的相关功能,支持C语言的任意数据结构最为key值,甚至可以采用多个值作为key,无论是自定义的struct还是基本数据类型,需要注意的是不同类型的key其操作接口方式略有不同。使用uthash代码时只需要包含头文件"uthash.h"即可。
基本用法
1. 包含uthash头文件
#include "uthash.h"
#include <stdio.h>
#include <stdlib.h>
2. 定义你的结构体并为其添加哈希表功能
你需要定义一个结构体,并在这个结构体中添加一个UT_hash_handle
类型的字段,这个字段会被uthash
库用来管理哈希表。
typedef struct example_user_t {
int id; /* 用户ID */
char name[100]; /* 用户名 */
UT_hash_handle hh; /* 使得结构体支持哈希表 */
} example_user_t;
说明
(1)关于结构体
-
id是键(key),也可以是其他的数据结构,不同的数据结构对应hash操作可能不一样
-
name是值(value),其类型根据实际情况定义
-
hh是内部使用的hash处理句柄,在使用过程中,只需要在结构体中定义一个UT_hash_handle类型的变量即可,不需要为该句柄变量赋值,但必须在该结构体中定义该变量。
(2)关于UT_hash_handle hh:
UT_hash_handle hh
是连接用户定义的数据结构(我们称之为“条目”或“元素”)与 uthash
哈希表内部机制的关键桥梁。
每个使用 uthash
的数据结构都需要包含一个 UT_hash_handle
类型的成员(尽管在代码中我们通常将其命名为 hh
或其他名称,但重要的是理解其类型)。这个成员是一个结构体,它包含了几个关键的字段,如 next
、prev
(用于双向链表),以及可能的 key_len
、hashv
(用于存储哈希值和键的长度,尽管这些字段可能不总是必需的,取决于哈希表的使用方式)。
当我们将一个条目添加到哈希表中时,uthash
会使用这些字段来将条目链接到哈希表的适当位置。具体来说,next
和 prev
指针用于将条目组织成一个双向链表,这个链表包含了所有具有相同哈希值的条目(即,解决哈希冲突的一种方式)。
由于每个条目都通过 UT_hash_handle
成员与哈希表的其他部分相连,因此 uthash
提供的宏(如 HASH_FIND
、HASH_ADD
、HASH_ITER
等)能够利用这些链接来访问、添加、删除和遍历哈希表中的条目。
3. 创建哈希表
在uthash
中,哈希表是通过指向结构体实例的指针来管理的。开始时,你不需要做任何特殊的初始化,只需有一个指向你结构体类型的空指针即可。
注意:定义一个hash结构的空指针users
,用于指向保存数据的hash表,必须初始化为空,在后面的查、插等操作中,uthash内部会根据其是否为空而进行不同的操作。
example_user_t *users = NULL;
4. 插入元素
使用HASH_ADD
宏来向哈希表中添加元素。你需要指定哈希表(即你的空指针变量)、要插入的结构体实例、哈希键(通常是结构体中的一个字段)以及一个可选的哈希函数长度(如果哈希键是字符串)。
// 添加元素到哈希表
void add_user(int user_id, const char *name) {
example_user *s;
//查找用户,插入前先查看key值是否已经在hash表users里面
HASH_FIND_INT(users, &user_id, s);
if (s == NULL) {
s = (example_user*)malloc(sizeof(example_user));
s->id = user_id;
strcpy(s->name, name);
// 添加用户,id为hash结构里面,hash key值的变量名
HASH_ADD_INT(users, id, s); }
}
说明:
①HASH_ADD_INT函数中参数id不需要取地址,但HASH_FIND_INT的user_id需要取地址
②由于uthash要求键(key)必须唯一,而uthash内部未对key值得唯一性进行很好的处理,因此它要求外部在插入操作时要确保其key值不在当前的hash表中。若插入相同的key值到一个hash中,会被当做一种错误。
③关于HASH_FIND_INT(users, &user_id, s):
HASH_FIND_INT
是一个宏,用于处理整数类型的键。这里,users
是指向哈希表头部的指针,&user_id
是指向要查找的键的指针,而s
是一个指向uthash结构体
(或任何包含UT_hash_handle hh;
成员的结构体)的指针,用于存储找到的条目(如果找到的话)。
-
如果找到了键为
user_id
的条目,s
将指向该条目,并且可以直接使用s
来访问条目的其他字段(如s->name
)。 -
如果没有找到,
s
将保持为NULL
。
③关于strcpy(s->name, name):
这行代码假设HASH_FIND_INT
找到了一个条目(即s
不是NULL
),并且正在将字符串name
复制到找到的条目的name
字段中。这里使用strcpy
是危险的,因为它没有检查目标缓冲区(s->name
)的大小,这可能导致缓冲区溢出。更安全的选择是使用strncpy
并确保字符串以空字符\0
结尾。
④关于HASH_ADD_INT(users, id, s):
HASH_ADD_INT
同样是一个宏,它处理整数类型的键。id
是s
结构体中用作键的字段名,而s
是指向要添加的条目的指针。这行代码用于将新的条目s(即s也是一个uthash结构体)
添加到哈希表users
中,前提是s
尚未在哈希表中。
注意:如果s
之前已经通过HASH_ADD_INT
或其他方式添加到了users
中,再次调用HASH_ADD_INT
可能会导致未定义行为(通常是内存泄漏或数据损坏),因为uthash
不会自动处理重复的条目。
5. 查找元素
使用HASH_FIND_INT
宏(或对应的类型,如HASH_FIND_STR
)来查找哈希表中的元素
// 查找哈希表中的元素
example_user *find_user(int user_id) {
example_user *s;
HASH_FIND_INT(users, &user_id, s);
return s;
}
6. 遍历哈希表
使用HASH_ITER
宏来遍历哈希表中的所有元素。
// 遍历哈希表
void print_users() {
example_user *s, *tmp;
HASH_ITER(hh, users, s, tmp) {
printf("user %d: %s\n", s->id, s->name);
}
}
说明:关于HASH_ITER(hh, users, s, tmp):
①hh: 这是一个字符串,表示在你的结构体中用于哈希处理的 UT_hash_handle
成员的名称。哈希表本身是无需的,其遍历通过hh来实现
②users: 这是一个指向哈希表头部元素的指针;
③s: 一个指向正在遍历的当前元素的指针。
④tmp: 这是一个在宏内部使用的临时指针,用于安全地遍历哈希表。不能在宏的外部访问或修改这个指针。当HASH_ITER
宏在内部处理迭代时,它可能会更新内部状态以跟踪下一个要访问的元素。然而,如果直接在当前迭代中删除了当前元素(如通过HASH_DEL
),则内部状态可能会变得无效,因为被删除的元素可能不再存在于哈希表中。这时,tmp
就作为一个“备份”指针,允许迭代器在删除s
之后安全地移动到下一个元素
这行代码是遍历哈希表users
的开始。HASH_ITER
是一个宏,用于迭代哈希表中的条目。这里,hh
是MyStruct
(或相应的结构体)中UT_hash_handle
成员的名称,users
是指向哈希表头部的指针,s
是当前迭代到的条目的指针,而tmp
是一个临时指针,用于内部迭代。
遍历哈希表时,通常会在一个循环内部使用HASH_ITER
,并在循环体内处理每个条目。
7.删除哈希表中具有特定键的元素
使用HASH_DEL宏来删除哈希表中具有特定键的元素
void DeleteUser(int ikey) //假设键id的类型是int
{
struct example_user *tmp = NULL; // 初始化一个指向example_user的指针tmp,用于存储查找结果
HASH_FIND_INT(users, &ikey, s);
// 在哈希表users中查找键为ikey的元素,并将找到的元素的地址存储在tmp中
if (s != NULL) {
HASH_DEL(users, s); // 将找到的元素从哈希表中删除
free(s);
}
}
8. 释放哈希表
使用HASH_ITER和HASH_DEL宏来释放哈希表
void free_users() {
example_user *current_user, *tmp;
HASH_ITER(hh, users, current_user, tmp)
{
//HASH_DEL 只将结构体从hash表中移除,并未释放结构体内容
HASH_DEL(users, current_user);
free(current_user); //释放内存
}
}
说明:关于HASH_DEL(users, current_user)
这行代码用于从哈希表users
中删除指定的条目current_user
。
example_user *current_user
:这是一个指向example_user
类型(假设是用户数据结构的类型)的指针,用于在遍历哈希表时存储当前访问的元素的地址。
example_user *tmp
:这是一个辅助指针,用于在哈希表遍历过程中保持对当前元素之前元素的引用,以便在删除当前元素后能够继续遍历。这是因为在删除元素后,当前元素的指针可能会变得无效。
HASH_DEL
是一个宏,它接受哈希表头部指针和要删除的条目的指针作为参数。调用HASH_DEL
后,current_user
所指向的内存应该被认为是未定义的,因为它可能已经从哈希表中移除(此时将使用辅助指针tmp来确保遍历继续进行)
注意:在遍历哈希表并删除条目时,应小心使用循环变量(如s
在HASH_ITER
中)。通常,在删除条目后,需要更新循环变量以避免跳过任何条目或访问已删除的内存。然而,uthash
的HASH_ITER
宏提供了一种安全的遍历和删除机制,通过使用临时指针(如tmp
)来确保遍历过程不受删除操作的影响。
参考链接:uthash简介-CSDN博客
LeetCode例题:多数元素
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:nums = [3,2,3] 输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2] 输出:2
提示:
n == nums.length
1 <= n <= 5 * 104
-109 <= nums[i] <= 109
题解:
typedef struct Hash
{
int data;
int count;
UT_hash_handle hh;
}Hash;
int majorityElement(int* nums, int numsSize)
{
if(numsSize==1)
return nums[0];
else
{
Hash* hash=NULL;
Hash* p;
int count=0;
for(int i=0;i<numsSize;i++)
{
HASH_FIND_INT(hash,&nums[i],p); //查找当前元素是否录入了哈希表
if(p==NULL) //若没有
{
p=(Hash*)malloc(sizeof(Hash));
p->data=nums[i];
p->count=1;
HASH_ADD_INT(hash, data, p); //别忘记添加这个宏来实际将元素添加到哈希表中
}
else //若能找到
{
p->count++;
}
}
Hash *pd,*tmp;
HASH_ITER(hh,hash,pd,tmp)
{
if(pd->count > (numsSize/2) )
return pd->data;
}
return NULL;
}
}