算法快学笔记(五):散列表

1. 介绍

当需要根据给定的值需要快速得到想要值的时候,散列表是一个非常有用的数据结构,假设你在一家杂货店上班。有顾客来买东西时,你得在一个本子中查
找价格,如果本子的内容不是按字母顺序排列的,你可以使用简单查找法,从头到尾以一个一个的找,时间复杂度为O(n),如果本子的内容是按字母顺序排列的,可使用二分查找来找出苹果的价格,这需要的时间会短一些,为O(log n)。

二分查找的速度非常快。但作为收银员,在本子中查找价格是件很痛苦的事情,哪怕本子的内容是有序的。要是能够记住所有商品价格,就马上知道答案而且不用查找了。

散列函数是这样的函数,简单的说,即无论你给它什么数据,它都会给你一个代表位置的数字。

但散列函数必须满足一些要求。

  • 它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都必须为4。如果不是这样,散列表将毫无用处。
  • 它应将不同的输入映射到不同的数字。 例如,如果一个散列函数不管输入是什么都返回1,
    它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。

2. 原理介绍

散列函数返回的数字到底有什么用了?下面我们来看下这个数字是如何帮助我们快速查找的。

首先创建一个空数组:

在这里插入图片描述

你将在这个数组中存储商品的价格。下面来将商品的价格加入到这个数组中,放到数组中的位置由商品名称经过散列函数处理的结果决定,例如

  • Apple 的散列值为3
  • Milk 的散列值为0
  • 计算其他商品的散列值

放入了Apple以及Milk后数组,变成了下面的样子:

在这里插入图片描述

现在假设需要知道Apple的价格。你无需在数组中查找,只需将Apple作为输入
交给散列函数, 他会告诉你苹果的价格存储在索引3处,于是你就得到了苹果的价格。

散列函数准确地指出了价格的存储位置,你根本不用查找!之所以能够这样,具体原因如下。

  • 散列函数总是将同样的输入映射到相同的索引。每次你输入Apple,得到的都是同一个
    数字。因此,你可首先使用它来确定将鳄梨的价格存储在什么地方,并在以后使用它来
    确定鳄梨的价格存储在什么地方。
  • 散列函数将不同的输入尽量映射到不同的索引。
  • 散列函数知道数组有多大,只返回有效的索引。如果数组包含5个元素,散列函数就不会返回无效索引100

3. 重要特性

3.1 冲突

还是以查询商品价格为例,假设你有一个长度为26的数组,而你使用的散列函数也很简单,就是按照字母表顺序来分配商品在数组中的位置。

对于该种情况,Apple(苹果)与Avocados(鳄梨)分配的位置相同,该种情况被称为冲突。处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表,结构如下:
在这里插入图片描述

在这个例子中, apple和avocado映射到了同一个位置,因此在这个位置存储一个链表。在需要查询香蕉的价格时,速度依然很快。但在需要查询苹果的价格时,速度要慢些:你必须在相应
的链表中找到apple。如果这个链表很短也没什么,但假设你工作的杂货店只销售名称以字母A打头的商品的话,会出现下面的情况:

  • 第一个位置包含了所有的商品,此时的查找变成了链表查找
  • 除了第一个位置外,其他的位置都是空的,资源分配严重不均!

通过前面的描述,可以说明散列函数的好坏很重要,最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。

3.2 性能

在平均情况下,散列表的查找(获取给定索引处的值)速度与数组一样快,而插入和删除速度与链表一样快,因此它兼具两者的优点!但在最糟情况下,散列表的各种操作的速度都很慢。
因此,在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:

  • 较低的填装因子;
  • 较好的散列函数

3.2.1 填装因子

散列表的填装因子=散列表包含的元素数/位置总数,如果一个元素需要放入包含3个位置的散列表中,则填装因子就为1/3.

填装因子大于1意味着商品数量超过了数组的位置数。一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度(resizing) 。
例如,假设有一个像下面这样相当满的散列表(数组长度为4,已经包含3个元素),你就需要调整它的长度。为此,

  • 你首先创建一个更长的新数组:通常将数组增长一倍,长度现在变成了8
  • 你需要使用函数hash将所有的元素都插入到这个新的散列表中

这个新散列表的填装因子为3/8,比原来低多了!填装因子越低,发生冲突的可能性越小,散列表的性能越高,一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度

由于重新调整大小涉及到重新分配内存以及重新插入操作,因此调整长度是开销很大的。

3.2.2 良好的散列结构

良好的散列函数让数组中的值呈均匀分布,糕的散列函数让值扎堆,导致大量的冲突。

4. 总结

  • 散列表适合用于模拟映射关系
  • 平均情况下,散列表所有操作的运行时间都为O(1)
  • 最糟情况下,散列表所有操作的运行时间都为O(n)
  • 你可能不需要自己去实现散列表,每种语言基本都提供了散列表实现
  • 一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值