【Java集合学习】HashMap源码之“拉链法”散列冲突的解决

1.HashMap的概念

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它是线程不安全的。它的key、value都可以为null。此外,HashMap中的映射是无序的。

  本文重点是介绍HashMap中的“拉链法”解决散列冲突。如果想了解其他方面的知识可参考http://www.cnblogs.com/skywang12345/p/3310835.html。

下面先介绍一下散列表及散列冲突的基本概念。

 

2.Hash表的概念

  我们知道,有一种数据结构能够快速地查找所需的对象(O(1)时间复杂度),这就是散列表(hash table)。散列码(hash code)是由对象的实例产生的一个整数。

更准确的说,具有不同的数据域的对象将产生不同的散列码。如下图(这里顺带提一下散列表的概念,即哈希表的概念):

  设所有可能出现的关键字集合记为U(简称全集)。实际发生(即实际存储)的关键字集合记为K(|K|比|U|小得多)。
     散列方法是使用函数h将U映射到表T[0..m-1]的下标上(m=O(|U|))。这样以U中关键字为自变量,以h为函数的运算结果就是相应结点的存储地址。从而达到在O(1)时间内就可完成查找。
    其中:
     ① h:U→{0,1,2,…,m-1} ,通常称h为散列函数(Hash Function)。散列函数h的作用是压缩待处理的下标范围,使待处理的|U|个值减少到m个值,从而降低空间开销。
     ② T为散列表(Hash Table)。
     ③ h(Ki)(Ki∈U)是关键字为Ki结点存储地址(亦称散列值或散列地址)。
     ④ 将结点按其关键字的散列地址存储到散列表中的过程称为散列(Hashing)

3.散列冲突

  两个不同的关键字,由于散列函数值相同,因而被映射到同一表位置上。该现象称为冲突(Collision)或碰撞。发生冲突的两个关键字称为该散列函数的同义词(Synonym)。

上图中的k2≠k5,但h(k2)=h(k5),故k2和K5所在的结点的存储地址相同。

有了冲突,那自然要尽可能地去避免冲突。那么如何安全避免冲突呢?有两个条件:

  1. 其一是|U|≤m;
  2. 另一个是设计合适的哈希函数。

   这只适用于|U|较小,且关键字均事先已知的情况,此时经过精心设计散列函数h有可能完全避免冲突。通常情况下,h是一个压缩映像。虽然|K|≤m(已知的关键字),但|U|>m(未知的关键字),故无论怎样设计h,也不可能完全避免冲突。因此,只能在设计h时尽可能使冲突最少。同时还需要确定解决冲突的方法,使发生冲突的同义词能够存储到表中。

  HashMap中采用的“拉链法”就是一种冲突解决的方式(hash函数的设计才是冲突避免,但不是一种完全的冲突解决方法),如下图所示为“拉链法”结构。

  但是HashMap中的节点是Map.Entry类型的,而不是简单的value,如下图所示,左边是一个Node<K,V>[] table数组(在jdk6中是Entry<K,V>数组),Node是Map.Entry的实现类。

  那么它是如何解决冲突的呢?即key值不同的两个或多个Map.Entry<K,V>可能会插在同一个桶下面,但是当查找到某个特定的hash值的时候,下面挂了很多个<K,V>映射,怎么确定哪个是我要找的那个<K,V>呢?这就是HashMap底层结构的一个亮点,在它的Entry中不仅仅只是插入value的,他是插入整个Entry 的,里面包含key和value的,所以能识别同一个hash值下的不同Map.Entry,想要了解更多,建议查看源码。

posted @ 2017-08-21 14:31  wells163  阅读(4385)  评论(0编辑  收藏  举报