这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
本系列文章为个人学习总结,如果有发现错误或存在疑问之处,欢迎留言指点!
本文是重学数据结构系列的第七篇,系列文章如下:
1.算法时间复杂度和空间复杂度
2.重学数据结构--链表
3.重学数据结构--队列
4.重学数据结构--栈
5.重学数据结构--树
6.重学数据结构--图
哈希表
哈希表也称散列表(散列映射、映射、字典、关联数组),哈希表是一种根据关键字值(key - value)而直接进行访问的数据结构。它基于数组,通过把关键字映射到数组的某个下标来加快查找速度,但是又和数组、链表、树等数据结构不同,在这些数据结构中查找某个关键字,通常要遍历整个数据结构,也就是O(N)的时间级,但是对于哈希表来说,只是O(1)的时间级。
这里有个重要的问题就是如何把关键字转换为数组的下标,这个转换的函数称为哈希函数(也称散列函数),转换的过程称为哈希化。
1.散列函数
散列函数是这样的函数,即无论你给它什么数字,它都还你一个数字(将输入映射到数字)。散列函数需要满足一些要求:
- 散列函数总是将同样的输入映射到相同的索引(输入相同返回相同)
- 散列函数将不同的输入映射到不同的索引。
我们不需要自己去实现散列表,任何一门优秀的语言都提供了散列表的实现。
2.散列表应用
- 用于查找:存储数据之后,你只要提供key就可以很快找到
- 防止重复:对应数据的key唯一(例如投票,以手机号哈希转换得到的key进行存储)。
- 缓存:例如有个小朋友,总是问你关于星球的问题,月球距地球有多远,你每次都要百度搜索,这可能需要几分钟,现在假设他老是问你月球离地球有多远,很快你就记住了238900英里。因此不用再去百度搜索,你可以直接告诉他答案。这就是缓存的工作原理(网站记住数据,而不再从新计算)
3.冲突
前面讲到散列表需要满足需求,将不同的输入映射到不同的位置,实际上我们几乎不能编写出这样的散列函数。
我们来看一个简单的示例,假设有一个数组,包含26个位置。而你使用散列函数非常简单,它按字母表顺序分配数组的位置
如果你要将Apple的价格存储到散列表中,分配给你的是第一个位置,接下来你存储banana的价格时存储在第二个位置,再接下来你要存储avocado的价格,分配给你的又是第一个位置。这时候你会发现出问题了,这个位置已经存储了苹果的价格!这种情况称为冲突:给两个键分配的位置相同。如果你讲avocado的价格存储到这个位置,Apple的价格将被覆盖,以后再查询Apple的价格时,得到的是avocado的价格,这是个很大的问题,必须要避免。处理冲突的方式很多,最简单的办法是:如果两个键映射到了同一个位置,就在这个位置上存储一个链表。
在这个例子中,Apple和avocado映射到了同一个位置,因此在这个位置存储一个链表。在需要查询banana的价格时,依然很快,但是需要查询苹果的价格时,速度慢些:你必须在相应的链表中找到Apple。如果这个链表很短,也没什么大不了,只需要找几个元素。但是,假设存储的都是以A开头的商品。
看上去很糟糕,除第一个位置外,整个散列表都是空的,而第一个位置包含一个很长的列表。这个散列表中所有的元素都在这个链表中,这与一开始就将所有元素存储到一个链表中一样糟糕:散列表的速度会很慢。
这里的经验教训有两个:
- 散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是, 散列函数将键均匀地映射到散列表的不同位置。
- 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长!
4.性能
散列表同数组和链表比较
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速 度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。 因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:
-
较低的填装因子:一旦填装因子大于0.7,就调整散列表的长度。
填装因子=散列表中包含的元素数/位置总数
-
良好的散列函数