哈希表原理(hashTable)

哈希表原理(hashTable):数组+链表

哈希表是一种根据关键字key来访问值value的一种数据结构。
它通过把key值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

1、Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值. 也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。
2、查找:哈希表,又称为散列,是一种更加快捷的查找技术。我们之前的查找,都是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等,缩小范围,继续查找。而哈希表是完全另外一种思路:当我知道key值以后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次又一次的查找!
3、Hash表在海量数据处理中有着广泛应用。
4.hashTable结合了数组和链表的优点,通过hash函数算法将key映射到数组中的某一个位置,将value值存放于一个链表,把链表地址存放在数组中;

哈希表的基本原理

哈希表的本质是数组加哈希函数。数组不难理解,那什么是哈希函数?
在哈希表中,它的作用就是将哈希表的某个key作为输入,然后经过一系列的运算后,得到数组的某个索引。一种很朴素的思路是,先用key通过哈希算法计算出一个很大的数,然后对数组长度取模,从而得到索引。
得到索引后就可以通过索引对数组执行插入或查找的操作,因为本质上是通过索引来访问数组,所以哈希表的插入和查找的效率非常高,时间复杂度都是O(1)。
image

哈希冲突

当不同的键生成了相同的索引的时候-----》 处理冲突------》拉链法和线性探索法。
我们不难发现哈希函数是整个哈希表的关键。所以为了更好的性能,我们希望在尽可能短的时间内,相同的key经过哈希函数的计算,可以得到相同的索引,不同的key经过哈希函数的计算,可以得到不同的索引,但在实际中往往事与愿违,不同的key小概率会计算出相同的索引,这就是哈希冲突(collision),几乎所有的哈希函数都存在这个问题。
这里介绍几个常见的解决哈希冲突的方法:

开放寻址法

开放寻址是一种思想,如果通过哈希函数计算出的索引所对应的空间已经被占用了,就再找一个还没被占用的空间将数据存进去。
常见的体现开放寻址思想的方法:
线性探测法 :简单来说就是从当前被占用的空间的索引开始,向下遍历整个数组,直到找到空闲空间为止。如下图所示:
image

线性探测法是开放寻址法解决哈希冲突的一种方法,基本原理为,使用大小为M的数组来保存N个键值对,其中M>N,我们需要使用数组中的空位来解决碰撞冲突。
image
对照前面的拉链法,在该图中,”Ted Baker”有唯一哈希值153的,但是由于153被”Sandra Dee”占用了。而原先”Sandra Dee”和”John Smith”的哈希值都是152的,但是在对”Sandra Dee”进行哈希的是偶发现152已经被占用了,所以往下找发现153没有被占用,就将其存放在153。后面”Ted Baker”哈希到153上,发现被占用了,就会往下找,发现154没有被占用,所以将其存放到154上面。
开放寻址法中最简单的是线性探测法:当碰撞发生时即一个键的散列值被另外一个键占用时,直接检查散列表中下一个位置,即将索引值加1,这样的线性探测有三种结果:
<1>命中,该位置的键个被查找的键相同;
<2>未命中,键为空;
<3>继续查找,该位置的键和被查找的键不同。

双重哈希法:使用多个哈希函数来计算索引,如果第一个哈希函数计算得到的索引所对应的空间已被占用,就用第二个,第二个被占用就用第三个,以此类推,直到计数出没被占用的空间对应的索引。

拉链法

拉链法是一种更加常见的解决哈希冲突的方法,Java中的HashMap就是采用这种方法。

将大小为M的数组的每一个元素指向一个链表,链表中的每一个节点都存储散列值为该索引的键值对,这个就是拉链法。
”John Smith”和”Sandra Dee”通过哈希函数指向152这个索引,该索引又指向了一个链表,在链表中依次存储了这两个字符串。
该方法的基本思想就是选择足够大的M,使得所有的链表都尽可能的短小,以保证查找的效率。对采用拉链法的哈希表实现的查找分为两步,首先是根据散列值找到对应的链表,然后沿着链表的顺序找到相应的键。
image

在这种方法中,数组索引对应的空间并不直接存储数据,而是存储一个链表的地址,而数据存在链表中。如下图所示:
image
这样发生冲突时,就可将冲突的key对应的数据存在同一个链表上,当需要取数据时,就先找到key对应的链表,然后遍历链表。

重写放入hashmap中的对象数据的hashCode和equals方法,key通过hashcode算法计算value存储的索引位置,这个数组索引位置存放一个链表地址,value存放在这个链表中,当下一个添加元素相同的key找到同样的位置,会通过equals比较链表中的数据,如果相等数据不存储,因为按照对象内容重写hashCode和equals方法,不会存储相同内容的对象;重写放入hashmap中的对象数据的hashCode和equals方法,key通过hashcode算法计算value存储的索引位置,这个数组索引位置存放一个链表地址,value存放在这个链表中,当下一个添加元素相同的key找到同样的位置,会通过equals比较链表中的数据,如果相等数据不存储,因为按照对象内容重写hashCode和equals方法,不会存储相同内容的对象;

拉链法和线性探索法对比

对于拉链法,查找的效率在于链表的长度
对于线性探索法,动态调整数组的大小需要对所有的值重新进行散列并插入新的表中。

对于拉链法,查找的效率在于链表的长度,一般我们应该保证长度的M/8-M/2之间,如果链表的长度大于M/2,我们可以扩充数组长度。如果长度在0~M/8时,我们可以缩短数组的长度。对于线性探索法,动态调整数组的大小需要对所有的值重新进行散列并插入新的表中。
不管是拉链法还是散列法,这种动态调整链表或者数组的大小以提高查询效率的同时,还应该考虑动态改变链表或者数组大小的成本。散列表长度加倍的插入需要进行大量的探索,这种均摊成本很多时候需要考虑。

不管是采用拉链法还是开放寻址法解决冲突,在后面查找的时候都需要进行多次探测或者查找,在很多时候会使得哈希表的查找效率退化,而不再是常数时间。如下图描述的那样。
image
拉链法的链表长度不能过长,数组的长度要合适,链表的内存地址存储位置是通过key的哈希值与数组长度的取模
哈希攻击就是通过精心构造哈希函数,使得所有的键进过函数函数后都会映射到同一个或者几个索引上,将哈希表退化为一个单链表,这样哈希表的各种操作,比如插入、查找都会从O(1)退化到了链表的查找操作,这样会消耗大量的CPU资源,导致系统无法响应,从而达到拒绝服务供给(Denial of Service,DOS)的目的。

数组扩容

避免哈希冲突加剧,进而使链表的长度增加,链表的长度增加,就会使得查找的性能降低
上面说的方法只能在一定程度上解决哈希冲突,因为毕竟数组的容量有限,当频繁插入数据时,因为数组的容量有限,所以就会使哈希冲突加剧,进而使链表的长度增加,链表的长度增加,就会使得查找的性能降低,这不是我们想看到的结果,所以要对数组扩容。
image

那什么时候给数组扩容呢?
装载因子(已插入元素的数量除以数组容量)超过某一阈值时就进行扩容,Java中HashMap的装载因子是0.75,当然,也可以是别的值。
因为之前插入的元素都是按照原数组的长度来计算索引的,所以一旦数组扩容后,长度改变,就要重新进行计算,然后将已插入的元素移动到新的位置上,所以数组扩容不仅仅只是将容量增大而已。

posted @ 2023-04-14 07:37  Jimmyhus  阅读(783)  评论(0编辑  收藏  举报