一看就懂的哈希表(上)

关于哈希表这个数据结构的内容有很多,所以我打算用三个博客进行描述,这一篇主要介绍什么是哈希思想,我们想彻底明白哈希表肯定要先知道他的思想是吧。之后是对哈希函数的描写和如何处理哈希冲突。

哈希思想

哈希表(hash table)是数组的一种扩展,是经过数组演化过来的,底层依赖数组支持按照下标来进行访问元素的特性。下面举一个简单的例子,先来了解一下简单的哈希思想是什么。

假设有89个学生参加运动会,每名学生依次编号1到89,现在如何通过编号能够快速获得选手的信息呢?

我们可以把这89名选手依次加入到数组中,下标为1存放1号选手,然后依次类推,直到下标为k的位置存放编号为k的选手。选手的编号和数组下标一一对应,当需要查询编号为x的选手信息时,只需要将下标为x的元素信息取出就可以。

这个例子就是简单的哈希思想,怎么说呢,选手的编号和下标一一对应,形成了一种映射关系,不过这个例子还是不够明显,还得稍微改变一下。

把参赛选手个年级和班级这些信息也需要加上,此时的号码信息可以写成这样051167,05代表的是五年级,11代表的是11班,最后两位才是编号,现在出现问题了,如何存储现在的选手信息呢?

其实很简单,我们不能把编号直接当作数组下标进行使用,但是我们可以截取编号的后两位作为数组下标,这样问题不就解决了吗。大家看一个图肯就明白了。

这就是典型的哈希思想。其中选手的编号作为健(key),我们有key来标识一个选手,我们把选手编号转化为数组下标的映射方法称为哈希函数(hash function),哈希函数计算出来的值称为哈希值。

结论:哈希表利用的是数组按照下标访问元素的时间复杂度是O(1)这个特性。我们通过哈希函数把key映射为数组下标存储在数组下标对应的位置。当需要使用key来进行查询元素的时候,我们同样可以使用哈希函数来获取数组下标,进而读取数组下标对应的元素。

哈希函数

从上面的例子可以看到,哈希函数在哈希表中的位置非常重要。哈希函数首先是个函数可以定义为hash(),key表示元素的键值,可以定义为hash(key)。

下面通过伪代码将上面的情况进行实现

int hash(int key){
   //获取最后两位字符
   String lastTwoChars = id.substr(length-2,length);
   //将最后两个元素转化为整数
   int hashValue = convert lastTwoChars to int-type;
   return int-type;
}

所以大家可以看到,哈希函数很简单,但是设计一个没有冲突的哈希函数很困难,下面是设计哈希函数时的一些基本要求。

1.哈希函数计算得到的哈希值是一个非负整数

2.如果key1 = key2,那么hash(key1) == hash(key2)

3.如果key1 != key2,那么hash(key1) != hash(key2)

关于哈希函数的设计,下一篇博客进行详细讲解,本篇只需要知道他是什么就行。

哈希冲突

对于哈希冲突简单的理解就是一个Key通过hash(key)得到一个数组下标,恰巧这个数组下标在数组中已经没有空间了,也就是这个位置被占用了,此时就造成了哈希冲突。

关于如何结构哈希冲突,常见的方法有开放寻址法和链表法。

开放寻址法

开放寻址法的核心思想是一旦出现了哈希冲突,就通过重新探测新位置的方法来解决冲突。那么问题来了,怎么去重新探测新位置呢?说到这不得不吐槽一下,这些问题一套一套的,真是老母猪带套,一套又一套,解决一个又出现新的问题,阿西吧!

最简单的探测方法是线性探测法(linear probing)

当向哈希表插入数据时,如果某个数据经过哈希函数计算之后,对应的存储位置已经被占用了,我们就从这个位置开始,从数组中依次往后找,知道找到空闲的位置。

举个例子,如上图所示,黄色区域是空闲区域,橙色区域表示已经存储了数据,大家应该都能人得颜色吧。图中可以看出,哈希表的大小为10,在数据x插入元素之前,已经存储了六个元素,现在假如x通过哈希函数计算得到的数组下标为7,发现此时的位置已经有数据了,然后向下进行遍历,如果遍历到结尾发现也没有空位,此时就从头再开始遍历,所以x应该存储到下标为2的位置。

在哈希表中查找元素的过程有点类似于插入,如上图中,x通过哈希函数计算得到的哈希值围为7,此时发现这个位置中没有y这个元素 ,所以继续向后进行遍历查找,发现到头也没有,那么就再重头进行遍历,发现在下标3的位置查找到。或者,遍历整个数组中的空闲位置还没有找到,说明要查找的数据在哈希表中并不存在。

哈希表不仅支持插入,查找操作,还支持删除操作。对于使用线性探测法解决冲突的哈希表,删除操作稍微有些特别,不能单纯地把待删除元素所在位置设置为空(NULL)。

在查找数据地时候,一旦通过线性探测法遍历到空闲位置,我们就认定哈希表中不存在这个数据。但是,如果这个空闲位置是后来删除地,就会导致原来地查找算法失效。本来存在地数据又可能认定不存在。此时我们就可以将待删除数据地存储空间特殊标记为deleted。当利用线性探测法查找数据地时候,遇到deleted的空间,并不会直接停下来,而是继续往下探测。

对于线性探测法,当哈希表中插入的数据越来越多的时候,空闲的位置会越来越少,哈希冲突发生的概率会越来越大,线性探测的时间会越来越长,此时就会发生时间复杂度达到O(n)的情况。

对于开放寻址法,除了线性探测法之外,还有两种经典的探测方法:二次探测法和双重哈希法

二次探测法和线性探测法很像,线性探测法的探测步长是1,探测的下标为hash(key)+1,hash(key)+2...而二次探测法的步长变为"二次方",即hash(key)+1^2,hash(key)+2^2...

双重哈希法使用多个哈希函数,hash1(key),hash2(key),hash3(key),如果第一个哈希函数计算得到的存储位置被占用,此时就再用第二个哈希函数,依此类推。

链表法

链表法是一种更加常用的解决哈希冲突的方法。在哈希表中,每个“槽”会对应一个链表,我们把哈希值相同的元素放到相同槽对应的链表中。

当插入数据的时候,我们只需要通过哈希函数计算出对应的“槽”,然后将数据插入到这个“槽”对应的链表。插入数据的时间复杂度是O(1)。对于基于链表法解决冲突的哈希表,查找和删除操作的时间复杂度与链表的长度k成正比,也就是O(k)。对于哈希比较的哈希函数,从理论上来讲,k= n/m,其中n表示哈希表中数据的个数,m表示哈希表中“槽”的个数。当k是一个不大的常量时,我们可以粗略得认为,哈希表中查找和删除数据得时间复杂度为O(1)。

我们把k称为装载因子,装载银子用公式表示出来就是:装载因子=哈希表中的元素个数/哈希表得长度(“槽”的个数)。装载银子越大,说明链表长度越长,哈希表的性能就会降低。

下面的链接四下一篇:一看就懂的哈希表(中) 

​​​​​​​https://mp.csdn.net/mp_blog/creation/editor/120587366

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值