# 哈希表

静态图片

哈希表是第一种非常重要的数据结构。

哈希表通常是基于数组实现的,但是相对于数据,他有很多优势:

# 哈希表的特点

  • 可以提供非常快速的插入-删除-查找操作
  • 无论多少数据,插入和删除值需要解禁常量的时间,即O(1)的时间级,实际上,只需要几个机器指令即可完成
  • 哈希表的速度比树还快,基本可以瞬间查找到想要的元素
  • 哈希表相对于树来说编码要容易很多

# 数组的缺点

静态图片

数组插入数据操作时候,效率比较低,需要将元素挪位置

数组进行查找操作的效率:

如果是基于索引查找效率非常高,如果基于内容查找(比如查找name = '小鱼')需要不停的对比元素

数组进行删除操作,需要将被删除后面的元素依次往前移动,效率也不高。

修改操作和查找操作一样,需要依次对比数据找到元素后进行修改,效率也不高。

# 哈希表的不足

哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素。

通常情况下哈希表的key是不允许重复的,不能放置相同的key,用于保存不同的元素。

# 什么是哈希表

他的结构是数组,但是他神奇的地方在于对下标值的一种变换,这种变换可以称之为哈希函数,通过哈希函数可以获取HashCode

下面有三个案例,需要挑选某种数据结构,而你会发现最好的选择就是哈希表

案例一:使用一种数据结构保存所有员工

案例二:设计一个数据结构,保存联系人和电话

案例三:使用一种数据结构存储单词信息,比如有50000个单词,找到单词后每个单词有自己的翻译&读音&应用等

  • 案例介绍

假如一家公司有1000个员工,现在需要将这些员工的信息使用某种数据结构来保存,你会采用什么数据结构呢?

方案一:数组

按照顺序将所有员工依次存入一个长度为1000的数组中,每个员工的信息都保存在数组的某个位置上。但是要查找某个具体员工的信息怎么办呢?一个个找吗?不太好找。数组最大的优势是什么?通过下标值获取信息。

所以为了可以通过数组快速定位到某个员工,最好给员工信息中添加一个员工编号(工号),而编号对应的就是员工的下标值。当查找某个员工的信息时,通过员工编号就可以快速定位到员工的信息。

方案二:链表

链表对应插入和删除数据有一定的优势。但是对于获取员工的信息,每次都必须从头遍历到尾,这种方式显然不是特别合适。

最终方案

链表和数组对比,似乎就是数组了,但是数组还有缺点,什么缺点呢?假如想查看一下 Hurry 这位员工的信息,但是又不知道 Hurry 的员工编号,怎么办呢?

当然,你说我可以问 Hurry ,但是你每查一个员工都问一下这个员工的编号么,显然不这样做不好。

线性查找呢?效率非常的低。

能不能有一种办法,让Hurry的名字和他的员工编号产生直接的关系呢?也就是通过 Hurry 这个名字,就能获取到他的索引值,而在通过索引值就能获取到 Hurry 的信息呢? 这样的方案已经存在了,就是使用哈希函数,让某个 key 的信息和索引值对应起来。

# 认识哈希化

怎样才能将一个字符串转成数组的下标值呢?

计算机中有很多编码方案就是用数字代替单词的字符即字符编码。比如ASCII编码,a 是 97, b 是 98 以此类推 122 代表 z。我们可以设计一个自己的编码系统,比如 a 是 1 , b 是 2, c 是 3 以此类推 z 是 26。空格用 0 表示。有了对应的编码,一个单词如何转成数字呢?

方案一:数字相加

一种转换单词的简单方案就是把单词的每个字符编码求和。例如单词 cats 转成数字: 3 + 1 + 20 + 19 = 43。那么 43 就作为 cats 单词的下标存在数组中。

按照这种方案有一个明显的问题就是很多单词最终的下标可能都是 43。 比如 was 、tin 、tend 、 moan 等。数组中一个下标位置只能存储一个数据。如果存入后来的数据,必然会造成数据的覆盖。一个下标存储这么多单词显然是不合理的。

方案二:幂的连乘

通过一个种算法,让 cats 转成数字后不那么普通(数字相加的方案过于普通)。

cats = 3 * 27^3 + 1 * 27^2 + 20 * 27 + 19 = 60337。这样可以基本保证他的唯一性。不会和别的单词重复。

但是如果存放一个单词,就需要创建一个 60337 大的数组,也是不合理的,数组太大没有意义,有很多无效的空数据。

第一种方案把数字相加求和,产生的数组下标太少

第二种方案与27幂相乘求和产生的数组下标又太多

# 需要一种压缩方法,把幂的连乘方案中得到的巨大整数范围压缩到可接受的数组范围。

如果只有50000 个单词,可能会定义一个长度为 50000 的数组。但是实际情况中,往往需要更大的空间来存储这些单词,比如两倍的大小 100000,因为不能保证单词会映射到每一个位置。

有一种简单的解决方法就是使用取余操作符,他的作用是得到一个数被另一个数整除后的余数。假设把从 0 ~ 199 的数字(largeNumber)压缩为 0 到 9 的数字(smallRange)。 下标值的结果: index = largeNumber % smallRange 即当一个数被 10 整除时,余数一定在 0 ~ 9 之间。比如 13 % 10 = 3 , 157 % 10 = 7。 当然中间还是有重复的,不过重复的数量明显变小了。

# 哈希化

哈希化:将大数字转化成数组范围内下标的过程,称之为哈希化。

哈希函数:通常我们会将单词转成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数我们称为哈希函数

哈希表:最终将数据插入到这个数组中,对整个结构的封装,称之为哈希表

# 什么是冲突

0 - 199 的数字选取 5 个放在长度为10 的单元格中,如果随机选出来的数字是 33、82、11、45、90 那么最终他们的位置会是 3、2、1、5、0,此时没有冲突。

如果在加一个 73呢,那么就有两个 3 了。

需要针对这种冲突,提出一些解决方案。

  • 链地址法
  • 开放地址法