HashMap稍微详细的理解

本文深入探讨了哈希表的概念,包括其工作原理和优势,并详细介绍了Java中的HashMap类。HashMap是基于哈希表实现的,通过哈希函数将键映射到表中的位置,实现快速查找。文章还讨论了哈希冲突的解决方法,如开放地址法、再哈希法和拉链法,并特别提到了HashMap中使用的拉链法。此外,还介绍了HashMap的扩容机制和1.8版本引入的红黑树优化。

此文章用来记录hashmap的一些特点(在学习中的所了解的,如有不足,请指正)什么是hash表概念先来一段百度百科的的解释散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。所谓的hash表在我看来嘛就是映射嘛,以前嘛要查找一个数或者一个值嘛是通过遍历的形式,这样的话就会有一个问题,那就是太浪费时间了,时间效率非常低,也不能非常低嘛,时间复杂度是O(n)。于是呢,人们为了更快的找到需要查找的值呢就想到了一种办法,将存储的位置与存储的值对应起来,这样查找的效率不就高了很多。但是怎么转换呢,聪明的人类想到了一种办法,利用一种函数映射的形式来解决,这个映射用的函数就叫做hash函数,这个表呢就叫hash散列表,但是呢这是有问题的。那就是hash表冲突很好理解嘛,不同的值可能经过hash函数生成同样的索引,这样的话就有冲突了,怎么解决?请看hash表冲突的解决我所了解的常用的直接寻址,也叫开放地址法,就是这个不能放我不放了,我放到下一个去,要是下一个还有就继续往后直到找到可以插入的位置,要是都没有,那就考虑一下扩容呗hash再散列,就是用别的hash算法再算一遍拉链法,这个方法就是hashmap中用到的方法。不是有冲突嘛,统统拿来,统统放这,一个别想跑。其实就是利用链表,冲突了就追加节点(不是同一个的话才追加)建立公共溢出区,就是冲突了嘛,没坑了,那就走吧,不要呆在这里了以上就是我所了解的,估计也是常用的吧,不然我也不会了解HashMapmap的意思嘛,就是映射,才不是地图。Java中的HashMap就是利用hash表加链表实现的K,V形式的数据结构,和python中的字典是一样的。hashmap中的hash冲突的解决利用的是拉链法。1.7之前的拉链是只有链表,而在1.8增加了一个红黑树结构,这是因为,当链表长度太长的时候查找效率比较低。所以在hash桶数据的容量大于等于64以及hash桶内的元素数量大于等于8时就会转换为红黑树。今天我们进入源码一探究竟,先来看个静态常量static final int MIN_TREEIFY_CAPACITY = 64;
树化:treeifyBinfinal void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果说table 为null或者说容量小于64就扩容,不执行树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 如果说 所在的位置表不为空
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
上面的就是进行树化的条件了,具体流程就算了吧,不看了扩容:resize()方法先准备一个重要的方法,resize()方法final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//判断老表是否为null为null的话长度就是0
int oldThr = threshold; // 保存原来的老阈值
int newCap, newThr = 0; //先将新表的长度 阈值设置为0
if (oldCap > 0) {
//如果说老表的容量大于0且容量大于等于最大容量(MAXIMUM_CAPACITY = 1 << 30)
//就将阈值设为Integer.MAX_VALUE,然后直接返回也就是不再扩容了,仅仅将阈值增大就行了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果说老容量乘以2小鱼最大容量以及大于等于默认的容量( DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16)
// 就将原来的阈值也扩充为两倍 就是说这里没啥意外容量就定下来了,也是一般的扩容情况
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果说老表的容量小于等于0,但是老阈值大于0,就将新的容量设置为老阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//如果老表的容量以及老阈值都不大于0,就执行初始操作,将新表的容量设置为16,计算新表的阈值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果说得出的新表阈值等于0的话就用新表的容量乘以负载因子,然后如果说新表的容量小于最大值以及新的阈值小于最大值,就将新阈值设为所求,否则就是Integer.MAX_VALUE
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
MAX_VALUE (int)ft : Integer.MAX_VALUE);
}
//到此用新阈值覆盖老阈值阈值的更新操作完成
threshold = newThr;
@SuppressWarnings({“rawtypes”,“unchecked”})
//利用新的容量创建一个表(这有个问题,就是如果是两个线程的话会创建两个表)
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//新的容量覆盖老的,容量至此也已确定
table = newTab;
//若原来的表不等于空,就进行移动,等于空的话就直接返回,因为原来的没有东西嘛,也就不用转移值了
if (oldTab != null) {
//这里对老表进行遍历,采用for循环
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//先将老表的值记录下来,然后进行判空,如果不等于空的话就继续下一步
//等于空的话也不进行任何操作
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//在这里看一下是不是还有下一个节点,没有的话就计算一下新的索引所在的位置然后结束
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果说e是treeNode节点,也就是说,这个hash桶里边的节点已经树化过了
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//下面这个else的意思是如果有下一个节点而且没有树化,也是说是链表形式的至少有两个节点
else { // preserve order
//这里就是将链表分成两种一种是高位链表一种是低位链表,至于什么是高低位链表,咱们往下看
//loHead低位头节点
//loTail低位尾节点
Node<K,V> loHead = null, loTail = null;
//hiHead高位头节点
//hiTail低位尾节点
Node<K,V> hiHead = null, hiTail = null;
//next节点
Node<K,V> next;
//开始do…While循环,因为肯定有next节点嘛,不然也到不了这里
do {
//保存一下next节点
next = e.next;
//注意这个与的 是与原来的容量进行比较的没有进行减一哈,减一是求索引用的。
//这里的意思举个例子来说就是比如原来的容量就是16吧,因为这里是位运算嘛,转换成二进制就是10000
//因为这里是等于0 的情况嘛,所以就假设e.hash二进制为1011001111吧,索引算出来就是1111
//运算开始 因不足用0补齐嘛
//1011001111
// 10000
//----------
//0000000000
//嗯 就是这种情况,因为原来容量二进制是5位也就是说如果hash值第五位是0,那么就扩容以后不会有任何变化
//因为扩容是变为原来的2倍,也就是左移一位变为100000。
//那么减1以后就是11111,刨去后边的4个1,两个最高位都是1也就是相同的,可以直接运算
//如果说此时元素的hash值在这个最高位是0的话,那么算出的索引与原来是一样的,这也就是低位索引
//这里只是将低位放在一起
if ((e.hash & oldCap) == 0) {
//如果尾节点为空就初始化(说明头节点也没值)
if (loTail == null)
//这里的头节点指示头所在的位置,以后追加就是用为节点了,高位链表一样如此
loHead = e;
else
//让尾节点的next指向e,
loTail.next = e;
//然后尾节点向后移一位
//这里写成loTail=loTail.next我感觉比较好理解一些
loTail = e;
}
//如果说不是0的话,说明hash值的高位是1,经过运算后就是11111就是原来的索引加上2^4
//就是原来的表的长度,所以高位链表只需要原来的索引加上原来的表的长度就是新的索引
//这里只是将高位放到一起
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//循环结束后,如果说低位链表不为空的话就说明执行了分高低位的工作,而且有低位的存在
//然后只需要将hash桶的节点指向低位链表的头节点,而且因为是低位链表嘛,索引跟原来的一样
if (loTail != null) {
//这里将为节点的next设置为null,因为这再遍历的时候尾节点的next与尾节点指向同一个位置
//因为已经遍历完了嘛,next也就没有值了,所以就清空。高位链表类似
loTail.next = null;
newTab[j] = loHead;
}
//这里判断高位链表是否为空,空的就说明没有高位链表嘛
//不空的话就将原来的索引加上老表的容量,至于为什么,上面已经解释过
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//至此返回新的链表
return newTab;
}
你以为到这里就结束了?no,no,no,还有一个重要的方法,那就是如果是红黑树的话,怎么进行操作呢,就是这个红黑树节点里边的split(this, newTab, j, oldCap)方法了final void split(HashMap<K,V> map, Node<K,V>[] tab, int index,
int bit//这个bit什么意思呢,我猜是老表的容量,上面传过来的oldCap
) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//开头雷击,梦回链表
//这怎么和链表的操作差不多呢,也是分成高低位
//其实注释上面也写了嘛
//将树箱中的节点拆分为较高和较低的树箱,如果现在太小,则取消树化。
//为什么红黑树的节点也可以这样呢,因为
/**
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
// needed to unlink next upon deletion
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
*/
//所以有个成员变量是next,那不就跟链表一样了
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
//这个lc吧 low count:低位的数量
//hc呢 high count:高位的数量
int lc = 0, hc = 0;
//显而易见,这里遍历treeNode
//这里为什么不用while循环呢,是不用在外面声明变量嘛
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
//这里高低位一分,对应的count++就不展开细说了
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}

if (loHead != null) {
    if (lc <= UNTREEIFY_THRESHOLD)
        //如果说数量小于等于UNTREEIFY_THRESHOLD=6,就弄成链表
        tab[index] = loHead.untreeify(map);
    else {
        tab[index] = loHead;
        //如果不小于6且高位链表不为null就树化
        //为啥需要高位链表不为空呢,
        /**
        这里个人理解,高位链表如果为空,说明旧数组下的红黑树中的元素在新数组中仍然全部在同一个位置,
        且先后顺序没有改变,也就是注释中的已经树化了,没有必要再次树化;而当高位节点不为空,
        说明原链表元素被拆分了,且位红黑树节点个数大于6,不满足转链表条件,需要重新树化。
        此处来自https://blog.csdn.net/hengwu1817/article/details/107095871/ 
        */
        //下面的高位链表也是如此
        if (hiHead != null) // (else is already treeified)
            loHead.treeify(tab);
    }
}
if (hiHead != null) {
    if (hc <= UNTREEIFY_THRESHOLD)
        tab[index + bit] = hiHead.untreeify(map);
    else {
        tab[index + bit] = hiHead;
        if (loHead != null)
            hiHead.treeify(tab);
    }
}

}
插入数据 put调用put(k,v)方法实际上调用的是putVal方法public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
所以只需要分析putVal方法即可final V putVal(int hash, K key, V value, boolean onlyIfAbsent,//是否不存在才插入
boolean evict//文档给的是创建表的模式,我的理解是可读可写
) {
Node<K,V>[] tab;
Node<K,V> p; //p是table[i]所在的头
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//如果说原来的表不存在或者为空就执行resize()方法,上面已经进入看了一下
//如果说原来的表的位置等于空的话就直接放进去 不存在冲突
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//这里才是重点 到这里说明发生冲突
Node<K,V> e;//e表示要要插入的节点
K k;
//这个判断是看原来的老的hash值跟我传进来的hash值是否相同并且key也相同 或者说key不为空并且相同
//也就是判断一下是不是相同的key 是的话就将p赋值给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//不是就判断一下头节点是否是红黑树节点
//是的话就进行红黑树的操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//不是的话就执行 也就是寻找要插入的位置的前一个节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果说大于等于8-1也就是7的话就树化 因为要插入元素了嘛,所以插入以后就等于8了
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//这里key相同的话就中断
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//找到了要插入的节点后 如果e不为null 说明key是一样的 只需要替换一下值就好了
if (e != null) { // existing mapping for key
//保存一下旧的值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//如果不是存在就不写的情况就换成新的值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//记录一下修改次数,这个有个作用就是快速失败
++modCount;
//然后判断是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
对于1.7的hashmap的死循环问题以及本篇文章的1.8的死循环数据覆盖问题,以后再总结

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:

TYPEASCIIHTML
Single backticks'Isn't this fun?'‘Isn’t this fun?’
Quotes"Isn't this fun?"“Isn’t this fun?”
Dashes-- is en-dash, --- is em-dash– is en-dash, — is em-dash

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t   . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=0tz1etdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

Mon 06 Mon 13 Mon 20 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML 图表

可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:

张三 李四 王五 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 李四想了很长时间, 文字太长了 不适合放在一行. 打量着王五... 很好... 王五, 你怎么样? 张三 李四 王五

这将产生一个流程图。:

链接
长方形
圆角长方形
菱形
  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart的流程图:

Created with Raphaël 2.2.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值