【数据结构】:一文帮你完全弄懂哈希(hash) , 并且帮你手搓std::unordered_map

详细讲解Hash以及如何通过C++实现Hash表

在这里插入图片描述

在这里插入图片描述

引言

如果是刚开始学习数据结构的读者在不了解Hash的时候,肯定觉得哈希是一种很高级,很复杂,很难实现的数据结构,但是错了,相对来说,哈希对比各种平衡二叉树,图结构来说,反而设计相当简单,并且不仅简单,查询数据的效率还是o(1),杠杠的香数据结构,那么话不多说,线面我们就具体的谈谈Hash这个结构,或者说Hash这种思想。

什么是哈希

哈希又称为散列,是一种组织数据的方式。我们从散列的角度来理解,很多数据分散在数组中的各个位置,他们通过某种约定进行分布,使得我们在查找他们的时候能够迅速定位,这就是哈希的思想。

上面我们谈到了,数据通过某种约定分布在数组中, 下面我们详细塔谈这个约定。

哈希的本质就是通过约定组织数据

细谈约定

约定使我们事先商量好的一种共识,我们所有的数据通过这个约定进行组织。

直接定址法

这是最简单的一种利用约定组织数据的方式

比如说现在我有一个利用ASCII编码的字符串,现在我想要统计这个字符串中各种字符的个数,如果是你,你会怎么做?

如果是我,我会定义一个128大小的数组,每个字符的ASCII值代表一个位置。

// 现在我有一个std::string的buffer
int hash[128] = { 0 };
for(char i : buffer) hash[i]++;

你没看错这里的hash就是一个简单的hash表,我们这里的约定就是
ASCII码映射具体的位置。

哈希函数

哈希函数的就是我们的约定,上面那个问题的哈希函数就是
pos = (int)(ch) 对吧,这就是利用ASCII编码作为哈希函数。

一个好的哈希函数能够将数据均匀分布到数组中,但是显然想要设计一个通用的Hash函数,是很困难的。

    1. 除法散列法 / 除留余数法

    当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是M,那么key % M
    本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。如:
    {63,31}看起来没有关联的值,如果M是16,也就是2^4,那么计算出的哈希值都是15,因为63的⼆
    进制后8位是00111111,31的⼆进制后8位是00011111。如果是 ,就更明显了,保留的都是
    10进值的后x位,如:{112,12312},如果M是100,也就是10 ^ 2,那么计算出的哈希值都是12。

如果还想了解更多的方法,可以自行查阅。

冲突

我们想象的很美好,不是所有的情况都能够像上面ASCII映射一样一一对应,当然我们希望的场景就是一一对应,那么冲突是什么。冲突就是我们假定哈希函数为f (x),冲突就是当两个变量映射到同一个为止,即a != b, f(a) = f(b), 这就冲突的,哈希冲突很难保证不存在,所以合理降低冲突时程序员应该做的事情。

如何解决冲突

对于如何解决冲突,目前一般有两种主流的方式,一个叫做开放定制法, 一个叫做链地址法

负载因子

负载因子就是我们当前的有效数据 和 总空间的的比值, 比如我们存入了3个数据,数组长度为5, 负载因子就是0.6 ,我们保证负载因子 < 0.7。

开放定制法

首先简单阐述一下什么是开放定制法, f(a) = pos , 如果pos这个位置已经存储了数据,我们就往后找,因为负载因子 < 1, 我们一定能够找到一个位置没有数据。
在这里插入图片描述

// 我们现在有一个数组 array 
// 存储新数据的逻辑
size_t pos = hash_function(data); // 通过哈希函数找到位置
if(status[pos] == busy) 
while(status[++pos] == busy);
// 找到空闲的位置
array[pos] = data;
status[pos] = busy;

但是聪明的计算机工程师,发现这样的解决方式还有优化的地方

探测的方式

    1. 线性探测

    我们上面在移动的时候,默认都是移动一个单位,但是我们发现如果已经映射一个位置,探测的偏移量越来越大,显著影响我们插入的效率。

    1. 二次探测
      二次探测的思想就是,我们每次移动 i ^ 2步,比如第一次移动1步,不行,移动4步,不行,移动9步,扩大步长可以减少冲突的效率。

下面就是更加重要,也是使用更多的解决哈希冲突的方式,链式法

在这里插入图片描述
从名称我们可以获取到一些思路,链式法就是我们每个位置存储的不是一个数据,而是一个链表,我们将相同Hash值的数据通过一个链表连接起来。

size_t pos = hash_function(data);
array[pos].push(pos);
缺点

可能因为大量的数据映射到同一个为止,导致一个链表过程,最终退化成一个链表的数据结构,这就需要我们通过优化哈希函数,降低冲突的情况。

细节

无论是,开放定址法还是链式哈希桶,当我们的大于负载因子的时候,都涉及到扩容的问题,扩容就设计到,将所有的数据提取出来,重新根据新的长度来通过哈希函数进行映射。

我自己手写的基于链式法的哈希设计思路

讲解代码逻辑

    1. HashTable类
      在这里插入图片描述
    • table : 存储hash表
    • size : 长度
    • harsh : 哈希函数(支持用户直接修改)
    • koft : 不用了解,一种模版设计技巧
    1. 各种组件的定义,包括迭代器,指针,引用的重命名。
      在这里插入图片描述
    1. 可以看到,HashDataType里面存储的就是链表。
      在这里插入图片描述
    1. 插入的逻辑
      在这里插入图片描述
    1. 删除的逻辑
      在这里插入图片描述

总结

以上就是哈希的原理和设计思路,因为这里我是写了一个访stl ,基于模版编写的容器,后面基于这个容器可以直接实现std::unordered_map, std::unordered_set, 如果还想进一步了解C++ 哈希的接口,可以直接去查看
说了半天,hash的本质就是通过一种约定(哈希函数),将数据映射到数组的某个位置,但是在实际开发中,想要设计出好用的哈希哈数,还是需要各位计算机工程师继续研究。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值