线性探测解决哈希冲突的哈希表
哈希表是一种不用遍历,可以随机访问元素,实现时间复杂度为O(1)的一种数据结构,大大节省了时间。
一. 哈希表定义
我们一般定义一个数组用来表示一个哈希表的存储元素的空间,并且通过哈希函数来实现确定某个元素的存入位置、查找某个元素、删除某个元素。
哈希函数
我们定义哈希函数,一般采用除整取余法:hash(key) = key % m(m为内存单元个数,可以自己定义)。这里我们定义为10。我们就可以根据该哈希函数计算元素的存储位置:
hash(1) = 1;hash(2) = 2;hash(8) = 8;hash(5) = 5;hash(10) = 0
所以,这组数据存储如下:
这样,我们就可以根据数组下标随机访问我们想要查找的元素,实现时间复杂度O(1)。
这里的每一个下标对应一个元素,就类似于一个中文单词对应有一个英文单词,这个我们叫做键值对。
二. 哈希冲突
利用以上所说的哈希函数来存储元素时,我们可能会遇到取到的余数相同,即存储位置相同。比如说hash(1)=1,hash(11)=1,但是下标为1的位置我们只能存入一个元素,这样的情况就叫做哈希冲突。
为了解决哈希冲突,当然相应的也会有很多办法。其中,我们最常用的有两种:开散列法、闭散列法。
开散列法:每个下标对应的元素不是单纯的只放一个元素,而是存放一个链表,将相同余数的元素当做一个结点都放在链表上。(该方法见另一篇博客开散列实现哈希表)
闭散列法:发生冲突时,若要插入位置已有元素,但是哈希表还有存储位置,就线性向后探测,遇到的第一个可插入元素位置就可以进行操作。(本文即使用该方法实现哈希表)
负载因子:因为使用哈希表的目的是实现时间上的好处,避免了遍历,可以随机访问,使得操作变得简单并且高效。但是若哈希表中存储了很多元素,这样产生哈希冲突的可能性就很大,使用哈希表目的也得不到实现。这个时候就有了负载因子的出现,负载因子就是控制哈希表中存入元素的个数,若是存入元素的个数占哈希表可存元素的比例,达到或超过了负载因子,这个时候我们就应该扩充哈希表,或者是停止使用当前的哈希表。
三. 哈希表的实现——闭散列法
1. 哈希表的结构定义
//除整取余法:线性向后探测存放 #pragma once #include <stdio.h> #define SHOW_NAME printf("\n====================%s====================\n", __FUNCTION__); #define HASHMAXSIZE 1000 typedef int KeyType; typedef int ValType; typedef size_t (*HashFunc)(KeyType key);//函数指针,用来调用哈希函数,控制元素存入位置 typedef enum { Empty,//表示当前元素为空状态 Valid,//表示当前元素为有效状态 Deleted,//表示当前元素为被删除状态 }Stat; typedef struct HashElem//表示哈希表中的一个元素,该元素包括键值对 { KeyType key; ValType val; Stat stat;//表示当前元素的状态 }HashElem; typedef struct HashTable { HashElem data[HASHMAXSIZE]; HashFunc func; size_t size;//表示当前哈希表中有效元素的个数,哈希表不是线性结构,所以不能用[0,size)表示有效元素区间 }HashTable;
如上代码,我们定义一个枚举用来表示每个元素的状态,除了有效之外,其他两种状态我们都可以对它进行插入操作。我们在哈希表中存的每一个元素包括key与它对应的值,以及该元素的状态。整个哈希表我们用一个数组表示,调用一个函数用作实现该哈希表的哈希函数,哈希表中还包括一个size用以计算当前哈希表中存入了多少了元素。下面就是哈希表的基本操作:
2. 初始化
size_t HashFuncDefault(KeyType key)//哈希函数 { return key%HASHMAXSIZE; } void HashInit(HashTable* ht, HashFunc func)//初始化 { ht->size = 0; ht->func = func; size_t i = 0; for(i=0; i<HASHMAXSIZE; ++i) { ht->data[i].stat = Empty; } return; }
3. 销毁
//2.销毁 void HashDestroy(HashTable* ht)//销毁 { ht->size = 0; ht->func = NULL; size_t i = 0; for(i=0; i<HASHMAXSIZE; ++i) { ht->data[i].stat = Empty; } return; }
3. 打印函数,用以测试
//3.打印函数,用于测试 void HashPrint(HashTable* ht, const char* msg)//打印函数 { printf("[%s]\n", msg); if(ht == NULL) return; size_t i = 0; for(; i<HASHMAXSIZE; ++i) { if(ht->data[i].stat == Valid) { printf("%d: [%d:%d]\n", i, ht->data[i].key, ht->data[i].val); } } return; }
4. 插入操作
(1)根据负载因子判断当前哈希表是否能继续插入
(2)根据key调用哈希函数计算插入位置offset
(3)若当前offset状态不为有效,直接进行插入操作
(4)若当前offset状态有效,就线性向后探测直至遇到一个不为有效状态的位置,进行插入
(5)插入操作结束之后,++size
//4.插入操作 void HashInsert(HashTable* ht, KeyType key, ValType val)//插入 { if(ht == NULL)//非法输入 return; //1.根据负载因子判断哈希表是否可以继续插入 //我们约定负载因子为0.8 if(ht->size >= 0.8*HASHMAXSIZE) { //当前哈希表已达到负载因子上限,插入失败 return; } //2.根据key计算插入位置offset size_t offset = ht->func(key); while(1) { //3.若offset位置状态不为Valid,直接插入 if(ht->data[offset].stat != Valid) { ht->data[offset].stat = Valid; ht->data[offset].key = key; ht->data[offset].val = val; //6.插入结束后++size ++ht->size; return; } //5.若发现哈希表中已有key相同的元素,我们约定为插入失败 //也可以约定为继续向后查找插入或者替换已有的key键值对,这个由程序员自行决定 else if(ht->data[offset].key == key && ht->data[offset].stat == Valid) { return; } //4.当前offset位置状态是Valid,线性向后查找,找到第一个不为Valid状态的位置插入 else { ++offset; //若已探测到数组最后,就从头开始 if(offset >= HASHMAXSIZE) offset = 0; }
5. 给定一个key值,查找对应的value值
(1)根据key调用哈希函数计算key对应的位置offset
(2)若当前offset状态有效且key与查找的key相同,查找成功
(3)若当前offset状态有效,但key不相同,就线性向后探测直至遇到一个不为有效状态的位置,查找失败
//5.给定一个key,查找对应的val int HashFind(HashTable* ht, KeyType key, ValType* val)//查找 { if(ht == NULL || val == NULL)//非法操作 return 0; if(ht->size == 0)//哈希表为空 return 0; //1.根据key计算offset size_t offset = ht->func(key); while(1) { //2.从offset开始线性向后查找 //3.找到了相同的key,返回val,操作成功 if(ht->data[offset].key == key && ht->data[offset].stat == Valid) { *val = ht->data[offset].val; return 1; } //5.查找过程直至遇到一个不为Valid状态的元素,说明查找失败 else if(ht->data[offset].stat != Valid) return 0; //4.找不到相同的key,继续向后查找 else { offset++; if(offset >= HASHMAXSIZE) offset = 0; } } }
6. 给定一个key,删除值为key的元素
这里查找过程与上面相同,删除过程即将该元素的状态设置为Deleted即可,要注意的是删除之后要--size。
//6.给定一个key,删除对应元素 void HashRemove(HashTable* ht, KeyType key)//删除操作 { if(ht == NULL)//非法操作 return; if(ht->size == 0)//空哈希表 return; //1.根据key计算offset size_t offset = ht->func(key); //2.从offset线性向后查找要删除元素 while(1) { //3.若当前key与要删除的key相同,删除元素即将其状态设置为Deleted if(ht->data[offset].key == key && ht->data[offset].stat == Valid) { ht->data[offset].stat = Deleted; --ht->size; return; } //4.若查找过程中遇到状态为Empty的元素,则查找失败 else if(ht->data[offset].stat == Empty) return; //5.除3、4情况以外,++offset,线性探测下一个元素 else { ++offset; offset = offset >= HASHMAXSIZE ? 0 : offset; } } }
7. 以下为以上函数的测试代码
void TestInit() { SHOW_NAME; HashTable ht; HashInit(&ht, HashFuncDefault); printf("expected is 0, actual is %d\n", ht.size); printf("expected is %p, actual is %p\n", HashFuncDefault, ht.func); } void TestDestroy() { SHOW_NAME; HashTable ht; HashInit(&ht, HashFuncDefault); HashDestroy(&ht); printf("expected is 0, actual is %d\n", ht.size); printf("expected is nil, actual is %p\n", ht.func); } void TestInsert() { SHOW_NAME; HashTable ht; HashInit(&ht, HashFuncDefault); HashInsert(&ht, 1, 1); HashInsert(&ht, 2, 2); HashInsert(&ht, 1001, 3); HashInsert(&ht, 1002, 4); HashPrint(&ht, "插入4个元素"); } void TestFind() { SHOW_NAME; HashTable ht; HashInit(&ht, HashFuncDefault); HashInsert(&ht, 1, 1); HashInsert(&ht, 2, 2); HashInsert(&ht, 1001, 3); HashInsert(&ht, 1002, 4); HashPrint(&ht, "插入4个元素"); int value; int ret = HashFind(&ht, 1001, &value); printf("expected is 3, actual is %d\n", value); ret = HashFind(&ht, 2001, &value); if(ret == 0) printf("查找失败\n"); } void TestRemove() { SHOW_NAME; HashTable ht; HashInit(&ht, HashFuncDefault); HashInsert(&ht, 1, 1); HashInsert(&ht, 2, 2); HashInsert(&ht, 1001, 3); HashInsert(&ht, 1002, 4); HashPrint(&ht, "插入4个元素"); HashRemove(&ht, 1001); HashPrint(&ht, "删除key=1001的元素"); HashRemove(&ht, 2001); HashPrint(&ht, "删除key=2001的元素"); }