1)哈希表简介:
散列表又称为哈希表,也被称为散列映射,映射,字典,关联数组等。散列表的查找、插入、删除速度都很快,目前大多数优秀的编程语言都提供了散列表实现。
除此之外,因为哈希表的key不允许重复,所以散列表还非常适合用于防止重复。
散列表存储元素的形式由键和值组成。
2)散列表的结构:
散列表这种数据结构由散列函数和数组组成;
相比于数组和链表,散列表更为复杂一些些,它使用散列函数来确定元素的存储位置;
在平均情况下,散列表的查找速度与数组一样快,而插入和删除的速度和链表一样快,因此它兼具两者的优点。
散列函数总是将同样的输入映射到相同的索引,来快速的查找出元素;
散列函数知道数组有多大,只返回有效的索引;
3)散列函数:
这里有一个数组,我们要将右边的数据存入到一个数组中,如何确定每个元素在数组中的存储位置呢?
这里有一个公式:存储位置=fun(关键字)
关键字指的就是可以标识出我们的每一条数据的一个标识;
这个fun()就是我们的散列函数/哈希函数。散列函数有多种,比如说直接定址法、数字分析法、平方取中法、取余法、随机数法等等。
- 一个良好的散列函数让数组中的值呈均匀分布;
- 一个糟糕的哈希函数让元素扎堆,导致大量的冲突;
4)散列冲突的解决方案
我们知道,无论使用那种散列函数去存储数据,都有可能产生地址冲突的问题。
那么散列冲突的解决方案分为两种,①开放地址法和②链地址法。
其中开放地址法又分为三种,线性探测法、平方探测法、以及再哈希法。
线性探测法:
所谓的线性探测法就是,将产生冲突的数据存入紧接着的下一个位置:
比如:
-->
比如:21,先来到11的位置,冲突了,再往后存,与12又冲突了,那就继续往后存,与22也冲突了,继续往后存,直到没有冲突。
如图所示:
比如说23这个数据:本来应该存入下标为3的位置,但是发现已经有数据了冲突了,那就继续往后存,直到没有冲突为止。
可以发现,线性探测法会导致大量的数据聚集在那一片区域,所以导致解决冲突的效率并不是很高。
平方探测法:
上边的线性探测法是将下标索引往后边多找一位,平方探测法就是将下标索引往后多找1的平方、2的平方、3的平方这样往后找。
比如:21这条数据,与11冲突了,往后多找1的平方,到了12所在的位置,仍然冲突,继续往后找,下标在12的位置基础上去加上2的平方,所以到了这个位置,如图所示:
再哈希法:
再哈希法的意思就是,我们不止有一个哈希函数,可能有两个或者三个甚至更多,(一般最多三个就可以解决大部分的问题)。
当将要存入的这个数据与已有的数据产生了哈希冲突时,此时将该数据通过第二个哈希函数进行哈希计算,按照第二个哈希函数计算出来的位置重新存入数据,如果还是冲突,再继续通过第三个哈希函数进行计算...。
有没有可能出现使用了所有的哈希函数之后还是存在哈希冲突的情况,有的,不过概率很低,如果真的出现了,那么可以再接着使用线性探测法或者平方探测法去解决这个哈希冲突。
链地址法:
链地址法就是,首先,我们新建一个数组,但是数组中不再直接存储数据,而是存储一个指针指向数据的位置:比如说1这个下标存储的指针指向11这个数据,2这个下标存储的指针指向12这个位置;
当22这个数据要存储的时候,与12这个数据产生了哈希冲突了,此时使用链地址法该怎么办呢,是将12这个数据的指针指向22这个数据,从而形成了一个链表。如图所示:
这就是链地址法。要解决散列冲突的问题,实际上使用的时候更多的是使用链地址法。因为不管是线性探测法还是平方探测法,都会导致数据产生一系列的偏移,不利于后期的查找。而链地址法的话,数据都还是在数组中的同一个位置,这样的话更符合散列函数的定义。
5)如何避免哈希冲突:
在平均情况下,散列表的查找速度与数组一样快,而插入和删除的速度和链表一样快,因此它兼具两者的优点。
但在最糟糕的情况下,散列表的各种操作的速度都很慢,而要避免冲突,需要有:
- 较低的填装因子;
- 良好的散列函数;
5.1)填装因子:
散列表的填装因子很好计算:散列表包含的元素个数 / 数组长度;
散列表使用数组来存储数据;因此你需要计算数组中被占用的位置数,以及数组的长度;
例如:
这个散列表的填装因子是:3/6= ;
这个散列表的填装因子是: 1/2 = ;
这个散列表的填装因子是:7/7=1;
填装因子大于1意味着元素的数量超过了数组的长度,此时再继续存储元素就一定会发生哈希冲突;
填装因子越低,发生冲突的可能性就越小,散列表的性能即越高;
一个不错的经验是:一旦填装因子大于0.7,就调整散列表的长度;
如何调整散列表的长度呢:
- 首先,创建一个更长的数组,通常将数组增长一倍;
- 接下来,使用hash函数将所有的元素都插入到这个新的散列表中。
调整散列表长度的工作的开销很大,需要很长时间!因此尽量避免频繁调整散列表的长度的操作。但平均而言,即便考虑到调整长度所需的时间,散列表操作所需的时间也为O(1)。