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

数组替代map实现性能优化

概念

数组map,即在某些情况下使用数组模拟一个map,利用key独一无二的特征可以把数组的下标作为key,value即数组中所存储的内容,这样可以模拟出一个Map类型的map,使用数组模拟map相当于是从数据结构层面提升了代码性能,相关案例如下

  • 去除重复字母
  • 下一个更大元素
  • 同构字符串

数组和map开销对比

hash算法

HashMap的底层数组长度为何总是2的n次方,通过(h = key.hashCode()) ^ (h >>> 16)得出最终结果,原因是由于hashCode底层算法,生成hashCode的时候会在某些位上呈规律性分布,不做处理可能会导致hash聚集,而通过右移运算符移位16位则是保证这个key的hashCode的二进制数的第16位与高16位共同参与运算,确保得到key的hash值是绝对随机的,最后通过i = (n - 1) & hash来确保这个节点随机插入到数组的任意位置

插入方面

hashMap入参中没有关于容量size的参数的话默认就使用懒加载,首次插入的时候会给它默认分配一个容量为16的数组,通过高性能hash算法计算出插入位置,然后为插入的键值对生成链表节点,如果数组当前位置为空就直接插入,如果当前数组不为空,首先对当前数组上的元素做判重,判重算法是插入元素的hash值和原先map中已有的元素key的hash值重复,且这两个key的地址一样或通过equals比较返回true,如果被判为重复,则会用新值替换旧值并把旧值返回

如果插入数组位置已经形成红黑树,那么就把元素插入到红黑树里

如果插入数组位置不为空且已经形成链表,那么采用尾插法,通过遍历这条链表获取到链表尾,然后通过链表尾的next指针完成插入,如果发下链表元素大于8就对当前链表进行树化

如果插入的是一个全新的节点,那么map的size++,如果超过阈值会触发扩容,阈值 = 当前容量 * 扩容因子

查找

当你调用 hashMap.get(key) 时,首先会计算该键的哈希值,会使用这个哈希值来确定在内部数组中的索引位置

  • 如果位置上只有一个元素,那么查找效率为O(1)
  • 如果位置上是一个链表,那么查询效率为O(N),其中N为链表长度
  • 如果位置上是一个红黑树,那么查询效率为O(logN),其中N为树中的节点数目

扩容

扩容时机

当HashMap中的元素数量超过了当前数组容量与负载因子的乘积(即容量 * 负载因子)时,会触发扩容操作。默认情况下,负载因子为0.75

扩容因子

负载因子越大,空间开销越大,但查询速度会越快,因为这样大大减少了出现链表和红黑树的概率,查询起来更接近于数组查询,负载因子越小,空间开销越小,但查询速度也会相应变慢

扩容过程

HashMap的底层数组长度为何总是2的n次方,因为hash算法依赖数组的长度这个数值,所以每次扩容都意味着数组的长度会翻倍,所以这一过程是最容易出现OOM的

数据测试

内存开销

测试代码,我的jvm堆空间为,数组存储的上限是(1 << 30),精确数据大约为1.9 * (1 << 29),已经非常接近 (1 << 30) 了

 public static void main(String[] args) {int testSize = (1 << 30);int[] arr = new int[testSize];
}

map存储的上限比较复杂,无论如何调整参数,大约都是只能存储不到(1 << 26)数量级的元素,默认负载因子的时候,数组的容量会变为2被的testSize,所以最后是在数组扩容阶段OOM,次数真实的存储容量大约是1.5 * (1 << 25);

 public static void main(String[] args) {int testSize = (1 << 30);Map map = new HashMap(testSize);for (int i = 0; i < testSize; i++) {map.put(i, i);}}

如果把负载因子调整为一个不小于1的值,也就是说整个插入过程中数组元素始终固定死是testSize了,那么次数OOM问题会抛出在创建Node对象的过程

 public static void main(String[] args) {int testSize = (1 << 30);Map map = new HashMap(testSize, 1);for (int i = 0; i < testSize; i++) {map.put(i, i);}}

综合来说数组所能存储元素的上限大约是map所能存储元素上限的16倍,推测主要是Node对象的空间占用过大,大约是int数值的8倍

插入速度比较

 public static void main(String[] args) {Long start1 = System.currentTimeMillis();int testSize = (1 << 25);int[] arr = new int[testSize];for (int i = 0; i < testSize; i++) {arr[i] = i;}long endTime = System.currentTimeMillis();System.out.println("arr spend time:" + (endTime - start1));Map map = new HashMap();Long start2 = System.currentTimeMillis();for (int i = 0; i < testSize; i++) {map.put(i, i);}System.out.println("map spend time:" + (System.currentTimeMillis() - endTime));

使用1 << 25个元素测试,数组平均耗时20ms,map平均耗时1400毫秒,
如果给map指定初始大小和扩容因子,保证整个插入流程没有额外扩容流程的话,那么map的平均耗时为1100毫秒

Map map = new HashMap(testSize, 1);

这种情况其实已经接近当前最优解了,有人会说,继续增大map容器的初始容量,那这样数组上链表和黑红树出现的概率会更小,所以速度会更快,理论是这样,但实际上你再去增大初始容量对速度的提升也很有限了,因为hash算法可以确保hash值的随机性从而保证元素随机分布到数组的任意位置,当map中数组容量和待插入元素数量一致时,数组上形成链表和红黑树的数量会比较小,可能只有1% - 5%的位置有树和链表,对整体插入速度影响不大

同步扩大扩容因子和初始容量,这种样插入过程中依然不会出现扩容的情况,但平均耗时却变成了1700ms,主要这种情况下数组上会出现大量链表和红黑树导致插入速度慢了很多

Map map = new HashMap(testSize / 10, 10);

查找速度比较

public static void main(String[] args) {int testSize = (1 << 25);int[] arr = new int[testSize];for (int i = 0; i < testSize; i++) {arr[i] = i;}Long start1 = System.currentTimeMillis();int tmp = 0;for (int i = 0; i < testSize; i++) {tmp = arr[i];}long endTime = System.currentTimeMillis();System.out.println("arr spend time:" + (endTime - start1));
}        
 public static void main(String[] args) {Map<Integer, Integer> map = new HashMap(16, 0.5f);int testSize = (1 << 25);for (int i = 0; i < testSize; i++) {map.put(i, i);}Long start = System.currentTimeMillis();int tmp = 0;for (int i = 0; i < testSize; i++) {tmp = map.get(i);}System.out.println("map spend time:" + (System.currentTimeMillis() - start));}

数组平均耗时2ms, map平均耗时500毫秒

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

相关文章:

  • Multimodal Transformer Training in Personalized Federated Learning
  • 配送算法17 AFramework for Multi-stage Bonus Allocation in meal delivery Platform
  • 替换数字(字符串算法)
  • 宋红康 JVM 笔记 Day08|堆
  • SMTPman,smtp协议是什么协议的核心功能!
  • 大数据毕业设计选题推荐-基于大数据的存量房网上签约月统计信息可视化分析系统-Hadoop-Spark-数据可视化-BigData
  • MySQL 8.0 事务深度解析:从核心特性到实战应用
  • 国产化Excel开发组件Spire.XLS教程:Python 将 CSV 转换为 Excel(.XLSX)
  • 【重磅发布】flutter_chen_updater-版本升级更新
  • 【开题答辩全过程】以 汽车售后管理系统的设计与实现为例,包含答辩的问题和答案
  • 首次创建Django项目初始化
  • Spring Boot 启动优化实战指南:从原理到落地的全链路性能调优
  • 我的6年!
  • Vue 组件循环 简单应用及使用要点
  • 算法加训 动态规划熟悉30题 ---下
  • 【ARM】MDK出现:Unable to find ARM libraries
  • ros2与gazebo harmonic机械臂仿真项目Moveit2YoloObb的优化
  • Linux 禁止 su 的几种限制手段:从 NoNewPrivileges 到 PAM 配置
  • Linux shell getopts 解析命令行参数
  • CRMEB小程序订阅消息配置完整教程(PHP版)附常见错误解决
  • 【论文阅读】PEPNet
  • 6.10 vue3 的nextclick
  • More Effective C++ 条款14:审慎使用异常规格(Exception Specifications)
  • 19、大数据处理系统分析与设计
  • [特殊字符] 监控体系里常见的角色
  • Python绝对引用与相对引用的核心差异
  • 架构评审:构建稳定、高效、可扩展的技术架构(下)
  • 深度学习篇---VGGNet网络结构
  • 阿里云轻量服务器的系统镜像和应用镜像的区别在哪?
  • 从零开始的python学习——浅谈python