散列表可能是最有用的,也被称为散列映射、映射、字典和关联数组。
性能
散列表包含一个元素还是10亿个元素,从其中获取数据所需的时间都相同。实际上,你以前见过常量时间——从数组中获取一个元素所需的时间就是固定的:不管数组多大,从中获取一个元素所需的时间都是相同的。在平均情况下,散列表的速度确实很快。
在最糟情况下,散列表所有操作的运行时间都为O(n)——线性时间,这真的很慢。散列表同数组和链表比较一下。
在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:
- 较低的填装因子
- 良好的散列函数
冲突
散列函数总是将不同的键映射到数组的不同位置,这是不对的。实际上,几乎不可能编写出这样的散列函数。看一个简单的示例。假设有一个数组,它包含26个位置。而使用的散列函数非常简单,它按字母表顺序分配数组的位置。如果将苹果(Apple)的价格存储到散列表中,分配的是第一个位置。接下来,将香蕉(Bananas)的价格存储到散列表中,分配的是第二个位置。一切顺利!但现在要将鳄梨(Avocados)的价格存储到散列表中,分配的又是第一个位置。这种情况被称为冲突(collision):给两个键分配的位置相同。这是个问题。如果将鳄梨的价格存储到这个位置,将覆盖苹果的价格,以后再查询苹果的价格时,得到的将是鳄梨的价格!冲突很糟糕,必须要避免。处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
在这个例子中,apple和avocado映射到了同一个位置,因此在这个位置存储一个链表。在需要查询香蕉的价格时,速度依然很快。但在需要查询苹果的价格时,速度要慢些:必须在相应的链表中找到apple。如果这个链表很短,也没什么大不了——只需搜索三四个元素。但是,假设杂货店只销售名称以字母A打头的商品。
除第一个位置外,整个散列表都是空的,而第一个位置包含一个很长的列表!换言之,这个散列表中的所有元素都在这个链表中,这与一开始就将所有元素存储到一个链表中一样糟糕:散列表的速度会很慢。
教训
- 散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。
- 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长!
总结
- 可以结合散列函数和数组来创建散列表。
- 冲突很糟糕,应使用可以最大限度减少冲突的散列函数。
- 散列表的查找、插入和删除速度都非常快。
- 散列表适合用于模拟映射关系。
- 一旦填装因子超过0.7,就该调整散列表的长度。(填装因子= 数列表包含的元素数/位置总数)
- 散列表可用于缓存数据(例如,在Web服务器上)。
- 散列表非常适合用于防止重复。