Java集合知识图谱:问题:
https://www.processon.com/view/link/61bf27a17d9c087834f1d352
链表中的key放的到底是什么,Hash值吗,hash会有冲突吗,hash生成的原理是什么一、二三树 2-3树是一种绝对平衡多叉树,在这棵树中他的任意一个节点的左右节点高度是相同的。2-节点表示节点中保存一个元素,3-节点则表示节点中保存两个元素。
map中的阈值是如何确定下来的,意义是什么
文章图片
二、红黑树 1、红黑树的五大概念
- 每个节点要么是红色要么是黑色
- 根节点一定是黑色
- 每个叶子节点一定是黑色(这里面说的叶子节点是指空的叶子节点)
- 如果一个节点是红色那么他的左右节点一定都是黑色
- 从任意一个节点到叶子节点所经过的黑色节点数量一样多
从概念上而言,您可以将 List 看作是具有数值键的 Map。而实际上,除了 List 和 Map 都在定义 java.util 中外,两者并没有直接的联系。本文将着重介绍核心 Java 发行套件中附带的 Map,同时还将介绍如何采用或实现更适用于您应用程序特定数据的专用 Map。
list的实现方式中一个是数组一个是链表,如果要是通过数组形式实现的还可以理解索引就是key但是如果是通过链表实现的,那么键值对的实现就只能说是一种你看到的,他实际上是没有索引的只不过是能够通过一定的算法找到固定位置上的相应元素,这里面采用的是优化过后的二分查找算法。1、Map中相关的方法
- 被覆盖的方法
方法 | 备注 |
---|---|
equals(Object o) | 比较指定对象与此 Map 的等价性 |
hashCode() | 返回此 Map 的哈希码 |
- 可以更改Map的方法
方法 | 备注 |
---|---|
clear() | 从 Map 中删除所有映射 |
remove(Object key) | 从 Map 中删除键和关联的值 |
put(Object key, Object value) | 将指定值与指定键相关联 |
putAll(Map t) | 将指定 Map 中的所有映射复制到此 map |
- 查看Map
【大工篇|Java集合——HashMap源码】前两个视图返回的是Set对象,第三个返回的是Collection,如果需要进行迭代还需要获得他们的迭代器:
- 所有键值对 — 参见 entrySet()
- 所有键 — 参见 keySet()
- 有值 — 参见 values()
Iterator keyValuePairs = aMap.entrySet().iterator();
Iterator keys = aMap.keySet().iterator();
Iterator values = aMap.values().iterator();
- 遍历Map的几种方式
@Test
public void test01(){
//map创建过程中两种性能的比较
HashMap hashMap = new HashMap<>();
hashMap.put("a1","b1");
hashMap.put("a2","b2");
hashMap.put("a3","b3");
hashMap.put("a4","b4");
//这几通过迭代器的方式进行访问
System.out.println("~~~Iterator类型访问~~~");
Iterator iterator = hashMap.entrySet().iterator();
int mapSize = hashMap.size();
for (int i=0;
i entry:hashMap.entrySet()){
System.out.println("Key: "+entry.getKey()+"Value: "+entry.getValue());
}
hashMap.values().forEach(item->{
System.out.println(item);
});
System.out.println("~~~Array类型的访问~~~");
Object[] array = hashMap.entrySet().toArray();
for (int i = 0;
i < array.length;
i++) {
Map.Entry entry = (Map.Entry) array[i];
System.out.println("Key: "+entry.getKey()+"Value: "+entry.getValue());
}
}
- Map的访问与测试
get(Object key) | 返回与指定键关联的值 |
---|---|
containsKey(Object key) | 如果 Map 包含指定键的映射,则返回 true |
containsValue(Object value) | 如果此 Map 将一个或多个键映射到指定值,则返回 true |
isEmpty() | 如果 Map 不包含键-值映射,则返回 true |
size() | 返回 Map 中的键-值映射的数目 |
2、核心Map
- 通用 Map,用于在应用程序中管理映射,通常在 java.util 程序包中实现
- HashMap
- Hashtable
- Properties
- LinkedHashMap
- IdentityHashMap
- TreeMap
- WeakHashMap
- ConcurrentHashMap
- 专用 Map,您通常不必亲自创建此类 Map,而是通过某些其他类对其进行访问
- java.util.jar.Attributes
- javax.print.attribute.standard.PrinterStateReasons
- java.security.Provider
- java.awt.RenderingHints
- javax.swing.UIDefaults
- 一个用于帮助实现您自己的 Map 类的抽象类
- AbstractMap
哈希映射结构由一个存储元素的内部数组组成。由于内部采用数组存储,因此必然存在一个用于确定任意键访问数组的索引机制。
int hashvalue = https://www.it610.com/article/Maths.abs(key.hashCode()) % table.length;
int hashvalue = (key.hashCode() & 0x7FFFFFFF) % table.length;
put() 方法还包含相应 get() 的算法,这是因为插入包括搜索映射索引处的项以查明该键是否已经存在。(即 get() 方法与 put() 方法具有相同的算法,但 get() 不包含插入和覆盖代码。) 使用链接列表并不是解决冲突的唯一方法,某些哈希映射使用另一种“开放式寻址”方案
将您的所有 Map 变量声明为 Map,而不是任何具体实现,即不要声明为 HashMap 或 Hashtable,或任何其他 Map 类实现。
Map criticalMap = new HashMap();
//好
HashMap criticalMap = new HashMap();
//差
直到需要时再选择 Map 实现 — 如果随处使用“Map”声明的变量,则更改应用程序中任何特殊 Map 的 Map 实现只需要更改一行,这是一种开销很少的调整选择。
4、使用负载因子
为确定何时调整大小,而不是对每个存储桶中的链接列表的深度进行记数,基于哈希的 Map 使用一个额外参数并粗略计算存储桶的密度。Map 在调整大小之前,使用名为“负载因子”的参数指示 Map 将承担的“负载”量,即它的负载程度。负载因子、项数(Map 大小)与容量之间的关系简单明了:
- 如果(负载因子)x(容量)>(Map 大小),则调整 Map 大小
- 对于负载因子为 0.75 的 100 个项,应将容量设置为 100/0.75 = 133.33,并将结果向上取整为 134(或取整为 135 以使用奇数)
负载因子本身是空间和时间之间的调整折衷。较小的负载因子将占用更多的空间,但将降低冲突的可能性,从而将加快访问和更新的速度。使用大于 0.75 的负载因子可能是不明智的,而使用大于 1.0 的负载因子肯定是不明知的,这是因为这必定会引发一次冲突。使用小于 0.50 的负载因子好处并不大,但只要您有效地调整 Map 的大小,应不会对小负载因子造成性能开销,而只会造成内存开销。但较小的负载因子将意味着如果您未预先调整 Map 的大小,则导致更频繁的调整大小,从而降低性能,因此在调整负载因子时一定要注意这个问题。
四、HashMap基础数据结构
HashMap中说的链表是单链表但是红黑树中也是有链表的,这个链表是双向链表。那么红黑树的数据结构是怎么构成的?以及为什么为在里面存在双向链表他存在的意义是什么?当需要对红黑树进行遍历的时候树结构的遍历速度太慢而链表形式的就要快很多
1、HashMap的构造函数
- public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
}
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
}
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
- public HashMap(int initialCapacity)
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- public HashMap()
- public HashMap(Map extends K, ? extends V> m)
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
2、创建Table数组
判定条件就是table为空或是长度为0,这里面有一个防空指针的操作是值得学习的,在有的情况下是通过&&进行连接的(逻辑与、逻辑或)
if ((tab = table) == null || (n = tab.length) == 0) {
// eg1: resize返回(Node[]) new Node[16],所以:tab=(Node[]) new Node[16], n=16
n = (tab = resize()).length;
}
进行条件判断之后开始进行resize(),在这个操作中主要分为两种场景:
不管是那种场景都是需要根据已有的信息通过对容量、阈值来进行重置,重置之后再进行统一的新数组创建。在对容量以及阈值进行重置的时候会进行越界相关的判断,同时也会对扩容后的安全进行一个判定:
- 原来没有数组:
- 有数组但是容量不够了:需要考虑进行数据重构因为hash后的位置会不同
/** 如果将老Table的长度增长2倍作为新的容量长度(newCap),是否小于2^30(1 << 30) 并且 老Table长度是否大于等于16(1 << 4)
* 这一步也是确保数组扩容的安全性*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
// eg6: newCap=32, newThr=24
newThr = oldThr << 1;
}
关于数值重置这一块的代码写的是很优雅的,思路变现为通过在判断体中进行赋值精简代码的结构、对数值进行确定之后再统一的使用这些数值来进行新数组的创建。
文章图片
3、插值时候的两种简单操作
如果没有发生Hash冲突我们只需要将创建的节点赋值给tab,这是属于第一种情况,如果发生了冲突但是在寻找插入位置的时候发现这个key已经存在,这时候会根据put中的参数值进行判断是否进行覆盖。同时这个覆盖也不是立即的而是确定下来之后统一进行一个插入
如果发生了Hash冲突但是查找下来却也没有已经存在的相同的Hash值这时候有会分为我们要插入的是树节点还是普通节点,这个判断判断的是tab节点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Efx4uspI-1639917521939)(F:\typroa\aimages\image-20211208205004374.png)]
4、单向链表中插入元素
5、putTreeVal——寻址根节点
- TreeNode的数据结构
文章图片
在确定根节点来源于哪里的时候可以看出来也是使用了三目运算符:
TreeNode root = (parent != null) ? root() : this;
这里面this的用法时值得借鉴的,以前没有见过:
final TreeNode root() {
for (TreeNode r = this, p;
;
) {
if ((p = r.parent) == null) {
return r;
}
r = p;
}
}
root()中的执行逻辑很简单就是通过parent来不断的向上寻址,直到找到他的父元素为空的节点也,但是既然p已经是tab的元素了那么为什么他不是root呢,应该是的呀?
文章图片
6、确定元素的掺入位置
确定元素的掺入位置是在一个大的for循环中的,在for循环中有我们可以分为两步一个是确定插入元素的位置一个是构造新的元素并将元素掺入到需要掺入的位置。
在寻找位置的时候与普通的二叉树数没有太大的区别,不过他是根据hash值的大小来进行判定插入的位置的:
文章图片
7、构造TreeNode并插入到相应位置
文章图片
8、平衡红黑树
推荐阅读
- Spring|从零搭建SpringBoot脚手架与SpringCloud生态
- 数据库|SpringBoot脚手架工程快速搭建
- Java|【Java笔记】一网打尽Java中的集合知识
- 毕业设计|SpringMVC+Vue项目疫情社区管理系统
- 毕业设计|SpringMVC+Vue项目网上办公自动化系统
- 课程设计|SpringMVC+Vue项目智慧社区管理系统
- 课程设计|SpringMVC+vue实现前后端分离的药品管理系统
- Spring全家桶|【SpringMVC】学习总结(一篇就够了)
- 算法|【玩转数据结构 从入门到进阶12学习笔记】红黑树