告别数组模拟!用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) {
// 命中
}
这种方式的 三大致命缺陷 :
- Key类型受限 :仅适用于整数且范围较小的情况
- 空间浪费 :需要预先分配最大可能Key值的空间
- 冲突处理缺失 :无法处理不同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
定义哈希表结构体的黄金法则:
- 包含用户定义的Key和Value字段
- 必须 添加
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)查找,双向链表维护访问顺序。
&spm=1001.2101.3001.5002&articleId=161874444&d=1&t=3&u=5da0e29310e7488cbacd19dc4edaeb23)
321

被折叠的 条评论
为什么被折叠?



