散列

概念

散列是一种用于以常数平均时间,执行插入、删除、查找的技术。
散列中要查找的数据项叫做关键字(key)。
映射:如果表的大小是tableSize,将每个关键词与0-tableSize-1的下标一一对应起来的过程叫做映射。


图中 dave—>0 john->3 phil->4 这种关系称为映射。
散列函数:f(关键词)=下标,这样的f 叫做散列函数。
如果两个关键词映射到同一个下标,这叫做冲突

散列函数

字符串的ASCII码

public static int hash(String key,int tableSize){
    int hashVal = 0;
    for(int i=0;i<key.length();i++){
        hashVal += key.charAt(i);
    }
    return hashVal%tableSize;
}

优点:计算简单
缺点:如果表很大,函数将不会很好的分配关键字。如果tableSize = 10 007,设字符串最长8个字符,字符ASCII码最大127,因此散列函数只能假设值在0-1016(=127*8)之间,分布不均匀。

27进制算法

假设每个字符串至少有3个字符,tableSize = 10 007。如果前三位字符随机(从26个字母和空格中随机选择),那么有 263=17576 种可能,与表的大小相近,会很均匀,但是实际中不是随机的,3个字符的不同组合有2 851种,最多只能占表的28%。

public static int hash(String key,int tableSize){
    return (key.charAt(0)+27*key.charAt(1)+27^2*key.charAt(2))%tableSize;
}

所有字符参与计算

使用所有字符参数hansh计算。计算公式: sumkeysize1i=0Key[keysizei1]37i 根据Horner法则,将式子转为 hk=((k2)37+k1)37+k0 如果关键字key=”hello”, ‘h’* 374+e373+l372+l371+o370

public static int hash(String key, int tableSize) {
    int hashVal = 0;
    for (int i = 0; i < key.length(); i++) {
        hashVal = hashVal*37+key.charAt(i);
    }
    hashVal =  hashVal % tableSize;
    if(hashVal<0){
        hashVal = hashVal+tableSize;
    }
    return hashVal;
}

优点:一般可以分布得很好。
缺点:如果key很长,计算量很大。
说实话没看出比上一个方法好在哪里了。

类型

要散列的对象不仅可以是String,任何实现了equals和hashCode函数的对象都可以散列。

冲突解决

冲突解决有几种:分离链接法和开放定址法

分离链接法

分离链接法是将散列到同一个值的所有元素保留到一个表中。
缺点:因为这些表是双向列表,浪费空间。

这里写图片描述

装填因子 λ=/
没看明白:一次插入操作、不成功的查找操作耗时 λ ,一次成功的查找大约 XXX。

开放定址法

开放定址法是当遇到冲突的单元时,尝试下一个单元,直到成功。 hi(x)=(hash(x)+f(i)modtableSize i是正在尝试的数组下标。
根据 f(i)的不同分为线性探测法、平方探测法、双散列。

线性探测

逐个探测可用单元。
例如f(i) = i,hash(x)=x%tableSize。在一个tableSize=10的数组nums中。
插入89,nums[9]=89;
插入18,nums[8] = 18;
插入49,hash(49)=9,nums[9]已经有值,则尝试i=1, h1(49)=(9+1)mod10=0 ,nums[0]=49;
插入58,hash(58)=8,nums[8]已经有值,尝试i=1, h1(48)=(8+1)mod10=9 ,nums[9]已经有值;尝试i=2, h2(48)=(8+2)mod10=0 ,nums[0]已经有值;尝试i=3, h3(48)=(8+3)mod10=1 ,nums[1]=58.
一次聚集:即使表相对较空,占据的单元会开始形成一些区块。散列到区块中的任何关键字需要多次试选才能插入。
一次插入或者不成功的查找,探测次数为 12(1+1(1λ)2) ,一次成功的查找探测次数: 12(1+1(1λ))
如果 λ=0.5 ,平均操作插入需要2.5次探测,查找需要1.5次探测。速度还是挺快的。(与分离链接法比较也挺浪费空间的) λ 越大,需要的探测次数越多, λ 越小,空间浪费越严重。

平方探测

与上面相似,只是 f(i)=i^2。
定理:如果使用平方探测,表的大小是素数,那么当表至少有一半是空的时候,总是能够一次插入一个新的元素。
散列到相同位置的元素将探测相同的备选单元,这叫做二次聚集

双散列

双散列是指在发生冲突之后,再次使用散列函数计算散列值。 fi=ihash2(x) 是一种常用的选择。 hash2 需要保证所有元素都能被探测到。列表的大小一定要是素数。否则备用单元可能被提前用完。

双散列在理论上很有吸引力,在实践中平方探测速度可能更快,用的更广泛。

再散列

使用平方探测的开放地址散列法,如果散列表填的太满,操作的运行时间将开始耗时过长。这时一种解决方法是建一个原表约两倍大小的表,使用一个新的散列函数,扫描原始表,重新计算散列值,插入新表。这个操作叫做再散列。
什么时候再散列有几种做法。第一种,只要表填满到一半。第二种,插入操作失败的时候。第三种,当装填因子达到一定值的时候进行。

可扩散列

当数据量大到装载不到内存中的时候,需要将数据存放在硬盘。那么算法的时间将主要取决于多少次磁盘操作。假设每个时刻有N条记录要存储,N随时间而变化。最多可以把M条记录放在一个磁盘区块。M=4.
B树具有深度O( logM/2N ) ,随着M增加,深度降低。如果M非常大,N=1,那将是最理想的情况,但是这样分支系数太高,以至于为了确定数据存在哪个分支上需要花费大量的操作。如果确定分支有规律可循,那么速度将大大提升。
假设每条数据由几个6比特整数组成。每个树包含4个链,它们由这些数据的前两个比特确定在哪个分支存储,每个树叶有M=4个数据。
这里写图片描述
用D代表根节点使用的比特数,目录中的项数= 2D dL 为所有树叶L所有元素共有的最高位的比特位数。不同的树叶 dL 不同。上图中恰好,每个树叶的 dL=2
如果现在要插入 100 100,它将插入到第三片树叶中,但是第三片树叶已经满了,这时候需要分裂第三片树叶,分裂后的树叶由前三个比特确定分支。这时候D=3。
这里写图片描述
分裂后目录数= 23 。第三片、第四片树叶的 dL=3 。其他树叶由两个相邻的目录所指。

如果现在插入000 000 妈妈第一片树叶分裂。
这里写图片描述

需要注意的细节:1 如果插入的数字引起元素有多余D+1位相同,就会引起多次分裂。例如在上图插入111 010,111 011,这样就需要D=5才可以。2 关键字重复怎么处理。3 如果有>M个重复元素怎么处理。

树叶的期望个数= N/Mlog2e 。平均树叶满的程度为ln2=0.69。这和B树相同。目录的期望大小D=O( N1+1/M/M )。

应用

1 编译器使用散列跟踪源代码中声明的变量。
2 应用在图论中。图论中节点的名字一般为有顺序的字母。
3 应用在游戏程序中。
4 在线拼写检查程序。
5 字符串A=’12314123’,求’123’在A中出现的次数。希望以O(n)的时间完成。使用f(‘123’)= 1102+210+3=123 映射关系。在A中就是查找f(‘123’)=?,f(‘234’)=?…如果A的长度为n,字符串长度为m,那么会产生n-m+1个子串。每个子串计算f(子串)需要m次,那么复杂度是(n-m+1)*m,是O(nm)。要想简化,这里使用滑窗的思想。
m1= 1102
m2= 210
m3=m2+3(3是A串第3位) ,与子串的值比较=(‘123’与’123’比较)
m4= m310
m5=m4+1(1是A串第4位),与子串值比较=(‘231’与’123’比较)
m6= m510
m7=m6+4(4是A串第5位),与子串值比较=(‘314’与’123’比较)
这样每次计算f(子串)只需要一次乘法,一次加法,变为O(1),整个时间复杂度变为O(n)。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值