散列
数据结构之散列
散列是一种以常数平均时间执行插入、删除和查找的技术。但是,那些需要元素间任何排序信息的树操作将不会得到有效支持。
一般想法
理想的散列表的数据结构只不过是一个包含一些项(item)的固定大小的数组。
每个关键字被映射到从0到TableSize - 1这个范围内的某个数,并且被放到适当的单元中。这个映射就叫作散列函数(hash function)。
这是散列的基本想法,剩下的问题就是要选择一个函数,决定当两个关键字散列到同一个值的时候,应该做什么以及如何确定散列表的大小。
散列函数
好的散列函数的选择使得表的大小是素数。
一个简单的散列函数
//把字符串中字符的ASCII码(或Unicode码)值加起来
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;
}
}
上述的散列函数实现起来简单并且能快速的给出答案。不过,如果表很大,函数将不会很好的分配关键字。
一个好的散列表
public static int hash(String key,int tableSize)
{
int hashVal = 0;
for(int i = 0 ; i < key.length(); i++){
hashVal = 37*hashVal + key.charAt(i);
}
hashVal %= tableSize;
if(hashVal < 0)
hashVal += tableSize;
return hashVal;
}
解决冲突的两种方法
分离链表法
将散列到同一个值的所有元素保留到一个表中。
这些表是双向链表,浪费空间,故应该避免使用。
不用链表的方法—探测散列表
线性探测法
平方探测法
如果使用平方探测,且表的大小是素数,那么当表至少有一半是空的时候,总能够插入一个新的元素。
双散列

再散列

再散列可以用平方探测以多种方法实现。一种做法是表只要填到一半就再散列。另一种方法就是出现插入失败才再散列。第三种方法是设置装填因子来控制再散列。
标准库种的散列表
标准库包括Set和Map的单列表实现,即HashSet类和HashMap类
最坏情况下O(1)访问的散列表
目前我们讨论过的散列表都具有的性质是,当有合理的装填因子和合适的散列函数时,可以以期望插入、删除和查找的平均花销都是O(1)。
在某些应用中,如路由器和内存缓存的的查询表的硬件实现,令查找具有确定的(例如常数级的)完成时间是特别重要的。假设我们事先知道N的值,于是不需要再散列。如果我们可以在插入过程中重新排列各项,则查找的O(1)最坏情形花销是可以达到的。
完美散列
为了简单起见,假设所有N项都事先已知。如果分离链接的实现可以保证每张表最多有常数个项,问题就解决了。
于是接下来我们只需要决定表的个数M的大小。不幸的是,M必须非常大。如果M=N^2,那么我们可以证明散列表有至少1/2的概率是没有冲突的。
使用N^2个表是不现实的。但还有另一种选择:只用N个盒子,但是用单列表去解决每个盒子的冲突,而不是用链表。
按照最原始的思路,每个二级散列表将用一个不同的散列函数进行构造,直到没有冲突为止。如果产生的冲突次数高于要求的值,主散列表也可以被构建多次。这种方法称为完美散列。
布谷鸟散列
在布谷鸟散列中,假设有N个项。我们维护两个分别超过半空的表,且有两个独立的散列函数,可以把每个项分配到每个表中的一个位置。布谷鸟散列保持不变的是一个项总是会被存储在这两个位置之一。
跳房子散列
跳房子散列是一种新的算法。它尝试赶紧经典的线性探测算法。
跳房子散列的思路是,用事先确定的,对计算机底层体系结构而言是最优的一个常数,给探测序列的最大长度加个上界。这样做可以给出常数级的最坏常数时间,并与布谷鸟散列一样,查询可以并行化,以同时检查可用位置的有限集。
如果某次插入要把一个新的项放到距离它的散列位置太远的地方,我们会很有效地掉头向散列位置走,替换掉潜在的项。如果足够谨慎,那么替换可以很快完成,并且保证哪些被替换的项都不会放到距离他们的散列位置太远的地方。
浙公网安备 33010602011771号