Hello 算法学习--哈希表实现(C语言)

哈希表概念

哈希表又被称为散列表,它通过建立键key与值value之间的映射,实现高效的元素查询

即我们想哈希表中输入一个键,则我们可以在O(1)的时间内查找到这个键key所对应的值

哈希表的数组实现

我们考虑最简单的情况,仅仅使用一个数组来实现哈希表

在哈希表中,我们将数组的每个空位称为桶,每个桶都可以存储一个键值对,因此查找键key就是查找到key所对应的桶,并在桶中取出值value

那我们如果通过键key来实现对桶的定位呢?

答:这是通过哈希函数实现的,哈希函数是将一个较大的输入空间映射到一个较小的输出空间,在哈希函数中,key代表的就是所有的输入空间,而输出空间就是所有桶的索引,即数组的下标,因此我们可以可以通过哈希函数得到该key对应的键值在数组中的存储位置

实现的步骤为:

1.通过某种哈希函数计算出哈希值

2.将哈希值对数组长度(桶的数量)取模,从而获得该key所对应的数组索引

哈希函数:

在我们设计哈希函数时,需要尽可能的简单,能够快速计算key对应的地址

哈希函数映射的地址应该均匀的分布在整个地址空间,可以避免冲突

简单的哈希函数实现

头文件:function.h  //该文件包含了变量及函数的声明

#ifndef HASHFUCTION
#define HASHFUCTION

#define HASH_SIZE 20

//存储键对值
typedef struct
{
        int key;      //定义键
        char *value;  //定义值
}Pair;

//使用数组来存储哈希表
typedef struct
{
        Pair *buckets[HASH_SIZE];
}ArrayHashMap;

//定义MapSet用于存储键的集合
typedef struct
{
        void *set; //指向键或者值数组的指针
        int len;   //数组的长度
}MapSet;

//函数声明
ArrayHashMap *newHashArrayMap(void);
int hash_function(int key);
void put(ArrayHashMap *hmap, const int key , char *value);
void removeItem(ArrayHashMap *hmap , const int key);
void pairSet(ArrayHashMap *hmap, MapSet *set);
void KeySet(ArrayHashMap *hmap, MapSet *set);
void valueSet(ArrayHashMap *hmap, MapSet *set);
void print(ArrayHashMap *hmap);
Pair *reseach_HASH(ArrayHashMap *hmap , const int key);

#endif

函数定义的文件:function.c

#include"function.h"
#include<stdlib.h>
#include<string.h>
#include<stdio.h>

//构造一个哈希函数
ArrayHashMap *newHashArrayMap(void)
{
        //创造一个哈希表,并为其分配内存空间
        ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));
        //返回新创造的哈希表的地址
        return hmap;
}

//哈希函数的实现
int hash_function(int key)
{
        return key % 10;
}

//向哈希表中添加新的元素
//hmap为要添加元素的哈希表,key为键,value为值
void put(ArrayHashMap *hmap , const int key , char *value)
{
        //为新添加的元素分配内存空间
        Pair *pair = (Pair *)malloc(sizeof(Pair));
        //将新传入的键进行存储
        pair->key = key;
        //存储新传入的值,因为C字符串以空字符结尾,因此需要加1
        pair->value = malloc(sizeof(value)+1);
        strcpy(pair->value, value);

        //查找到key对应的next数组下标,将键值存储到哈希表中
        int index = hash_function(key);
        hmap->buckets[index] = pair;
}

//删除哈希表中的元素
void removeItem(ArrayHashMap *hmap , const int key)
{
        //先查找到key对应的数组索引
        int index = hash_function(key);
        //删除哈希表中存储的值,释放该内存
        free(hmap->buckets[index]->value);
        free(hmap->buckets[index]);
        hmap->buckets[index] = NULL;
}

//获取所有的键值对
void pairSet(ArrayHashMap *hmap , MapSet *set)
{
        //指向Pair结构体的指针,用于存储键值对的数组
        Pair *entries;
        //遍历哈希表,查找哈希表的元素
        int i = 0 , index = 0;

        //计算哈希表中有效的键对数
        int tatol = 0;
        for(i = 0 ; i < HASH_SIZE ; i++)
        {
                if(hmap->buckets[i] != NULL)
                        tatol++;
        }
        //分配键对值的数组内存空间
        entries = malloc(sizeof(Pair) * tatol);
        //遍历哈希表,将有效的键值对存储到entries数组中
        for(i =  0; i < HASH_SIZE ; i++)
        {
                //判断哈希表中的元素是否有效
                if(hmap->buckets[i] != NULL)
                {
                        //将哈希表中的键存储到entries数组中对应位置的key成员中
                        entries[index].key = hmap->buckets[i]->key;
                        //动态分配内存空间,便于存储字符串
                        entries[index].value = malloc(strlen(hmap->buckets[i]->value)+1);
                        strcpy(entries[index].value , hmap->buckets[i]->value);
                        index++;
                }
        }
        //将存储键值的数组指针赋值给MapSet中的set成员
        set->set = entries;
        set->len = tatol;
}




//获取所有的键
void KeySet(ArrayHashMap *hmap , MapSet *set)
{
        //定义一个指向整型数组的指针,用于存储键值
        int *key;
        //循环计算器及数组下标
        int i = 0, index = 0;
        //存储有效的键的数量
        int tatol = 0;

        //查找有效的键
        for(i = 0 ; i < HASH_SIZE ; i++)
        {
                if(hmap->buckets[i] != NULL)
                        tatol++;
        }
        //为key数组分配内存空间
        key = (int *)malloc(sizeof(int)*tatol);
        //再次遍历数组,如果key有效,则存储到key数组里
        for(i = 0 ; i < HASH_SIZE ; i++)
        {
                if(hmap->buckets[i] != NULL)
                        key[index++] = hmap->buckets[i]->key;
        }

        //将key数组传递给set,并传入有效值的数量
        set->set = key;
        set->len = tatol;
}

//获得所有值
void valueSet(ArrayHashMap *hmap , MapSet *set)
{
        //定义一个指向指针数组的指针,用来存储有效值
        char **value;

        int i = 0 , index = 0;
        int tatol = 0;

        //计算有效值的数量
        for(i = 0 ; i < HASH_SIZE ; i++)
        {
                if(hmap->buckets[i] != NULL)
                        tatol++;
        }
        //为value分配内存空间
        value = (char **)malloc(sizeof(char*) * tatol);
        //遍历哈希表,将有效值存储到value数组中
        for(i = 0 ; i < HASH_SIZE ; i++)
        {
                if(hmap->buckets[i] != NULL)
                        value[index++] = hmap->buckets[i]->value;
        }

        //将获取的有效值存储到MapSet,并传入有效值的数量
        set->set = value;
        set->len = tatol;
}

//查找哈希表中的值
Pair *reseach_HASH(ArrayHashMap *hmap , const int key)
{
        //通过key查找到其在哈希数组中的下标
        int index = key % HASH_SIZE;
        Pair *pair =  hmap->buckets[index];
        if(pair == NULL || pair->key != key)
                return NULL;
        return pair;
}

//打印哈希表
void print(ArrayHashMap *hmap)
{
        int i;
        //用于存储所有键值对的集合
        MapSet set;
        //获取所有的键值对
        pairSet(hmap,&set);
        //将set中的键值对转化为Pair类型的数组,因为set定义时是void类型的
        Pair *entries = (Pair *)set.set;
        for(i = 0 ; i < set.len ; i++)
        {
                printf("%d -> %s\n",entries[i].key,entries[i].value);
        }
        free(set.set);
}
    

主函数所在的文件main.c //对函数进行了测试

#include"function.h"
#include<stdio.h>

int main(int argc, char **argv)
{
        //构造新的哈希表
        ArrayHashMap *hmap = newHashArrayMap();
        //向哈希表中添加元素
        put(hmap,1001,"小白");
        put(hmap,1022,"小红");
        put(hmap,1064,"小篮");
        put(hmap,1063,"小黑");
        print(hmap);
        printf("\n");
        //删除小红
        removeItem(hmap,1022);
        print(hmap);
        printf("\n");
        //查找key对应的元素
        if(reseach_HASH(hmap,1010))
                printf("1010 -> %s/n",reseach_HASH(hmap,1010)->value);
        else
                printf("NO\n");
        if(reseach_HASH(hmap,1063))
                printf("1063 -> %s/n",reseach_HASH(hmap,1063)->value);
        else
                printf("NO\n");
        printf("\n");


        return 0;
}

测试所得的结果

哈希冲突

从本质上来看,哈希函数就是将所有的key构成的空间映射到数组索引构成的输出空间,而输入的空间往往会大于输出空间,因此理论上一定存在着多个输入对应一个输出的情况,例如

1001 % 10 = 1

1011 % 10 = 1

这种多个输入对应一个输出的情况,就是哈希冲突

我们可以知道,哈希数组容量的容量越大,那么多个key被分配到同一个桶中的概率就越低,冲突也就越少,因此我们可以通过扩容哈希表来减少哈希冲突,但是这种扩容的操作效率低

哈希冲突的解决方法

1.改变哈希表的结构,使得哈希表能够在发生冲突时也能正常工作

通过链式结构来进行解决

在原始的哈希表中,每个哈希桶只能存储一个键值对,链式结构将每个元素都转化为链表,将键值作为链表的节点,将所有发生冲突的值都存储在同一个链表中

此时使用链式地址来实现哈希表的操作就发生了变化,此时

查询元素:输入一个key,通过哈希函数找到对应哈希桶的索引,即可以访问的链表的头结点,通过该头结点去遍历链表,对比key值以查找目标键对值

添加元素:先通过哈希函数找到链表的头节点,然后将该元素作为节点再添加链表中

删除元素:通过哈希函数找到对应的链表的头结点,依次遍历链表找到符合条件的值进行链表节点的删除操作

缺点:

占用的内存空间大,查询元素效率低(遍历链表)

2.使用开放寻址的方法

开放寻址的方法是指不使用额外的数据结构,通过多次的探测来处理哈希冲突,包括线性探测,平方探测,多次哈希

线性探测

线性探测采用固定步长的线性搜索来进行探测,其操作方法与普通哈希表有所不同。

插入元素:通过哈希函数计算桶索引,若发现桶内已有元素,则从冲突位置向后线性遍历,步长通常为1,即在原来的索引基础上+1

查找元素:若发现哈希冲突,则使用相同步长向后进行线性遍历,直到找到对应元素,直接返回值,如果遇到空桶,说明目标元素不在哈希表中,返回空

例如:

200 % 100 = 0; index = 0

500 % 100 = 0; 发现冲突,index = 0+1 ;

缺点:

线性探测容易产生聚集现象,这又会导致哈希冲突发生的可能性增加

注意:我们不能直接在开放寻址的哈希表中直接删除元素,这样会导致空桶的产生,当我们查询元素时,线性探测会该空桶就会返回,因此在该空桶之下的元素都无法再被访问到,程序可能误判这些元素不存在

解决方法:

使用懒删除机制,即不直接从哈希表中删除元素,而是使用一个常量来代替它,空和该常量都代表这个桶为空,但线性探测到常量时应该继续遍历,因为剩下的部分可能存在键值对,

但是,懒删除会导致哈希表性能下降,因为每次删除操作都会产生一个删除标记,随着常量数量的增加,搜索时间也会增加,因为线性探测可能需要跳过多个常量才能找到目标元素,为此,考虑在线性探测中记录遇到的首个常量的索引,并将搜索到的目标元素与该常量交换位置。这样做的好处是当每次查询或添加元素时,元素会被移动至距离理想位置(探测起始点)更近的桶,从而优化查询效率。

平方探测

平方探测和线性探测一样,但是跨过的步长不一样,线性探测每次跨过固定的步长,而平方探测每次跨过的探测次数的平方的步数

这样就能缓解线性探测的聚集现象,因为每次跳过的距离更大,因此能让数据分布得更加均匀

缺点:

1.仍然有聚集现象,即某些位置比其他位置更容易被占用

2.由于平方的增长,平方探测可能不会探测整个哈希表,这意味着即使哈希表中有空桶,平方探测也可能无法访问到它

多次哈希

在遇到冲突时,使用多个哈希函数进行探测

在插入元素时,如果发生冲突,则使用多个哈希函数,直到找到空位,在空位插入

在查找元素时,在相同的哈希函数的顺序下进行查找,直到找到目标元素再进行返回,如果遇到空位或者哈希函数已经全部使用,则说明该元素不存在,返回空

优点:

不容易产生聚集现象

缺点:

增加了计算的次数

总结

跟随Hello算法这本书使用C语言来实现了数组的哈希,理解了哈希的原理已经工作的方式,了解了哈希冲突发生的原因以及解决的办法,但使用的还不够熟练,而且没有进行解决哈希冲突的代码实现,需要进行补充学习

  • 33
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值