散列表

散列表是一种使用非常广泛的数据结构,可能也是最有用的数据结构。在我接触散列表之前我就已经开始使用它了,只是当时知识不够,没有意识到。好了,废话不多说,进入正题吧。

先举个栗子。假如你面前有一本厚厚的电话簿,里面记载了大量的姓名和电话号码。你的工作就是从其中快速找到某一个人的电话号码。(当然现实不会有这么无聊的工作的)可能你会想到使用二分查找,但是这依然需要不少的时间。而为了追求效率,我们希望的是得到姓名之后,马上就能找到这个人的电话号码。假如你有一个助手,他已经牢记了电话簿上所有内容,只要你告诉他姓名,他就可以立刻告诉你电话号码。是不是棒极了?没错,这个助手就是散列表。

我们都知道数组,他由键和值组成,键必须是数字,值可以是任何东西。其实散列表可以看做是高级的数组。为什么?因为他的键可以是除数字以外的其他事物,比如前面的人名。如此一来,我们就可以通过键直接获取对应的值,我们都知道在数组中获取一个元素是非常快的,散列表就具备数组的这个特性!因为他其实就是一个数组。

散列表的原理

要想了解散列表的原理我们首先必须明白一个概念:散列函数,这可以说是他的精髓。我把散列函数想象成一台万能的榨汁机。你放进去一个苹果,出来的是苹果汁,放进去葡萄,出来的是葡萄汁,哪怕放进去石头,出来的也是石头汁!!!回到第一个例子:当我们想要创建一个存储电话簿的散列表时,计算机首先在内存中开辟一个数组。首先,你要把一个人名"TOM"传入散列函数时,会返回一个唯一(不会有另一个和他相同的结果)的索引,比如4,然后,“TOM”的电话号码会被存在数组的4这个位置,同理,你传入“Bob”,散列函数会返回一个唯一的索引,比如2,bob的电话号码就被存在这个位置。如此一来,当你需要查找某个人的电话时,只需要获得他的名字,就可以通过散列表迅速找到他对应的值(他的电话号码)。

从上文我们可以看出:散列函数有以下两个特点(我这里说的是理想的散列函数,至于为什么这么讲,下文会有答案)

        (1)他必须是一致的,当你输入tom,每次输出的结果必须都为4,并且唯一

        (2)他必须将不同的输入映射到不同的数字,换句话说,种瓜得瓜,种豆得豆。输入的人名不通过,返回的结果必须不一样

散列表是一个有额外逻辑的数据结构。也被成为散列映射、映射、字典和关联数组,在几乎所有的语言中都实现了散列表这一数据结构,你可以轻松的使用它,我相信你可能已经见过他了。

散列表冲突问题

之前已经说过了理想的散列函数该有的特征,那如果散列函数不是那么优秀,会产生什么情况呢?往下看

如果我要在电话簿里存一个Mary,通过散列函数生成索引9,Mary电话存在数组的9号位置,这没问题,我再接着存一个Maggie,通过散列函数生成了索引,也是9!!!怎么办,如果我存了,那数组里Mary的电话将被覆盖,到时候查询这两个小姐姐的电话号码是一样的,撩错了怎么办???是的,这就是不好的散列函数可能带来的问题:冲突

如何解决冲突。

最简单的办法是在数组相应的位置存储一个链表,将Mary和Maggie的电话号码存进链表里,这样就解决了冲突问题,当这个链表不是特别长的时候,并不会影响我们的查询效率。

可是问题又来了,如果我这个电话簿上基本上都是M开头的人的电话,那这个链表岂不是特别长(链表查询的时间复杂度是O(n),数组是O(1)),这无疑会极大的降低我的散列表的性能,更坑比的是,我后面的数组位置居然都是空着的!!!这简直就是最烂的散列函数了

由此可见,一个好的散列函数对于散列表非常重要。好的散列函数必须要讲键均匀的映射的散列表的不同位置,这样才能尽可能减小冲突,并不影响性能。

当然这个问题其实不用你操心,你只要会用散列表就可以了。当然,如果你想有更深入的了解,请继续往下看

要想获得良好的性能.以下两点非常重要

1)较低的装填因子,最好低于0.7(大佬的经验)。

2)良好的散列函数

什么是装填因子。它等于元素的个数除以拥有位置的总数。举个栗子。有4个元素,而我的数组有9个位置,那么装填因子就是4/9,只有保证较低的装填位置才能减少冲突(为什么?就像你上澡堂洗澡,人多了挤不挤?挤的话是不是就容易起冲突?),因此,我们要想办法降低装填因子,怎么降,增加长度。

散列函数我就不说了,如果想继续了解的请自行查阅。

你可能感兴趣的:(算法之路)