散列

一.什么是散列java

散列使用一个散列函数,将一个键映射到一个索引上。
散列很是高效。使用散列将耗费O(1)时间来查找、插入、及删除一个元素。数组

映射表是一种用散列实现的数据结构,映射表是一种存储条目的容器,每一个条目包含两个部分:一个键(key)和一个值(value)。键又称为搜索键用于查找对应的值。
映射表(map)又称为字典(dictionary)、散列表(hash table)或者关联数组(associate array)。数据结构

Java合集框架定义了java.util.Map接口,三个具体的的实现为java.util.HashMap、java.util.linkedHashMap以及java.util.TreeMap。java.util.HashMap使用散列实现,java.util.linkedHashMap使用linkedList,java.util.TreeMap使用红黑树。多线程

 

二.散列函数和散列码
框架

1.散列函数
将键映射到散列表的索引上的函数称为散列函数(hash function)。理想的,将每一个搜索的键映射到散列表中的不一样索引上,这样的函数称为完美散列函数。然而,很难找到一个完美的散列函数,当两个或更多的键映射到一个散列值上的时候,咱们称之为产生了一个冲突(collision)。函数

典型的散列函数首先将搜索键转换成为一个整数值,称之为散列码。而后将散列码压缩为散列表中的索引。this

2.equals和hashCode
Java的根类Object具备hashCode方法,返回一个整数的散列码。默认的,该返回值是一个该对象的内存地址。hashCode通常有以下约定:
1)当equals方法被重写时,应该重写hashCode方法,以保证两个相等的对象返回一样的散列码。
2)程序执行中,若是对象的数据没有被修改,则屡次调用hashCode将返回一样的整数。
3)两个不相等的对象可能具备一样的散列码,可是应该在实现hashCode方法时避免太多这样的情形出现。spa

3.基本数据类型的散列码
对于byte、short、int、char类型,简单讲它们转为int,这些类型中的任何一个不一样的搜索键将有不一样的散列吗。
对于float,使用Float.floatToIntBits(key)做为散列码,方法返回一个int值。该值得比特表示和浮点数f的比特表示相同。

对于long类型的搜索键,简单地转为int不是很好的选择,由于没法反应前面32高位的不一样。考虑到这种状况,将64比特分为两部分,并执行异或操做将两部分结合,这个过程称为折叠(folding)。
一个long类型键的散列码为:线程

int hashCode=(int)(key^(key>>32));

对于double类型的搜索键,首先使用Double.doubleToLongBits方法转为long值,再执行折叠操做。code

4.字符串类型的散列码
一个比较直观的方法是将全部字符的unicode求和做为字符串的散列码。这个方法可能有较大的冲突,也没法区分dog与dgo。
一个更好的方法是考虑字符的位置,而后产生散列码:

这里Si为s.charAt(i),这个方法被称为多项式散列码。计算时,对于长的字符串会致使溢出,但Java中会忽略溢出。要最小化冲突,关键是选择合适的b,实验显示,b较好的取值为31,33,37,39,41。

java.lang.String.java中的多项式散列实现:

/** * Returns a hash code for this string. The hash code for a * {@code String} object is computed as * <blockquote><pre> * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] * </pre></blockquote> * using {@code int} arithmetic, where {@code s[i]} is the * <i>i</i>th character of the string, {@code n} is the length of * the string, and {@code ^} indicates exponentiation. * (The hash value of the empty string is zero.) * * @return a hash code value for this object. */
    public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }

5.压缩散列码
键的散列码多是一个很大的整数,超过了散列表索引的范围,所以须要将它缩小。假设散列表的索引处于0到N-1之间,经常使用的压缩作法是:
h(hashCode)=hashCode%N

保证索引均匀扩展,选择N大于2的素数。

上面的式子等价于:
h(hashCode)=hashCode&(N-1)

为保证散列是均匀分布的,java.util.HashMap的实现中采用了补充的散列函数与主散列函数一块儿使用:

private static int supplementalHash(int h){ h^=(h>>>20)^(h>>>12); return h^(h>>>7)^(h>>>4); }

完整的散列函数:

h(hashCode)= supplementalHash(hashCode)%N

这个式子与下面式子同样:

h(hashCode)= supplementalHash(hashCode) &(N-1)

 

三. 开放地址法

当两个键映射到散列表中的同一个索引,冲突发送,一般有2种方法处理冲突:开放地址法与链地址法

开放地址法有如下几个变体。
1.线性探测
线性探测法在发生冲突时,按顺序找到下一个可用的位置。连续冲突时继续按序检查,直到找到可用位置。
当探测到表的终点时,则返回表的起点。所以,散列表被当成是循环的。

线性探测法容易致使散列表中连续的单元组被占用。这样的每一个组称为一个簇(cluster)。

2.二次探测
线性探测法从索引k的位置检查连续单元,二次探测法则从索引为(k+j^2)%N位置开始检查,其中j>=0。

二次探测法避免了线性的成簇问题,但有本身自己的成簇问题,称为二次成簇,即产生冲突的条目将采用一样的探测序列。

3.再哈希法
避免成簇问题的另外一个方法是再哈希法。

 

 

四. 链地址法
链地址法将具备相同的散列索引的条目都放在一个位置,每一个位置使用一个桶来放置这些条目。

 

链地址法是种普遍使用的方法。
(在JDK中有很多基于此方法的实现,如jdk1.7的hasMap实现方案,在多线程环境中发生链循环等错误,jdk1.8中重写了链的处理,修正了此错误等故事。)

 

五. 装填因子和再散列
装填因子(load factor)衡量一个散列表有多满。若是装填因子超出,则增长散列表的大小,并从新装载条目到一个新的更大的散列表中,这称为再散列。
装填因子=条目数n/容量N,若是散列表满了,装填因子=1
当装填因子接近1时,冲突的可能性就增大。通常对于开发地址法,装填因子须要控制在0.5下,链地址法一般维持在0.9下。

在java.util.HashMap的实现中,采用了装填因子0.75的阈值。一旦超过阈值,就须要增长散列表的大小,并进行再散列。再散列的代价比较大,为避免频繁的再散列,一旦扩容时应该至少将散列表的大小翻倍。

/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */
    static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
相关文章
相关标签/搜索