告别数组模拟!用uthash在C语言里玩转结构体当哈希表Key(附LeetCode实战)

告别数组模拟!用uthash在C语言里玩转结构体当哈希表Key(附LeetCode实战)

在算法竞赛和日常开发中,哈希表几乎是处理查找问题的首选数据结构。对于C语言开发者来说,当Key是简单整数时,用数组模拟哈希表确实方便——直到你遇到结构体、字符串或指针作为Key的场景。这时, uthash 这个轻量级库就能展现出它"与Key类型无关"的独特优势。

1. 为什么数组模拟哈希表不够用?

用数组模拟哈希表的典型代码如下:

#define MAX_SIZE 100000
int hash[MAX_SIZE] = {0};

// 插入元素
hash[key] = value;

// 查找元素
if (hash[target_key] != 0) {
    // 命中
}

这种方式的 三大致命缺陷

  1. Key类型受限 :仅适用于整数且范围较小的情况
  2. 空间浪费 :需要预先分配最大可能Key值的空间
  3. 冲突处理缺失 :无法处理不同Key哈希到同一位置的情况

当遇到以下场景时,数组模拟完全失效:

  • Key是 struct Point {int x; int y;}
  • Key是字符串(如统计单词频率)
  • Key是指针(如链表节点地址)

2. uthash的核心优势与基础用法

2.1 安装与基本结构

只需将 uthash.h 头文件放入项目目录:

wget https://github.com/troydhanson/uthash/raw/master/src/uthash.h

定义哈希表结构体的黄金法则:

  1. 包含用户定义的Key和Value字段
  2. 必须 添加 UT_hash_handle hh 成员
#include "uthash.h"

// 以二维坐标点作为Key的示例
struct PointKey {
    int x;
    int y;
};

struct HashItem {
    struct PointKey key;  // 自定义Key
    int value;            // 存储的值
    UT_hash_handle hh;    // 必须存在的句柄
};

2.2 关键操作宏解析

uthash通过宏提供所有操作,最常用的三个宏:

宏名称 作用 时间复杂度
HASH_ADD 添加键值对 O(1)
HASH_FIND 查找Key O(1)
HASH_DEL 删除元素 O(1)

添加元素的标准流程

struct HashItem *hashMap = NULL;  // 必须初始化为NULL

void addToHashMap(struct PointKey key, int val) {
    struct HashItem *item = malloc(sizeof(struct HashItem));
    item->key = key;
    item->value = val;
    
    // 关键参数说明:
    // hh: 固定参数
    // hashMap: 哈希表指针
    // key: 结构体中的key字段名
    // item: 要添加的条目
    HASH_ADD(hh, hashMap, key, item);
}

注意:当Key是结构体时, HASH_ADD 会复制整个结构体内容作为Key,因此修改原结构体不会影响已存入的Key

3. 复杂Key处理实战技巧

3.1 结构体作为Key的完整示例

解决LeetCode 149. 直线上最多的点数问题:

struct Point {
    int x;
    int y;
};

struct SlopeKey {
    int dx;  // 化简后的x差值
    int dy;  // 化简后的y差值
};

struct HashItem {
    struct SlopeKey key;
    int count;
    UT_hash_handle hh;
};

int maxPoints(struct Point* points, int pointsSize) {
    if (pointsSize < 3) return pointsSize;
    
    int maxCount = 0;
    for (int i = 0; i < pointsSize; i++) {
        struct HashItem *hashMap = NULL;
        int samePoint = 1;
        
        for (int j = i + 1; j < pointsSize; j++) {
            // 计算斜率Key
            int dx = points[j].x - points[i].x;
            int dy = points[j].y - points[i].y;
            
            // 处理重合点
            if (dx == 0 && dy == 0) {
                samePoint++;
                continue;
            }
            
            // 约分斜率(避免浮点数精度问题)
            int gcd = computeGCD(dx, dy);
            struct SlopeKey key = {dx/gcd, dy/gcd};
            
            // 查找或添加
            struct HashItem *item;
            HASH_FIND(hh, hashMap, &key, sizeof(struct SlopeKey), item);
            if (!item) {
                item = malloc(sizeof(struct HashItem));
                item->key = key;
                item->count = 1;
                HASH_ADD(hh, hashMap, key, item);
            } else {
                item->count++;
            }
        }
        
        // 更新最大值
        maxCount = fmax(maxCount, samePoint);
        struct HashItem *curr, *tmp;
        HASH_ITER(hh, hashMap, curr, tmp) {
            maxCount = fmax(maxCount, curr->count + samePoint);
        }
        
        // 清空当前哈希表
        HASH_CLEAR(hh, hashMap);
    }
    return maxCount;
}

3.2 字符串作为Key的特殊处理

uthash为字符串Key提供了专用宏:

Key类型 添加宏 查找宏
字符串指针 HASH_ADD_KEYPTR HASH_FIND_STR
字符数组 HASH_ADD_STR HASH_FIND_STR

统计单词频率的典型实现:

struct WordCount {
    char *word;    // 字符串指针作为Key
    int count;
    UT_hash_handle hh;
};

void countWords(char **words, int wordsSize) {
    struct WordCount *countMap = NULL;
    
    for (int i = 0; i < wordsSize; i++) {
        struct WordCount *item;
        HASH_FIND_STR(countMap, words[i], item);
        
        if (item) {
            item->count++;
        } else {
            item = malloc(sizeof(struct WordCount));
            item->word = words[i];
            item->count = 1;
            // 注意:strlen(words[i])+1 确保包含终止符
            HASH_ADD_KEYPTR(hh, countMap, item->word, strlen(item->word), item);
        }
    }
    
    // 遍历哈希表
    struct WordCount *curr, *tmp;
    HASH_ITER(hh, countMap, curr, tmp) {
        printf("%s: %d\n", curr->word, curr->count);
        HASH_DEL(countMap, curr);  // 边遍历边删除
        free(curr);
    }
}

关键细节:使用 HASH_ADD_KEYPTR 时,uthash只会保存字符串指针,不会复制字符串内容。如果字符串是临时变量,需要自行分配内存复制字符串。

4. LeetCode实战:从两数之和到复杂应用

4.1 经典两数之和的uthash实现

对比数组模拟和uthash的实现差异:

// 数组模拟版(仅适用于小范围非负整数)
int* twoSumArray(int* nums, int numsSize, int target, int* returnSize) {
    int hash[100000] = {0};
    for (int i = 0; i < numsSize; i++) {
        int complement = target - nums[i];
        if (hash[complement] != 0) {
            int* res = malloc(2 * sizeof(int));
            res[0] = hash[complement] - 1;
            res[1] = i;
            *returnSize = 2;
            return res;
        }
        hash[nums[i]] = i + 1;
    }
    *returnSize = 0;
    return NULL;
}

// uthash通用版(处理任意整数范围)
struct NumMap {
    int key;    // 数值
    int value;  // 索引
    UT_hash_handle hh;
};

int* twoSumHash(int* nums, int numsSize, int target, int* returnSize) {
    struct NumMap *map = NULL;
    for (int i = 0; i < numsSize; i++) {
        int complement = target - nums[i];
        struct NumMap *item;
        HASH_FIND_INT(map, &complement, item);
        if (item) {
            int* res = malloc(2 * sizeof(int));
            res[0] = item->value;
            res[1] = i;
            *returnSize = 2;
            return res;
        }
        item = malloc(sizeof(struct NumMap));
        item->key = nums[i];
        item->value = i;
        HASH_ADD_INT(map, key, item);
    }
    *returnSize = 0;
    return NULL;
}

4.2 进阶应用:设计LRU缓存

结合uthash和双向链表实现LeetCode 146. LRU缓存:

#include "uthash.h"

typedef struct {
    int key;
    int value;
    struct DLinkedNode *node;
    UT_hash_handle hh;
} LRUCacheItem;

typedef struct DLinkedNode {
    int key;
    struct DLinkedNode *prev;
    struct DLinkedNode *next;
} DLinkedNode;

typedef struct {
    int capacity;
    int size;
    LRUCacheItem *hashMap;
    DLinkedNode *head;
    DLinkedNode *tail;
} LRUCache;

void addToHead(LRUCache *obj, DLinkedNode *node) {
    node->prev = obj->head;
    node->next = obj->head->next;
    obj->head->next->prev = node;
    obj->head->next = node;
}

void removeNode(DLinkedNode *node) {
    node->prev->next = node->next;
    node->next->prev = node->prev;
}

DLinkedNode *removeTail(LRUCache *obj) {
    DLinkedNode *node = obj->tail->prev;
    removeNode(node);
    return node;
}

LRUCache *lRUCacheCreate(int capacity) {
    LRUCache *obj = malloc(sizeof(LRUCache));
    obj->capacity = capacity;
    obj->size = 0;
    obj->hashMap = NULL;
    obj->head = malloc(sizeof(DLinkedNode));
    obj->tail = malloc(sizeof(DLinkedNode));
    obj->head->prev = NULL;
    obj->head->next = obj->tail;
    obj->tail->prev = obj->head;
    obj->tail->next = NULL;
    return obj;
}

int lRUCacheGet(LRUCache *obj, int key) {
    LRUCacheItem *item;
    HASH_FIND_INT(obj->hashMap, &key, item);
    if (item == NULL) return -1;
    
    // 移动到头节点
    removeNode(item->node);
    addToHead(obj, item->node);
    return item->value;
}

void lRUCachePut(LRUCache *obj, int key, int value) {
    LRUCacheItem *item;
    HASH_FIND_INT(obj->hashMap, &key, item);
    
    if (item == NULL) {
        if (obj->size == obj->capacity) {
            // 删除尾节点
            DLinkedNode *tail = removeTail(obj);
            LRUCacheItem *tmp;
            HASH_FIND_INT(obj->hashMap, &(tail->key), tmp);
            HASH_DEL(obj->hashMap, tmp);
            free(tmp);
            free(tail);
            obj->size--;
        }
        
        // 创建新节点
        DLinkedNode *node = malloc(sizeof(DLinkedNode));
        node->key = key;
        addToHead(obj, node);
        
        item = malloc(sizeof(LRUCacheItem));
        item->key = key;
        item->value = value;
        item->node = node;
        HASH_ADD_INT(obj->hashMap, key, item);
        obj->size++;
    } else {
        // 更新值并移动到头节点
        item->value = value;
        removeNode(item->node);
        addToHead(obj, item->node);
    }
}

这个实现展示了uthash与自定义数据结构的完美配合,哈希表负责O(1)查找,双向链表维护访问顺序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值