神奇的散列表

散列表也叫哈希表,这种数据结构提供了键(Key)和值(值)的映射关系。只要给出一个Key,就可以高效查找到它所匹配的值Value,时间复杂度接近于O(1)。了解散列表的原理之前我们了解一下哈希函数。散列表在本质上也是一个数组。可是数组只能根据下标,像a[0]、a[1]、a[3] 这样来访问,而散列表的key则是以字符串类型为主的。例如以学生的学号作为key,输入00213,查询到李四;或者以单词key,输入by,查询到数字520…,所以我们需要一个中转站,通过某种方式,把key和数组下标进行转换,这个中转站就叫做哈希函数。在不同的语言中,哈希函数的实现方式是不一样的。这里以java的常用集合HashMap为例,来看一看哈希函数在java中的实现。在java及大多数面向对象的语言中,每一个对象都有属于自己hashcode,这个hashcode是区分不同对象的重要标识,无论对象自身的类型是什么,它们的hashcode都是一个整型变量。既然都是整型变量,想要转换成数组的下标也就不难实现了,JDK中的哈希函数采用位运算的方式来优化性能。
散列表的读写操作。写操作就是在散列表中插入新的键值对(在JDK中叫Entry),第一步,通过哈希函数把key转换成数组下标,第二步如果数组下标对应的位置没有元素,就把这个Entry填充到数组下标对应的位置,但是由于数组的长度是有限的,当插入Entry越来越多时,不同的key通过哈希函数获得的下标有可能是重复的,这种情况叫做哈希冲突。哈希冲突是无法避免的,但是有解决方法,一种是开放寻址法,一种是链表法。开放寻址法的原理很简单,当一个key通过哈希函数获得对应的数组下标已经被占用了,我们可以另谋高就,寻找下一个空档位置。在java中,ThreadLocal所使用的就是开放寻址法。另一种解决哈希冲突的方法就是链表法,这种方法被用在了HashMap当中。HashMap数组的每一个元素不仅是一个Entry对象,还是一个链表的头结点,每一个Entry对象通过next指针指向它的下一个Entry节点,当新来的Entry映射到与之冲突的数组位置时,只需要插入对应的链表中即可。
既然散列表是基于数组实现的,那么散列表也要涉及扩容的问题,当经过多次元素插入,散列表达到一定的饱和度时,key映射位置发生冲突的概率会逐渐提高,这样一来,大量元素拥挤在相同的数组下标位置,形成很长的链表,对后续的插入操作和查询操作的性能都有很大的影响。这时散列表需要扩展它的长度,也就是进行扩容。对于JDK中的散列表的实现类HashMap来说,影响其扩容的因素有两个:
Capacity。即HashMap的当前长度,LoadFactor,即HashMap的负载因子,默认值为0.75f。散列表的扩容操作需要经历以下两个步骤:1、创建一个空的Entry空数组,长度是原来的两倍。2、重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新素组中,为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。经过扩容,原本拥挤的散列表重新变得稀疏,原有的Entry也重新得到了尽可能均匀的分配。以上就是散列表的各种操作原理,需要注意的是,关于HashMap,JDK8和以前的版本有着很大的区别,当多个Entry被Hash到同一个数组下标时,为了提升插入和查找的效率,HashMap会把Entry的链表转化为红黑树这种数据结构。关于红黑树的更多细节后面给打家补充。

发布了13 篇原创文章 · 获赞 10 · 访问量 399

猜你喜欢

转载自blog.csdn.net/qq_41426449/article/details/104495051