C语言库uthash常用库函数以及LeetCode例题(多数元素)

 在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 或其他名称,但重要的是理解其类型)。这个成员是一个结构体,它包含了几个关键的字段,如 nextprev(用于双向链表),以及可能的 key_lenhashv(用于存储哈希值和键的长度,尽管这些字段可能不总是必需的,取决于哈希表的使用方式)。

当我们将一个条目添加到哈希表中时,uthash 会使用这些字段来将条目链接到哈希表的适当位置。具体来说,next 和 prev 指针用于将条目组织成一个双向链表,这个链表包含了所有具有相同哈希值的条目(即,解决哈希冲突的一种方式)。

由于每个条目都通过 UT_hash_handle 成员与哈希表的其他部分相连,因此 uthash 提供的宏(如 HASH_FINDHASH_ADDHASH_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同样是一个宏,它处理整数类型的键。ids结构体中用作键的字段名,而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是一个宏,用于迭代哈希表中的条目。这里,hhMyStruct(或相应的结构体)中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来确保遍历继续进行)

注意:在遍历哈希表并删除条目时,应小心使用循环变量(如sHASH_ITER中)。通常,在删除条目后,需要更新循环变量以避免跳过任何条目或访问已删除的内存。然而,uthashHASH_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;
    }
}

题解二:此消彼长法(参考leetCode题解)

解题思想:将各个不同的元素看做好几个不同军队抢夺一个高地,他们一对一消耗,同一个元素则增加,不同一个元素则减少。
因为有个军队超过了n/2,经过消耗后,他还有人活着。

解题思路:循环遍历每一个nums数组元素,使用count来计数,相同则增加,不同则减少,当当前key小于等于0时,这个数组元素也就丧失了争夺高地的资格,由下一个数组元素来争夺,直到最后存活下来的就是赢家

int majorityElement(int* nums, int numsSize){
    int key = nums[0];
    int count = 0;
    for (size_t i = 0; i < numsSize; i++)
    {
        if(nums[i] == key)
            count++;
        else
            count--;
        
        if(count <= 0)
        {
            key = nums[i+1];
        }
        
    }
    return key;
}

作者:world
链接:https://leetcode.cn/problems/majority-element/solutions/77680/liang-chong-fang-fa-onhe-onlogn-by-world-16/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

题解二(2):使用栈

这个解题思想也可以使用栈来实现(参考leetCode题解)

使用top作为栈指针,入栈数组第一个元素,再遍历后续数组元素,若当前元素与栈内元素相同则入栈,不同则出栈;若栈空,即top==-1,则入栈当前元素;由于某个元素数量超过了n/2,故最后剩下来的栈中元素就是多数元素

int majorityElement(int* nums, int numsSize){
     int *stack=malloc(sizeof(int)*numsSize);
     int top=-1;
     for(int i=0;i<numsSize;i++){
         if(top==-1){
             stack[++top]=nums[i];
         }
         else if(stack[top]==nums[i]){
             stack[++top]=nums[i];
         }
         else top--;
     }
     return stack[0];
}

作者:ChrisWWWWAng
链接:https://leetcode.cn/problems/majority-element/solutions/876523/qiao-yong-zhan-de-si-xiang-by-chriswangi-o5vz/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值