hash表的实现和hash桶的示例(c实现)

本文深入探讨哈希表的工作原理及其在现代高性能系统中的应用。介绍了如何通过散列函数将数据快速定位,并讨论了几种常见的散列方法,如直接定址法、除留取余法等。此外,还讲解了处理哈希冲突的两种主要方法——分离链接法和开放定址法,以及具体的哈希桶实现细节。

hash是以空间换时间的结构,现在空间越来越大,而且对性能要求越来越高的年代,这绝对是值得的。

hash含义就是散列,就是把我们本来想​查找的一大群结构体数据分散开,更容易查找。一个好的hash函数应该做到对所有元素平均分散排列,尽量避免或者降低他们之间的冲突(Collision)。hash函数的选择必须慎重,如果不幸所有的元素之间都产生了冲突,那么hash表将退化为链表,其性能会大打折扣,时间复杂度迅速降为O(n),绝对不要存在任何侥幸心理,因为那是相当危险的。历史上就出现过利用Linux内核hash函数的漏洞,成功构造出大量使hash表发生碰撞的元素,导致系统被DoS,所以目前内核的大部分hash函数都有一个随机数作为参数进行掺杂,以使其最后的值不能或者是不易被预测。这又对 hash函数提出了第二点安全方面的要求:hash函数最好是单向的,并且要用随机数进行掺杂。提到单向,你也许会想到单向散列函数md4和md5,很不幸地告诉你,他们是不适合的,因为hash函数需要有相当好的性能。

散列函数: 将字符串等传入我们的散列函数,而散列函数的职责就是给我们返回一个value值,我们通过这个值引做hash表下标去访问我们想要查找的数据;例如这样的函数:

int Hash(char *key, int TableSize)
{
     unsigned int HashVal = 0;
     while(*key != '\0')
             HashVal += *key++;
     return HashVal % TableSize;
}

这就是一个简单的hash函数,就是把我们传入过来的key(由我们的数据中一个或者多个结构体成员的成员来作为key)来得到一个返回值,这个返回值就是我们的value值。

一个好的hash​函数就是把我们的说有数据尽可能均匀的分散在我们预设的TableSize大小的hash表中。哈希表的几种方法:

1、直接定址法:取关键字key的某个线性函数为散列地址,如Hash(key) = key 或 Hash(key) = A*key+B;A,B为常数

2、除留取余法:关键值除以比散列表长度小的素数所得的余数作为散列地址。Hash(key) = key % p;

3、平均取中法:先计算构成关键码的标识符的内码的平方,然后按照散列表的大小取中间的若干位作为散列地址。

4、折叠法:把关键码自左到右分为位数相等的几部分,每一部分的位数应与散列表地址位数相同,只有最后一部分的位数可以短一些。把这些部分的数据叠加起来,就可以得到具有关键码的记录的散列地址。分为移位法和分界法。

5、随机数法:选择一个随机函数,取关键字的随机函数作为它的哈希地址。

6、数学分析法:设有N个d位数,每一位可能有r种不同的符号。这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布均匀些,每种符号出现的机会均等;在某些位上分布不均匀,只有某几种符号经常出现。可根据散列表的大小,选取其中各种符号分布均匀的若干位作为散列地址。

但是无论我们怎么样去选择这个函数,都不可能完全避免不同key值的hash[value]​指向会映射到同一散列地址上。这样就会造成哈希冲突/哈希碰撞。所以我们需要找到处理这种冲突的方法,大概分为这两种:分离链接法和开放定址法。

分离链接法:其实就是我们说的hash桶的含义了。哈希桶就是盛放不同key链表的容器(即是哈希表),在这里我们可以把每个key的位置看作是一个桶,桶里放了一个链表

这里写图片描述

相信大家可以看出来,使用一个数组来存放记录方法的哈希冲突太多,基于载荷因子的束缚,空间利用率不高,在需要节省空间的情况下,我们可以用哈希桶来处理哈希冲突。

哈希桶是使用一个顺序表来存放具有相同哈希值的key的链表的头节点,利用这个头节点可以找到其它的key。哈希桶就是为了解决哈希冲突的。举个例子,有一组序列为[1,2,3,4,5,6,7,8,9],使用的哈希函数为f(key) = key mod 5,那么依次得到的hasvalue就是[1,2,3,4,0,1,2,3,4],显然在key值为1、6的时候得到的哈希值都为1,如下所示:

key123456789
f(key)123401234

这个时候就产生了冲突了,也就是不同的key通过映射后得到了同样值的hashvalue。而所谓的哈希桶算法其实就是链地址解决冲突的方法,如上面的例子所示,就可以设置桶的个数为5,也就是f(key)集合的个数,而这样的话,hashvalue就可以作为桶的索引,将1,2,3,4,5分别通过f(key)得到1,2,3,4,0,则可将这几个key放入桶1,2,3,4,0的首地址所指的内存中,然后处理值为6的key,得到hashvalue值为1,这个时候需要放入桶1中,但桶1的首地址已经有了元素1,怎么办?那么就可以为每个桶开辟一片内存,内存中存放所有hashvalue相同的key,冲突的key之间用单向链表进行存储,这样就解决了哈希冲突,在查找对应key的时候,只需要通过key索引到对应的桶,然后从桶的首地址对应的节点开始查找,就是链表顺序找到,对比key的值,直到找到对应key的信息,所以,在冲突的时候,特别是冲突率比较高的时候,桶内的链表就会很长,使得查找效率比较低,而在最坏的情况下,所有的key都对应同一个hashvalue,当然这种情况不会出现,这样的哈希函数选取的也没有意义了,假设这种情况出现,那么哈希表就退化成了单链表,其他桶内存浪费,且将查找效率从O(1)直接降到了O(N),所以上面才说,哈希函数的选择也是很关键的。

下面把完整的一套函数分开讲:

1、首先是创建我们需要的结构体:

数据的结构体(也就是我们表中需要存放的数据):

 typedef struct node_s
{
        int key;    //这个值是我们得到我们的value值的依据,当然也可以能使字符串等,看你的数据类型了;

       struct node *next;

}NODE;

hash表的节点:​

typedef struct hash_s
{
        NODE **list;   //这个就是我们所有的hash桶的首个节点了,我们用它来查找到我们的桶的位置,为什么是**类型呢 ,因为这是地址的地址,例如某个桶 i 的位置*list[i];这样就找到我们的桶了;然后再在桶下面看看有没有我们要查找的NODE节点了

}HASH;


2、初始化hash表:

HashTable InitializeTable(int TableSize)
{
        Hash H;
        int i = 0;
        H = calloc(1, sizeof(HASH));
        if (H ==  NULL)
                return -1;

        H->TableSize = NextPrime();   //就是和TableSize最近的下一个素数;
        H->hlist = calloc(1, sizeof(&NODE) * H->TableSize);
        if (H->hlist == NULL)
                return -1;

        for (i = 0; i < H->TableSize; i ++)     
        {
                *(H->hlist[i)] = calloc(1, sizeof(NODE));
                if (*hlist[i] == NULL)
                        return -1;
                else
                        *(H->hlist[i])->next = NULL;
        }
}


3、查找NODE:

NODE *Find(int key, HASH H)
{
        NODE *p;
        NODE *list;

        list = H->hlist[Hash(key, H->TableSize)];
        p = list->next;
        while(p != NULL && p->key != key)
                p = p->next;
        return p;

}    //先找到这个桶的头结点list,然后再往后面遍历查找,这时候基本是链表查找操作了;


4、插入NODE:

void Insert(int key, HASH H)
{
        NODE *p;
        NODE *list;
        NODE *tmp;

        if (Find(key, H) == NULL)
        {
                tmp = calloc(1, sizeof(NODE));
                if (tmp == NULL)
                        return -1;

                list = H->hlist[Hash(key, H->TableSize)];
                tmp->key = key;
                tmp->next = list->next;//这里是直接把我们的节点插入到桶的首节点;
                list->next = tmp;
        }
        return;
}


以上基本完成以hash桶​为处理冲突的hash表的实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

造夢先森

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值