浅谈什么是散列表

散列表

前言

请允许我先举一个小例子。像我们现在QQ上都有许多的好友,假设你有100个QQ好友且你的QQ好友没有进行分组。如果QQ列表上的姓名是随机排列的,没有一定的规律(实际上QQ比较智能,它会按照一定顺序进行排列)。那么假如你需要给你的好友A发送消息,那么你可能直接去QQ提供的搜索框中输入好友A的名字,然后QQ给你返回一个聊天的按钮。那么QQ背后是怎样执行的呢?假设你的QQ好友在列表中是以数组的方式进行存储。

在这里插入图片描述

那么当QQ根据你下达的指令,去寻找你的好友A时,就有可能需要翻看每一行去查找(这就有点像我们的简单查找),这需要的操作时间为O(n)。好,现在假设QQ列表上的姓名是按照一定规律进行排序的,例如首字母在26个字母当中的位置,那么当QQ要查找好友A的时候,它就可以利用二分查找的方法快速的找到你好友所处的位置,这需要的操作时间为O(log(n))。

QQ好友的数量 O(n) O(log(n))
100 10s 1s
1000 1.6min 1s
10000 16.6min 2s

尽管二分查找的速度很快,但我们都知道,当我们在查找一个好友的时候如果多等一秒都会觉得很不舒服,这种体验感极差,这就有可能会流失大量的客户。那么有没有一个更好的解决方法呢?答案是有的!那就是使用散列表。

什么是散列表?

按照百度百科的话来讲就是:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

说实话我一开始看到这一段文字的时候也有点懵,下面我来分享一下我的理解。

① 在讲散列表之前,我们先谈谈什么是散列函数。散列函数其实也就是一种函数,例如f(x)=y,只不过我们高中学的是数字与数字之间的关系,而散列函数是任意数据与数字之间的关系,即无论x为什么数据,y都为一个数字。就像我们的平常所见的函数一样,只要我们的x是不变的,然后返回值y就一定不变,对于这条性质,散列函数同样适用。散列函数还有一个性质就是,如果我们输入的数据不同,它应该给我们返回不同的值(这是最理想的情况),但如果返回相同的值那么它将不是一个比较好的散列函数。

② 那么什么是散列表呢?我接着用上面的例子,假设我们有一个长度为100的数组,下面我们将我们的好友的信息添加到这个数组中,我们通过把好友的信息输入到散列函数,然后散列函数返回我们一个数字,然后我们根据这个数字找到数组对应的下标,并把信息存放在那个位置,不断的重复,直到100个位置被全部填满。那么下次我们要查找好友A时,我们只需要在搜索框中输入好友A的信息那么经过散列函数的计算,它会迅速的返回一个数字给我们,然后去数组当中寻找。这样的操作时间为O(1)(实际上,无论数组的长度有多长,它的操作时间都为O(1),而当我们使用简单查找则为O(n),二分查找则为O(log(n))相比来说,这快了很多了)那么我们肯定有一个疑惑,散列函数会不会返回一个无效的索引呢?事实上,我们的散列函数还是挺聪明的,它知道我们的数组的长度,所以它只会返回一个有效的索引,而不会返回一个无效的索引。

③ 但我们肯定又会想,万一我们输入两个不同的数据,通过散列函数计算,得到相同的结果那又该怎么办呢?对,存在这种可能,这就是我们散列冲突。事实上我们很难能编写出一个输入不同的数据而返回不同的结果的散列函数。那假如发生冲突那又该怎么办呢?其实假如发生了冲突,最简单的办法就是在发生冲突的位置存储一个链表。问题似乎得到了解决,但实际上并没有。假设我们这个散列函数写的非常的不好,即输入不同的数据返回的数字都为1,那么所有的数据都存储在数组位置为1的位置上,以链表的形式存储。这时候就出现了一个问题,我们除了位置为1的位置上有数据,数组上其他的99个位置都为空,这大大浪费了空间,而且当我们要查找一个数据的时候,我们其实还是通过链表的形式一个一个去查找这样的操作时间是O(n),就达不到我们使用散列表的初衷了。

散列表(avg) 散列表(bad) 数组 链表
查找 O(1) O(n) O(1) O(n)
插入 O(1) O(n) O(n) O(1)
删除 O(1) O(n) O(n) O(1)

④ 那么有什么解决办法呢?解决办法其实有很多,下面我提一下双重散列方法。所谓双重散列法,实际上就是不仅仅使用一个散列函数,而是使用一组散列函数,当使用第一个散列函数发生冲突后,就使用下一个散列函数,直到不发生冲突为止。

⑤ 实际上,无论我们采用哪种方式,当散列表空余位置不多时,发生冲突的概率会越来越大。为了最大化保证散列列表的操作效率,我们应该尽可能的保证散列表中有较大的空余的空间。我们一般使用填装因子来表示散列表的空余位置。计算方式也很简单(散列表包含的元素个数/总位置数)。最佳的情况下是每一个元素都有自己的一个位置,就拿上面的例子来说,散列表包含100个位置,我们刚好有100个好友,这样填装因子就为1。但假设我们散列表只包含50个位置呢?那么我们的填装因子就为2,因为我们不可能让每一个好友的信息都有一个自己的位置,所以当填装因子大于1的时候,那么就一定发生了冲突。原因是我们的散列表包含的位置太小了。

⑥ 那么这个问题又该如何解决呢?我们可以设置一个阈值,当填装因子超过这个这个阈值时(一般为0.7),我们重新调整散列表的长度,(一般是扩大一倍,当然,我们也可以扩大更多的倍数,但这样会造成空间上的浪费),然后我们把我们存放的数据都插入到这个新的散列表中。

猜你喜欢

转载自blog.csdn.net/qq_35540187/article/details/107529117