hashmap的原理

hashmap的原理,面试难点

一、Java中的hashCode和equals

1、关于hashCode

  1. hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的
  2. 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同
  3. 如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点
  4. 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里“

再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。

以下对hashCode的解读摘自其他博客:

1.hashcode是用来查找的,如果你学过数据结构就应该知道,在查找和排序这一章有
例如内存中有这样的位置
0  1  2  3  4  5  6  7
而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,如果不用hashcode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。
但如果用hashcode那就会使效率提高很多。
我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除 8求余数直接找到存放的位置了。
2.但是如果两个类有相同的hashcode怎么办那(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,回答是:可以这样。那么如何判断呢?在这个时候就需要定义 equals了。
也就是说,我们先通过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过 equals 来在这个桶里找到我们要的类。
那么。重写了equals(),为什么还要重写hashCode()呢?
想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶,光重写equals()有什么用啊

2、关于equals

1.equals和==
==用于比较引用和比较基本数据类型时具有不同的功能:
比较基本数据类型,如果两个值相同,则结果为true
而在比较引用时,如果引用指向内存中的同一对象,结果为true;

equals()作为方法,实现对象的比较。由于==运算符不允许我们进行覆盖,也就是说它限制了我们的表达。因此我们复写equals()方法,达到比较对象内容是否相同的目的。而这些通过==运算符是做不到的。

2.object类的equals()方法的比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:
java.io.file,java.util.Date,java.lang.string,包装类(Integer,Double等)
String s1=new String("abc");
String s2=new String("abc");
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
运行结果为false true

二、HashMap的实现原理

1.    HashMap概述

    HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

    在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

其中Java源码如下:

复制代码
/**
 * The table, resized as necessary. Length MUST Always be a power of two.
 */
transient Entry[] table;

static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; …… }
复制代码

可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

2、HashMap实现存储和读取

1)存储

复制代码
 1 public V put(K key, V value) {
 2     // HashMap允许存放null键和null值。  3 // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。  4 if (key == null)  5 return putForNullKey(value);  6 // 根据key的keyCode重新计算hash值。  7 int hash = hash(key.hashCode());  8 // 搜索指定hash值在对应table中的索引。  9 int i = indexFor(hash, table.length); 10 // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。 11 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 12  Object k; 13 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 14 // 如果发现已有该键值,则存储新的值,并返回原始值 15 V oldValue = e.value; 16 e.value = value; 17 e.recordAccess(this); 18 return oldValue; 19  } 20  } 21 // 如果i索引处的Entry为null,表明此处还没有Entry。 22 modCount++; 23 // 将key、value添加到i索引处。 24  addEntry(hash, key, value, i); 25 return null; 26 }
复制代码

根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

hash(int h)方法根据key的hashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。

1 static int hash(int h) {
2 h ^= (h >>> 20) ^ (h >>> 12); 3 return h ^ (h >>> 7) ^ (h >>> 4); 4 }

我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。

根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

1 void addEntry(int hash, K key, V value, int bucketIndex) {
2         Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点
3         table[bucketIndex] = new Entry<>(hash, key, value, e);
4         if (size++ >= threshold) //如果大于临界值就扩容
5             resize(2 * table.length); //以2的倍数扩容
6     

参数bucketIndex就是indexFor函数计算出来的索引值,第2行代码是取得数组中索引为bucketIndex的Entry对象,第3行就是用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。

第4行和第5行就是判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容,HashMap扩容是扩为原来的两倍。resize()方法如下:

复制代码
 1 void resize(int newCapacity) {
 2         Entry[] oldTable = table;
 3         int oldCapacity = oldTable.length;
 4         if (oldCapacity == MAXIMUM_CAPACITY) {
 5             threshold = Integer.MAX_VALUE;
 6             return;
 7         }
 8 
 9         Entry[] newTable = new Entry[newCapacity];
10         transfer(newTable);//用来将原先table的元素全部移到newTable里面
11         table = newTable;  //再将newTable赋值给table
12         threshold = (int)(newCapacity * loadFactor);//重新计算临界值
13     }
复制代码

扩容是需要进行数组复制的,上面代码中第10行为复制数组,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

通过这种方式就可以高效的解决HashMap的冲突问题。

2)读取

复制代码
 1 public V get(Object key) {
 2     if (key == null)  3 return getForNullKey();  4 int hash = hash(key.hashCode());  5 for (Entry<K,V> e = table[indexFor(hash, table.length)];  6 e != null;  7 e = e.next) {  8  Object k;  9 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 10 return e.value; 11  } 12 return null; 13 }
复制代码

从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

3)归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

3、HashMap的resize

       当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

       那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

 

总结:HashMap的实现原理:

  1. 利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
  4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

转载于:https://www.cnblogs.com/DaTouDaddy/p/6627233.html

<think>好的,我现在需要详细解释哈希映射(HashMap)的数据结构原理。首先,我得回想一下自己对HashMap的理解,并确保覆盖所有关键点。用户提供的引用内容中提到了哈希表作为底层结构、哈希函数、冲突处理等,这些都是需要涵盖的。 首先,HashMap的基本结构是什么?它基于哈希表,使用数组来存储键值对。哈希函数将键转换为数组的索引,这样可以直接访问对应位置的数据。这里要解释哈希函数的作用,比如将任意大小的数据映射到固定大小的值,作为数组下标。 接下来,哈希冲突是不可避免的,因为不同的键可能生成相同的哈希值。如何处理冲突?常见的方法有开放寻址法和链地址法。根据引用中的内容,用户可能已经提到过链地址法,即每个数组位置是一个链表,存放所有哈希冲突的键值对。需要详细说明这个过程,比如当发生冲突时,将新元素添加到链表中。 然后,需要讨论哈希函数的设计。好的哈希函数应该均匀分布键,以减少冲突。可能提到取模运算,比如用键的哈希值对数组长度取模得到索引。但数组的长度通常选择质数,以减少重复,比如引用中可能提到的。 扩容机制也是一个关键点。当元素数量超过负载因子(比如0.75)时,数组需要扩容,通常翻倍,并重新哈希所有元素到新的数组中。这会影响到性能,但能保持低冲突率。需要解释负载因子的作用和扩容的过程。 另外,时间复杂度方面,理想情况下,插入、删除、查找都是O(1),但最坏情况下(比如所有键冲突)退化为O(n)。所以强调良好的哈希函数和扩容策略的重要性。 可能还需要提到HashMap的应用场景,比如缓存、字典实现等,但用户主要问的是原理,这部分可以简要带过或者放在相关问题里。 现在检查是否有遗漏的部分。引用1提到C语言实现,引用2讲工作原理,引用3提到哈希表和HashMap的实现方法。所以需要综合这些信息,确保覆盖哈希表结构、哈希函数、冲突处理、扩容机制。 需要确保使用正确的中文术语,比如“链地址法”而不是“链表法”。另外,按照系统级指令,行内数学表达式用$...$,比如哈希函数h(key) = key % capacity,这里的公式需要用符号表示,可能需要用LaTeX格式。 在解释过程中,要自然引用提供的引用内容,比如在提到哈希表作为底层结构时引用[^1],在解释哈希函数时引用[^2],在冲突处理时引用[^3]。 最后,生成相关问题,这些问题应该HashMap原理相关,比如哈希函数设计、冲突处理比较、扩容机制的影响、线程安全等,确保问题有针对性,帮助用户进一步学习。 总结思考步骤: 1. 定义和基本结构:哈希表、键值对存储。 2. 哈希函数的作用和设计。 3. 冲突处理方法,特别是链地址法。 4. 扩容机制和负载因子。 5. 时间复杂度分析。 6. 引用相关文献。 7. 生成相关问题。 现在将这些点组织成连贯的解释,确保符合用户的要求,结构清晰,分点说明,并正确使用格式。</think>### 哈希映射(HashMap数据结构原理分步解释 #### 1. **基本结构** 哈希映射的核心是**哈希表**,它是一个数组结构,每个位置称为一个“桶”(Bucket)。每个桶存储键值对(Key-Value Pair)的集合。例如,一个容量为10的数组,索引为$0$到$9$,每个索引对应一个桶。 #### 2. **哈希函数** 哈希函数将键(Key)转换为数组索引,公式为: $$ \text{index} = h(\text{key}) \% \text{capacity} $$ 其中$h(\text{key})$是键的哈希值,$\text{capacity}$是数组容量。例如,若键为`"apple"`,哈希值为$1234$,容量为$10$,则索引为$1234 \% 10 = 4$。 **设计要求**: - **均匀分布**:减少冲突概率。 - **高效计算**:时间复杂度为$O(1)$。 #### 3. **冲突处理** 不同键可能产生相同的索引(冲突),常用**链地址法**解决: - 每个桶维护一个链表(或红黑树)。 - 冲突时,新键值对追加到链表末尾。 例如,键`"apple"`和`"banana"`同时映射到索引$4$,则链表存储这两个键值对[^3]。 #### 4. **扩容机制** 当元素数量容量的比值(负载因子,默认$0.75$)超过阈值时,触发扩容: 1. 新建一个容量翻倍的数组。 2. 重新计算所有键的哈希值并分配到新桶。 此过程保证桶的负载降低,维持$O(1)$操作效率[^3]。 #### 5. **时间复杂度** - **理想情况**:插入、查找、删除均为$O(1)$(无冲突)。 - **最坏情况**:所有键冲突,退化为链表遍历$O(n)$。 优化手段包括使用红黑树(Java 8+)将链表操作优化至$O(\log n)$。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值