海量数据下的快速定位,HashTable 算法详解及其应用

引言

在今天这个大数据的时代,我们当中的大部分人或许并没有海量的请求需要应对,但相信你总会遇到有大量数据需要处理的时候。为了在后期能够更加高效地对这些数据进行管理和操作,相信你一定不会愿意将宝贵的时间浪费在数据的搜索、查询和定位上。或许你应该掌握更加合理的数据处理办法,来提高你的程序及项目的运作效率。

一、什么是哈希表?

0x01 什么是哈希表?
  哈希表是一种数据结构,它可以提供快速的插入操作和查找操作。
  第一次接触哈希表时,它的优点多得让人难以置信。不论哈希表中有多少数据,查找、插入和删除(有时包括侧除)只需要接近常量的时间——0。对于常规的搜索定位方式来说,在巨量的数据中找到目标数据可能需要几十毫秒、几百毫秒甚至是按秒算,但在哈希表中查找不过是一瞬间的事。
  为了更好地理解哈希表算法的基本原理,此处假定一个场景:Sun开发了一款即时通讯软件W,而Sun需要在W发布之前想好如何应对发布后越来越多用户的帐号操作请求(以搜索好友为例,为方便说明,简单假设数据均在程序数组内,不考虑数据库等外部因素)。于是Sun设想现在W已拥有100万活跃用户,数组A内存放了这些用户的数据。而此时某用户想要查询W号为66666的用户信息,如果你是Sun,你将如何在100万W号中找到66666,并返回其信息?

0x02 一般方法下的寻找过程?
  在正式说到哈希之前,我们先来看一下没有相关经验的新人可能会使用的笨办法——直接遍历。
  在遍历的过程中,程序首先查看A[1]内的W号,若不是66666则向下查看A[2],直到找到A[x]内的W号为66666,返回A[x]。
图1
(注:本文中默认数据均从下标为1开始存储)
  若足够幸运以至于A[1]就是66666,那么一步就完成了检索。若不凑巧A[100000]才是用户66666呢?那要花费多少时间和资源?这种方法虽然简单却有明显的缺陷,而这一类请求在今后是将高频出现的,有时候许多用户同时向服务器发起请求,显然不会得到理想的效果。

0x03 哈希表算法如何瞬间定位?
  哈希表算法的高效率得益于数据存放的结构。让我们先来看一看一条数据是如何被放进哈希表当中的。
图2
  从上图可以看出,数据在哈希表中所存放的位置adr,是与数据本身具有函数映射关系的(即散列函数),也就是说我们依据某种规则,使得数据与其存放的地址之内能够相互转化,这样就无需盲目地从第一个找到最后一个,而是直接得到目标地址了!

二、初探哈希表算法

0x01 简单的例子及哈希表算法的弊端
  举个例子,数组A={1, 3, 6, 13},哈希表为数组B,散列函数H(x)=2x,那么根据上图流程:
    · A[1]在哈希表中的位置为12=2,
    · A[2]在哈希表中的位置为3
2=6,
    · A[3]在哈希表中的位置为62=12,
    · A[4]在哈希表中的位置为13
2=26。
  最终得到的哈希表:
    · B[2] = 1
    · B[6] = 3
    · B[12] = 6
    · B[26] = 13
  其余的B[x]均为零。
  那么当我们想要找13这个数据在哈希表中的位置时,我们不需要一个一个地试过去,而是直接得到地址adr=H(13)=26,即B[26]内存放了数据13。
图3
  但由此我们就发现了空间的浪费,原本只有4个数据,而为了哈希表正常运作我们必须至少提供26个位置,也就是说有22个位置根本没有用到。可以说,哈希表算法就在是用空间换时间。为了得到更好的效果,我们必须牺牲许多在一般方法中不必要的空间,但在这个硬件越来越强大的时代,用空间换时间这一做法是绝对值得的。

0x02 寻求合适的散列函数
  虽然哈希表算法有不可避免的空间浪费,但设定一个合适的散列函数可以大大地降低空间的浪费率。以上面的数组A为例,散列函数选为H(x)=2x时,哈希表空间的最大利用率为

$\frac{4}{26}$=0.154

   可以说是非常低了。但如果我们以H’(x)=x%11(注:x除以11所得的余数)作为散列函数呢?我们将得到A[1]在B内的地址为1%11=1,以此类推,最终:
    · B[1] = 1
    · B[3] = 3
    · B[6] = 6
    · B[2] = 13
  此时最小的哈希表只需要6个位置,其空间利用率变成了

$\frac{4}{6}$=0.667
  这要比H(x)=2x高出4倍多。这一通过求余数得到映射地址的方法即是我们处理数字数据的主要方法,这一方法被称作**除留余数法**。为了限制篇幅,在本篇中仅深入介绍这一种。   现在让我们改变原来的数组,使得A={273, 131, 207, ...},会出现什么问题呢?   根据之前的除留余数法,我们得到:     · A[1]在哈希表中的位置为273%11=9,     · A[2]在哈希表中的位置为131%11=10,     · A[3]在哈希表中的位置为207%11=9,     · ...   计算得到A[3]的地址为9时,我们就发现了问题,9这个地址内已经存放了A[1],即B[9]=273,那么A[3]应该怎么处理? ![图4](4.png) **0x03 冲突处理 I (开放寻址法)**   在0x02中出现了地址被占用的情况,这种情况被称作**冲突(碰撞)**。我们有多种途径可以解决哈希表中的冲突问题,首先我们可以选择远离2的整数幂的素数作为求余数来初步降低碰撞概率(本文使用的是11)。   至于实际遇到了碰撞的处理办法,为了限制篇幅,在此仅介绍**线性探测(Linear Probing)**和**伪随机探测再散列**两种办法。   **(1) 线性探测 **   线性探测处理冲突可以说是简单明了,即对发生冲突的地址进行线性加一递增的处理。例如adr=H'(A[3])=9,但B[adr]!=0,即发生了冲突,于是我们将adr++,发现B[adr+1]内也有数据131了,于是继续递增,发现B[adr+2]内是空的,于是将A[3]存放进B[adr+2]即B[11]内,冲突得到了解决。   这种方法虽然简单,但是却容易导致**同类哈希聚焦(Primary Clustering)**。现在我们反过来查找207在哈希表中的位置,我们第一次得到的地址是9,但发现B[9]内并不是207,于是看B[10],发现B[10]内也不是207而是131,直到第三次才在B[11]内找到了207。如此下来若是大量的数据%11恰好都是9,那么哈希表算法就沦落得无异于普通的遍历了。   **(2) 伪随机探测再散列**   伪随机探测再散列的方法并无定态,本例中使用
**Hn=H'(adr+k%10+n)+1**
  作为处理函数。在线性探测中我们将1作为探测值,即newAdr=adr+1,而在此处的探测值为k%10+n,由于n不是定值(一般取作冲突次数),因此称为伪随机探测。随后对探测后得到的adr+k%10+n再次散列即H'(x)=x%11,最后再加一得到新地址。伪随机探测再散列相较于线性探测的优点在于**能够将数据分散得更开,即更能够做到随机分配,不会出现数据扎堆在表中某一块位置的情况,**但仍然无法完全避免糟糕的数据所造成的哈希聚焦。 ![图5](5.png) ## 待更新: **0x04 冲突处理 II (链表法)** **0x05 动态扩容** ### **三、哈希表算法的代码实现** ### **四、总结**
--- END ---
如果你觉得文章内容对你有帮助,可以打赏支持一下哦!

0%