散列表(哈希表)

一)散列表(哈希表)

散列表(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倍的散列表,把原先的关键字重新散列到新的散列表中。

 

四)散列表(除留余数法,二次探测法)

HashMapHashtable就是散列表的实现方式,利用了链表的方式实现,可以参考源码。

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类型数据,例如平方探测法,随机探测法(会有一定的误差情况)。

 

识别二维码关注个人微信公众号

本章完结,待续,欢迎转载!
 
本文说明:该文章属于原创,如需转载,请标明文章转载来源!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值