散列表
散列表(Hash Table)是一种基于散列函数(Hash Function)的数据结构,用
于实现快速的数据查找。散列函数将键(Key)映射到存储桶(Bucket)或槽位
(Slot)的位置上,以便能够快速定位到对应的值(Value)。
定义
输入:散列表(Hash Table)、待查找的键(Key)
输出:找到的值(Value)或表示键不存在的特定值(如NULL)
过程
1、根据给定的键使用散列函数计算键的散列值(Hash Value)。散列函数将键
转换为一个固定大小的整数,用于确定键在散列表中的位置。
2、使用散列值映射到散列表的索引位置。散列表通常是一个数组,每个元素代
表一个桶(Bucket),通过散列值的映射,待查找的键应该被存储在对应的桶中。
3、在散列表的索引位置上查找桶。如果桶为空,表示散列表中不存在待查找的
键,查找结束,返回表示键不存在的特定值(如NULL)。
4、如果桶不为空,可能存在冲突(多个键映射到了同一个桶),需要进行冲突解
决。常用的冲突解决方法有以下两种:
(1)链地址法(Separate Chaining):每个桶中保存一个链表,链表节点存
储冲突的键值对。在桶中搜索时,通过遍历链表来找到匹配的键值对。
(2)开放地址法(Open Addressing):在桶中直接存储冲突的键值对,当遇
到冲突时,通过探测(Probing)方法寻找下一个可用的桶。常见的探测方法有
线性探测、二次探测和双重散列等。
5、在桶中搜索待查找的键。如果找到了匹配的键,返回对应的值;如果未找到,
则继续冲突解决过程,直到找到匹配的键,或确定键不存在为止。
构造方法
直接定址法:将数据的某个固定部分作为散列地址。例如,对于整数数据,可以
将最高位或最低位作为散列地址。
数字分析法:根据对输入数据的分析,选择其中的某些位作为散列地址。例如,
对于日期数据,可以提取年份作为散列地址。
平方取中法:将数据平方后取中间的几位作为散列地址。这种方法可以减小重复
冲突的概率。
折叠法:将数据按固定位数分割,然后将这些部分相加得到散列地址。这种方法
可以在数据长度较大时减小冲突的概率。
随机数法:使用随机数生成器生成随机的散列地址。这种方法可以降低冲突的可
能性。
求余法:将数据除以散列表的大小,然后取余数作为散列地址。这是一种常用的
散列函数构造方法。
处理散列表冲突的方法
链地址法(Chaining):
实现原理:将冲突的元素存储在同一个位置的链表中。每个散列表的槽位都指
向一个链表的头节点,当发生冲突时,将新元素添加到链表的末尾。
插入操作:通过散列函数计算出元素的位置,如果该位置已经有元素存在,则
将新元素添加到链表的末尾。
查找操作:通过散列函数计算出目标元素的位置,然后遍历链表找到目标元素。
开放地址法(Open Addressing):
实现原理:当发生冲突时,通过一定的探测方式找到下一个可用的槽位。
线性探测法(Linear Probing):
当发生冲突时,线性地向后探测,直到找到一个空槽或者遍历整个散列表。
二次探测法(Quadratic Probing):
当发生冲突时,二次地向后探测,即第一次探测步长为1,第二次探测步长为2,
第三次探测步长为3,以此类推。
双重散列法(Double Hashing):
当发生冲突时,使用第二个哈希函数计算出一个步长,然后按照步长向后探测。
建立一个更大的散列表:
实现原理:当散列表的负载因子(已存储元素个数与槽位总数的比值)超过某
个阈值时,重新创建一个更大的散列表,并将原有的元素重新插入到新的散列
表中。这样可以减少冲突的概率。
再哈希法:
使用不同的哈希函数来处理冲突,当发生冲突时,再次计算哈希值,直到找到
一个空槽位。
伪随机数法:
通过伪随机数生成算法,将冲突的元素插入到散列表的不同位置,以减少冲突
的概率。
总结
每种方法都有其优缺点,选择合适的方法需要考虑散列表的具体应用场景和性能
需求。例如,链地址法适用于存储大量数据的情况,但需要额外的空间来存储链
表;开放地址法适用于空间有限的情况,但可能导致聚集现象。再哈希法和伪随
机数法可以提供较好的散列性能,但需要更复杂的实现。
算法实现
java
复制代码
import java.util.LinkedList;
public class HashTable {
private LinkedList<Entry>[] table;
private int capacity;
private int size;
public HashTable(int capacity) {
this.capacity = capacity;
this.table = new LinkedList[capacity];
this.size = 0;
}
public void put(String key, Object value) {
int index = getIndex(key);
if (table[index] == null) {
table[index] = new LinkedList<>();
}
for (Entry entry : table[index]) {
if (entry.getKey().equals(key)) {
entry.setValue(value);
return;
}
}
table[index].add(new Entry(key, value));
size++;
}
public Object get(String key) {
int index = getIndex(key);
if (table[index] != null) {
for (Entry entry : table[index]) {
if (entry.getKey().equals(key)) {
return entry.getValue();
}
}
}
return null;
}
public void remove(String key) {
int index = getIndex(key);
if (table[index] != null) {
for (Entry entry : table[index]) {
if (entry.getKey().equals(key)) {
table[index].remove(entry);
size--;
return;
}
}
}
}
public int size() {
return size;
}
private int getIndex(String key) {
int hashCode = key.hashCode();
return Math.abs(hashCode) % capacity;
}
private class Entry {
private String key;
private Object value;
public Entry(String key, Object value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
}
性能分析
散列函数的设计:散列函数将关键字映射到散列表的槽位上,一个好的散列函数
能够尽可能均匀地将关键字分布到不同的槽位上,减少冲突的概率。一个较差
的散列函数可能导致冲突增加,从而降低查找性能。
负载因子:负载因子是指已存储元素个数与槽位总数的比值。负载因子较高时,
冲突的概率会增加,查找性能会下降。通常情况下,负载因子的合理范围是0.7
到0.8。
冲突处理方法:不同的冲突处理方法会对查找性能产生影响。链地址法在发生冲
突时,将冲突的元素存储在链表中,查找时需要遍历链表。开放地址法通过一
定的探测方式找到下一个可用的槽位,查找时需要按照相同的探测方式进行查
找。选择合适的冲突处理方法可以提高查找性能。
散列表的大小:散列表的大小直接影响到槽位的数量,较大的散列表可以容纳更
多的元素,减少冲突的概率。当散列表的负载因子超过一定阈值时,可以考虑
重新创建一个更大的散列表来提高查找性能。
性能总结
总体来说,散列表的查找性能是较高的,平均情况下,查找操作的时间复杂度为
O(1),即常数时间。但是在最坏情况下,如果发生大量冲突,查找操作的时间复
杂度可能会退化为O(n),其中n为散列表的大小。因此,在设计散列表时需要综
合考虑散列函数的设计、负载因子的控制以及合适的冲突处理方法,以提高查找
性能。