散列表

散列表是一种非常常用的数据结构,在python中,字典与集合的底层实现都是散列表,也叫hash表。

1.散列函数

想要了解散列表首先需要知道散列函数,散列函数的使用非常简单,当你传入一个不可变类型的数据,他会返回一个固定的数字,并且他满足如下的条件

1.散列函数的结果必须是一致的,例如,在你输入apple时候得到的散列值5,那么每次输入apple得到的结果一定是5
2.对于不同的输入,他应该尽量映射到不同的数字如果一个散列函数不管输入是什么都返回1,它就不是好的散列函数。我们最理想的情况,希望所有不同的输入都有不同的输出。

对于绝大部分的程序员都不需要编写散列函数,因为你使用的语言一定实现了散列函数且性能较佳。

2.散列表

当我们使用散列函数之后,就可以对于一个固定的输入得到一个固定的输出,那么这个输出就可以作为索引来寻找值。我们知道,对于一个已知的索引,数组的查找数据的时间复杂度是O(1)。所以当我们使用散列函数配合数组,就组成一个简易版的散列表。

image-20201211144450773

image-20201211144456963

image-20201211144505164

image-20201211144512100

image-20201211144517651

不断重复以上的过程,最终我们的数组将被填满(理想情况下)

image-20201211144551294

此时如果我们需要查找apple的值,只要通过散列函数得到他的值

image-20201211144638722

就可以通过数组直接拿到他的值。

3.散列表的冲突

上面有提到,我们希望散列函数尽量满足所有的输入都得到不同的输出,但是在实际上,很难有这种算法作为支撑,即可能会出现两个不同的输入得到相同的输出。

比如我们自定义一个散列函数,定义个长度为26的数组(对应英文字母)如下:

image-20201211145105203

此时我们如果有apple和arocados(鳄梨),都是存入数组为0的位置,此时就产生了冲突,此时有几种解决冲突的方法,第一是改变我们的散列函数,让其能够分配更对的输出,但是注意,分配非常大的数组对于内存消耗也是很大的。

第二种我们可以配合数组和链表,在冲突的地方,设计链表,形成下面的样子。

image-20201211145656996

当然上面的设计如果散列函数有问题,那么也会造成问题,比如下面的情况,我们只存储a开头的数据。

image-20201211145825164

所以有两点很重要:

1.散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。

2.如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长!

4.散列函数性能

在平均情况下,散列表执行各种操作的时间都为O(1)。O(1)被称为常量时间。他的意思不是马上返回数据,而是不管多大的散列表,所需的时间都一样。我们知道,遍历查询的时间复杂度是O(n)。二分查找速度快一些,达到了O(logn)。所以当我们使用for循环在两个都有一千万数据的字典和列表中查询数据的时候,速度差别是很多的。当数据量越来越大,列表查询会越慢,而字典的查询速度几乎没有变化。

在最糟的情况下,散列表所有的操作时间也都为O(n),这和你使用的散列函数有很大关系

image-20201211150552674

填装因子:代表了这个散列表中的剩余空白情况

image-20201211150828929

5.总结

1.我们可以结合散列函数和数组来创建散列表
2.冲突很糟糕,你应使用可以最大限度减少冲突的散列函数。
3.散列表的查找、插入和删除速度都非常快。
4.散列表适合用于模拟映射关系。
5.一旦填装因子超过0.7,就该调整散列表的长度。
6.散列表可用于缓存数据(例如,在Web服务器上)。
7.散列表非常适合用于防止重复。(所以在python中,dict和set的key都是不可重复的。因为底层就是散列表)
posted @ 2020-12-11 15:10  王寄鱼  阅读(133)  评论(0编辑  收藏  举报