散列表:
定义:
散列表,又叫做哈希表(Hash Table),是能够通过给定的散列码直接访问到具体对应的元素值的一个数据结构。也就是说,把给定的散列码通过散列函数(或者称哈希函数)映射成该散列码在散列表中的一个位置,这个位置存储给定散列码所对应的元素值,以这种方式加快查找速度。
散列码: 是由元素导出来的一个整数值,散列码是没有规律的。
散列函数:它本质就是一个函数,我们把它定义为 hash(key),key 就是元素的散列码,通过 hash函数得到的值就是在表中的散列地址(或者叫做哈希值,散列值),也即是上述的 表中位置标注。
散列冲突:我们时常会碰到两个散列码key1!=key2,但是hash(key1)==hash(key2)的这种 情况,散列码不同,散列地址却一样,这种情况我们叫做散列冲突,并且把key1和 key2称为这个散列函数的同义词。
特点:
- 访问速度快:由于散列表有散列函数,可以将指定的 散列码 都映射到一个地址上,所以在访问时不用一个一个查找,可以直接跳到那个地址。故此速度快。
- 无序:散列表还有一个非常明显的特点,那就是无序。为了能够更快地访问元素,散列表是根据散列函数直接找到存储地址的,这样我们的访问速度就能够更快,但是没办法应对有序。
- 冲突:装填因子a代表填入表中的个数/散列表长度。故a是标志着散列表的装满程度,a越大,那么产生冲突的可能性就越大,这个时候就需要冲突的解决方案。(如果提前知道元素数量,则可以提前设置好散列表长度,在Java中默认的长度为16,默认的装填因子为0.75,如果超过该值,那么就需要进行再散列,创建一个更大的散列表,将原有散列表中的所有元素重新插入到新的散列表中,然后丢弃原有的散列表)
-
需要额外的空间:因为散列表实际上是存不满的,在冲突越来越多时,通常会选择进行扩容空间。
散列函数的构造方法:
一个好的散列函数的设计可以参考两个原则:1.计算简单;2.散列地址分布均匀。
- 直接定址法:即为取散列码的某一个线性函数值为散列地址。优点为简单而且匀称,适合表 小且连续的情况,但是却不常用。
- 数字分析法:抽取一部分的散列码来计算散列地址,适合关键字位数多且事先知道散列码的分布和散列码的若干位分布匀称。
- 平方取中法:先求出散列码的平方值,然后按需取平方值的中间几位作为散列地址。适合不知道散列码分布,且位数又不是很多的情况。
- 折叠法:把散列码从左往右分割为几部分,然后将这几部分叠加求和,并且按照表长,取和的后几位作为散列地址。适合不知道散列码分布且关键字位数较多的情况。
- 除留余数法:(最常用的散列函数构造方法)函数公式为hash(key)=key%p(p<=表长),这个方法关键在于选择合适的p。
- 随机数法:选择一个随机数,取散列码的随机函数值为它的散列地址,也即是hash(key)=random(key)。当散列码的长度不等时,就适合这个方法。
处理散列冲突的方法:
即使设计的再好的散列函数也不可能完全避免散列冲突,所有处理散列冲突的方法诞生:
- 开放定址法:所谓的开放定址法就是一旦发生了冲突,就去找下一个空的散列地址,只要散 列表足够大,空的散列地址总能找到。开放定址法可以分为三种:第一种,线性探测法:hi(key)=(f(key)+di)mod m(di=1,2,3……m-1),m为表长。线性的一个一个探测出下一个散列地址。第二种,二次探测法:当我们在用线性探测法时,探测到表尾后,但是表的前面有空的散列地址,那么线性探测法只能通过求余的方法得到前面的散列地址,但是这种情况下效率很差;二次探测法就是一种解决元素太过聚集,散列地址过于集中的问题,原理是通过平方运算双向寻找新的空散列地址,为hi(key)=(f(key)+di)mod m(di=1^2,-1^2,2^2,-2^2……q^2,-q^2,其中q<=m/2)。第三种,随机探测法:在面对冲突时,对于di采用随机函数计算得到,为hi(key)=(h(key)+随机函数)mod m。
- 再散列函数法:事先准备多种散列函数,每当发生一个散列冲突时,就换一个散列函数,但是会增加相应的计算时间,为hi(key)=RHi(key),i=1,2……k。其中RHi是代表不同的散列函数。
- 链表法:链表法是一种更为常用的解决散列冲突的方法。在散列表中每个散列地址对应一个链表,所有经过散列函数得到的散列值相同的元素,我们都放到该散列地址所对应的链表中。以这种方法不论有多少冲突,都只是给链表增加结点的问题,所以适合可能造成很多冲突的散列函数,但也会带来查找时要遍历链表的性能损耗。
简述HashSet和HashMap:
HashSet:
定义:HashSet是基于HashMap来实现的,所以允许空值,且不是线程安全的(区别就在 于:HashMap中输入一个键值对,而在HashSet中只输入一个值)。HashSet还继承了 AbstractSet 抽象类,实现了Set接口,同时还实现了Cloneable(允许克隆),Serializable (可序列化)。同时它是不允许重复值的,不保证集合的迭代顺序,所以随着时间元素的顺 序可能会改变。
实现:在JDK 7版本中,散列表是采用数组+链表实现。每个链表被称为桶(bucket),当产生了散 列冲突时就可以把冲突的元素存储在链表当中。与JDK 7不同的是, 在JDK 8版本中,是通 过数组+链表+红黑树实现。当某个桶(链表)经常发生散列冲突时,该链表长度将会变的非 常长,为此当桶中对象数量超过8个时,在JDK 8 中会将该链表转换为红黑树进行存储,用 这种方法来提高性能。
API链接:HashSet (Java Platform SE 8 ) (oracle.com)
HashMap:
定义:HashMap是Java集合框架中的一个类,用于散列表中存储键值对。它继承了 AbstractMap <K,V>抽象类,实现了Map
接口,同时还实现了Cloneable(允许克隆), Serializable(可序列化)。允许空值,且不是线程安全的,也不保证键值的顺序。
实现:与上面一样,在JDK 7中,散列表是采用数组+链表实现。而在JDK 8版本中,是通过数组 +链表+红黑树实现。