白话文讲HashMap

在这片文章开始之前,我先抛出几个问题,读者可以先回忆或者思考一下,然后再继续往下看,看与读者之前的认识是否有冲突
1、HashMap底层是一种什么样的结构?
2、一个对象最后是如何确定到一个Hash桶的(如何确定数组中的一个位置)?
3、发生Hash冲突了如何解决?
4、为什么HashMap需要扩容?
5、为什么HashMap容量是2的幂次方
6、引入红黑树解决了什么样的问题?
7、什么时候扩容?是否不达到阈值就一定不扩容呢?

由于都是我自己的观点,所以希望读者能给我留言,说说读后感,交流一下,若有错误欢迎拍砖:

个人觉得HashMap这个东东可以挖掘出很多很多基础知识,当然这些东西都是我有意挖掘后整理的。

首先第一个问题:HashMap底层是一种什么样的结构?
答案大家肯定都知道:数组+链表,如下图所示:

这里写图片描述

为什么会是这样一种结构呢?
现在来说第二个问题,一个对象是如何定位到一个数组中的?
HashMap是一种key,value的键值对存储形式,当添加一个对象到HashMap中的时候,要进过以下过程:

1、获得该对象的hashcode

现在咱们假设有3个对象需要加入到HashMap中,这三个对象的hashcode分别是:
A:0100 0101 0110 1111 0101 0111 0110 0111
B:0100 0111 1111 0000 0010 1101 0011 1111
C: 0100 0111 1111 0000 0010 1101 0011 0001

2、经过一次扰乱函数(扰乱函数其实就是把对象的hashcode高16与低16位作与运算)重新获得32位的二进制数,这里博主主要想描述的不在于此,所以就假设经过扰乱函数之后,A、B、C三个对象的hashcode都不变 仍然是:

A:0100 0101 0110 1111 0101 0111 0110 0111
B:0100 0111 1111 0000 0010 1101 0011 1111
C: 0100 0111 1111 0000 0010 1101 0011 0001

3、用HashMap的size-1与第二步得到的hashcode作&(与运算),得到的数字是几,就把这个对象放在数组的第几个位置上,举例说明:

假设HashMap的size为8,则size二进制为:
0000 0000 0000 0000 0000 0000 0000 1000
那size-1之后的二进制数为:
0000 0000 0000 0000 0000 0000 0000 0111
这个时候size-1分别与A,B,C做与运算
例如与A作与运算:
0100 0101 0110 1111 0101 0111 0110 0111
0000 0000 0000 0000 0000 0000 0000 0111 &
————————————————————————
0000 0000 0000 0000 0000 0000 0000 0111

B、C与运算结果分别为:
0000 0000 0000 0000 0000 0000 0000 0111
0000 0000 0000 0000 0000 0000 0000 0001

上面说了,作完与运算之后数字就是放在数组中的位置,位运算之后结果转为10进制分别是 A:7,B:7,C:1
这个时候A和B由于计算结果相等(即发生了hash冲突)所以需要放在数组中的同一个位置,数组的一个位置只能放一个元素嘛,所以有了链表,所以HashMap才有了数组+链表的结构。

现在让我们再看看第4个问题:为什么HashMap需要扩容?
这个我觉得其实很有意思,大家学习容器的过程应该一般都是先学了ArrayList,LinkedList,然后学了HashSet,TreeSet,最后学了HashMap,不知道大家是不是这样,至少我是这样一个过程,导致了我很长一段时间内对HashMap的“扩容”产生了误解,我们知道ArrayList为什么需要扩容?就是因为ArrayList所维护的数组满了,放不下元素了,才扩容。

那HashMap扩容是不是也因为是放不下元素呢?
答案是:HashMap的扩容其实跟放不放得下完全没关系,大家想一下HashMap这种结构是不是永远不会满?如果HashMap中的数组满了,有新元素添加进来,也一定会插入到数组中的某条链表上,对吧?

那HashMap既然永远不会满,那为什么要扩容?扩容解决了什么样的问题呢?
先让我们想想HashMap这种数据结构设计的初衷:理想情况下,可以通过key的hash值在时间复杂度为O(1)拿到对应的Value对吧?那现在假设HashMap中的数组上每一个位置都有一条特别特别长的链表,那当我们通过key去定位到数组的某个位置了之后,是不是还需要遍历这个位置上的链表最终才能拿到那个元素?那时间复杂度就不是O(1)而近似为O(元素个数/数组长度)。

达不到理想状态怎么办呢?我们总得去解决这个问题吧?
于是有了两种解决方案:
1、在适当的时候,将数组扩容,你想想一下如果数组无限长是不是不同对象定位到数组中的同一个位置的概率就非常小了(数组无限长的时候只有在不同对象的Hashcode相等的时候才会出现)?
所以才有了什么负载因子,扩容机制这些东东,这些东东就解决一件事情,降低发生hash冲突的概率,降低发生hash冲突的概率就是跟我开始说的HashMap设计的初衷相合,即:发生hash冲突的次数越小,越接近理想情况,在时间复杂度为O(1)的情况下通过key去get到对应的value。

2、第二种解决方案比较有意思,即当链表长度达到8的时候,会执行树化函数,其实就是把链表转成一颗红黑树(个人觉得转红黑树是一种治标不治本的无奈之举),由于本文讨论的是HashMap,这里作者不需要专门去看红黑树,读者可以简单的理解红黑树是一颗近似平衡的二叉排序树,如果你也不知道什么是二叉排序树也不要紧,你就先记住在红黑树上找一个对象比在链表上找一个对象要快就行了,说白了链表转为红黑树就是为了提高HashMap中查找元素的速率。

文章一开始提到的问题,这里已经解决了一大半,既然说到了扩容,我先再好好说说扩容的时候发生了什么,之间原数组中的元素是放在新数组的同样索引的位置吗?
这个时候会重新定位数组中的每个对象的位置,让我们重新回顾一下A,B之前经过扰乱函数之后的值:

A:0100 0101 0110 1111 0101 0111 0110 0111
B:0100 0111 1111 0000 0010 1101 0011 1111
在数组长度为8的时候,A,B与(数组长度-1)作与运算之后都为7,大家应该都记得吧,A,B之前是在同一条链表上,扩容之后他们将重新定位到新的数组上,大家可以先根据前面知识,想一想A,B在新的数组上会不会发生hash冲突,即仍然定位到新数组上的同一位置呢?

答案是:不会。 why?

数组扩容后,新数组长度为16
二进制位:
0000 0000 0000 0000 0000 0000 0001 0000

新数组长度-1的二进制是:
0000 0000 0000 0000 0000 0000 0000 1111

然后用A、B的最终hash值与上述二进制作与运算,来看结果:

A:
0000 0000 0000 0000 0000 0000 0000 1111
0100 0101 0110 1111 0101 0111 0110 0111 &
————————————————————————
0000 0000 0000 0000 0000 0000 0000 0111

-

B:
0000 0000 0000 0000 0000 0000 0000 1111
0100 0111 1111 0000 0010 1101 0011 1111
————————————————————————
0000 0000 0000 0000 0000 0000 0000 1111

发现没有?A是0111 而B是1111,A是7,B是15,换句话说A定位在新数组的第7位置上,B定位在新数组的第15位置上,分开了,很神奇有木有?

结论来了:在扩容的时候,原数组的链表会分成两条链表,一条链表在新数组的原索引位置,一条链表在新数组的新索引位置,而且这两条链表在概率上是近乎相等的,可能有点绕,读者就看上面A、B的情况,A之前在旧数组的第7个位置,在扩容后,A仍在在新数组的第7位,而B之前在旧数组的第7位,扩容后在新数组的第15位。有点扯,扩容就可以把链表变短也~扩容一次链表几乎变短一半,很神奇有没有~~

还没写完,这是我来百度的第二天,现在时间是晚上10:08分,周围还很热闹,我没事干写了这篇文章,有点点累准备回家睡觉了~~ 之后接着更新

打飞机(Fighter)是一款有趣又益智的游戏。它可以培养游戏者的逻辑推理能力,适合各种年龄段的人玩。 该游戏原本是我读本科时在宿舍里和同学一起玩的。需要两人以上合作,互相布局互相“开炮”。非常好玩。这次,我把游戏改成计算机版本,(可选计算机自动布局和手动布局两种模式),可以单机玩,也可以由别人布局在由你玩。 1、游戏界面 游戏启动后左边为10×10的“天空”,其中暗藏了三架以“士”字形分布的飞机,其中红色块表示飞机头,灰色块表示其他机身部位,黑色块表示此格无飞机,为空。 主窗口右边顶端分布六个按钮,表示各种功能。 开始:开始一局新游戏。缺省状态为由计算机自动布局。 保存:表示保存当前游戏中的飞机布局。你在玩游戏时可能觉得某一次飞机的布局特别巧妙,很难推出飞机头所在。你就可以利用本功能把该飞机布局保存下来,发给你的朋友,考考他的推理能力。 载入:载入飞机布局的文件,载入完毕后直接开始。 布局:手动布局。请按“士”字形在10×10的“天空”中分布好飞机,然后点击“开始”。游戏将自动检测飞机布局是否正确。 选项:选项里面包含许多信息。“设置”栏包括是否打开音效,是否在桌面上创建快捷方式,以及选择老板键打开的文件。“鼠标操作”栏提示游戏的鼠标操作;“关于”栏说明游戏的一些版权信息。 退出:离开本游戏。 游戏界面右下脚为游戏信息区,显示游戏的一些提示信息。再下面是游戏结果显示区。 鼠标操作: 游戏进行中: 左键双击:发炮弹。在推断飞机特别是在机头的所在格后请果断“开炮”。 左键单机:游戏中的辅助功能。用于表示你推断所点击的那格有飞机。再次点击取消推测。 右键单击:游戏中的辅助功能。用于表示你推断所点击的那格没有飞机。再次点击取消推测。 游戏手动布局中: 左键单击:表示该格分布为飞机头。再次点击取消该格布局。 右键单击:表示该格分布为飞机的其他机身部位。再次点击取消该格布局。 键盘操作: Enter、Escape:退出游戏。退出之前需要确认。 F2:重新启动新游戏。 Q、q:老板键,哈哈。老板过来时按此键游戏自动变小为任务栏上的小图标,随后自动打开一个cpp文件(或txt文件,由你在“选项”对话框中指定)“假装”你刚才在编程序。 玩法: 点击“开始”进行新游戏(技巧:可以直接双击“天空”开始)。然后双击“天空”。你需要分析已发炮弹的命中情况来推断三架飞机的机头。只有击中飞机头,才算击落该飞机。你的任务就是用最少的炮弹击落三架飞机。 游戏进行过程中会自动记录已发炮弹数,已击落飞机和炮弹命中率。 三架飞机均被击落后,游戏根据炮弹数进行排行。若进入前十名,则跳出对话框输入你的姓名,进入排行榜。 作者信息: 本游戏为自由软件。源代码公开,需要者可以向作者email联系。若你有其他建议,务必发email至:xiaogi@sohu.com 也欢迎访问作者主页:http://xiaogi.nease.net
### HashMap 与 Map 的区别 #### 定义层面 `Map` 是一个接口,定义了键值对存储的基本行为和规则。它规定了如何插入、删除、更新以及检索数据的方法签名[^2]。而 `HashMap` 是实现了该接口的一个具体类,提供了基于哈希表的实际功能实现[^1]。 #### 特性对比 - **线程安全性**: `Map` 接口本身并不涉及任何关于线程安全的具体说明;然而,其子类如 `HashMap` 并不保证线程安全。如果多个线程同时访问同一个未同步的 `HashMap` 实例,并且至少有一个线程修改了它的结构,那么就必须对外部同步加以控制[^3]。 - **null 值支持**: 根据设计原则,`HashMap` 允许一条记录中的 key 和任意数量的 values 可以为 null。但是这取决于具体的实现细节,其他一些继承自 `Map` 的容器可能不允许这样的情况发生[^1]。 - **性能表现**: 对于读写密集型应用来说,由于采用了高效的散列算法来定位元素位置的缘故,通常认为 `HashMap` 提供了接近 O(1) 时间复杂度的操作效率[^2]。 ### 使用 keySet 遍历 HashMap 当需要单独获取所有的键时可以考虑使用 `keySet()` 方法。此方法返回的是包含所有映射关系中键的一组视图形式——即不可变集合(Set),这意味着即使原始 map 发生变化,只要这些改变不会影响到 hashcode 计算逻辑的话,那么所得到的结果集也会相应反映出来最新的状态信息[^3]。 以下是利用 `keySet` 来遍历整个 hashmap 中所有条目的方式之一: ```java import java.util.*; public class Main{ public static void main(String[] args){ // 初始化hashmap Map<String,Integer> sampleData=new HashMap<>(); sampleData.put("one",1); sampleData.put("two",2); sampleData.put("three",3); // 获取keys集合 Set<String> keys=sampleData.keySet(); // 开始迭代 for (String currentKey : keys ){ Integer currentValue = sampleData.get(currentKey); System.out.println("Key:"+currentKey+", Value:"+currentValue); } } } ``` 在这个例子当中,我们先创建了一个简单的整数关联字符串类型的 hashmap 数据源,接着通过调用 `keySet()` 函数提取出了其中全部可用作索引查询依据的关键字列表(set 结构天然具备去重能力),最后借助增强版for循环语句完成逐项打印展示工作流程[^3].
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值