散列表
散列函数:传入一个数据,返回一个数字。
即:将输入映射到数字。
散列函数必须满足一些要求:
1、一致性:同样的输入映射的是相同的数字。
2、差异性:将不同的输入映射到不同的数字。
应用:散列表(hash table)。
别称:散列映射、映射、字典和关联数组。
任一优秀的语言都提供了散列表实现。Python提供的散列表实现为字典。
散列表的优势:
1、散列函数总是将同样的输入映射到相同的索引。
2、 散列函数将不同的输入映射到不同的索引。
3、 散列函数知道数组有多大,只返回有效的索引。
现实生活中的案列:
散列表被用于大海捞针式的查找:
DNS解析使用的就是散列表提供的功能。
你在访问类似http://adit.io这样的网站时,计算机必须将adit.io转换为IP地址。
这就是将网址映射到IP地址。
检查重复
使用散列表来检查是否重复,速度非常快,而不必使用简单查找去搜索整个列表。
用作缓存
目的:让web服务器少做工作,提高网站的访问速度。
原理:记住每次访问网站的数据,下一次再次访问时直接使用上一次访问得到的数据,而不用再去请求服务器。
缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则是存储在散列表中!
冲突
关于前面散列函数的叙述“散列函数总是将不同的键映射到数组的不同位置”实际上是不合适的。
实际上几乎不可能编写出这样的散列函数。
这种情况下冲突就产生了:我们会给两个键分配相同的位置,而先分配的键会被后分配的键覆盖。
例如:假设我有一个数组,它包含26个位置。而我使用的散列函数非常简单,它按字母表顺序分配数组的位置。
现在我要将苹果Apple的价格存储到散列表中,它将被分配到散列表的第一个位置上。
接下来我要将香蕉Banana的价格存储到散列表中,它将被分配到散列表的第二个位置上。
但是现在我要将鳄梨Avocados的价格存储到散列表中,它又将被分配到散列表的第一个位置。
这个时候你会说不好了,第一个位置我已经存储上苹果Apple的价格了,这个时候如果给鳄梨分配位置,那么我以后查询苹果的价格实际上得到的是鳄梨的价格。
如何解决冲突
1、最简单的办法如下:
如果两个键映射到了同一个位置,就在这个位置存储一个链表。
如下图:
经验教训
1、散列函数很重要。理想的散列函数是将键均匀的映射到散列表的不同位置。
2、如果散列表存储的链表很长,散列表的速度将急剧下降。
性能
**在平均情况下,散列表执行各种操作的时间都为O(1),即常量时间。**也就是说不管散列表有多大,所需的时间都相同。
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除的速度与链表一样快,因此它兼具两者的优点!
如图:
但是在最糟情况下,散列表的各种操作的速度都很慢。因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要具备以下条件:
1、较低的填装因子。
2、良好的散列函数。
填装因子
填装因子的计算:
散列表包含的元素数/位置总数
填装因子用来度量散列表中有多少位置是空的。
最佳情况下每个元素都有自己的位置,然而如果没有足够的位置即填装因子大于1则意味着元素数量超过了数组的位置数。一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度。
为此我们首先需要创建一个更长的新数组:通常将数组增长一倍。
接下来,我们使用散列函数将所有的元素都插入到这个新的散列表中。
**结论:**填装因子越低,发生冲突的可能性越小,散列表的性能越高。
**经验规则:**一旦填装因子大于0.7,就调整散列表的长度。
通常情况下,即便考虑到调整长度需要的时间,散列表操作所需的时间也为O(1)。
良好的散列函数
良好的散列函数能让数组中的值呈均匀分布;反之,糟糕的散列函数会让值扎堆,导致大量的冲突出现。
例如:SHA函数