【策略与优化 - 001】- 在特定场景下,如何对双层循环进行降级,加速数据匹配?

一、场景介绍

假设某次搜索结果中有 100_0000 篇文章,而你的个人收藏中有 10000 篇,如何在短时间内快速识别 100_0000 中哪些是 “已收藏”, 哪些是 “未收藏” ?

二、正常逻辑(双层for 循环)

public class ForEachTest {
    public static void main(String[] args) {
        // user book list
        List<String> ubList = new ArrayList<>();

        // book list
        List<String> bList = new ArrayList<>();

        // 收藏的数据条数
        int collectionNum = 10000;
        // 总条数
        int total = 100_0000;

        // 预计要存储的结果,也可以定义在要返回到页面实体的状态中
        Map<String, Integer> resMap = new HashMap<>(total);

        // 初始化 个人中心收藏的数据
        for (int i = 0; i < collectionNum; i++) {
            ubList.add(String.valueOf(i));
        }

        // 初始化 搜索结果中返回的数据,同时维护一个 个人与文章 相关的状态map
        // 状态初始结果为 0:“未收藏”, 1:“已收藏”
        for (int i = 0; i < total; i++) {
            bList.add(String.valueOf(i));

            //如果这个 bList 中存储的是 实体对象,则可以在存入数据的时候,就初始化一个未收藏的状态
            resMap.put(String.valueOf(i), 0);
        }

        // 记录开始时间
        long start = System.currentTimeMillis();

        /* for 双层循环*/
        for (String b : bList) {
            for (String ub : ubList) {
                if (b.equals(ub)) {
                    resMap.put(b, 1);
                }
            }
        }

        // 结束时间
        long end = System.currentTimeMillis();
        // 用时时长 ms
        System.out.println("耗时 ms: " + (end - start));
    }
}

注: 在这几次的测试中,都没有涉及内存的消耗与数据准备的时间,具体查看时间计算的区间!

测试数据:

  collectionNum: 10000

  total : 100 0000

  消耗时间:

结论:双层 for 循环,遍历了 100亿次,用时 62秒 左右

三、利用 HashMap 底层,减少无效的遍历

public class ForEachTest {
    public static void main(String[] args) {
        // user book list
        List<String> ubList = new ArrayList<>();

        // book list
        List<String> bList = new ArrayList<>();

        // 收藏的数据条数
        int collectionNum = 10000;
        // 总条数
        int total = 100_0000;

        // 预计要存储的结果,也可以定义在要返回到页面实体的状态中
        Map<String, Integer> resMap = new HashMap<>(total);

        // 初始化 个人中心收藏的数据
        for (int i = 0; i < collectionNum; i++) {
            ubList.add(String.valueOf(i));
        }

        // 初始化 搜索结果中返回的数据,同时维护一个 个人与文章 相关的状态map
        // 状态初始结果为 0:“未收藏”, 1:“已收藏”
        for (int i = 0; i < total; i++) {
            bList.add(String.valueOf(i));

            //如果这个 bList 中存储的是 实体对象,则可以在存入数据的时候,就初始化一个未收藏的状态
            resMap.put(String.valueOf(i), 0);
        }

        // 记录开始时间
        long start = System.currentTimeMillis();

        // 将 个人中心收藏的数据,转化存储到 map 中,
        // 注意 收藏的文章的id 作为key,value 随意,这里使用同样使用了 id
        Map<String, String> ubMap = new HashMap<>();
        for (String ubId : ubList) {
            ubMap.put(ubId, ubId);
        }

        // 开始遍历 搜索结果中的 100万条数据,是否有被个人收藏过的,有就改变返回的状态。
        for (String bId : bList) {
            // 直接使用 ubMap 的查找key 值是否存在的方式,判断该文章是否已经收藏。
            if (ubMap.containsKey(bId)){
                resMap.put(bId, 1);
            }
        }

        // 结束时间
        long end = System.currentTimeMillis();
        // 用时时长 ms
        System.out.println("耗时 ms: " + (end - start));
    }
}
测试数据:

  collectionNum: 10000

  total : 100 0000

  消耗时间:

结论:一层循环,加上内部的 hash 计算,用时 51ms

四、总结

条件: 两组数据分别没有重复的数据(id 或者 根据对比的字段不重复,也可以根据业务琢磨,即放在 map 中的 key 值不重复)

需求:对比一组数据中的数据,是否在另一组中有对应的匹配数据

结论:使用 hashmap 的 key值 进行查找,明显快于双层 for 循环,for 循环消耗的时间是 key 值查找的 100多倍!!!

五、原理解析

1. 双层 for 循环就不需要多解释,纯粹的 10000 x 100 00000 = 100 亿 的遍历次数

2. HashMap 快的原因在于将 id 值作为 map 的key存储在map中,而 map 底层是 数组在存储数据,此处不对链表和树结构进行说明。通过计算 id 的 hashcode 值,再与 map 的容量 size 求余数,直接获取到该条数据在 hashMap 中的下标,而不是逐一的去查找数据。故 使用 hashmap 只循环了一次  + 少量运算,速度明显有所突破。

3. 根据需要控制内存消耗大小,你可以自定义将 数据多的放在 map 或者将数据少的放在 map 中,也就是在控制外层循环的次数,外层大,则占用内存就小,时间上可能会有所增加。

原文地址:https://www.cnblogs.com/SelfCoding/p/10224168.html

时间: 2024-11-08 22:41:27

【策略与优化 - 001】- 在特定场景下,如何对双层循环进行降级,加速数据匹配?的相关文章

自己总结的C#编码规范--3.特定场景下的命名最佳实践

特定场景下的命名最佳实践 命名空间 要使用PascalCasing,并用点号来分隔名字空间中的各个部分. 如Microsof.Office.PowerPoint 要用公司名作为命名空间的前缀,这样就可以避免与另外一家公司使用相同的名字. 要用稳定的,与版本无关的产品名称作为命名空间的第二层 不要使用公司的组织架构来决定命名空间的层次结构,因为内部组织结构经常改变. 不要用相同的名字来命名命名空间和该空间内的类型. 例如,不要先将命名空间命名为Debug,然后又在该空间中提供Debug类.大部分编

特定场景下SQL的优化

1.大表的数据修改最好分批处理. 1000万行的记录表中删除更新100万行记录,一次只删除或更新5000行数据.每批处理完成后,暂停几秒中,进行同步处理. 2.如何修改大表的表结构. 对表的列的字段类型进行修改,改变字段宽度时还是会锁表,无法解决主从数据库延迟的问题. 解决办法: 1.创建一个新表. 2.在老表上创建触发器同步老表数据到新表. 3.同步老表数据到新表. 4.删除老表. 5.将新表重新命名为老表. 可以使用命令,完成上面的工作: pt-online-schema-change –a

多线程场景下延迟初始化的策略

1.什么是延迟初始化 延迟初始化(lazy initialization,即懒加载)是延迟到需要域的值时才将它初始化的行为.如果永远不需要这个值,这个域就永远不会被初始化.这种方法既静态域,也适用于实例域. 最好建议“除非绝对必要,否则就不要这么做”. 2.延迟初始化线程安全的一个策略:同步 延迟初始化的一个好处,是当域只在类的实例部分被访问,并且初始化这个域的开销很高,那就可能值得进行延迟初始化. 但是在大多数情况下,正常的初始化要优先于延迟初始化.因为在多线程的场景下,采用某种形式的同步是很

关于并发场景下,通过双重检查锁实现延迟初始化的优化问题隐患的记录

首先,这个问题是从<阿里巴巴Java开发手册>的1.6.12(P31)上面看到的,里面有这样一句话,并列出一种反例代码(以下为仿写,并非与书上一致): 在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患,推荐解决方案中较为简单的一种(适用于JDK5及以上的版本),即目标属性声明为volatile型. 1 public class Singleton { 2 private static Singleton instance=null; 3

高并发场景下System.currentTimeMillis()的性能问题的优化

前言 System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我也不知道,不过听说在100倍左右),然而该方法又是一个常用方法,有时不得不使用,比如生成wokerId.打印日志什么的,在高并发情形下肯定存在性能问题的,但怎么做才好呢? System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道.那什么快?内存!如果该方法从内存直接取数,那不就美滋滋了. 代码实现 package com.nyvi.support.uti

高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器

package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; /** * 高并发场景下System.currentTimeMillis()的性能问题的优化 * <p><p> * System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我还没测试过,有人说是100

[转帖]etcd 在超大规模数据场景下的性能优化

etcd 在超大规模数据场景下的性能优化 阿里系统软件技术 2019-05-27 09:13:17 本文共5419个字,预计阅读需要14分钟. http://www.itpub.net/2019/05/27/1958/ 不明觉厉 作者 | 阿里云智能事业部高级开发工程师 陈星宇(宇慕) 划重点 etcd 优化背景 问题分析 优化方案展示 实际优化效果 本文被收录在 5 月 9 日 cncf.io 官方 blog 中,链接:https://www.cncf.io/blog/2019/05/09/p

高并发场景下的限流策略

高并发场景下的限流策略: 在开发高并发系统时,有很多手段来保护系统:缓存.降级.限流. 当访问量快速增长.服务可能会出现一些问题(响应超时),或者会存在非核心服务影响到核心流程的性能时, 仍然需要保证服务的可用性,即便是有损服务.所以意味着我们在设计服务的时候,需要一些手段或者关键数据进行自动降级,或者配置人工降级的开关. 缓存的目的是提升系统访问速度和增大系统处理的容量,可以说是抗高并发流量的银弹:降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉某些功能,等高峰或者问题解决后再打开:

缓存在高并发场景下的常见问题

缓存一致性问题 当数据时效性要求很高时,需要保证缓存中的数据与数据库中的保持一致,而且需要保证缓存节点和副本中的数据也保持一致,不能出现差异现象.这就比较依赖缓存的过期和更新策略.一般会在数据发生更改的时,主动更新缓存中的数据或者移除对应的缓存. 缓存并发问题 缓存过期后将尝试从后端数据库获取数据,这是一个看似合理的流程.但是,在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 “雪崩”现象.此外,当某个缓存key在被更新时,同时也可能被大量请求在获取,