散列数据结构的目的
常数时间进行插入,删除,查找的结构。但是会丢失任何元素的排序,以及位置的信息。在后面我将用自己的几种实现来测试常数时间的运行。
散列的一些定义
散列表:固定大小的数组,来存储数据,每个数据有对应的关键字来进行查找,每个存放的数据通过关键字映射到散列表进行存储
散列函数:关键字映射到的位置 被称为散列函数
冲突:多个关键字映射到同一位置,后面会介绍解决冲突的办法以及对比
填装因子:散列表中元素个数/散列表总大小大
散列函数的选择
在选择散列函数之前需要说明散列函数的选取原则
- 计算方便,理解方便,也就是不复杂。
- 散列的位置相对均匀
接下来进行散列函数的选取
关键字为整数
一般情况下整数的选择是 key mod tablesize 关键字对散列表大小求余,且散列表的大小为素数。
/**
* 整数为关键字
* 求余的方式进行散列函数的选取
*/
private int hash(int key, int tableSize) {
key %=tableSize;
if (key < 0) {
key += tableSize;
}
return key;
}
表大小的控制相关代码如下
/**
* 判断是否为素数
*/
private boolean isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0)
return false;
}
return true;
}
/**
* 获取下一个素数
*/
private int nextPrime(int domain) {
if (isPrime(domain)) return domain;
if (domain % 2 == 0) domain++; //如果是偶数 +1
while (!isPrime(domain)) {
domain += 2;
if (domain <= 2) return 2;
}
return domain;
}
关键字为字符串
一般java语言的关键字选取是取对象的hashcode,上面的求余散列函数已经可以满足。但是也有字符串作为散列函数的情况,参考数据结构预算法分析java语言描述,字符串的散列函数采用多项式函数。
/**
* 字符串为关键字
* 多项式的散列函数 h=((k2)*37+k1)*37+k0 的表现形式
*/
private 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;
}
仅做了解,我针对这两种散列函数做了冲突的测试,有兴趣的话我会发出来。
解决冲突的方式
分离连接法
就是将散列到同一个位置的元素装在集合中,如linkedList.每有一个冲突元素则linkedList的长度就增加1.
直接上实现代码
/**
* 自己实现的分离散列法散列表
* 散列函数选取 key% tableSize
*/
public class LinkedHashTable<T> {
private LinkedList<T>[] hashTable;
/**
* 默认大小为101
*/
private static final int DEFAULT_SIZE = 101;
private int currentSize;
public LinkedHashTable() {
this(DEFAULT_SIZE);
}
public LinkedHashTable(int size) {
hashTable = new LinkedList[nextPrime(size)];
for (int i = 0; i < hashTable.length ; i++) {
hashTable[i] = new LinkedList();
}
}
private int hash(int key, int tableSize) {
key %= tableSize;
if (key < 0) {
key += tableSize;
}
return key;
}
}
初始化的操作,默认大小为101,如果自己指定大小,则表的大小会选取指定数字的下一个素数。
public boolean contains(T t) {
LinkedList<T> linkedList = hashTable[hash(t)];
return linkedList.contains(t);
}
public void insert(T t) {
LinkedList<T> linkedList = hashTable[hash(t)];
if (!linkedList.contains(t)) {
linkedList.add(t);
// 如果负载因子大于0.75
if (currentSize++ > (hashTable.length*0.75)) {
rehash();//重新分配 hash表
}
}
}
public void remove(T t) {
LinkedList<T> linkedList = hashTable[hash(t)];
if (!linkedList.contains(t)) {
linkedList.remove(t);
currentSize--;
}
}
都是先定位散列位置,然后再执行相应的操作
添加不允许重复元素,注意的是重新分配散列表的操作,称为再散列,运行时间为O(n),因为有n个元素需要重新散列,是一个时间开销非常大的操作。具体操作后面介绍,这里主要说明重新散列的时机。
一般来说当散列因子达到某值时进行再散列操作,负载因子越大时说明冲突的几率越高,当负载因子大于1时则必定会有冲突。 平均遍历次数为 (1+ 负载因子/2) 次, java标准库的 HashSet、HashMap 的负载因子为0.75 平均遍历次数为1.37,所以合理的方式是当填装因子大于0.75时进行再散列操作。
所以对于一个散列表,真正重要的是填装因子的大小,直接决定散列表的性能。