前言:
数组的特点是:寻址容易,插入和删除困难;
链表的特点是:寻址困难,插入和删除容易;
我们可以构造一种结合两种优点的“链表散列”的数据结构,可以理解为链表的数组,HashMap就是基于其实现的。
1.哈希表的缺点有和优点
优点:
相对数组可以节省存储空间;
插入和寻址都很快;
在散列表中,查找一个元素的时间和链表中是相同的,都为O(n),但是在实践中散列表效率是很高的,查找一个元素的期望的时间为O(1);
缺点:
它是基于数组的, 数组创建完后扩展比较难, 所以当哈希表被填满的时候,性能会下降很多;所以,最好是知道表中要存储多少数据;
2. 理解寻址
在理解Hashmap之前,先理解哈寻址
直接寻址方式:
哈希寻址:
关键字是k的元素被散列到槽h(k);
所以现在就剩下几个问题:
1.如何哈希化
//JDK源码
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
哈希函数的设计其实很有讲究, 目标就是尽量减少冲突,同时把寻址控制在一定范围内;
具体的理论现在还理解不了, 源码的分析可以参考:
http://pengranxiang.iteye.com/blog/543893
2.如何解决冲突
指定的数组大小是需要存储的数据量的两倍,因此,可能有一半的单元是空的.
当冲突发生,
方法一:找到数组的一个空位,把数据插入,称为开放地址法;
方法二:创建一个存放链表的数组, 数组内不直接存放数据,这样当冲突发生,新的数据项直接接到这个数组下标所指的链表中;(链地址法)
2.1 开放地址法:
一种简单的就是:当要插入的数据的位置是1234, 如果位置被占了, 那么就看看1235, 以此类推,直到找到空位, 这样的方式叫线性探测;
当然,还有其他更好的改进的探测方法,就不仔细说了;
2.2 链地址法:
在链地址法中,如果需要在N个单元的数组中存放大于N个数据,因此装填因子大于1;
装填因子为2/3左右的时候,开发地址法的哈希表效率会下降很多, 而链地址法当因子为大于1,且对性能影响不是很大;
当然, 如果链表中有许多项, 存储时间会变长, 因为存储特定的数据需要搜索链表一半的长度;
2.3 JDK的链地址法具体实现
(这部分原文是来自 http://xiaolu123456.iteye.com/blog/1485349)
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
/*判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。
如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。
Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。
系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),
那系统必须循环到最后才能找到该元素。
*/
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
理解HASHMAP冲突最重要的一句话: 冲突是不可避免的,所以要去解决, 但是要尽最大努力,减少冲突的机会;
个人的理解是:减少冲突一方面是体现在哈希函数的设计上, 另外,作为使用者也要注意下容量是否合适;
HashMap的API里面有一句:
通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。
3. 哈希表怎么扩容
上面提到了默认的加载因子为0.75, 那么什么时候JDK里面的Hashmap数组会扩容? 扩多大?
在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。
如果 初始容量*加载因子<最大数据条目,则会发生扩容操作。
//JDK源码
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
每次在原来的基础上增大1倍(table.lenght*2)
所以在使用的过程中, 合理使用扩容.
参考: http://blog.csdn.net/likika2012/article/details/40510007
http://www.cnblogs.com/matrix-skygirl/archive/2013/01/17/2864919.html