Uthash总览
哈希表介绍
哈希表就是通过一个映射函数f(key)将一组数据散列存储在数组中的一种数据结构。在这哈希表中,每一个元素的key和它的存储位置都存在一个f(key)的映射关系,我们可以通过f(key)快速的查找到这个元素在表中的位置
Uthash简介
Uthash是一个用C语言编写的开源哈希表库,它提供了简单易用的接口来创建和操作哈希表。Uthash支持多种数据类型作为键,并且可以动态调整哈希表的大小以适应数据量的变化
hot100中Uthash的应用
1. 两数之和
LeetCode 1. 两数之和
问题描述: 给定一个整数数组nums和一个目标值target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标
Uthash应用: 使用Uthash存储数组元素及其索引,在遍历数组时,计算目标值与当前元素的差值,并在哈希表中查找该差值是否存在
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
typedef struct{ int key; int val; UT_hash_handle hh; } hashTable;
hashTable* table;
hashTable* find(int key){ hashTable* tmp; HASH_FIND_INT(table,&key,tmp); return tmp; }
void insert(int key,int val){ hashTable* node=find(key); if(node==NULL){ hashTable* tmp=(hashTable*)malloc(sizeof(hashTable)); tmp->key=key; tmp->val=val; HASH_ADD_INT(table,key,tmp); } else{ node->val=val; } }
int* twoSum(int* nums, int numsSize, int target, int* returnSize){ table=NULL; for(int i=0;i<numsSize;i++){ hashTable* tmp=find(target-nums[i]); if(tmp){ int* ret=(int*)malloc(sizeof(int)*2); ret[0]=tmp->val; ret[1]=i; return ret; } else{ insert(nums[i],i); } } *returnSize=0; return NULL; }
|
本题的解题思路是:
- 遍历数组,对于每个元素,计算目标值与当前元素的差值,并在哈希表中查找该差值是否存在
- 如果存在,返回对应的索引;如果不存在,将当前元素及其索引插入哈希表中
- 最后是在O(n)的时间复杂度内解决问题
2. 字母异位词分组
LeetCode 49. 字母异位词分组
问题描述: 给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串
Uthash应用: 使用Uthash存储排序后的字符串作为键,原始字符串列表作为值,将异位词分组
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
|
typedef struct{ char* key; char** list; int cnt; int capacity; UT_hash_handle hh; } hashTable;
hashTable* table;
int cmp(const void* a,const void* b){ return (*(char*)a-*(char*)b); }
hashTable* find(char* key){ hashTable* tmp; HASH_FIND_STR(table,key,tmp); return tmp; }
hashTable* insert(char* key,char* str){ hashTable* node=find(key); if(node==NULL){ hashTable* tmp=(hashTable*)malloc(sizeof(hashTable)); tmp->key=malloc(strlen(key)+1); strcpy(tmp->key,key); tmp->capacity=10; tmp->cnt=0; tmp->list=malloc(sizeof(char*)*tmp->capacity); tmp->list[tmp->cnt]=malloc(strlen(str)+1); strcpy(tmp->list[tmp->cnt],str); tmp->cnt++; HASH_ADD_STR(table,key,tmp); } else{ if(node->cnt>=node->capacity){ node->capacity*=2; node->list=realloc(node->list,sizeof(char*)*node->capacity); }
node->list[node->cnt]=malloc(strlen(str)+1); strcpy(node->list[node->cnt],str); node->cnt++; } }
char*** groupAnagrams(char** strs, int strsSize, int* returnSize, int** returnColumnSizes){ table=NULL;
for(int i=0;i<strsSize;i++){ int len=strlen(strs[i]); char* sortedStr=malloc(len+1); strcpy(sortedStr,strs[i]); qsort(sortedStr,len,sizeof(char),cmp); insert(sortedStr,strs[i]); free(sortedStr); }
*returnSize=HASH_COUNT(table); char*** res = malloc(sizeof(char**) * (*returnSize)); *returnColumnSizes = malloc(sizeof(int) * (*returnSize)); int idx=0; hashTable* node,*tmp;
HASH_ITER(hh,table,node,tmp){ res[idx]=malloc(sizeof(char*)*node->cnt); (*returnColumnSizes)[idx]=node->cnt; for(int j=0;j<node->cnt;j++){ res[idx][j]=malloc(strlen(node->list[j])+1); strcpy(res[idx][j],node->list[j]); } idx++; }
HASH_ITER(hh,table,node,tmp){ free(node->key); for(int j=0;j<node->cnt;j++){ free(node->list[j]); } free(node->list); HASH_DEL(table,node); free(node); } return res; }
|
本题的解题思路是:
- 遍历字符串数组,对于每个字符串,排序后作为键插入哈希表
- 如果键已存在,则将原始字符串添加到对应的值列表中
- 最后遍历哈希表,构建结果集
3. 最长连续序列
LeetCode 128. 最长连续序列
问题描述: 给定一个未排序的整数数组,找出最长连续序列的长度
Uthash应用: 使用Uthash存储数组元素,遍历数组时,检查每个元素的前后连续元素是否存在,计算最长连续序列长度
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #define MAX(a,b) ((a)>(b)?(a):(b))
typedef struct{ int key; UT_hash_handle hh; } hashTable;
int longestConsecutive(int* nums, int numsSize){ if(numsSize==0) return 0; hashTable* table=NULL;
for(int i=0;i<numsSize;i++){ hashTable* tmp; HASH_FIND_INT(table,&nums[i],tmp); if(tmp==NULL){ tmp=(hashTable*)malloc(sizeof(hashTable)); tmp->key=nums[i]; HASH_ADD_INT(table,key,tmp); } }
int longest=0; hashTable* node,* findNode,* tmp; HASH_ITER(hh,table,node,tmp){ int cur=node->key; int prev=cur-1; HASH_FIND_INT(table,&prev,findNode); if(findNode!=NULL) continue; int cnt=1; int next=cur+1; HASH_FIND_INT(table,&next,findNode); while(findNode!=NULL){ next++; cnt++; HASH_FIND_INT(table,&next,findNode); } longest=MAX(cnt,longest); }
HASH_ITER(hh,table,node,tmp){ HASH_DEL(table,node); free(node); } return longest; }
|
本题的解题思路是:
- 使用Uthash构建哈希表,这一步可以去重
- 遍历哈希表,找到每个可能连续序列的起点,这一步算是一个优化,不需要对每个数字都找到最后,只需要对序列起点开始寻找
cnt,计算连续长度
- 最后比较
longest与cnt,返回最长序列长度即可
4. 路径总和Ⅲ
LeetCode 437. 路径总和 III
问题描述: 给定一个二叉树和一个整数目标和targetSum,找出路径总和等于目标和的路径数量。路径不需要从根节点开始或结束,但必须向下移动(只能从父节点到子节点)
Uthash应用: 使用Uthash存储前缀和及其出现次数,在遍历二叉树时,计算当前路径的前缀和,并查找是否存在满足条件的前缀和
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| typedef struct{ long long sum; int cnt; UT_hash_handle hh; } PrefixSumNode;
static PrefixSumNode* prefixMap=NULL;
void dfsPathSum(struct TreeNode* root,long long currSum,int targetSum,int* ans){ if(root==NULL) return; currSum+=root->val; long long need=currSum-targetSum; PrefixSumNode* find; HASH_FIND(hh,prefixMap,&need,sizeof(long long),find); if(find){ *ans+=find->cnt; } PrefixSumNode* currNode=NULL; HASH_FIND(hh,prefixMap,&currSum,sizeof(long long),currNode); if(currNode==NULL){ currNode=(PrefixSumNode*)malloc(sizeof(PrefixSumNode)); currNode->sum=currSum; currNode->cnt=1; HASH_ADD(hh,prefixMap,sum,sizeof(long long),currNode); } else{ currNode->cnt++; } dfsPathSum(root->left,currSum,targetSum,ans); dfsPathSum(root->right,currSum,targetSum,ans); currNode->cnt--; if(currNode->cnt==0){ HASH_DEL(prefixMap,currNode); free(currNode); } }
int pathSum(struct TreeNode* root, int targetSum){ int ans=0; prefixMap=NULL; PrefixSumNode* baseNode=(PrefixSumNode*)malloc(sizeof(PrefixSumNode)); baseNode->sum=0; baseNode->cnt=1; HASH_ADD(hh,prefixMap,sum,sizeof(long long),baseNode);
dfsPathSum(root,0,targetSum,&ans); PrefixSumNode* node,*tmp; HASH_ITER(hh,prefixMap,node,tmp){ HASH_DEL(prefixMap,node); free(node); } return ans; }
|
本题使用哈希表的优势如下:
- 使用
Uthash存储前缀和及其出现次数,可以在O(1)时间内查找是否存在满足条件的前缀和
- 通过前缀和的概念,可以高效地计算路径和,类似与两数之和的去查找
need
- 本题的一个重点就是
need=currSum-targetSum,通过这个公式可以快速定位到满足条件的前缀和,从而计算路径数量
解题思路如下:
- 使用DFS遍历二叉树,使用哈希表储存当前节点的前缀和(当前路径上的),通过前缀和之差计算出其中子路径的路径之和,判断是否等于
targetSum
- 哈希表还要储存
cnt,表示当前前缀和出现的次数,因为可能存在多条路径的前缀和相同
- 注意本题还需要回溯,在DFS返回上一层时,需要将当前前缀和的计数减一,确保哈希表中只包含当前路径上的前缀和信息
5. 从前序与中序遍历序列构造二叉树
LeetCode 105. 从前序与中序遍历序列构造二叉树
问题描述: 给定两个整数数组preorder和inorder,其中preorder是二叉树的前序遍历,inorder是二叉树的中序遍历,请构造二叉树并返回其根节点
Uthash应用: 使用Uthash存储中序遍历数组中每个元素的索引,便于在构建二叉树时快速定位根节点在中序遍历中的位置,本题使用哈希表主要在于优化查找位置
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| typedef struct{ int key; int val; UT_hash_handle hh; } hashTable;
hashTable* table;
struct TreeNode* buildTreeHelper(int* preorder,int lpre,int rpre,int* inorder,int lin,int rin){ if(lpre>rpre || lin>rin) return NULL; int rootVal=preorder[lpre]; hashTable* find; HASH_FIND_INT(table,&rootVal,find); int k=find->val; struct TreeNode* root=(struct TreeNode*)malloc(sizeof(struct TreeNode)); root->val=rootVal; int leftSize=k-lin;
root->left=builedTreeHelper(preorder,lpre+1,lpre+leftSize,inorder,lin,k-1); root->right=builedTreeHelper(preorder,lpre+leftSize+1,rpre,inorder,k+1,rin); return root; }
struct TreeNode* buildTree(int* preorder, int preorderSize, int* inorder, int inorderSize){ table=NULL; for(int i=0;i<inorderSize;i++){ hashTbale* tmp=(hashTable*)malloc(sizeof(hashTable)); tmp->key=inorder[i]; tmp->val=i; HASH_ADD_INT(table,key,tmp); } struct TreeNode* root=buildTreeHelper(preorder,0,preorderSize-1,inorder,0,inorderSize-1);
hashTable* tmp,* curr; HASH_ITER(hh,table,curr,tmp){ HASH_DEL(table,curr); free(curr); } return root; }
|
本题相比如下的原始版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| struct TreeNode* Tree(int* preorder,int lpre,int rpre,int *inorder,int lin,int rin){ if(lpre > rpre || lin > rin) return NULL; int k; for(k=lin;k<=rin;k++){ if(inorder[k]==preorder[lpre]){ break; } } struct TreeNode* root=(struct TreeNode*)malloc(sizeof(struct TreeNode)); root->val=preorder[lpre]; int leftSize = k - lin; root->left=Tree(preorder,lpre+1,lpre+leftSize,inorder,lin,k-1); root->right=Tree(preorder,lpre+leftSize+1,rpre,inorder,k+1,rin); return root; } struct TreeNode* buildTree(int* preorder, int preorderSize, int* inorder, int inorderSize) { return Tree(preorder,0,preorderSize-1,inorder,0,inorderSize-1); }
|
使用哈希表的优势如下:
- 使用
Uthash存储中序遍历数组中每个元素的索引,可以在O(1)时间内查找根节点在中序遍历中的位置
- 避免了在每次递归中使用循环查找根节点位置,提升了整体构建二叉树的效率
解题思路如下:
- 使用
Uthash构建哈希表,存储中序遍历数组中每个元素的索引
- 递归构建二叉树,利用哈希表快速定位根节点在中序遍历中的位置,划分左右子树
- 最后释放哈希表内存,返回构建好的二叉树根节点
6. 多数元素
LeetCode 169. 多数元素
问题描述: 给定一个大小为n的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于⌊ n/2 ⌋的元素
Uthash应用: 使用Uthash存储数组元素及其出现次数,遍历数组时,更新哈希表中的计数,最后找到出现次数超过n/2的元素
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| typedef struct{ int key; int val; UT_hash_handle hh; } hashTable;
int majorityElement(int* nums, int numsSize){ hashTable* table=NULL; for(int i;i<numsSize;i++){ hashTable* node; HASH_FIND_INT(table.&nums[i],node); if(node==NULL){ node=(hashTable*)malloc(sizeof(hashTable)); node->key=nums[i]; node->val=1; HASH_ADD_INT(table,key,node); } else{ node->val++; } }
int ans; int maxCnt=0; hashTable* node,* tmp; HASH_ITER(hh,table,node,tmp){ if(node->val>maxCnt){ max=node->val; ans=node->key; } } return ans; }
|
本题除了技巧方法,其他方法较为好想,在这里使用哈希表主要还是为了熟悉哈希表的使用
7. 随机链表的复制
LeetCode 138. 随机链表的复制
问题描述: 给定一个链表,每个节点包含一个额外的随机指针,该指针可以指向链表中的任何节点或null。请实现一个函数来复制这个链表
Uthash应用: 使用Uthash存储原始节点与其对应的复制节点的映射关系,在遍历原始链表时,创建复制节点并更新随机指针
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| typedef struct{ struct Node* key; struct Node* val; UT_hash_handle hh; } hashTable;
struct Node* copyRandomList(struct Node* head){ if(!head) return NULL; hashTable* table=NULL; struct Node* curr=head; while(curr){ struct Node* newNode=(struct Node*)malloc(sizeof(struct Node)); newNode->val=curr->val; newNode->next=NULL; newNode->random=NULL; hashTable* tmp=(hashTable*)malloc(sizeof(hashTable)); tmp->key=curr; tmp->val=newNode; HASH_ADD_PTR(table,key,tmp);
curr=curr->next; }
curr=head; while(curr){ hashTable* find; HASH_FIND_PTR(table,&curr,find); struct Node* newNode=find->val;
if(curr->next){ HASH_FIND_PTR(table,&curr->next,find); newNode->next=find->val; } else{ newNode->next=NULL; }
if(curr->random){ HASH_FIND_PTR(table,&curr->random,find); newNode->random=find->val; } else{ newNode->random=NULL; }
curr=curr->next; }
hashTable* findHead; HASH_FIND_PTR(table,&head,findHead); struct Node* newHead=findHead->val;
hashTable* curr,* tmp; HASH_ITER(hh,table,curr,tmp){ HASH_DEL(table,curr); free(curr); }
return newHead; }
|
本题的解题思路是:
- 遍历原始链表,使用哈希表存储每个原始节点与其对应的复制节点的映射关系
- 再次遍历原始链表,使用哈希表更新复制节点的
next和random指针
- 最后返回复制链表的头节点
除了上述的方法外,还有一种哈希表+递归的方法:
- 我们用哈希表记录每一个节点对应新节点的创建情况。
- 遍历该链表的过程中,我们检查
当前节点的后继节点和当前节点的随机指针指向的节点的创建情况。如果这两个节点中的任何一个节点的新节点没有被创建,我们都立刻递归地进行创建。
- 当我们拷贝完成,回溯到当前层时,我们即可完成当前节点的指针赋值。
- 注意一个节点可能被多个其他节点指向,因此我们可能递归地多次尝试拷贝某个节点,为了防止重复拷贝,我们需要首先检查当前节点是否被拷贝过,如果已经拷贝过,我们可以直接从哈希表中取出拷贝后的节点的指针并返回即可。
下面我会给出代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| typedef struct{ struct Node* key; struct Node* val; UT_hash_handle hh; } hashTable;
hashTable* table;
struct Node* deepCopy(struct Node* head){ if(head==NULL) return NULL; hashTable* tmp; HASH_FIND_PTR(table,&head,tmp); if(tmp==NULL){ struct Node* newNode=(struct Node*)malloc(sizeof(struct Node)); newNode->val=head->val; tmp=(hashTable*)malloc(sizeof(hashTable)); tmp->key=head; tmp->val=newNode; HASH_ADD_PTR(table,key,tmp); newNode->next=deepCopy(head->next); newNode->random=deepCopy(head->random); } return tmp->val; }
struct Node* copyRandomList(struct Node* head){ table=NULL; struct Node* newHead=deepCopy(head); hashTable* curr,* tmp; HASH_ITER(hh,table,curr,tmp){ HASH_DEL(table,curr); free(curr); } return newHead; }
|
8. 相交链表
LeetCode 160. 相交链表
问题描述: 给定两个单链表的头节点headA和headB,找出并返回两个链表相交的起始节点。如果两个链表没有交点,返回null
Uthash应用: 使用Uthash存储第一个链表的节点地址,在遍历第二个链表时,检查节点是否存在于哈希表中,找到相交节点
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| typedef struct{ struct ListNode* key; UT_hash_handle hh; } hashTable;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB){ hashTable* table=NULL; struct ListNode* curr=headA; while(curr){ hashTable* tmp=(hashTable*)malloc(sizeof(hashTable)); tmp->key=curr; HASH_ADD_PTR(table,key,tmp); curr=curr->next; }
curr=headB; while(curr){ hashTable* find; HASH_FIND_PTR(table,&curr,find); if(find){ return curr; } curr=curr->next; }
return NULL; }
|
本题的解题思路是:
- 遍历第一个链表,将每个节点的地址存储在哈希表中
- 遍历第二个链表,检查每个节点是否存在于哈希表中
- 如果找到相交节点,返回该节点;如果遍历结束未找到,返回
null
9. 和为k的子数组
LeetCode 560. 和为k的子数组
问题描述: 给定一个整数数组nums和一个整数k,请你统计并返回该数组中和为k的连续子数组的个数
Uthash应用: 使用Uthash存储前缀和及其出现次数,在遍历数组时,计算当前前缀和,并查找是否存在满足条件的前缀和
前缀和思想 本题与之前的路径总和Ⅲ(- 4. 路径总和Ⅲ)类似,都是利用前缀和的思想来解决问题。前缀和是指从数组开头到当前位置的元素之和。通过计算当前前缀和与目标值的差值,我们可以快速定位到满足条件的前缀和,从而计算出连续子数组的个数
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| typedef struct { int sum; int count; UT_hash_handle hh; } HashMapEntry;
int subarraySum(int* nums, int numsSize, int K) { HashMapEntry* sum_map = NULL; HashMapEntry* entry; int current_sum = 0; int count = 0;
entry = (HashMapEntry*)malloc(sizeof(HashMapEntry)); entry->sum = 0; entry->count = 1; HASH_ADD_INT(sum_map, sum, entry);
for (int i = 0; i < numsSize; i++) { current_sum += nums[i]; int target = current_sum - K;
HASH_FIND_INT(sum_map, &target, entry); if (entry) { count += entry->count; }
HASH_FIND_INT(sum_map, ¤t_sum, entry); if (entry) { entry->count += 1; } else { entry = (HashMapEntry*)malloc(sizeof(HashMapEntry)); entry->sum = current_sum; entry->count = 1; HASH_ADD_INT(sum_map, sum, entry); } }
HashMapEntry *tmp, *current_entry; HASH_ITER(hh, sum_map, current_entry, tmp) { HASH_DEL(sum_map, current_entry); free(current_entry); }
return count; }
|
本题的解题思路是:
- 使用
Uthash构建哈希表,存储前缀和及其出现次数
- 遍历数组,计算当前前缀和,并查找是否存在满足条件的前缀和
- 如果存在,累加计数;如果不存在,更新当前前缀和在哈希表中的计数
- 最后返回计数结果
总结
以上题目均是我在刷题过程中碰见的有关哈希表的题目,目前还在c语言,没有自带的哈希表数据结构,只能使用Uthash来进行实现,这些题目既可以帮助我学习哈希表的使用方法,也可以帮助我入门Uthash的使用,等到后面使用c++和python的时候应该就会轻松很多