`
lobin
  • 浏览: 425804 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Java第2篇: 哈希表

 
阅读更多

Java提供了一种很重要的结构HashMap。在将HashMap之前,先讲讲哈希表这种结构。

哈希表

HashMap其实就是一种哈希表结构。在哈希表这种结构中,数据是以key-value的形式进行存储的,主要有put和get这两种基本操作,put操作将数据以key-value的形式插入到哈希表,get操作根据key查询对应的value。

 

在向哈希表结构中插入数据时,以key-value的形式,根据key计算一个hash值,这个哈希值决定该数据存放哪个bucket。

 

除了HashMap,Java还提供了多种哈希表的实现,Java中比较常见的Hash实现有HashMap,Hashtable,ConcurrentHashMap。

 

这种结构在Java中也有很多地方使用到。

 

 

Hash通常是无序的。Java中也提供了一种有序的TreeMap。

HashMap (java.util.HashMap)是Map接口最基本的一个哈希表实现,它不是线程安全的,同时允许key为null。HashMap中的元素是无序的,也不保证顺序。在没有触发扩容的扩容的情况下,HashMap的基本操作如get和put性能稳定,也就是在时间复杂度上是稳定的。同时,有两个影响HashMap性能的参数:初始容量和加载因子。

 

负载因子

哈希表的负载因子定义为:α=填入表中的元素个数 / 哈希表的长度。α是散列表装满程度的标志因子。

 

默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

也就是如果哈希中元素的个数达到(大于或等于)某个阈值,就触发扩容,这个阈值的计算为哈希表的长度*负载因子。

 

容量设置

HashMap容量设置为一个2的n次方的数, 即power-of-two,如1,2,4,8,16,32,64,128 ...,在构造HashMap时,如果指定了初始容量,如通过public HashMap(int initialCapacity, float loadFactor)构造函数进行构造,这里第一个参数指定初始容量,但并不是简单的将HashMap容量设置为该参数值,而是像这样的:

 

// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
  capacity <<= 1;

 

它实际上指定的是一个2的N次方的一个值,且这个值刚刚大于(不小于)指定的初始容量。从这里可以看出,HashMap的容量一定是一个2的N次方的一个值,且最大为1 << 30。

 

有没有奇怪HashMap的容量为什么要设置为2的幂这样的值?Hashtable就不是这样。而ConcurrentHashMap的内部实现也和HashMap一样为2的幂这样的值?

 

最大容量

HashMap最大容量为1 << 30,也就是2的30次方。

 

static final int MAXIMUM_CAPACITY = 1 << 30;

 

 

 

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.

 

 

 

这个是HashMap中桶的最大容量,但并不是最大可put的元素的最大数量。最大可put的元素的数量为int的最大值,即2的31次方-1。

 

 

默认容量

默认情况下,如果没有指定初始容量,默认初始化容量为16。

 

static final int DEFAULT_INITIAL_CAPACITY = 16;

哈希函数

Java中哈希函数挺不好理解的,因为Java 中的哈希表结构所使用的hash函数都依赖于一个hashCode方法,而该方法是从基类Object继承过来的,且由jvm实现。另外在子类可以重写hashCode方法, jdk提供的很多类都重写了该方法,尽管都实现的很简单,但也给java中的哈希表结构的哈希函数很大的灵活性和理解的困难。即便是java中最简单的哈希表实现HashMap,要理解它的哈希函数也是很困难。

 

 

 

HashMap的哈希函数

HashMap在对key计算哈希以决定将数据存在哪个bucket中涉及到3个哈希函数。

 

它首先通过hashCode方法计算key的hashCode

 

public native int hashCode();
 然后对hashCode再次计算了一次hash,如下:

 

 

static int hash(int h) {
  // 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);
}

 

这是第2次哈希计算,由于HashMap使用power-of-two的容量大小,才有这步,如果不是使用power-of-two的容量大小,就不需要这步了,这样也就是跟HashTable一样的计算哈希了。

 

这次的hash计算用于确保在各个bit上发生碰撞的次数是有限的,没具体研究。

 

Java 8的第2次哈希计算:

 

h ^ (h >>> 16)

 

源代码如下:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

 

 

 

最后在根据第2次哈希计算的结果调用indexFor方法计算出最终的一个哈希值,它最终确定数据存在哪个bucket:

 

int i = indexFor(hash, table.length);
 这步很简答,indexFor方法如下:
static int indexFor(int h, int length) {
  return h & (length-1);
}

 

由于HashMap桶的容量取的是大于指定容量capacity的第一个2的n次方的值,所以h & (length-1)和m%length结果一样,但这种位运算的写法比模余的写法效率更高。

 

 

这段代码是什么意思?

if (oldCapacity == MAXIMUM_CAPACITY) {
  threshold = Integer.MAX_VALUE;
  return;
}

 

 

在进行扩容时,如果当前容量已经达到最大容量,这将threshold值设置为为int的最大值,即2的31次方-1,并不再进行扩容,也就不需要再重新计算哈希。从这里可以看出:HashMap最大容量为1 << 30,也就是2的30次方;最大可put的元素的数量为int的最大值,即2的31次方-1。

 

关于HashMap最大可放多少个的问题(即最大可put的元素的个数)

之前是认为最大可放int的最大值,即2的31次方-1个。

我觉得理论上是无限的。

 

 

 

 

HashMap构造

public HashMap()

 

 
Constructs an empty <tt>HashMap</tt> with the default initial capacity (16) and the default load factor (0.75).

 

public HashMap(int initialCapacity)
 
Constructs an empty <tt>HashMap</tt> with the specified initial capacity and the default load factor (0.75).

 

public HashMap(int initialCapacity, float loadFactor)
 
Constructs an empty <tt>HashMap</tt> with the specified initial capacity and load factor.

 

public HashMap(Map<? extends K, ? extends V> m)

 

 
Constructs a new <tt>HashMap</tt> with the same mappings as the specified <tt>Map</tt>. The <tt>HashMap</tt> is created with default load factor (0.75) and an initial capacity sufficient to hold the mappings in the specified <tt>Map</tt>.

 

 

HashMap允许key为null,当key为null时,数据存在第一个bucket,也就是下标为0的bucket。

private V putForNullKey(V value) {
  for (Entry<K,V> e = table[0]; e != null; e = e.next) {
    if (e.key == null) {
      V oldValue = e.value;
      e.value = value;
      e.recordAccess(this);
      return oldValue;
    }
  }
  modCount++;
  addEntry(0, null, value, 0);
  return null;
}

 

 

 

 

 

Threshold设置

 

threshold = (int)(capacity * loadFactor);

 

 

关键字比较

 

e.hash == hash && ((k = e.key) == key || key.equals(k))

 

扩容

当Map中的元素数目达到或超过threshold值时,将自动扩容,扩容后的容量是之前的两倍。

resize(2 * table.length)
 上面提得到HashMap的容量设置为2的n次方的一个数,扩容后新的容量为当前容量的2倍,也就是2的n+1次方。

 

重新计算哈希

在进行扩容时,需要重新计算哈希。

 

关键代码

 

 

HashMap实现的好吗?

HashMap实现的很好,但还不是很好。除了初始容量和加载因子这两个影响HashMap性能的参数,它还依赖于key的hashCode算法。同时,在计算哈希时计算了两次哈希。

 

Segment

Java并发编程包中提供的支持并发编程环境下的Map实现ConcurrentHashMap,并发编程通常是在多线程的环境下,需要保证类是线程安全的,ConcurrentHashMap通过Segment来分段,Segment继承了ReentrantLock类,因此支持加锁同步,通过这种方式来保证线程安全。同时通过这种分段的方式,减少每次操作map时都需要执行同步操作而必须阻塞从而无法继续执行的可能,必须等加锁的线程释放锁之后才能继续执行下去,从而提高并发性能。

 

因为同步操作在Segment上,在执行map操作时,对key计算hash后,由于对不同的key计算出来的hash不可能都一样,从而落在不同的分段上,而能够并发执行。

 

 

 



 

 

 

 

 

 

构造Segment需要指定一个初始容量和一个加载因子,用于指定一个初始容量和设置一个threshold,在执行put操作时,如果map中元素的数目超过了这个threshold,执行rehash和扩容操作。

 

Threshold设置:

threshold = (int)(newTable.length * loadFactor);

 

 

Segment构造:

Segment(int initialCapacity, float lf)

 

Segment最大容量为1 << 30,也就是2的30次方。

static final int MAXIMUM_CAPACITY = 1 << 30;

 

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 to ensure that entries are indexable using ints.

这个是Segment中桶的最大容量,但并不是最大可put的元素的最大数量。最大可put的元素的数量为int的最大值,即2的31次方-1。

 

计算落桶的位置:

int index = hash & (tab.length - 1);

 

和普通的HashMap及ConcurrentHashMap一样,默认的初始容量为16;默认的加载因子为0.75。

 

关键字比较:

e.hash != hash || !key.equals(e.key)

 

删除remove操作:

V remove(Object key, int hash, Object value)

如果value为null,则只匹配key,否则同时匹配key和value。

 

HashSet

 

HashSet,虽然带有Hash,但其实是一种集合,HashSet是Java中最基本的一个集合实现。我们没有在集合中过多的讲哈希表这种结构,包括HashMap等,因为它们都实现的是Map接口,而不是Collection接口。从Java的角度看,像HashMap等这种并不认为是一种集合。

 

分享到:
评论

相关推荐

    哈希表-浙大数据结构课件

    其中,开放寻址法是在哈希表中寻找下一个空位置,链地址法是每个槽位用链表连接所有映射至此的元素,再哈希法是使用第二个或更多的哈希函数解决冲突,而公共溢出区则是为所有冲突元素预留一个额外的存储区域。...

    Java哈希表性能优化

    #### 二、Java哈希表特点分析 ##### 2.1 装载因子恒定 Java标准库中的哈希表实现主要依赖于`java.util.Hashtable`类。在创建`Hashtable`对象时,用户可以通过构造函数指定初始容量`C`以及装载因子`F`。一旦对象被...

    哈希表的设计与实现.rar

    在“第四组_哈希表的设计与实现”这个压缩包中,可能包含了文档资料、源代码示例,帮助我们深入了解哈希表的设计过程和实现细节。通过学习这些内容,不仅可以提升对数据结构的理解,还能提高编程实践能力。

    java-leetcode面试题解哈希表第349题两个数组的交集-题解.zip

    标题中的“java-leetcode面试题解哈希表第349题两个数组的交集-题解.zip”表明这是一个关于Java编程语言的LeetCode面试题解答,具体是针对第349题,该题目涉及使用哈希表解决两个数组的交集问题。LeetCode是一个知名...

    java-leetcode面试题解哈希表第170题数据结构设计-题解.zip

    本题解将围绕LeetCode面试题中的第170题展开,通过Java实现来探讨哈希表在解决实际问题中的策略。 哈希表是一种以键值对形式存储数据的数据结构,它通过计算键的哈希值来确定元素在数组中的位置,从而实现快速访问...

    Java语言程序设计:ch04 数组、字符串、向量与哈希表.ppt

    Java语言程序设计的第四章中,讨论了数组、字符串、向量与哈希表等数据结构。下面是本章的知识点总结: 一、数组 * 数组是一种静态的数据结构,元素类型相同,占用连续的内存地址。 * 数组的静态性,使得一旦创建...

    java-leetcode面试题解哈希表第1207题独一无二的出现次数-题解.zip

    2. 如果元素已经在哈希表中,将其对应的值加1。 遍历完成后,哈希表中存储的就是每个元素及其出现的次数。接下来,我们需要构建一个新的数组,只包含那些出现次数独一无二的元素。可以再次遍历哈希表,检查每个元素...

    java-leetcode面试题解哈希表第447题回旋镖的数量-题解.zip

    在本题解中,我们将深入探讨Java编程语言中与LeetCode面试相关的哈希表技术,特别是在解决第447题“回旋镖的数量”时的应用。哈希表是一种高效的数据结构,它允许我们在常数时间内执行查找、插入和删除操作,这在...

    java-leetcode面试题解哈希表第36题有效的数独-题解.zip

    在本压缩包中,我们关注的是Java编程语言与LeetCode面试题目,特别是关于哈希表的应用。这道题目编号为第36题,名为“有效的数独”。在求职面试中,掌握这类问题的解决能力是至关重要的,因为它涉及到数据结构和算法...

    java-leetcode面试题解哈希表第387题字符串中的第一个唯一字符-题解.zip

    1. 使用哈希表:创建一个大小为26的哈希表(可以使用Java的HashMap或者HashSet),用于存储每个字符出现的次数。 2. 遍历字符串s,对于每个字符,将其作为键存入哈希表,并将值加一。这样,哈希表中每个键对应的值...

    哈希表,开放地址法之再哈希代码(JAVA)

    当第一次哈希冲突发生时,我们会使用 `(index + hash2(key)) % tableSize` 来获取新的探测位置,其中 `index` 是初始位置,`tableSize` 是哈希表的大小。 在Java中实现开放地址法中的双哈希代码,我们可以创建一个...

    个人第一次接触的无敌哈希表

    哈希表,也被称为散列表,是数据结构中一种非常重要的实现方式,它在计算机科学中扮演着不可...在编程实践中,可以尝试使用不同编程语言(如C++、Python或Java)实现自己的哈希表,并通过实际问题来测试和优化其性能。

    java-leetcode面试题解哈希表第1题两数之和-题解.zip

    哈希表在这类问题中的优势在于它可以快速地进行查找操作,避免了对整个数组进行二次遍历,提高了效率。在LeetCode等在线编程平台,这类基于哈希表的解决方案通常能得到较高的时间复杂度评分。 在面试中,熟练掌握并...

    java-leetcode面试题解哈希表第202题快乐数-题解.zip

    这道第202题的“快乐数”就是一道典型的使用哈希表来优化算法的例子。 快乐数是指那些经过特定计算规则后能够达到数字1的数。计算规则是:将一个数的每一位平方和相加,然后重复这个过程,直到结果为1或者进入循环...

    java-leetcode面试题解哈希表第350题两个数组的交集II-题解.zip

    标题中的“java-leetcode面试题解哈希表第350题两个数组的交集II-题解.zip”指的是一个关于Java编程语言的LeetCode面试题解析,具体是第350题,题目要求解决“两个数组的交集II”的问题,而解题方法主要涉及哈希表的...

    java-leetcode面试题解哈希表第217题存在重复元素-题解.zip

    2. 遍历输入数组`nums`,对于每个元素,将其添加到哈希表中。 3. 如果在添加过程中发现某个元素已经在哈希表中(即`add()`方法返回`false`),那么说明存在重复元素,返回`true`。 4. 遍历完成后,如果没有发现重复...

    java-leetcode面试题解哈希表第49题字母异位词分组-题解.zip

    总之,LeetCode第49题的解法展示了哈希表在处理字母异位词问题时的高效性和实用性,是学习和提高Java编程技巧的一个宝贵案例。通过反复练习和分析,开发者可以更好地掌握这类问题的解决策略,为求职面试做好充分准备...

    java-leetcode面试题解哈希表第219题存在重复元素II-题解.zip

    这道题目是LeetCode中的第219题,名为"存在重复元素II",主要考察的是哈希表的应用,这对于求职面试来说是一个常见的考察点。下面我们将深入探讨这个问题以及哈希表在解决此类问题中的作用。 首先,让我们明确题目...

    Hash map 哈希表

    同样,Java的HashMap类也是基于哈希表原理。 哈希表的实例可以用来解决多种问题,如查找、去重、统计、关联数据等。在"www.pudn.com.txt"和"哈希表"这两个文件中,可能包含了一些关于哈希表的示例代码或者更深入的...

Global site tag (gtag.js) - Google Analytics