1.哈希表的概述
- 哈希表是一种重要的数据结构。
- 我们将一点点来实现一个自己的哈希表,通过实现来理解哈希表背后的原理和它的优势。
2.数组的优缺点
- 数组进行插入操作的时候,效率比较低。
- 数组进行查找(修改)的效率
- 如果是基于索引进行查找的话效率非常高。
- 如果是基于内容去查找效率就比较低了。
- 数组进行删除操作的效率也不高。
3.哈希表的介绍
- 哈希表是基于数组实现的的,但是对比数组而言,他有很多优势。
- 它可以提供非常快的插入-删除-查找操作。
- 无论多少数据,插入和删除需要接近常量的时间:即O(1)的时间级,实际上,只需要几个机器指令就可以完成。
- 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。
- 但是哈希表相对于树来说编码要容易得多。
4.哈希表相对数组的一些不足
- 哈希表中的数据是没有顺序的,所以不能通过一种固定的方式(比如从大到小)来遍历其中的元素。
- 通常情况下,哈希表中的key值是不允许重复的,不能放置相同的key,用于保存不同的元素。
5.哈希表到底是什么?
- 哈希表不好理解的地方是不像数组和链表,甚至是树一样可以直接画出图形你就可以知道他的结构,甚至是原理了。
- 他的结构就是数组,但是神奇的地方在于对下标值的一种变化,这种变换我们可以称之为哈希函数,通过哈希函数可以获取到HashCode
6.我们通过三个案例来认识哈希表到底是什么?
-
案例一:公司使用一种数据结构来保存1000个员工的信息
-
案例二:设计一个数据结构,保存联系人和电话
-
案例三:使用一种数据结构存储单词信息,找到每个单词的读音和意思
-
引出了一个函数 -> 哈希函数,让名字和编号(下标值)对应起来,方便通过名字查找
7.字母转数字的方案(一)
- 似乎所有的案例都指向同一目标 — 将字符串转换成下标值
- 但是,怎么才能将一个字符串转换成数组的下标值呢?
- 单词/字符串转换为下标值,其实就是字母/文字转换为数字。
现在我们需要设计一种方案,可以将单词转换成适当的下标:
- 字符编码
- ASCLL码:a = 97 ,b = 98
- 但是有了编码系统后,如何将一个单词转换为数字呢?
- 我们也可以自己设计一个自己的编码系统,比如a是1,b是2,c是3等等。
单词转换为数字
-
方案1:数字相加
-
将单词的每个字符的编码求和。
-
例如cats转换为数字:3 + 1 + 20 + 19 = 43,那么43就作为cats单词的下标存在数组中。
-
bug 有些数字相加会得到相同的结果,容易造成数据的覆盖。
-
-
方案2:幂的连乘
-
例如 7654 = 710^3 + 610^2 + 5*10 + 4。
-
例如 cats = 3 * 27^3 + 1 * 27^2 + 20 * 27 + 17 = 60337。
-
基本可以保证每次获取到的单词的唯一性。
-
bug 数字过大,创建的数组太大了,有很多无效的单词,并且创建这么大的数组是没有意义的。
-
两种方案总结
- 第一种方案产生的数组下标太少
- 第二种方案产生的数组下标太多
8.认识哈希化(对幂的连乘的优化)
现在需要一种压缩方法,把幂的连乘的系统中得到的巨大整数范围压缩到可接受的数组范围中。
- 如何压缩呢?
有一种简单的方法就是使用取余操作符,他的作用是得到一个数被另外一个数整除后的余数。
取余操作的实现:
- 加入把从0199的数字用largeNumber表示,把09的数字用smallRange表示。
- 下标值的结果为: index = largeNumber / smallNumber
- 当一个数被10整除,余数一定再0~9范围内
- 这中间会有重复,但是重复的数量明显变小。
- 就好比你从0~199中间选取5个数字,放在这个长度为10的数组中,也会重复,但是重复的概率非常小(后面我们会讲到重复时的解决方法)
9.什么是哈希化?
-
哈希化: 将大数字转化和曾数组范围内下标的过程,我们就称之为哈希化。
-
哈希函数:通常我们将单词转化为大数字,大数字在进行哈希话的代码实现放在一个函数中,这个函数我们称为哈希函数。
-
哈希表:最终我们将这个数据插入到这个数组中,对整个结构的封装,我们就称之为是一个哈希表。
-
但是我们还有问题要解决。
虽然在一个100000的数组中,放50000个单词已经足够但是通过哈希化后的下标依然可能重复,如何解决这种重复问题呢?
10.什么是冲突?
冲突的出现
- 当两个单词通过哈希化之后得到的下标是相同的,这种情况我们称之为冲突。
- 我们当然不希望这种冲突发生,但是冲突是不可避免地,所以我们只能解决冲突。
11.解决冲突的方案
链地址法
1.每个下标值不再存放一个元素,而是存储一个链表或者数组
2.如果是链表,如果发现了重复的元素,将重复的元素插入链表的首端或者尾端
3.查询的时候,根据哈希化的结果先找到下标,然后取出链表或者数组,依次查询对应的数据
4.选择数组还是链表都要根据实际的需求来,效率都是线性查找
开放地址法
+ 1.线性查找
+ 经过哈希化得到的index = 2,但是在插入的时候,发现这里位置已经有了82,怎么办呢?
+ 线性探测就是从index位置+1开始一点点查找合适的位置来放置32,什么是合适的位置呢?
+ 空的位置就是合适的位置,在我们上面的例子中就是index = 3的位置,这时候32就放在这里。
+ 如何查询32呢?
+ 哈希化得到index =2,比如2的位置结果和查询的数值是否相同,相同的话直接返回。
+ 不相同 index + 1开始找和32一样的。
+ 一旦我们在查找的过程遇到了空位置,这时候就停止查找。 32 不可能跳过空位置去其他位置。
+ 删除某一项
+ 删除操作当我们找到这个数据后,不能将这个位置设置为null,否则查询停止,后面的元素不能检测到,而是设置为-1.
+ 自己的bug 在没有任何数据的时候,插入22-23-24-25-26,那么意味着2-3-4-5-6都有元素
+ 这一连串的填充单元就叫做聚集
+ 这时候再擦汗如一个32,会发现连续的单元不让我们插入数据,影响哈希表的性能
-
二次探测
- 每次探测 x + 1^2, x + 2^2 这样可以一次性探测较长的距离,避免那些聚集带来的影响
- bug
- 如果我们连续插入32-112-92-82-72,他们依次累加的时候步长是一样的
- 也就是说这种情况下会造成步长不一样的一种聚集,还是会影响效率
-
再哈希法
-
二次探测的算法产生的探测序列的步长是固定的:1,4,9,16以此类推
-
那么现在需要一种方法:产生一种依赖关键字的探测序列,而不是每个关键字都一样
-
那么,不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列
-
再哈希法的做法就是:把关键字用另外一个哈希函数,再做依次哈希化,用这次哈希化的结果作为步长
-
对于指定的关键字,步长在整个探测中是不变的,不过不同的关键字使用不同的步长
-
第二次哈希化特点
和第一个哈希函数不同(不然还是原来的位置)
不能输出为0(否则没有步长,进入到了死循环)
-
前辈提供给我们的哈希函数
- stepSize = constant - (key % constant)
- 其中constant是质数,且小于数组的容量
- 例如:step = 5 - (key % 5),满足需求,并且结果不可能为0
13.效率问题
- 链地址法相对来说效率是好于开放地址法的
- 所以在真实开发中, 使用链地址法的情况较多, 因为它不会因为添加了某元素后性能急剧下降.
- 比如在Java的HashMap中使用的就是链地址法.