散列表
- 常数平均时间插入、删除、查找
- 1关键字 查找对项的某个部分进行 这部分称为关键字
- 2散列函数 每个关键字被映射到0到tableSize-1这个范围中的某个数 映射即散列函数 在单元之间均匀的分配关键字
- 关键字
关键字是整数 key mod tableSize 保证表的大小是素数
关键字是字符串1把字符串中字符的ASCII码(Unicode码)值加起来 表大 分配不均匀
2假设key至少3个字符 key.charAt(0)+key.charAt(1)*27+key.charAt(2)*729 理论3个字符26^3个组合 词典上2851种 表大 分配不均匀
3sum(key[keySize-i-1]*37^i),同时将结果限制到适当的范围内 简单速度快
当关键字特别长,散列函数计算耗时 不使用所有字符
可以使用奇数位置上的字符
- 消除冲突
1分离链接法
将散列到同一个值的所有元素保留到一个表中一次查找:关键字-散列函数确定查找链表-遍历链表
一次插入:关键字-散列函数确定查找链表-遍历链表查看是否已存在
允许插入重复元 +1个额外的域 出现匹配事件时+1
新元素:插入到链表的前端
类架构
装填因子lamoda:散列表的元素个数对散列表的大小的比
链表的平均长度为lamoda
查找时间:计算散列函数的值+遍历链表的时间
平均查找代价:1+lamoda/2个节点
分离链表散列表的一般法则是使表的大小与预料的元素个数相等即lamoda=1
2 开放定址法
不用链表的散列表解决冲突时采用一些新单元 单元h0(x) h1(x) h2(x) h3(x)...依次被选中
hi(x)=(hash(x)+f(i))mod tableSize f0(0)=0
装填因子lamoda<0.5 称为探测散列表
1 线性探测法
线性探测中f(i)是i的线性函数 典型:f(i)=i 缺点:一次聚集2 平方探测法
hi(x)=(hash(x)+f(i))mod tableSizef(i)=i^2平方探测采用原哈希值加整数平方作为备选位置避免了一次聚集
会产生二次聚集,在备选位置上聚集
装填因子不能大于0.5,即至少有一半为空且表大小为素数才能保证插入元素总能成功
3 双散列
hi(x)=(hash(x)+f(i))mod tableSizef(i)=i*hash2(x) 将第二个散列函数应用到x
hash2(x)一定不能是0值
保证所有的单元能被探测到
Exmaple:hash2(x)=R-(x mod R) R是小于tableSize的素数
表大小不是素数,备选单元少,提前用完
解决了二次聚集 对比平方探测 增加了散列函数的计算量
- 再散列
建立一个相比之前两倍大的散列表重新散列
2*tableSize大的第一个素数为表大小
散列函数x mod tableSize
再散列策略:
1表填充到一半就散列
2插入失败时再散列
3当到达某个装填因子时再散列
再散列开销大 O(N) 平摊上每个插入操作为常数开销
- 标准库中的散列表
HashMap的性能常常优于TreeMap
可取方法:使用接口类型Map进行变量的声明,然后将TreeMap的实例变成HashMap的实例
String类有一个hashCode方法 存在对散列函数计算耗时的优化每个String对象内部存储有它的hashCode值,初始为0,
若hashCode被调用,值被记住,再散列时直接取出,避免二次计算
闪存散列代码 时空的交换有效原因:String类不可改变
public final class String{
public int hashCode(){
if(hash!=0)
return hash;
for(int i=0;i<length();i++){
hash=hash*31+(int)charAt(i);
}
return hash;
}
private int hash=0;
}
- 可扩散列
任一时刻有N个记录存储,N的值随时间变化而变化 最多可把M个记录放入一个磁盘区块
一次查找两次磁盘访问
插入操作很少的磁盘访问
性能:前提假设:位模式是均匀分布的
树叶的期望个数是(N/M)log2(e) 平均树叶满程度ln2=0.69
目录的期望大小为O(N^(1+1/M ) 当M很小,目录过大,树叶包含指向记录的链而不是实际的记录
- 散列表应用
1编译器使用散列表来追踪源代码中声明的变量 符号表 标识符变量一般都不长散列函数可以迅速被算出
2图论,图论问题中节点都有实际的名字
3游戏程序 变换表
4在线拼写检验程序 错拼检测 预先散列词典 常数时间检测单词