当前位置: 首页 > news >正文

【常见集合】HashMap

1 二叉树

1.1 两种实现形式

链式存储实现二叉树

数组存储实现二叉树

1.2 常见二叉树

完全二叉树

满二叉树

二叉搜索树

红黑树

1.2.1 二叉搜索树

二叉搜索树(BST树):树种任一节点,其左子树中的每个节点的值都小于其值,其右子树中的每个节点的值都大于其值

时间复杂度分析

通常情况下,增删查均为O(logn)

当其只有左子树或右子树时,退化为链表,时间复杂度变成O(n)

1.2.2 红黑树

红黑树(Red Black Tree)也称自平衡的二叉搜索树

性质:

1. 节点要么黑要么红

2. 根节点为黑色

3. 叶子节点都是黑色的空节点

4. 红色节点的子节点都是黑的

5. 从任一节点到叶子节点的所有路径包含相同数目的黑色节点

在增删改查时,不符合这五个性质会发生旋转,以达到所有性质

时间复杂度分析

查询操作:O(logn)

添加操作:先从根节点找到元素添加的位置,时间复杂度为O(logn),添加完成后,涉及复杂度为O(1)的旋转操作,总体时间复杂度为O(logn)

删除操作:先从根节点找到元素删除的位置,时间复杂度为O(logn),删除完成后,涉及复杂度为O(1)的旋转操作,总体时间复杂度为O(logn)

2 散列表

2.1 定义

散列表(Hash Table),又名哈希表,根据键(key)直接访问在内存的存储位置(Value),由数组演化而来,利用数组支持根据下标访问的特性

2.2 散列函数

定义:将键(key)映射为数组下标的函数叫散列函数

可表示为:hashValue = hash(key)

基本要求:

函数求得的散列值必须是大于等于0的整数,因为hashValue要作为数组下标

若 key1 == key2 ,那么经过hash后得到的哈希值也必须相同,即 hash(key1) == hash(key2)

若 key1 != key2 ,那么经过hash后得到的哈希值也必不相同,即 hash(key1) != hash(key2)

2.3 散列冲突

实际情况下想找一个能对不同key计算得到不同散列值的散列函数几乎不可能,即使像MD5,SSH等哈希算法也无法避免,这就是散列冲突,也称哈希冲突、哈希碰撞

2.4 拉链法

在散列表中,数组的每个下标位置都可以称之为桶(bucket)或槽(slot),每个桶(槽)都会对应一个链表,所有散列值相同的元素放到相同槽位链表上的对应位置

时间复杂度

插入操作:通过函数算出的槽位将其插入到链表中即可O(1)

查删操作:平均情况下为O(1),散列表可能退化成链表,此时复杂度为O(n)

可将链表改为其他更高效的动态数据结构,如红黑树

3 HashMap实现原理

数据结构:底层使用hash表结构,即数组和链表或红黑树

1.当向HashMap中put元素时,利用key的hashCode重新激素那出当前对象的元素在数组中的下标

2.存储时,如果出现哈希值相同的key,有两种情况:

        key值相同,则覆盖原值

        key值不同,则将当前的key-value放入链表或红黑树当中

3.获取时,直接找哈希值对应的下标再进行进一步判断key值是否相同,从而找到对应值

:在HashMap中,当链表长度大于等于8且数组长度大于等于64时会将该链表转换为红黑树

问:HashMap再1.7与1.8版本中的区别

1.8之前的HashMap采用拉链法,只是数组与链表结合的结构,1.8之后加入了红黑树,当链表长度大于等于8且数组长度大于等于64时会将该链表转换为红黑树

4 HashMap中put方法的具体流程

4.1 源码分析

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//判断数组是否未初始化if ((tab = table) == null || (n = tab.length) == 0)//如果未初始化,调用resize方法 进行初始化n = (tab = resize()).length;//通过 & 运算求出该数据(key)的数组下标并判断该下标位置是否有数据if ((p = tab[i = (n - 1) & hash]) == null)//如果没有,直接将数据放在该下标位置tab[i] = newNode(hash, key, value, null);//该数组下标有数据的情况else {Node<K,V> e; K k;//判断该位置数据的key和新来的数据是否一样if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//如果一样,证明为修改操作,该节点的数据赋值给e,后边会用到e = p;//判断是不是红黑树else if (p instanceof TreeNode)//如果是红黑树的话,进行红黑树的操作e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//新数据和当前数组既不相同,也不是红黑树节点,证明是链表else {//遍历链表for (int binCount = 0; ; ++binCount) {//判断next节点,如果为空的话,证明遍历到链表尾部了if ((e = p.next) == null) {//把新值放入链表尾部p.next = newNode(hash, key, value, null);//因为新插入了一条数据,所以判断链表长度是不是大于等于8if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//如果是,进行转换红黑树操作treeifyBin(tab, hash);break;}//判断链表当中有数据相同的值,如果一样,证明为修改操作if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;//把下一个节点赋值为当前节点p = e;}}//判断e是否为空(e值为修改操作存放原数据的变量)if (e != null) { // existing mapping for key//不为空的话证明是修改操作,取出老值V oldValue = e.value;//一定会执行  onlyIfAbsent传进来的是falseif (!onlyIfAbsent || oldValue == null)//将新值赋值当前节点e.value = value;afterNodeAccess(e);//返回老值return oldValue;}}//计数器,计算当前节点的修改次数++modCount;//当前数组中的数据数量如果大于扩容阈值if (++size > threshold)//进行扩容操作resize();//空方法afterNodeInsertion(evict);//添加操作时 返回空值return null;
}

4.2 流程图

4.3 具体流程

1. 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)

2. 根据键值key计算hash值得到数组索引

3. 判断table[i]==null,条件成立,直接新建节点添加

4. 如果table[i]==null ,不成立

        4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value

        4.2 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对

        4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操 作,遍历过程中若发现key已经存在直接覆盖value

5. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。

5 HashMap的扩容机制

5.1 源码分析

//扩容、初始化数组
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;//如果当前数组为null的时候,把oldCap老数组容量设置为0int oldCap = (oldTab == null) ? 0 : oldTab.length;//老的扩容阈值int oldThr = threshold;int newCap, newThr = 0;//判断数组容量是否大于0,大于0说明数组已经初始化if (oldCap > 0) {//判断当前数组长度是否大于最大数组长度if (oldCap >= MAXIMUM_CAPACITY) {//如果是,将扩容阈值直接设置为int类型的最大数值并直接返回threshold = Integer.MAX_VALUE;return oldTab;}//如果在最大长度范围内,则需要扩容  OldCap << 1等价于oldCap*2//运算过后判断是不是最大值并且oldCap需要大于16else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold  等价于oldThr*2}//如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,       			如果是首次初始化,它的临界值则为0else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//数组未初始化的情况,将阈值和扩容因子都设置为默认值else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//初始化容量小于16的时候,扩容阈值是没有赋值的if (newThr == 0) {//创建阈值float ft = (float)newCap * loadFactor;//判断新容量和新阈值是否大于最大容量newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(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 (int j = 0; j < oldCap; ++j) {Node<K,V> e;//判断当前下标为j的数组如果不为空的话赋值个e,进行下一步操作if ((e = oldTab[j]) != null) {//将数组位置置空oldTab[j] = null;//判断是否有下个节点if (e.next == null)//如果没有,就重新计算在新数组中的下标并放进去newTab[e.hash & (newCap - 1)] = e;//有下个节点的情况,并且判断是否已经树化else if (e instanceof TreeNode)//进行红黑树的操作((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//有下个节点的情况,并且没有树化(链表形式)else {//比如老数组容量是16,那下标就为0-15//扩容操作*2,容量就变为32,下标为0-31//低位:0-15,高位16-31//定义了四个变量//        低位头          低位尾Node<K,V> loHead = null, loTail = null;//        高位头		   高位尾Node<K,V> hiHead = null, hiTail = null;//下个节点Node<K,V> next;//循环遍历do {//取出next节点next = e.next;//通过 与操作 计算得出结果为0if ((e.hash & oldCap) == 0) {//如果低位尾为null,证明当前数组位置为空,没有任何数据if (loTail == null)//将e值放入低位头loHead = e;//低位尾不为null,证明已经有数据了else//将数据放入next节点loTail.next = e;//记录低位尾数据loTail = e;}//通过 与操作 计算得出结果不为0else {//如果高位尾为null,证明当前数组位置为空,没有任何数据if (hiTail == null)//将e值放入高位头hiHead = e;//高位尾不为null,证明已经有数据了else//将数据放入next节点hiTail.next = e;//记录高位尾数据hiTail = e;}} //如果e不为空,证明没有到链表尾部,继续执行循环while ((e = next) != null);//低位尾如果记录的有数据,是链表if (loTail != null) {//将下一个元素置空loTail.next = null;//将低位头放入新数组的原下标位置newTab[j] = loHead;}//高位尾如果记录的有数据,是链表if (hiTail != null) {//将下一个元素置空hiTail.next = null;//将高位头放入新数组的(原下标+原数组容量)位置newTab[j + oldCap] = hiHead;}}}}}//返回新的数组对象return newTab;}

5.2 流程图

5.3 扩容机制

1. 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)

2. 每次扩容时候,都是扩容之前容量的2倍

3. 扩容后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中

        3.1 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置

        3.2 如果是红黑树,走红黑树的添加

        3.3 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

6 HashMap的寻址算法

6.1 源码分析

6.2 常见问题

为什么HashMap的数组长度一定是2的次幂?

1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模

2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

http://www.dtcms.com/a/393034.html

相关文章:

  • Docker安装小白教程(阿里yum)
  • MySQL表结构变更详解:ALTER TABLE ADD COLUMN语法、最佳实践与避坑指南
  • 【LeetCode - 每日1题】设计电子表格
  • Spring 中 REQUIRED 事务的回滚机制详解
  • C++框架中基类修改导致兼容性问题的深度分析与总结
  • 学习笔记-SpringBoot项目配置
  • Java数据结构——时间和空间复杂度
  • 如何在接手新项目时快速上手?
  • Zynq开发实践(SDK之自定义IP2)
  • 数据库相关锻炼
  • PostgreSQL 入门与实践
  • pytorch基本运算-PyTorch.Tensor张量数据类型
  • 数据结构与算法 第三章 栈
  • Spring Boot 整合 MyBatis:从入门到企业级实践
  • FHook Java 层全函数 HOOK 框架
  • TDengine 聚合函数 STDDEV_POP 用户手册
  • 【 嵌入式Linux应用开发项目 | Rockit + FFmpeg+ Nginx】基于泰山派的IPC网络摄像头
  • 机器学习中的高准确、低召回
  • Go基础:Go基本数据类型详解
  • 项目管理(一)
  • 【STM8L101 执行函数FLASH_ProgramBlock出现问题】
  • ​​[硬件电路-278]:双向双电源电平转换收发器74AXP2T45DCH功能概述、管脚定义
  • 音视频同步的原理和实现方式
  • BUG调试案例十八:TPS5430输出震荡问题案例
  • Python读取Excel文件里面指定列中的指定范围行
  • C语言入门教程 | 阶段二:控制结构详解(条件语句与 switch 语句)
  • Linux 4.x hook系统调用的问题
  • 了解 Highcharts 响应式功能:构建适配各种屏幕的图表界面
  • 逻辑分析仪解码脚本实例解析——UART
  • 垃圾回收中的STW是什么?