请先参考深入了解散列表
什么是基于拉链法的散列表?
对于散列算法的碰撞处理,一种直接的办法就是将大小为M的数组中的每个元素指向一条链表,链表中的每个结点都存储了散列值为该元素的索引的键值对。
这种方法称为拉链法,因为发生冲的元素都被存储在链表中。
这个方法的基本思想就是选择足够大的M,使得所有的链表都尽可能的短以保证高效地查找。
基于拉链法的散列表的查找方法:
- 首先根据散列值找到对应的链表,
- 然后沿着链表顺序查找相应的键。
拉链法的一种实现方法
- 使用原始的链表数据类型。
- 采用一般性的策略(但效率稍低),为M个元素分别构建符号表来保存散列到这里的键。
拉链法中链的平均长度
因为我们要用M条链表保存N个键,无论键在各个链表中的分布如何,链表的平均长度肯定为
N/M
例如,假设所有的键都落在了第一条链表上,所有链表的平均长度仍然
(N+0+0+0+…+0)/M = N/M。
拉链法在实际情况中很有用,因为每条链表确实都大约有N/M个键值对。在一般情况中,我们能够依赖这种高效的查找和插入操作。
代码实现
public class SeparateChainingHashST<Key, Value> {
private static final int INIT_CAPACITY = 4;
private int n; // 键值对的总数
private int m; // 散列表的大小
private SequentialSearchST<Key, Value>[] st; // 存放链表对象的数组
public SeparateChainingHashST() { //无参构造
this(997);
}
public SeparateChainingHashST(int m) { //有参构造
this.m = m; //创建M条链表
st = (SequentialSearchST<Key, Value>[]) new SequentialSearchST[m];
for (int i = 0; i < m; i++)
st[i] = new SequentialSearchST<Key, Value>();
}
private int hash(Key key) { //获得键的hash值
return (key.hashCode() & 0x7fffffff) % m;
}
public Value get(Key key) { //获取符号表中的指定键
int i = hash(key);
return st[i].get(key);
}
public void put(Key key, Value val) { //存放一个键值对到符号表
if (val == null) {
delete(key);
return;
}
//如果列表的平均长度>= 2,则将表大小增加一倍
if (n >= 10*m) resize(2*m);
int i = hash(key);
if (!st[i].contains(key)) n++;
st[i].put(key, val);
}
public Iterable<Key> keys() { //返回符号表中键的迭代器
Queue<Key> queue = new Queue<Key>();
for (int i = 0; i < m; i++) {
for (Key key : st[i].keys())
queue.enqueue(key);
}
return queue;
}
这段简单的符号表实现维护着一条链表的数组,用散列表来为每个键选择一条链表。简单起见,我们使用了SequentialSearchST。在创建st[]时需要进行类型转换,因为Java不允许泛型的数组。默认的构造函数会使用997条链表,因为对于较大的符号表,这种实现比SequentialSearchST大约快1000倍。当你能够预知需要的符号表大小时,这种短小精悍的方案能够得到不错的性能。
一种更可靠的方案是动态调整链表数组的大小,这样无论在符号表中有多少键值对都能保证链表较短。
散列表的大小
在实现基于拉链法的散列表时,我们的目标是选择适当的数组大小M,既不会因为空链表而浪费大量内存,也不会因为链表太长而在查找上浪费大量时间。对于拉链法来说这并不是关键性的选择。
如果存入的键多余预期,查找所需的时间只会比选择更大的数组稍长;如果少于预期,虽然有些空间浪费但查找会非常快。
当内存不是很紧张时,可以选择一个足够大的M,使得查找所选要的时间变为常数;当内存紧张时,选择尽量大的M仍然能够将性能提高M倍。
删除操作
要删除一个键值对,先用散列值找到含有该键的SequentialSearchST对象,然后调用该对象的delete()方法。
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete() is null");
int i = hash(key);
if (st[i].contains(key)) n--;
st[i].delete(key);
// 如果列表的平均长度<= 2,则将表大小减半
if (m > INIT_CAPACITY && n <= 2*m) resize(m/2);
}
有序性相关操作
散列最主要的目的在于均匀地将键散布开来,因此在计算散列后键的顺序信息就丢失了。如果你需要快速找到最大键或者最小键,或是查找某个范围内的键,散列表都不是合适的选择,因为这些操作的时间都是线性的。
小小总结:
基于拉链法的散列表的实现简单。在键的顺序并不重要的应用中。它可能是最快的(也是使用最广泛的)符号表实现。