哈希表:哈希表是一种可以在O(1)时间内查找到某个元素的数据结构,它是用数组实现的,这依赖于内存的随机访问特性。那哈希表到底是什么样呢?哈希表是通过哈希函数(hashfunc)使元素的存储位置与关键码一一对应的关系,所以在进行元素查找的时候可以通过该函数快速的找到该元素。若结构中存在关键字和key相等的元素,则必然存储在f(k)的位置上。由此,不需要进行比较便可以直接取得所查记录。这个对应函数就被称为散列函数(哈希函数),依照这个思想所建立出来的表就被称为散列表(哈希表)。
常见的哈希函数有很多种,在这里我先用除留余数法简单的画一下哈希表是怎样存储元素的。
但是有没有发现一个问题,如果我们想再存入一个值为16的元素,应该把它存在哪里,这就是哈希冲突问题;所谓哈希冲突就是对于两个不同的数据元素m和n,存在HashFunc(m)==HashFunc(n);即不同的关键字计算出相同的哈希地址,这种现象称为哈希冲突或者哈希碰撞,把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
那我们应该怎样去很好的解决掉哈希冲突问题呢?
一般来说有三种解决方式:
第一种:引起哈希冲突的一个很可能的因素就是哈希函数设计的不够合理,哈希函数设计时应遵从下面几条原则:
1.哈希函数的定义域必须包含需要存储的所有关键码,即如果哈希表中有m个地址时,其值域必须在0~m-1之间。
2.哈希函数计算出来的哈希地址应该均匀的分布在整个哈希表中。
3.哈希函数的设计应该进可能的简单一点。
常见的哈希函数可自行百度。
第二种:闭散列
闭散列也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明哈希表中还有位置,可以把key放在下一个空位中去,那么如何去寻找下一个空余位置呢?
线性探测:举个例子:
根据闭散列线性的处理哈希冲突的方法,我们不难发现,不能随便删除哈希表中的元素,如果直接删除会影响其他元素的搜索。假设我们在上面例子中的哈希表中删除了元素32,当我们再想去查找元素12的时候,我们会首先去位置2的地方去找,此时我们发现位置2是空着的,我们就会认为12不存在于哈希表中,我们肯定就不再去向后进行查找了。(只有当位置2的不为空的时候,我们才会认为有可能发生了哈希冲突,才会去向后继续查找)。
采用线性探测的方法非常简单,但是当大量的哈希冲突聚集在一起,容易产生“数据堆积”,不同的关键码占据了可利用的空位置,使得寻找某一个关键字可能需要进行多次的比较,降低了搜索效率。那么如何缓解这种问题呢?我们引入了散列表的负载因子a=插入表中的元素个数/散列表的长度;a是散列表装满程度的标志因子,a越大,表明表中的元素越多,产生冲突的可能性就越大;反之,a越小,表明插入表中的元素就越少,产生哈希冲突的可能性就越小,实际上,散列表的平均查找长度是负载因子a的函数,只是不同处理冲突的方法有不同的函数。对于开放定址法,a非常重要,一般控制在0.7-0.8以下,当插入表中的元素个数=a*散列表长度时,我们就认为该哈希表已经满了,不可以再进行插入元素了。a超过0.8,查表时的cpu缓存命不中的概率呈指数上升趋势。
二次探测:发生哈希冲突时,二次探测法在表中查找下一个空位置的计算公式为: Hi=(H0+i^2)%m;Hi=(H0-i^2)%m;i=1,2,3...
m为哈希表的大小。
在上面的例子中,哈希冲突有三个元素:25,35和55,当插入5的时候直接存在了位置5处,插入35的时候发现位置5被占用了,所以就不能根据公式Hi=(H0+i^2)%m=(5+1)%10=6;尝试去往位置6出插入,发现位置6也被占用了,于是根据公式Hi=(H0-i^2)%m=(5-1)%10=4;尝试往位置4进行插入,位置4为空,所以将35插在了位置4处。接下来插入最后一个元素55时发现4和6的位置都被占用了,于是根据公式Hi=(H0+i^2)%m=(5+2^2)%10=9(此时i=2);尝试向位置9进行插入,并插入成功。
研究表明:当表的程度为质数且表的负载因子不超过0.5时,新的元素一定能够插入,而且任意一个位置都不会被探查两次,因此表中只要有一半的空位置,就不存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入元素的时候必须保证负载因子a不超过0.5,如果超出必须对哈希表进行增容。
用闭散列实现哈希结构的代码如下:
//hash_table.h #pragma once #include<stddef.h> #include<Windows.h> #include<stdio.h> #define Header printf("=============%s===============\n",__FUNCTION__); #define HashMaxSize 1000 typedef enum Stat { Empty, Valid, Invalid // 当前元素被删除了 } Stat; typedef int KeyType; typedef int ValType; typedef size_t (*Hashfunc)(KeyType); typedef struct HashElem { KeyType key; ValType value; Stat stat; // 引入一个 stat 标记来作为是否有效的标记 } HashElem; typedef struct HashTable { HashElem data[HashMaxSize]; size_t size; Hashfunc hash_func; } HashTable; void HashInit(HashTable* ht, Hashfunc hash_func); size_t HashfuncDefault(KeyType key); void HashPrint(HashTable* ht,const char* msg); int HashInsert(HashTable* ht, KeyType key, ValType value); // 输入key, 查找对应key的value. int HashFind(HashTable* ht, KeyType key, ValType* value); void HashRemove(HashTable* ht, KeyType key); int HashEmpty(HashTable* ht); size_t HashSize(HashTable* ht); void HashDestroy(HashTable* ht); void CountNum(HashTable* ht);//统计每个元素出现了几次
//hash_table.c #include "hash_table.h" size_t HashfuncDefault(KeyType key) { return key%HashMaxSize; } void HashInit(HashTable* ht, Hashfunc hash_func) { size_t i=0; if(ht==NULL)//非法输入 { return; } ht->hash_func=hash_func; ht->size=0; for(; i<HashMaxSize; ++i) { ht->data[i].stat=Empty; } return; } int HashInsert(HashTable* ht, KeyType key, ValType value) { size_t offset; if(ht==NULL) { return 0; } if(ht->size>HashMaxSize*0.8)//负载因子设为0.8 { return 0; } offset=ht->hash_func(key); while(1) { if(ht->data[offset].stat==Valid) { if(ht->data[offset].key==key) { return 0; } ++offset; if(offset>=HashMaxSize) { offset-=HashMaxSize; } } else { ht->data[offset].key=key; ht->data[offset].value=value; ht->data[offset].stat=Valid; ++ht->size; break; } } return 1; } void HashPrint(HashTable* ht,const char* msg) { size_t i=0; printf("%s>\n",msg); for(; i<HashMaxSize; ++i) { if(ht->data[i].stat!=Empty) { printf("[%lu] key:[%d] value[%d] stat[%d]",i,ht->data[i].key, ht->data[i].value,ht->data[i].stat); printf("\n"); } } } int HashFind(HashTable* ht, KeyType key, ValType* value) { size_t offset; if(ht==NULL) { return 0; } offset=ht->hash_func(key); while(1) { if(key==ht->data[offset].key&&ht->data[offset].stat==Valid)//找到了 { *value=ht->data[offset].value; return 1; } else if(ht->data[offset].stat==Empty) { return 0; } else { ++offset; if(offset>=HashMaxSize) { offset-=HashMaxSize; } } } return 0; } void HashRemove(HashTable* ht, KeyType key) { size_t offset; if(ht==NULL) { return; } offset=ht->hash_func(key); while(1) { if(key==ht->data[offset].key&&ht->data[offset].stat==Valid)//找到了 { ht->data[offset].stat=Invalid; --ht->size; return; } else if(ht->data[offset].stat==Empty) { break; } else { ++offset; if(offset>=HashMaxSize) { offset-=HashMaxSize; } } } } void HashDestroy(HashTable* ht) { size_t i=0; if(ht==NULL) { return; } for(; i<HashMaxSize; ++i) { ht->data[i].stat=Empty; } ht->size=0; ht->hash_func=NULL; return; } int HashEmpty(HashTable* ht) { if(ht==NULL) { return 1; } return ht->size==0?1:0; } size_t HashSize(HashTable* ht) { if(ht==NULL) { return 0; } return ht->size; } void CountNum(HashTable* ht) { int arr[]={1,2,4,2,1,4,1}; int i=0; int ret=0; int value=0; int sz=sizeof(arr)/sizeof(arr[0]); Header; HashInit(ht,HashfuncDefault); for(; i<sz; ++i) { ret=HashFind(ht,arr[i],&value); if(ret==0) { HashInsert(ht,arr[i],1); } else { HashRemove(ht,arr[i]); HashInsert(ht,arr[i],value+1); } } for(i=0; i<sz; i++) { printf("%d ",arr[i]); } printf("\n"); HashPrint(ht,"统计之后的结果为"); }
//测试用例test.c #include "hash_table.h" void TestInit() { HashTable ht; size_t i=0; Header; HashInit(&ht,HashfuncDefault); printf("size expect 0,actual %d\n",ht.size); printf("ht->hash_func expect %p,actual %p\n",HashfuncDefault,ht.hash_func); for(i=0;i<HashMaxSize; ++i) { if(ht.data[i].stat!=Empty) { printf("pos [%lu] elem error!\n",i); } } } void TestInsert() { HashTable ht; Header; HashInit(&ht,HashfuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,1001,200); HashInsert(&ht,1002,300); HashInsert(&ht,1003,400); HashInsert(&ht,2,500); HashInsert(&ht,5,600); HashPrint(&ht,"向哈希表插入六个元素"); } void TestFind() { HashTable ht; int ret; int value=0; Header; HashInit(&ht,HashfuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,1001,200); HashInsert(&ht,1002,300); HashInsert(&ht,1003,400); HashInsert(&ht,2,500); HashInsert(&ht,5,600); ret=HashFind(&ht,1002,&value); printf(" ret expect 1,actual %d\n value expect 300,actual %d\n",ret,value); ret=HashFind(&ht,3,&value); printf(" ret expect 0,actual %d\n",ret); } void TestRemove() { HashTable ht; Header; HashInit(&ht,HashfuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,1001,200); HashInsert(&ht,1002,300); HashInsert(&ht,1003,400); HashInsert(&ht,2,500); HashInsert(&ht,5,600); HashRemove(&ht,1002); HashPrint(&ht,"删除1002之后的结果为"); } void TestDestroy() { HashTable ht; Header; HashInit(&ht,HashfuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,1001,200); HashInsert(&ht,1002,300); HashInsert(&ht,1003,400); HashInsert(&ht,2,500); HashInsert(&ht,5,600); HashDestroy(&ht); HashPrint(&ht,"销毁后的结果为"); } void TestEmpty() { HashTable ht; int ret; Header; HashInit(&ht,HashfuncDefault); ret=HashEmpty(&ht); printf("ret expect 1,actual %d\n",ret); HashInsert(&ht,1,100); HashInsert(&ht,1001,200); ret=HashEmpty(&ht); printf("ret expect 0,actual %d\n",ret); } void TestSize() { HashTable ht; size_t size; Header; HashInit(&ht,HashfuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,1001,200); HashInsert(&ht,1002,300); HashInsert(&ht,1003,400); size=HashSize(&ht); printf("size expect 4,actual %lu\n",size); } void TestCountNum() { HashTable ht; Header; HashInit(&ht,HashfuncDefault); CountNum(&ht); } int main() { TestInit(); TestInsert(); TestFind(); TestRemove(); TestDestroy(); TestEmpty(); TestSize(); TestCountNum(); system("pause"); return 0; }
第三种:开散列
开散列法又叫做链地址法(开链法);首先对每个关键码使用哈希函数求哈希地址,具有相同地址的关键码归于同一个集合,每一个集合称为一个桶,各个桶中的元素通过一个单链表连接起来,各链表的节点存储在一个哈希表中。
那么开散列哈希桶是怎样存储的呢?
通常情况下,每个桶对应的节点很少,将n个关键码通过某一哈希函数,存放到哈希表中的m个桶中,那么每一个链表的平均长度为:n/m;以搜索平均长度为:n/m的链表代替搜索长度为n的顺序表,效率高了很多。
应用链地址法需要增加链接指针,这看似增加了存储开销,实际上,由于开地址法必须保证大量的空闲空间以确保搜索效率,如二次探测法要求负载因子a<0.5;而表项所占空间又比指针大的多,所以从本质上来说使用链地址法反而比开地址法更加节省空间。
下面是开散列的代码实现:
//hash_table.h #pragma once #include <stddef.h> #include<Windows.h> #include<stdio.h> #define HashMaxSize 1000 typedef int KeyType; typedef int ValType; typedef size_t (*HashFunc)(KeyType key); typedef struct HashElem { KeyType key; ValType value; struct HashElem* next; } HashElem; // 数组的每一个元素是一个不带头结点的链表 // 对于空链表, 我们使用 NULL 来表示 typedef struct HashTable { HashElem* data[HashMaxSize]; size_t size; HashFunc hash_func; } HashTable; void HashInit(HashTable* ht, HashFunc hash_func); // 约定哈希表中不能包含 key 相同的值. int HashInsert(HashTable* ht, KeyType key, ValType value); int HashFind(HashTable* ht, KeyType key, ValType* value); void HashRemove(HashTable* ht, KeyType key); size_t HashSize(HashTable* ht); int HashEmpty(HashTable* ht); void HashDestroy(HashTable* ht);
//hash_table.c #include "hash_table2.h" void HashInit(HashTable* ht, HashFunc hash_func) { size_t i=0; if(ht==NULL) { return; } ht->size=0; ht->hash_func=hash_func; for(; i<HashMaxSize; ++i) { ht->data[i]=NULL; } return; } HashElem* HashBucketFind(HashElem* head,KeyType key) { HashElem* cur; if(head==NULL) { return NULL; } cur=head; for(; cur!=NULL; cur=cur->next) { if(cur->key!=key) { continue; } else { return cur; } } return NULL; } HashElem* CreateHashNode(KeyType key,KeyType value) { HashElem* new_node=(HashElem*)malloc(sizeof(HashElem)); new_node->key=key; new_node->value=value; new_node->next=NULL; return new_node; } int HashInsert(HashTable* ht, KeyType key, ValType value) { size_t offset; HashElem* res; HashElem* new_node; if(ht==NULL) { return 0; } offset=ht->hash_func(key); res=HashBucketFind(ht->data[offset],key); if(res!=NULL) { return 0; } else { new_node=CreateHashNode(key,value); new_node->next=ht->data[offset]; ht->data[offset]=new_node; ++ht->size; return 1; } return 0; } int HashFind(HashTable* ht, KeyType key, ValType* value) { size_t offset; HashElem* res; if(ht==NULL||value==NULL) { return 0; } offset=ht->hash_func(key); res=HashBucketFind(ht->data[offset],*value); if(res==NULL) { return 0; } else { *value=ht->data[offset]->value; return 1; } return 0; } int HashBucketFindPrevandcur(HashElem* head,KeyType key,HashElem** cur_output,HashElem** prev_output) { HashElem* pre=NULL; HashElem* cur=head; if(head==NULL||cur_output==NULL||prev_output==NULL) { return 0; } for(; cur!=NULL; pre=cur, cur=cur->next) { if(cur->key==key) { *cur_output=cur; *prev_output=pre; return 1; } } return 0; } void DestroyHashNode(HashElem* ptr) { free(ptr); } void HashRemove(HashTable* ht, KeyType key) { size_t offset; int res; HashElem* cur; HashElem* pre; if(ht==NULL) { return; } offset=ht->hash_func(key); pre=NULL; cur=ht->data[offset]; res=HashBucketFindPrevandcur(ht->data[offset],key,&cur,&pre); if(res==0) { return; } if(cur==ht->data[offset]) { ht->data[offset]=cur->next; } else { pre->next=cur->next; } DestroyHashNode(cur); --ht->size; return; } size_t HashSize(HashTable* ht) { if(ht==NULL) { return 0; } return ht->size; } int HashEmpty(HashTable* ht) { if(ht==NULL) { return 0; } return ht->size==0?1:0; } void HashDestroy(HashTable* ht) { size_t i=0; HashElem* cur; HashElem* to_delete; if(ht==NULL) { return; } ht->hash_func=NULL; ht->size=0; for(i=0;i<HashMaxSize; ++i) { cur=ht->data[i]; while(cur!=NULL) { to_delete=cur; cur=cur->next; DestroyHashNode(to_delete); } } }
//测试用例test.c #include "hash_table2.h" #define HEADER printf("================%s=================\n",__FUNCTION__); size_t HashFuncDefault(KeyType key) { return key%HashMaxSize; } void HashPrint(HashTable* ht,const char* msg) { size_t i=0; HashElem* cur; if(ht==NULL) { return; } printf("[%s]\n",msg); for(i=0; i<HashMaxSize; ++i) { if(ht->data[i]!=NULL) { cur=ht->data[i]; for(; cur!=NULL; cur=cur->next) { printf("[%d] ",i); printf("%d:%d;",ht->data[i]->key,ht->data[i]->value); } printf("\n"); } } } void TestInit() { HashTable ht; size_t i=0; HEADER; HashInit(&ht,HashFuncDefault); printf("size expect 0,actual %d\n",ht.size); printf("HashFunc expect %p,actual %p\n",HashFuncDefault,ht.hash_func); for(i=0; i<HashMaxSize; ++i) { if(ht.data[i]!=NULL) { printf("pos[%lu] error!\n",i); } } } void TestInsert() { HashTable ht; HEADER; HashInit(&ht,HashFuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,2,200); HashInsert(&ht,1001,300); HashInsert(&ht,3,400); HashPrint(&ht,"插入四个元素"); printf("size %d\n",ht.size); } void TestFind() { HashTable ht; ValType value=0; int ret; HEADER; HashInit(&ht,HashFuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,2,200); HashInsert(&ht,1001,300); HashInsert(&ht,3,400); ret=HashFind(&ht,3,&value); printf("ret expect 1,ectual %d\n",ret); printf("value expect 400,actual %d\n",value); } void TestRemove() { HashTable ht; HEADER; HashInit(&ht,HashFuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,2,200); HashInsert(&ht,1001,300); HashInsert(&ht,3,400); HashRemove(&ht,1); HashPrint(&ht,"删除1之后的结果为"); } void TestSize() { HashTable ht; int ret; HEADER; HashInit(&ht,HashFuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,2,200); HashInsert(&ht,1001,300); HashInsert(&ht,3,400); ret=HashSize(&ht); printf("ret expect 4,actual %d\n",ret); } void TestEmpty() { HashTable ht; int ret; HEADER; HashInit(&ht,HashFuncDefault); ret=HashEmpty(&ht); printf("ret expect 1,actual %d\n",ret); HashInsert(&ht,1,100); HashInsert(&ht,2,200); HashInsert(&ht,1001,300); HashInsert(&ht,3,400); ret=HashEmpty(&ht); printf("ret expect 0,actual %d\n",ret); } void TestDestroy() { HashTable ht; HEADER; HashInit(&ht,HashFuncDefault); HashInsert(&ht,1,100); HashInsert(&ht,2,200); HashInsert(&ht,1002,300); HashInsert(&ht,3,400); HashDestroy(&ht); printf("size expect 0,actual %d\n",ht.size); } int main() { TestInit(); TestInsert(); TestFind(); TestRemove(); TestSize(); TestEmpty(); TestDestroy(); system("pause"); return 0; }