数据结构(二)--散列

什么是散列

散列首先是一个数组,所以它的访问速度快,它与数组的不同在于可以通过一个关键字访问数据,这个关键字与数组下标之间的映射关系有散列函数提供。
在这里插入图片描述

散列函数

所以说,散列函数对于一个散列是非常重要的,它的第一个主要功能就是将关键字映射到数组下标。比较简单的散列函数,比如关键字是正整数,那么散列函数可以是将关键字与数组长度进行取余操作,获取到下标值。或者关键字是字符串,我们通过某个方法,比如ASCII码将字符串转换为数字,然后再进行取余操作。

散列函数第二个主要功能是将关键字映射得到的下标尽量分散,比如我们有一个长度为8的散列,最好的情况是插入8个元素分别在不同的下标,最坏的情况是8个元素的关键字都得到相同的下标,这样就造成了hash冲突。所以为了减少hash冲突,散列函数需要尽量的分散,下面是《数据结构与算法分析》中提出的比较好的散列函数

public static 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;
}

在这里插入图片描述

hash冲突

由于数组长度是有限的,所以散列函数再完美也是无法避免hash冲突的。那么接下来的问题就是如何解决hash冲突。

分离链表法

分离链表法是将冲突的元素按照链表存储在数组中,也就是散列表存储一个链表数组。分离链表法实现简单,缺点是在链表长度过长会导致查找耗时,所以需要在长度达到一定程度的时候做些处理,在java中,HashMap使用分离链表法解决hash冲突,当链表长度超过阈值时,会将链表转换为树形结构存储。

代码见https://github.com/serpmelon/java_pk/blob/master/src/main/java/com/togo/java/data/structure/hashing/SeparateChainingHashTable.java

探测散列表

在分离链表法中,我们实现了另一种数据结构(链表)来解决hash冲突,在某些情况下我们可能不希望再实现另一种数据结构,那么我们只能在原表中寻找空的单元,所以探测散列表一般比分离链表法的表更大。
hi(x) = (hash(x) + f(i)) mod tableSize; f(i)是解约冲突的方法。

线性探测法

在线性探测法中,f(i)是i的线性函数,比如f(i) = i;这就相当于逐个位置查找,直到找到空单元。所以只要表足够大,肯定是可以找打空单元的。但是由于f(i)线性的关系,散列后的单元会形成一个区块,这样会导致,在表空间还比较空的时候,散列到区块中的任何关键字都要经过多次操作才能解决冲突,这就是一次聚集

平方探测法

平方探测就是形如f(x) = i^2; 在一定程度上解决了一次聚集,可能会导致二次聚集;平方探测需要注意一点,如果哈希表的大小比较特殊,可能导致还有空间但是无法找到位置的情况,因为
它不像线性探测可以遍历整张表,平方可能会始终在某几个位置来回跳转。所以如下定理很重要:
如果使用平方探测,且表的大小是素数,那么当表至少有一半是空的时候,总能够插入一个新的元素。

双散列

双散列就是f(x)=hasn2(x),与前面两个没有本质区别,只是函数的差别,它对于散列表的要求也是素数。

再散列

当容量达到一定程度的时候需要扩容,扩容后元素肯定没办法匹配之前的位置,所以需要再散列。
再散列的花销很大,但是发生的次数比较少,一般再散列是再装填因子达到某个值时触发(hashmap是0.75)。
以实际情况举例,比如Java中的HashMap,rehash的大概步骤如下(只考虑有值的情况):
1、设置扩容后的数组大小,如果已经为Integer.MAX则不扩容;其他情况数组大小扩大两倍;
2、遍历数组将数组中的元素放置在新数组中,HashMap使用的是分离链表法,所以也需要对于元素的链表进行遍历。HashMap对于链表有个优化,当链表长度过长(默认是8)会将链表转换为红黑树,所以遍历的时候需要考虑这两种情况。

//TODO 听过更好的rehash策略,可以不用移动旧数据,后面补充。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章