1、散列表的概述
2、散列函数的作用与构造
3、散列表查找的代码实现
终于来到本系列的最终章了,在前面说了那么多散列表查找的理论,本章会将散列表的查找进行代码实现。
HashTable:散列表结构
elem:动态数组
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 // 定义散列表长为数组的长度
#define NULLKEY -32768
typedef struct{
int *elem; // 数据元素存储基址,动态分配数组
int count; // 当前数据元素个数
}HashTable;
int m = 0; // 散列表表长,全局变量
/* 初始化散列表 */
Status InitHashTable(HashTable *H){
int i;
m = HASHSIZE;
H->count = m;
H->elem = (int *)malloc(m * sizeof(int));
for(i = 0; i < m; i++)
H->elem[i] = NULLKEY;
return OK;
}
为了计算插入时的地址,定义散列函数
/* 散列函数 */
int Hash(int key){
return key % m; //除留余数法
}
初始化完成之后,就可以对散列表进行插入操作。
/* 插入关键字进行散列表 */
void InsertHash(HashTable *H, int key){
int addr = Hash(key) // 求散列地址
while(H->elem[addr] != NULLKEY) // 如果不为空,则冲突
addr = (addr + 1) % m; // 开放定址法的线性探索
H->elem[addr] = key; // 知道有空位后插入关键字
}
散列表存在后,我们在需要时就可以通过散列表查找关键字
/* 散列表查找关键字 */
Status SearchHash(HashTable H, int key, int *addr){
*addr = Hash(key); // 求散列地址
while(H.elem[*addr] != key){
// 如果不为空,则冲突
*addr = (*addr + 1) % m; // 开方定址法的线性探索
if(H.elem[*addr] == NULLKEY || *addr = Hash(key)){
/* 如果循环回到原点 */
return UNSUCCESS; // 则说明关键字不存在
}
}
return SUCCESS;
}
从上面的代码我们可以看出,查找的代码与插入的代码非常相似,只需要做一个不存在关键字的判断而已。
如果没有冲突时,此时的散列表查找效率是最高的,时间复杂度只为O(1);
但可惜,这也只是如果,在实际应用中,冲突是不可避免的。
那散列查找的平均查找长度取决于哪些因素呢?
散列函数的好坏直接决定了出现冲突的频繁程度。但由于不同的散列函数对同一组随机的关键字,产生冲突的可能性是相同的,因此我们可以不考虑它对平均查找长度的影响。
相同的关键字、散列函数,但处理冲突的方法不同,会使得平均查找长度不同。
如线性探测处理冲突可能会产生堆积,显然就没有二次探测法好。
而链地址法处理冲突不会产生任何堆积,因此具有更佳的平均查找性能。
所谓的装载因子α = 填入表中的记录个数 / 散列表长度。
α 标志着散列表的装满的程度:
这就相当于,你去公共厕所里面上厕所,里面有12个坑位,但是已经有11个坑位有人了。
那么此时的装填因子α = 11 / 12 = 0.9167,此时你敲门碰到里面有人的几率就十分的大。
所以,散列表的平均查找长度取决于装填因子,而不是取决于查找集合中的记录个数。
综上所述,只要我们选择一个合适的装填因子以便将平均查找长度限定在一个范围内,此时我们散列查找的时间复杂度就真的是O(1)了。
通常我们都是将散列表的空间设置得比查找集合大,此时虽然是浪费了一定的空间,但换来的却是查找效率的大大提升,是一笔划算的买卖。
散列表是一种非常高效的查找数据结构,但它也有它的缺点。
散列表的优缺点:
所以,散列表对于那些查找性能要求高,记录之间关系无要求的数据有非常好的适用性。
细谈散列表系列到这里就结束啦,希望我们后会有期!!!
以上就是本篇文章的所有内容了,如果觉得有帮助到你的话,
麻烦动动小手,点赞收藏转发!!!
你的每一次点赞都是我更新的最大动力~
我们下期再见!