一)散列表(哈希表)
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
二)散列函数
散列函数:能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。
1)直接定址法:取关键字或关键字的某个线性函数值为散列地址,即f(key)=a * key +b,a和b为常数。需先知道关键字的分布情况,适合查找表较小且连续的情况。
2)数字分析法:根据关键字位数规则判断,通常适合处理关键字位数比较大的情况。
3)除留余数法:f(key) = key % p (p <= m),最常用。
4)平方取中法:当无法确定关键字的分布,而位数又不是很大的情况。可根据关键字的平方值,获取平方数中间的几位数,如,666^2=443556,就取35或4355。
5)折叠法:将关键字分成位数相同的几部分,比如关键字为987654321,按4位分为一组,为9876|5432|1,然后折叠求和,9876+5432+1=15309。不需要知道关键字的分布,适合关键字较多的情况。
6)随机数法:f(key)=random(key),random为一个随机函数。通常用于关键字长度不等的情况。
三)散列冲突
散列冲突:通过散列函数计算,可能会存在相同的散列值,称为散列冲突。散列冲突是一定会存在的,只能尽量避免这种情况。
1)开放定址法:一旦散列地址发生冲突,就去寻找下一个空的散列地址,需考虑散列表空间是否足够。常用的开放定址法有:线性探查法、二次探测法、随机探测法。缺点是如果数据密集聚集在一块,会造成数据一次聚集。
2)平方探测法:f(key) = (key*key)%maxSize,数据聚集情况下,也会造成二次聚集。
3)双散列:为了避免聚集,选择跳跃式探测,即重新定义一个hash函数,如:f(key) = hash1(key) + hash2(key)。
4)拉链法:将所有散列地址相同的记录存储在同一个单链表中,该单链表头指针存储在散列表中。
5)再散列:散列表是可以看做固定大小的数组,如果容量满之后,可以创建一个比原来大2倍的散列表,把原先的关键字重新散列到新的散列表中。
四)散列表(除留余数法,二次探测法)
HashMap和Hashtable就是散列表的实现方式,利用了链表的方式实现,可以参考源码。
HashMap中的散列函数:先计算key的hashCode,再异或和无符合右移,该方式的好处在于,只有key有一位数的变动,hash值就会不一样,充分的避免了散列冲突(哈希冲突)。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
第一步:初始化散列表(哈希表)
/**
* 散列表(哈希表)
* @author ouyangjun
*/
public class Hash<E extends Number> {
public Number[] elementData; // 散列表数组
public int size; // 散列表大小
public int initialCapacity; // 散列表默认容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public Hash() { // 默认初始容量
this(16);
}
public Hash(int initialCapacity) { // 指定初始容量
if (initialCapacity <= 0)
throw new IllegalArgumentException();
this.initialCapacity = initialCapacity;
this.elementData = new Number[initialCapacity];
}
}
第二步:散列函数(除留余数法)
/**
* 散列函数,除留余数法
* @param key
* @return
*/
public int hashFunction(Object key) {
return (key == null) ? 0 : Math.abs(key.hashCode()) % initialCapacity;
}
第三步:新增散列值(超出初始容量可扩容)
/**
* 新增散列值
* @param key
*/
public E add(E e) {
// 判断是否需要扩容
grow(size + 1);
// 计算
int address = hashFunction(e);
while(this.elementData[address] != null) {
// 随机探测法,数组元素位置可能会不断变化,但随机到原点时,可能会返回null,不稳定
//address = hashFunction(Math.random() * address);
address = hashFunction(address+1); // 开启线性探测,如果散列冲突,需要重新寻找地址,稳定
}
this.elementData[address] = e;
++size;
return e;
}
/**
* 扩容
* @param minCapacity
*/
private void grow(int minCapacity) {
if (minCapacity - elementData.length > 0) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (minCapacity < 0) {
throw new OutOfMemoryError();
}
newCapacity = minCapacity > MAX_ARRAY_SIZE ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
// 扩容,并把元素复制到一个新数组中
this.initialCapacity = newCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
第四步:查询散列值,返回散列值下标
/**
* 查询散列值,返回散列值下标
* @param key
* @return
*/
@SuppressWarnings("unchecked")
public E searchHash(E e) {
if (e == null) {
return null;
}
// 计算
int address = hashFunction(e);
while (elementData[address] == null || !elementData[address].equals(e)) {
// 随机探测法,数组元素位置可能会不断变化,但随机到原点时,可能会返回null,不稳定
//address = hashFunction(Math.random() * address);
address = hashFunction(address+1); // 开启线性探测,如果散列冲突,需要重新寻找地址,稳定
// 回到了原点,表示e不存在
if (address == hashFunction(e)) {
return null;
}
// 如果不为null, 并且相等, 就返回
if (elementData[address] != null && elementData[address].equals(e)) {
return (E)elementData[address];
}
}
return (E)elementData[address];
}
第五步:散列表main方法测试
// 测试
public static void main(String[] args) {
Hash<Double> hash = new Hash<Double>();
hash.add(56.12);
hash.add(37.12);
hash.add(13.12);
hash.add(13.12);
hash.add(54.12);
System.out.println("散列表(哈希表)元素: " + Arrays.toString(hash.elementData));
double num1 = 56.12;
System.out.println("散列表(哈希表)中元素"+num1+"是否存在: " + hash.searchHash(num1));
double num2 = 6.12;
System.out.println("散列表(哈希表)中元素"+num2+"是否存在: " + hash.searchHash(num2));
double num3 = 54.12;
System.out.println("散列表(哈希表)中元素"+num3+"是否存在: " + hash.searchHash(num3));
}
散列表(哈希表)效果图:
结论:通过对int类型和double类型数据进行散列,发现某一些方式不是很适合double类型数据,例如平方探测法,随机探测法(会有一定的误差情况)。
识别二维码关注个人微信公众号
本章完结,待续,欢迎转载!
本文说明:该文章属于原创,如需转载,请标明文章转载来源!