前几天有个同事和我聊HashMap,聊着就说到了数据结构中的这个散列查找,今天爬长城回来没事做,顺手拿起书柜里的《数据结构实用教程》,看了看之前老师的PPT,温习了下这部分知识,把自己整理的一些资料和大家分享下。
不管是顺序查找还是二分查找亦或者是分块查找,他们都有一个比较的过程,查找效率都依赖于比较次数,那能不能不经过比较,直接查找到元素了?稍等,这是个思维跨度比较大的设想,我们先来个简单的例子:
有一个集合:S={18,75,60,43},我们要以这个集合为起点来实现我们伟大的设想,在存储这个集合之前,我们根据集合中元素的特点将其存放到指定位置,当需要查找这个元素时,我们可以直接根据这个特点去定位元素。这里说的特点有点抽象,在上面的例子中,这个特点就是对集合元素取余,比如18的存储地址就是18%10,75的存储地址就是75%10.......,如果我们要查找43,可以计算出43的地址,然后直接去相应位置去取。是不是很简单?
其实我们上面所说的思路就是散列查找,取余的那个函数就是散列函数,计算出来的值就是散列地址。是不是很快?很牛逼?可能有的朋友已经看到上面的例子有问题了,比如有个元素是15,那15和75的散列地址就冲突了,这时候我们怎么存放?散列中引入了桶的概念,当有多个元素都对应到同一个存储空间时,我们就把这些元素都扔到桶里,到时候再去桶里拿自己想要的元素,这样问题就得到解决了。但是在散列查找中,我们是应该尽量去避免或者减少冲突的(你想想,一冲突就得放桶里,完了你又得去桶里找,能不慢么?),与冲突有关的三个主要因数有:
1)与装填因子a有关
a=n/m,n表示散列查找中元素个数,m表示散列查找地址空间大小,a越小,冲突可能性也越小,但是a越小空间利用率也越低(不用看那个公式,自己想想这道理)。
2)与采用的散列函数有关
构造散列函数的目标是使散列地址尽可能均匀分布在散列空间上,同时使计算尽可能简单,以节省时间。常见的就是除留取余法(用关键字K除以散列表长度m所得余数作为散列地址)。
3)与处理冲突的方法有关
冲突冲突的方法可以分为开放定址法和链接法两类,简单介绍下:
开放定止法(闭散列):如果在存放数据元素a时发现H(a.key)位置已被别的数据项占据,则在整个表中查找新的位置,如果表未被装满,则在允许的范围内必定有新的位置。
链表法(开散列):将所有关键字为同义词的数据元素存储在同一线性表中。设Hash地址空间为[0,m-1],则此种方法的Hash表中的每一个分量是一个指向关键字类型的指针。
链接法就是把发生冲突的同义词元素(结点)用单链表链接起来的方法。在这种方法中,散列表中的每个单元(即下标位置)不是存储相应的元素,而是存储相应单链表的表头指针。单链表中的每个结点由动态分配产生,同时因为每个元素被存储在相应的单链表中,在单链表中可以任意地插入和删除结点,所以填充因子a既可以小于等于l,也可以大于l。当向采用链接法解决冲突的散列表中插入一个关键字为K的元素时,首先根据关键字K计算出散列地址d,接着把由该元素生成的动态结点插入到下标为d的单链表的表头(可插入到单链表中的任何位置,但插入表头最方便)。查找过程也与插入类似,首先计算出散列地址d,然后从下标为d的单链表中顺序查找关键字为K的元素,若查找成功则返回该元素的存储地址,若查找失败则返回空指针。
用链接法处理冲突,虽然比开放定址法多占用一些存储空间用来存储链接指针,但它可以减少在插入和查找过程中同关键字平均比较的次数(即平均查找长度)。这是因为,在链接法中待比较的结点都是同义词结点,而在开放定址法中,待比较的结点不仅包含有同义词结点,而且包含有非同义词结点,往往非同义词结点比同义词结点还要多。 对于一个具体的散列表来说,要求出在插入或查找过程中的平均查找长度很容易,在随机插入或在查找每个元素概率相等的情况下,它等于所有元素的查找长度(即比较次数)之和除以所有元素的个数。 可见开放定址法处理冲突的平均查找长度要高于链接法处理冲突的平均查找长度。但它们都比以前所有查找方法的平均查找长度要短。这里虽然是对具体的散列表进行的分析,但其分析结果具有普遍意义。