Day-24 Hash Table(杂凑表)

字典(Dictionary) 抽象资料结构

在字典https://chart.googleapis.com/chart?cht=tx&chl=D里,有https://chart.googleapis.com/chart?cht=tx&chl=n个物品,每一样东西都会跟随着一个https://chart.googleapis.com/chart?cht=tx&chl=key(假设物品和物品之间,不会有相同的https://chart.googleapis.com/chart?cht=tx&chl=key),我们可以透过https://chart.googleapis.com/chart?cht=tx&chl=key去找出我们想要的物品,而在字典这个资料结构,支援了以下三种操作

  • Insert : 把一个物品插入到https://chart.googleapis.com/chart?cht=tx&chl=D
  • Delete : 把一个物品从https://chart.googleapis.com/chart?cht=tx&chl=D中移除
  • Search : 输入一个https://chart.googleapis.com/chart?cht=tx&chl=key,如果https://chart.googleapis.com/chart?cht=tx&chl=key有对应到的物品,回传该物品,如果找不到对应的物品,则回传NULL。

在字典中,如果我们使用Search搜寻不到https://chart.googleapis.com/chart?cht=tx&chl=key对应到的物品,我们是没有办法像是二元搜寻树一样,找到前一个物品或是下一个物品的。

我们要解决上面这个字典的问题,我们可以使用AVL tree,他可以使Insert, Delete, Search这三个操作的时间复杂度均为https://chart.googleapis.com/chart?cht=tx&chl=O(lgn),但我们希望在Search这个操作,我们希望能够达到https://chart.googleapis.com/chart?cht=tx&chl=O(1)的时间。

在python中,就存在dictionary这种资料结构,而他的底层细节即是使用杂凑表(hash table)的方式实现的


名词解释:

  • satellite data(卫星数据)与key(键值): 以C语言作为举例,假设我们定义学生为一个结构(struct)如下:
sturct student{
    int age;
    char* name;
    int ID;
};

如果我们想要对学生以年纪(age)进行排序,那麽对於排序来说,age就是排序的关键字,或是键值(key),而name和ID就称为satellite data(卫星数据)。可以想像成排序时,name和ID等satellite data是跟随age这个key进行排序。

Direct-address tables(直接寻址表)

运用直接寻址表,可以让我们在https://chart.googleapis.com/chart?cht=tx&chl=O(1)时间内完成Search的操作。

我们可以想像,我们开出了一个很大的阵列,而这个阵列中存放许多元素,每一个元素对应到的index当作是元素对应到的key,如图所示。

假设有一个集合https://chart.googleapis.com/chart?cht=tx&chl=U存在有https://chart.googleapis.com/chart?cht=tx&chl=n个元素,https://chart.googleapis.com/chart?cht=tx&chl=U%3D%5Cbegin%7BBmatrix%7D0%2C1%2C2%2C...m-1%5Cend%7BBmatrix%7D,每一个元素皆为keys(对应到阵列的index),且这些keys彼此之间相互独立。

https://chart.googleapis.com/chart?cht=tx&chl=T为一直接循址表(以阵列的形式存在),记为https://chart.googleapis.com/chart?cht=tx&chl=T%5B0...m-1%5D,其中https://chart.googleapis.com/chart?cht=tx&chl=T中每一个位置(槽, slot),对应到https://chart.googleapis.com/chart?cht=tx&chl=U集合中的key,也就是https://chart.googleapis.com/chart?cht=tx&chl=U集合中的元素。

https://chart.googleapis.com/chart?cht=tx&chl=T%5Bk%5D会指向集合中key值为https://chart.googleapis.com/chart?cht=tx&chl=k的元素,如果不存在,则https://chart.googleapis.com/chart?cht=tx&chl=T%5Bk%5D%3DNULL

https://chart.googleapis.com/chart?cht=tx&chl=U表示所有关键字(key)的集合,https://chart.googleapis.com/chart?cht=tx&chl=K表示该关键字有对应到的元素的关键字(key)集合,https://chart.googleapis.com/chart?cht=tx&chl=T为直接循址表,透过https://chart.googleapis.com/chart?cht=tx&chl=T去查询到欲寻找的元素,https://chart.googleapis.com/chart?cht=tx&chl=Uhttps://chart.googleapis.com/chart?cht=tx&chl=K集合就是https://chart.googleapis.com/chart?cht=tx&chl=T集合的index。

假设我们有一条纪录,他的key为15,那我们就去查询(呼叫Search)是否存在某一条纪录的key为15,如果存在,则https://chart.googleapis.com/chart?cht=tx&chl=T%5B15%5D存在,并回传https://chart.googleapis.com/chart?cht=tx&chl=T%5B15%5D.Data,否则为NULL。
而以下为SEARCH, INSERT, DELETE的虚拟码

DIRECT-ADDRESS-SEARCH(T, k)
return T[k]
DIRECT-ADDRESS-INSERT(T, x)
T[x.key]=x
DIRECT-ADDRESS-DELETE(T,x)
T[x.key] = NULL

而上面这些操作,都只需要https://chart.googleapis.com/chart?cht=tx&chl=O(1)的时间

但是在实务上,会遇到两个问题

  1. 如果https://chart.googleapis.com/chart?cht=tx&chl=m-1是一个很大的数值,那我们就必须要开一个超大的阵列。
  2. 诚如上面的学生排序问题,如果我们将学生姓名作为key,那会变得非常麻烦。(可以将字串转成ASCII储存,或是使用类似prehash的技术)

因为上面我的使用阵列的index作为元素关联的key。我们希望我们能够保存数据的同时,让表的规模越小越好。

https://chart.googleapis.com/chart?cht=tx&chl=T阵列中,每一个空格我们可以把它称为https://chart.googleapis.com/chart?cht=tx&chl=T的一个槽(slot)。

杂凑表(Hash table)与杂凑函数(Hash function)

为了避免掉直接循址表需要开出超级大的阵列这个问题,我们可以使用杂凑函数(Hash function)这项技术进行处理,使用杂凑函数去运算出key。也就是我们希望把一堆https://chart.googleapis.com/chart?cht=tx&chl=key构成的集合https://chart.googleapis.com/chart?cht=tx&chl=m,变成一个比较小的集合https://chart.googleapis.com/chart?cht=tx&chl=n

在使用直接循址的方式时,具有关键字https://chart.googleapis.com/chart?cht=tx&chl=k的元素会被存放在https://chart.googleapis.com/chart?cht=tx&chl=T%5Bk%5D中,也就是https://chart.googleapis.com/chart?cht=tx&chl=Thttps://chart.googleapis.com/chart?cht=tx&chl=k槽。而使用杂凑的方式,元素是存放在杂凑函数https://chart.googleapis.com/chart?cht=tx&chl=h(k)运算出的结果。由关键字https://chart.googleapis.com/chart?cht=tx&chl=k透过杂凑函数计算出槽所在的位置,也就是https://chart.googleapis.com/chart?cht=tx&chl=index%20%3D%20h(key),这里杂凑函数https://chart.googleapis.com/chart?cht=tx&chl=h会将元素为关键字的https://chart.googleapis.com/chart?cht=tx&chl=U集合映射(Mapping)到杂凑表https://chart.googleapis.com/chart?cht=tx&chl=Thttps://chart.googleapis.com/chart?cht=tx&chl=T%5B0...m-1%5D,而https://chart.googleapis.com/chart?cht=tx&chl=m的值会比直接循址中https://chart.googleapis.com/chart?cht=tx&chl=U集合的大小来的小的许多。

而此时我们可以说https://chart.googleapis.com/chart?cht=tx&chl=h(k)运算出的值是关键字https://chart.googleapis.com/chart?cht=tx&chl=k的杂凑值(Hashing)。

概念上就是有用到的https://chart.googleapis.com/chart?cht=tx&chl=keyhttps://chart.googleapis.com/chart?cht=tx&chl=n个,而整个Table的大小为https://chart.googleapis.com/chart?cht=tx&chl=m,我们希望https://chart.googleapis.com/chart?cht=tx&chl=m%3D%5CTheta(n)

上面的操作我们会发现一件事情,如果关键字经过杂凑函数後得到的杂凑值和其他关键字得到的杂凑值相同时,他们会存取到相同的数值,发生这样的情况,我们称为冲突(collision)。例如一个Table中,有两笔资料存到同一个slot中。

会发生冲突的原因,在於我们给的关键字https://chart.googleapis.com/chart?cht=tx&chl=k可以任意数值($U$集合中),但是我们产生出的杂凑值需要在一定的范围内,也就是在https://chart.googleapis.com/chart?cht=tx&chl=T的index内,因此会发生冲突。举例如下:

假设我们定义hash function为https://chart.googleapis.com/chart?cht=tx&chl=h(item)%20%3D%20(item)%20%5C%20mod%5C%20%20(table.size),假设https://chart.googleapis.com/chart?cht=tx&chl=table.size%20%3D%208,则如果我们要放入57和65这两个item,则经过hash function,https://chart.googleapis.com/chart?cht=tx&chl=h(57)%20%3D%2057%20%5C%20mod%5C%208%20%3D%201,https://chart.googleapis.com/chart?cht=tx&chl=h(65)%20%3D%2065%20%5C%20mod%5C%20%208%20%3D%201,这两个数值会存入到hash table中相同的slot,而这就是冲突。

冲突定义:https://chart.googleapis.com/chart?cht=tx&chl=h(k_i)%20%3D%20h(k_j)%2C%20k_i%20%5Cneq%20k_j

链接法(chaining)

链接法,概念为如果有超过一个以上的元素经过hash function後,储存到同样的slot,则在该slot使用linked list(链结串列)将资料全部储存在槽(slot)中,如下图所示

在使用这样的方法後,杂凑表$T$上的字典操作大概如下

CHAINED-HASH-INSERT(T, x)
insert x at the head of list T[h(x.key)]
CHAINED-HASH-SEARCH(T, k)
search for an element with key k in list T[h(k)]
CHAINED-HASH-DELETE
delete x from the list T[h(x.key)]

插入和删除的操作皆为https://chart.googleapis.com/chart?cht=tx&chl=O(1)(假设为双向链结串列),在最坏的情况下,就是所有资料都在同一个槽里面,而这时候Search的操作就会是https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta(n),那杂凑表的效率就跟链结串列的效率差不多了,但是在实务上,最坏情况几乎不会发生,他的平均情况是非常好的。

链接法效率分析

上面提到,Search的最差情况就是所有元素都在同一个槽(slot)中,而Search的时间会取决於链结串列的长度。我们假设有一个具有https://chart.googleapis.com/chart?cht=tx&chl=m个槽位的杂凑表https://chart.googleapis.com/chart?cht=tx&chl=T,总共有https://chart.googleapis.com/chart?cht=tx&chl=n个元素,有https://chart.googleapis.com/chart?cht=tx&chl=n个元素要放入https://chart.googleapis.com/chart?cht=tx&chl=m个槽位。

接着再做出一个假设,所有的https://chart.googleapis.com/chart?cht=tx&chl=key再经过杂凑函数运算後,他们被映射到杂凑表中每一个槽的机率皆是相等的,机率皆为https://chart.googleapis.com/chart?cht=tx&chl=1%2Fm,且每一次均为独立事件,我们可以估计在使用链接法的情况下,https://chart.googleapis.com/chart?cht=tx&chl=T中每一个槽的链结串列平均长度为https://chart.googleapis.com/chart?cht=tx&chl=n%2Fm(每一个元素映射到槽的机率为https://chart.googleapis.com/chart?cht=tx&chl=1%2Fm,总共https://chart.googleapis.com/chart?cht=tx&chl=n个元素),而https://chart.googleapis.com/chart?cht=tx&chl=n%2Fm我们会称他为杂凑表https://chart.googleapis.com/chart?cht=tx&chl=T的装载因子(load factor),https://chart.googleapis.com/chart?cht=tx&chl=n%2Fm会简称为https://chart.googleapis.com/chart?cht=tx&chl=%5Calphahttps://chart.googleapis.com/chart?cht=tx&chl=%5Calpha可以大於,等於,或是小於1,如果https://chart.googleapis.com/chart?cht=tx&chl=n%3Dm,则https://chart.googleapis.com/chart?cht=tx&chl=%5Calpha%20%3D%201,也就是当https://chart.googleapis.com/chart?cht=tx&chl=m%20%3D%20%5CTheta(n)https://chart.googleapis.com/chart?cht=tx&chl=%5Calpha%20%3D%20%5CTheta(1)成立。

我们发现到使用杂凑表,它的效率取决於杂凑函数将https://chart.googleapis.com/chart?cht=tx&chl=key分布在https://chart.googleapis.com/chart?cht=tx&chl=Thttps://chart.googleapis.com/chart?cht=tx&chl=m个槽位的均匀程度,我们在上面假设了每一个元素出现在https://chart.googleapis.com/chart?cht=tx&chl=m个槽位的机率皆是相等的,且为独立事件,我们称这个假设为简单均匀杂凑(simple uniform hashing)

杂凑表https://chart.googleapis.com/chart?cht=tx&chl=T每一个槽的平均长度,可以使用期望值以下面形式表示

对於https://chart.googleapis.com/chart?cht=tx&chl=j%3D0%2C1%2C...m-1, https://chart.googleapis.com/chart?cht=tx&chl=T%5Bj%5D的链结串列长度以https://chart.googleapis.com/chart?cht=tx&chl=n_j表示,则https://chart.googleapis.com/chart?cht=tx&chl=n%3Dn_0%2Bn_1%2B...n_%7Bm-1%7D,并且https://chart.googleapis.com/chart?cht=tx&chl=n_j的期望值为https://chart.googleapis.com/chart?cht=tx&chl=E%5Bn_j%5D%3D%5Calpha%3Dn%2Fm

而当我们使用Search寻找https://chart.googleapis.com/chart?cht=tx&chl=keyhttps://chart.googleapis.com/chart?cht=tx&chl=k的元素,会有两种情况,第一种为在https://chart.googleapis.com/chart?cht=tx&chl=T%5Bh(k)%5D中没有找到一个元素的key为https://chart.googleapis.com/chart?cht=tx&chl=k,第二种为成功找到key为https://chart.googleapis.com/chart?cht=tx&chl=k的元素。

  • 在第一次寻找不成功的情况下,我们要遍历链结串列的平均长度为https://chart.googleapis.com/chart?cht=tx&chl=%5Calpha,而假设计算杂凑函数需要https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta(1)的时间,则Search平均所需要的时间https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta(1%2B%5Calpha)
  • 在第一次就成功寻找到,我们需要考虑到每一个槽中的链结串列被寻找到的机率并不是均等的,所含元素越多的链结串列,被寻找到的机率越大。但是,Search的期望值仍然为https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta(1%20%2B%20%5Calpha)

证明 : 假设欲寻找的元素是n个元素中任意一个,且每一个元素被寻找到的机率皆均等。在一次就成功寻找到元素https://chart.googleapis.com/chart?cht=tx&chl=x的情况下,我们需要遍历的元素数量为https://chart.googleapis.com/chart?cht=tx&chl=x前面的元素数量加1,因为https://chart.googleapis.com/chart?cht=tx&chl=x之前的元素都是在https://chart.googleapis.com/chart?cht=tx&chl=x之後插入在链接串列中,为了确保https://chart.googleapis.com/chart?cht=tx&chl=x之前的元素都有被遍历到,对https://chart.googleapis.com/chart?cht=tx&chl=x所在的链结串列,对https://chart.googleapis.com/chart?cht=tx&chl=x以前插入到链结串列元素数量的期望值加1,在对所有https://chart.googleapis.com/chart?cht=tx&chl=n个元素https://chart.googleapis.com/chart?cht=tx&chl=x取平均,得到平均情况的机率。

https://chart.googleapis.com/chart?cht=tx&chl=x_i为插入到链结串列中第https://chart.googleapis.com/chart?cht=tx&chl=i个元素,并设https://chart.googleapis.com/chart?cht=tx&chl=k_i%3Dx_i.key。对於https://chart.googleapis.com/chart?cht=tx&chl=k_ihttps://chart.googleapis.com/chart?cht=tx&chl=k_j,定义指示器随机变数https://chart.googleapis.com/chart?cht=tx&chl=X_%7Bij%7D%3DI%5Cbegin%7BBmatrix%7Dh(k_i)%3Dh(k_j)%5Cend%7BBmatrix%7D,表示经过杂凑函数,出现在杂凑表中同一个slot的情况,如果为真则https://chart.googleapis.com/chart?cht=tx&chl=X_%7Bij%7D%3D1,反之为0。在前面的简单均匀杂凑的假设之下,https://chart.googleapis.com/chart?cht=tx&chl=Pr%5Cbegin%7BBmatrix%7Dh(k_i)%3Dh(k_j)%5Cend%7BBmatrix%7D%3D1%2Fm,发生两个https://chart.googleapis.com/chart?cht=tx&chl=key经过杂凑函数映射到杂凑表中同一个slot机率的期望值https://chart.googleapis.com/chart?cht=tx&chl=E%5Cbegin%7Bbmatrix%7DX_%7Bij%7D%5Cend%7Bbmatrix%7D%3D1%2Fm,因此,在一次成功就寻找到元素,期望遍历过的元素总数的期望值可以用以下式子进行表示:

n个元素选一个,遍历整个Hash table(https://chart.googleapis.com/chart?cht=tx&chl=%5Csum_%7Bi%3D1%7D%5En),每个Hash table需要检查的元素数量(https://chart.googleapis.com/chart?cht=tx&chl=1%2B%5Csum_%7Bj%3Di%2B1%7D%5En)

https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%7BE%5B%5Cfrac%201%20n%20%5Csum_%7Bi%3D1%7D%5En(1%2B%5Csum_%7Bj%3Di%2B1%7D%5EnX_%7Bij%7D)%5Dhttps://chart.googleapis.com/chart?cht=tx&chl=%3D%5Cdisplaystyle%20%5Cfrac%201%20n%5Csum_%7Bi%3D1%7D%5En(1%2B%5Csum_%7Bj%3Di%2B1%7D%5EnE%5BX_%7Bij%7D%5D)
https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20%3D%5Cfrac%201%20n%20%5Csum_%7Bi%3D1%7D%5En(1%2B%5Csum_%7Bj%3Di%2B1%7D%5En%5Cfrac%201%20m)%20
https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20%3D1%2B%5Cfrac%201%20%7Bnm%7D%5Csum_%7Bi%3D1%7D%5En(n-i)%20
https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20%3D1%2B%5Cfrac%201%20%7Bnm%7D(%5Csum_%7Bi%3D1%7D%5Enn-%5Csum_%7Bi%3D1%7D%5Eni)%20
https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20%3D1%2B%5Cfrac%201%20%7Bnm%7D(n%5E2-%5Cfrac%20%7Bn(n%2B1)%7D%202)%20
https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20%3D1%2B%5Cfrac%20%7Bn-1%7D%20%7B2m%7D%20
https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20%3D1%2B%5Cfrac%20%5Calpha%202%20-%5Cfrac%20%5Calpha%20%7B2n%7D%7D

因此,一次成功寻找到元素所需要的全部时间就是遍历元素个数的期望值加上计算杂凑函数的时间。

https://chart.googleapis.com/chart?cht=tx&chl=%5Cdisplaystyle%20%5CTheta(1)%20%2B%20%5CTheta(1%2B%5Cfrac%20%5Calpha%202%20-%5Cfrac%20%5Calpha%20%7B2n%7D)%20%3D%20%5CTheta(2%2B%5Cfrac%20%5Calpha%202%20-%5Cfrac%20%5Calpha%20%7B2n%7D)%20%3D%5CTheta(1%2B%5Calpha)

因此,成功一次就寻找到元素和第一次没有成功寻找到,他们的平均情况皆为https://chart.googleapis.com/chart?cht=tx&chl=%5CTheta(1%2B%5Calpha)

经过上面的证明,得到当每一个slot中皆为双向链结串列时,Insert, Delete, Search的操作皆为https://chart.googleapis.com/chart?cht=tx&chl=O(1)的时间可以完成。当然,上面这些证明是基於这是一个简单均匀杂凑的条件下。

不是hash table都会是https://chart.googleapis.com/chart?cht=tx&chl=O(1),实际上是取决於装载因子https://chart.googleapis.com/chart?cht=tx&chl=%5Calpha(如果有很多元素,但Hash table很小,那他必定不会是常数时间,如果能够保证Hash table随着元素的数量增长,才能够说他是常数时间)。Hash table之所以好用,就是因为我们可以在常数时间内完成动态集合的字典操作。

参考资料:Introduction to algorithms 3rd


<<:  跌破有色眼镜,就是打破惯性框架

>>:  自动化 End-End 测试 Nightwatch.js 之踩雷笔记:Regex

D25-(9/25)-群创(3481)-面板族群

注:发文日和截图的日期不一定是同一天,所以价格计算上和当日不同,是很正常的。 声明:这一系列文章并无...

Day27 订单 -- 分期付款

今天要说的是信用卡分期付款,分期付款也是信用卡付款的一种, 但我们需要透过结构上的改动以符合分期资料...

[Day 05] 开发之前,先把需求弄清楚

今天我们终於要开始进入主题了, 但是在我们写程序之前, 我们还需要先搞清楚一个东西, 那就是需求, ...

[Day1] 让开发者森77之前

前言 这个系列主要会介绍一些Web Application攻击手法和一些Real World的案例以...

[无广告]自动封锁,诈骗电话,骚扰电话,行销,广告,推销,来电未显示,不明的电话,响一声就挂,一接就挂,一接秒挂

安桌手机适用 Android 无广告 自动封锁,诈骗电话,骚扰电话,行销,广告,推销,来电未显示,不...