解密阿里大神写的天书般的Tree工具类,轻松搞定树结构!

news2024/11/19 17:40:14

首发公众号:赵侠客

一、引言

最近公司新进了不少新人,包括一些来自阿里、网易等大型企业的资深工程师。我们组的一位新同事是阿里来的专家,我在CR(Code Review, 简称CR)时看到了他编写的一个关于树操作的工具类,对其设计和实现深感佩服。为了进一步理解和学习,我对这个工具类进行了深入分析和整理,现在在本文中与大家分享。

二、树形结构介绍

2.1 简单的二叉树

首页简单简介一下树形数据结构,树形数据结构是一种层级化的数据组织方式,广泛用于表示具有层次关系的数据。由于其层级化组织特点,树形数据结构能够高效支持多种查找、插入和删除操作,因此在计算机科学和实际应用中得到了广泛应用。下面是一个简单的二叉树示例:

二叉树及遍历算法

▲二叉树及遍历算法

2.2 树的应用场景

树形数据结构的应用场景是通过ID关联上下级关系的对象,然后将这些对象组织成一棵树。主要有以下常用应用场景:

  1. 部门通讯录:通讯录中可以通过树形结构展示不同部门及其上下级关系,便于用户快速找到联系人
  2. 系统菜单: 系统中的菜单通常是分层的,通过树形结构可以方便地展示和管理各级菜单项
  3. 地址选择器:地理地址通常有多级关系,如省、市、县,通过树形结构可以方便用户选择具体的地址
  4. 文件夹目录: 文件系统中的文件夹和文件可以通过树形结构来组织和展示,便于用户进行文件操作
  5. 产品多级分类: 通过树形结构可以直观地展示和管理各级分类
  6. 评论回复: 通过树形结构可以展示帖子的回复关系,便于查看讨论的脉络

树形数据结构的应用场景通常是分层的,通过树形结构可以展示和管理各级流程节点及其关系。 这些场景中,树形结构的应用可以显著提升数据的组织和展示效率,帮助用户更直观地理解和操作系统。

树形结构在电商分类中的应用

▲:树形结构在电商分类中的应用

三、JAVA中的树形数据结构

3.1 JAVA树形结构对象定义

在JAVA中树形结构是通过对象嵌套方式来定义的,如MenuVo对象中有一个子对象subMenus:

@Data
public class MenuVo {
    private Long id;
    private Long pId;
    private String name;
    private Integer rank=0;
    private List<MenuVo> subMenus=new ArrayList<>();
}

3.2 JSON数据格式中的树形结构

JSON数据天然就是树形结果,如以下展示一个简单的JSON树形结构:

[
    {
        "id": 0,
        "subMenus": [
            {
                "id": 2,
                "subMenus": [
                    {
                        "id": 5,
                        "pid": 2
                    }
                ],
                "pid": 0
            }
        ],
        "pid": -1
    }
]

3.3 树形数据结构的储存

像文档型数据库MongDB、ElasticSearch可以直接存储JSON这种树形数据,而像Mysql这种关系型数据库不太适合直接存储具有上下级关系的树形数据,大都是按行存储然后通过id、pid之间关联上下级关系,这就导致我们需要经常将Mysql中的关系型数据转成JSON这种数据结构,因而TreeUtil这类数据转换工具类就起诞生了。

数据库中存储的城市数据结构

▲:数据库中存储的城市数据结构

四、TreeUtil代码分析

4.1 makeTree()构建树

直接看这神一样的方法makeTree():

public class TreeUtil {
    public static <E> List<E> makeTree(List<E> list, Predicate<E> rootCheck, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> setSubChildren) {
        return list.stream().filter(rootCheck).peek(x -> setSubChildren.accept(x, makeChildren(x, list, parentCheck, setSubChildren))).collect(Collectors.toList());
    }
     private static <E> List<E> makeChildren(E parent, List<E> allData, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> setSubChildren) {
        return allData.stream().filter(x -> parentCheck.apply(parent, x)).peek(x -> setSubChildren.accept(x, makeChildren(x, allData, parentCheck, setSubChildren))).collect(Collectors.toList());
    }
}

是不是完全看不懂?像看天书一样?makeTree方法为了通用使用了泛型+函数式编程+递归,正常人一眼根本看不这是在干什么的,我们先不用管这个makeTree合成树的代码原理,先直接看如何使用:

MenuVo menu0 = new MenuVo(0L, -1L);
MenuVo menu1 = new MenuVo(1L, 0L);
MenuVo menu2 = new MenuVo(2L, 0L);
MenuVo menu3 = new MenuVo(3L, 1L);
MenuVo menu4 = new MenuVo(4L, 1L);
MenuVo menu5 = new MenuVo(5L, 2L);
MenuVo menu6 = new MenuVo(6L, 2L);
MenuVo menu7 = new MenuVo(7L, 3L);
MenuVo menu8 = new MenuVo(8L, 3L);
MenuVo menu9 = new MenuVo(9L, 4L);
//基本数据
List<MenuVo> menuList = Arrays.asList(menu0,menu1, menu2,menu3,menu4,menu5,menu6,menu7,menu8,menu9);
//合成树
List<MenuVo> tree= TreeUtil.makeTree(menuList, x->x.getPId()==-1L,(x, y)->x.getId().equals(y.getPId()), MenuVo::setSubMenus);
System.out.println(JsonUtils.toJson(tree));

我们结合这个简单的合成菜单树看一下这个makeTree()方法参数是如何使用的:

  1. 第1个参数List list,为我们需要合成树的List,如上面Demo中的menuList
  2. 第2个参数Predicate rootCheck,判断为根节点的条件,如上面Demo中pId==-1就是根节点
  3. 第3个参数parentCheck 判断为父节点条件,如上面Demo中 id==pId
  4. 第4个参数setSubChildren,设置下级数据方法,如上面Demo中: Menu::setSubMenus

有了上面这4个参数,只要是合成树场景,这个TreeUtil.makeTree()都可以适用,比如我们要合成一个部门树:

@Data
public class GroupVO {
    private String groupId;
    private String parentGroupId;
    private String groupName;
    private List<GroupVO> subGroups=new ArrayList<>();
}

groupId是部门ID, 根部门的条件是parentGroupId=null, 那么调用合成树的方法为:

List<GroupVO> groupTree=TreeUtil.makeTree(groupList, x->x.getParentGroupId==null,(x, y)->x.getGroupId().equals(y.getParentGroupId), GroupVO::setSubGroups);

是不是很优雅?很通用?完全不需要实现什么接口、定义什么TreeNode、增加什么TreeConfig,静态方法直接调用就搞定。 一个字:绝!

五、神方法拆解

5.1 去掉泛型和函数接口

第一步我们可以把泛型和函数接口去掉,再看一下一个如何使用递归合成树:

    public static List<MenuVo> makeTree(List<MenuVo> allDate,Long rootParentId) {
        List<MenuVo> roots = new ArrayList<>();
        // 1、获取所有根节点
        for (MenuVo menu : allDate) {
            if (Objects.equals(rootParentId, menu.getPId())) {
                roots.add(menu);
            }
        }
        // 2、所有根节点设置子节点
        for (MenuVo root : roots) {
             makeChildren(root, allDate);
        }
        return roots;
    }
    public static MenuVo makeChildren(MenuVo root, List<MenuVo> allDate) {
        //遍历所有数据,获取当前节点的子节点
        for (MenuVo menu : allDate) {
            if (Objects.equals(root.getId(), menu.getPId())) {
                makeChildren(menu, allDate);
                //将是当前节点的子节点添加到当前节点的subMenus中
                root.getSubMenus().add(menu);
            }
        }
        return root;
    }

调用方法:

       List<MenuVo> tree2 = parseTree(menuList,-1L);

通过上面的两个方法可以合成树的基本逻辑,主要分为三步

  1. 找到所有根节点
  2. 遍历所有根节点设置子节点
  3. 遍历allDate查询子节点

5.2 使用函数优化

看懂上面的代码后,我们再给加上函数式接口:

    public static List<MenuVo> makeTree(List<MenuVo> allDate, Predicate<MenuVo> rootCheck, BiFunction<MenuVo, MenuVo, Boolean> parentCheck, BiConsumer<MenuVo, List<MenuVo>> setSubChildren) {
        // 1、获取所有根节点
        List<MenuVo> roots = allDate.stream().filter(x->rootCheck.test(x)).collect(Collectors.toList());;
        // 2、所有根节点设置子节点
        roots.stream().forEach(x->makeChildren(x,allDate,parentCheck,setSubChildren));
        return roots;
    }
    
    public static MenuVo makeChildren(MenuVo root, List<MenuVo> allDate,BiFunction<MenuVo, MenuVo, Boolean> parentCheck, BiConsumer<MenuVo, List<MenuVo>> setSubChildren) {
        //遍历所有数据,获取当前节点的子节点
        allDate.stream().filter(x->parentCheck.apply(root,x)).forEach(x->{
            makeChildren(x, allDate,parentCheck,setSubChildren);
            //将是当前节点的子节点添加到当前节点的subMenus中
            setSubChildren.accept(x,allDate);
        });
        return root;
    }

结合前面的方式再来看这个函数式接口是不是简单多了,只是写法上函数化了而已。使用函数优化的整体结构和最终的方法有点像了,最后再使用泛型优化就成了最终版本。从这个例子来看代码还是要不断优化的,大神可以直接写出神一样的代码,小弟一步步优化,一点点进步也是能写出大神一样的代码的。

六、其它操作Tree方法

6.1 遍历Tree

学习过二叉树都知道遍历二叉树有先序、中序、后序、层序,如果这些不清楚的可以先去学习一下,针对Tree的遍历这里提供了三个方法: 先序forPreOrder(),后序forPostOrder(),层序forLevelOrder():


public static <E> void forPreOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> getSubChildren) {
    for (E l : tree) {
        consumer.accept(l);
        List<E> es = getSubChildren.apply(l);
        if (es != null && es.size() > 0) {
            forPreOrder(es, consumer, getSubChildren);
        }
    }
}

public static <E> void forLevelOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> getSubChildren) {
    Queue<E> queue = new LinkedList<>(tree);
    while (!queue.isEmpty()) {
        E item = queue.poll();
        consumer.accept(item);
        List<E> childList = getSubChildren.apply(item);
        if (childList != null && !childList.isEmpty()) {
            queue.addAll(childList);
        }
    }
}

public static <E> void forPostOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> getSubChildren) {
    for (E item : tree) {
        List<E> childList = getSubChildren.apply(item);
        if (childList != null && !childList.isEmpty()) {
            forPostOrder(childList, consumer, getSubChildren);
        }
        consumer.accept(item);
    }
}
    

我们看测试方法:

//先序
StringBuffer preStr=new StringBuffer();
TreeUtil.forPreOrder(tree,x-> preStr.append(x.getId().toString()),Menu::getSubMenus);
Assert.assertEquals("0137849256",preStr.toString());

//层序
StringBuffer levelStr=new StringBuffer();
TreeUtil.forLevelOrder(tree,x-> levelStr.append(x.getId().toString()),Menu::getSubMenus);
Assert.assertEquals("0123456789",levelStr.toString());

//后序
StringBuffer postOrder=new StringBuffer();
TreeUtil.forPostOrder(tree,x-> postOrder.append(x.getId().toString()),Menu::getSubMenus);
Assert.assertEquals("7839415620",postOrder.toString());

通过这个Demo我们解释一下遍历中的几个参数:

  1. tree 需要遍历的树,就是makeTree()合成的对象
  2. Consumer consumer 遍历后对单个元素的处理方法,如:x-> System.out.println(x)、 postOrder.append(x.getId().toString())
  3. Function<E, List> getSubChildren,获取下级数据方法,如Menu::getSubMenus

有了这三个方法遍历Tree是不是和遍历List一样简单方便了?二个字:绝了!!

6.2 flat打平树

我们可以将一个List使用markTree()构建成树,就可以使用flat()将树还原成List

    public static <E> List<E> flat(List<E> tree, Function<E, List<E>> getSubChildren, Consumer<E> setSubChildren) {
        List<E> res = new ArrayList<>();
        forPostOrder(tree, item -> {
            setSubChildren.accept(item);
            res.add(item);
        }, getSubChildren);
        return res;
    }

使用方法:

    List<Menu> flat = TreeUtil.flat(tree, Menu::getSubMenus,x->x.setSubMenus(null));
    Assert.assertEquals(flat.size(),menuList.size());
    flat.forEach(x->{
        Assert.assertTrue(x.getSubMenus()==null);
    });

flat()参数解释:

  1. tree 需要打平的树,就是makeTree()合成的对象
  2. Function<E, List> getSubChildren,获取下级数据方法,如Menu::getSubMenus
  3. Consumer setSubChildren,设置下级数据方法,如: x->x.setSubMenus(null)

6.3 sort()排查

我们知道针对List,可以使用list.sort()直接排序,那么针对树,就可以调用sort()方法直接对树中所有子节点直接排序:

    public static <E> List<E> sort(List<E> tree, Comparator<? super E> comparator, Function<E, List<E>> getChildren) {
        for (E item : tree) {
            List<E> childList = getChildren.apply(item);
            if (childList != null && !childList.isEmpty()) {
                sort(childList,comparator,getChildren);
            }
        }
        tree.sort(comparator);
        return tree;
    }

比如MenuVo有一个rank值,表明排序权重

        MenuVo menu0 = new MenuVo(0L, -1L);
        MenuVo menu1 = new MenuVo(1L, 0L);
        menu1.setRank(100);
        MenuVo menu2 = new MenuVo(2L, 0L);
        menu2.setRank(1);
        MenuVo menu3 = new MenuVo(3L, 1L);
        MenuVo menu4 = new MenuVo(4L, 1L);
        MenuVo menu5 = new MenuVo(5L, 2L);
        menu5.setRank(5);
        MenuVo menu6 = new MenuVo(6L, 2L);
        MenuVo menu7 = new MenuVo(7L, 3L);
        menu7.setRank(5);
        MenuVo menu8 = new MenuVo(8L, 3L);
        menu8.setRank(1);
        MenuVo menu9 = new MenuVo(9L, 4L);
        List<MenuVo> menuList = Arrays.asList(menu0,menu1, menu2,menu3,menu4,menu5,menu6,menu7,menu8,menu9);
        //合成树
        List<MenuVo> tree= TreeUtil.makeTree(menuList, x->x.getPId()==-1L,(x, y)->x.getId().equals(y.getPId()), MenuVo::setSubMenus);
        System.out.println(JsonUtils.toJson(tree));

如查我们想按rank正序:

List<MenuVo> sortTree= TreeUtil.sort(tree, Comparator.comparing(MenuVo::getRank), MenuVo::getSubMenus);

如果我们想按rank倒序:

List<MenuVo> sortTree= TreeUtil.sort(tree, (x,y)->y.getRank().compareTo(x.getRank()), MenuVo::getSubMenus);

sort参数解释:

  1. tree 需要排序的树,就是makeTree()合成的对象
  2. Comparator<? super E> comparator 排序规则Comparator,如:Comparator.comparing(MenuVo::getRank)按Rank正序 ,(x,y)->y.getRank().compareTo(x.getRank()),按Rank倒序
  3. Function<E, List> getChildren 获取下级数据方法,如:MenuVo::getSubMenus

这个给树排序是不是和对List排序一样的简单:三个字:太绝了!!!

七、总结

看完这位大神编写的TreeUtil工具类后,我深感佩服,其设计与实现真是令人叹为观止。该工具类不仅优雅且高效,使得以往需要递归处理的树形结构操作变得更加简洁和便捷。未来处理树形数据时,只需直接使用该工具类即可,无需再编写复杂的递归代码。

最后附完成代码方便CV工程师,还不赶快点赞、关注、收藏


/**
 * @Description: 树操作方法工具类
 * @Author: 公众号:赵侠客
 * @Copyright: Copyright (c) 赵侠客
 * @Date: 2024-07-22 10:42
 * @Version: 1.0
 */
public class TreeUtil {

    /**
     * 将list合成树
     *
     * @param list 需要合成树的List
     * @param rootCheck 判断E中为根节点的条件,如:x->x.getPId()==-1L , x->x.getParentId()==null,x->x.getParentMenuId()==0
     * @param parentCheck 判断E中为父节点条件,如:(x,y)->x.getId().equals(y.getPId())
     * @param setSubChildren   E中设置下级数据方法,如: Menu::setSubMenus
     * @param <E>  泛型实体对象
     * @return   合成好的树
     */
    public static <E> List<E> makeTree(List<E> list, Predicate<E> rootCheck, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> setSubChildren) {
        return list.stream().filter(rootCheck).peek(x -> setSubChildren.accept(x, makeChildren(x, list, parentCheck, setSubChildren))).collect(Collectors.toList());
    }


    /**
     *  将树打平成tree
     * @param tree  需要打平的树
     * @param getSubChildren  设置下级数据方法,如: Menu::getSubMenus,x->x.setSubMenus(null)
     * @param setSubChildren 将下级数据置空方法,如: x->x.setSubMenus(null)
     * @return  打平后的数据
     * @param <E> 泛型实体对象
     */
    public static <E> List<E> flat(List<E> tree, Function<E, List<E>> getSubChildren, Consumer<E> setSubChildren) {
        List<E> res = new ArrayList<>();
        forPostOrder(tree, item -> {
            setSubChildren.accept(item);
            res.add(item);
        }, getSubChildren);
        return res;
    }


    /**
     * 前序遍历
     *
     * @param tree 需要遍历的树
     * @param consumer  遍历后对单个元素的处理方法,如:x-> System.out.println(x)、 System.out::println打印元素
     * @param setSubChildren  设置下级数据方法,如: Menu::getSubMenus,x->x.setSubMenus(null)
     * @param <E> 泛型实体对象
     */
    public static <E> void forPreOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> setSubChildren) {
        for (E l : tree) {
            consumer.accept(l);
            List<E> es = setSubChildren.apply(l);
            if (es != null && es.size() > 0) {
                forPreOrder(es, consumer, setSubChildren);
            }
        }
    }


    /**
     * 层序遍历
     *
     * @param tree 需要遍历的树
     * @param consumer 遍历后对单个元素的处理方法,如:x-> System.out.println(x)、 System.out::println打印元素
     * @param setSubChildren 设置下级数据方法,如: Menu::getSubMenus,x->x.setSubMenus(null)
     * @param <E> 泛型实体对象
     */
    public static <E> void forLevelOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> setSubChildren) {
        Queue<E> queue = new LinkedList<>(tree);
        while (!queue.isEmpty()) {
            E item = queue.poll();
            consumer.accept(item);
            List<E> childList = setSubChildren.apply(item);
            if (childList != null && !childList.isEmpty()) {
                queue.addAll(childList);
            }
        }
    }


    /**
     * 后序遍历
     *
     * @param tree 需要遍历的树
     * @param consumer 遍历后对单个元素的处理方法,如:x-> System.out.println(x)、 System.out::println打印元素
     * @param setSubChildren 设置下级数据方法,如: Menu::getSubMenus,x->x.setSubMenus(null)
     * @param <E> 泛型实体对象
     */
    public static <E> void forPostOrder(List<E> tree, Consumer<E> consumer, Function<E, List<E>> setSubChildren) {
        for (E item : tree) {
            List<E> childList = setSubChildren.apply(item);
            if (childList != null && !childList.isEmpty()) {
                forPostOrder(childList, consumer, setSubChildren);
            }
            consumer.accept(item);
        }
    }

    /**
     * 对树所有子节点按comparator排序
     *
     * @param tree 需要排序的树
     * @param comparator  排序规则Comparator,如:Comparator.comparing(MenuVo::getRank)按Rank正序 ,(x,y)->y.getRank().compareTo(x.getRank()),按Rank倒序
     * @param getChildren 获取下级数据方法,如:MenuVo::getSubMenus
     * @return 排序好的树
     * @param <E> 泛型实体对象
     */
    public static <E> List<E> sort(List<E> tree, Comparator<? super E> comparator, Function<E, List<E>> getChildren) {
        for (E item : tree) {
            List<E> childList = getChildren.apply(item);
            if (childList != null && !childList.isEmpty()) {
                sort(childList,comparator,getChildren);
            }
        }
        tree.sort(comparator);
        return tree;
    }
    
    private static <E> List<E> makeChildren(E parent, List<E> allData, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> children) {
        return allData.stream().filter(x -> parentCheck.apply(parent, x)).peek(x -> children.accept(x, makeChildren(x, allData, parentCheck, children))).collect(Collectors.toList());
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1955116.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用qt调试can通信,波特率如何设置

硬件环境介绍&#xff1a; 1、usb转can通信模块型号为创芯科技的USB-CAN适配器&#xff0c;厂家提供的测试软件和demo程序&#xff0c;如下图所示&#xff1b; 2、下位单片机STM32&#xff0c;can通信参数如下图&#xff0c;该测试程序时单片机一直在发送数据&#xff1b; 测试…

STM32F103 RT-thread配置LCD的FMC

使用的正点原子F103ZET6开发板&#xff0c;屏幕是一块4.3寸的TFTLCD&#xff0c;接下来直接讲配置流程 参考文章&#xff1a;基于正点原子F103精英板和CubeIDE的Hal库LCD驱动移植&#xff08;从零开始&#xff09;_正点原子 cubeide-CSDN博客 1&#xff0c;使用RT_Thread Stu…

最新版Bertom降噪,压缩,均衡,简单好用有效,win和mac,支持Intel和M芯片

一。Denoiser Classic 3.07 win&mac 1&#xff09; Denoiser Classic是一个零延迟降噪插件&#xff0c;用于音乐&#xff0c;后期制作和现场使用。 2&#xff09;产品特点&#xff1a; Bertom Denoiser是一个专为音乐和后期制作/对话设计的降噪插件。 一个简单的用户界面&…

深入理解计算机系统 CSAPP 家庭作业11.8

回收子进程是书本537页的内容 在tiny.c文件加以下代码,记得重新编译哦 书中提到CGI是在动态内容中的,所以题目的意思应该是在动态内容里面回收 void handler1(int sig) {int olderrno errno;while (waitpid(-1,NULL,0)>0){Sio_puts("Handler reaped child\n");…

光伏电站气象站:现代光伏系统的重要组成部分

光伏电站气象站&#xff0c;作为现代光伏系统的重要组成部分&#xff0c;集成了气象学、电子信息技术、数据处理与分析等多学科技术于一体&#xff0c;能够实时监测并记录包括温度、湿度、风速、风向、太阳辐射强度、降雨量在内的多种气象参数。这些数据不仅是评估光伏板发电效…

基于粒子群优化算法(PSO)永磁同步电机电流环多参数辨识MATLAB仿真模型

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 仿真模型简介 在同步旋转dq 轴坐标系下建立PMSM 数学模型&#xff0c;将定子dq 轴电压设为辨识模型和实际测量值的输入&#xff0c;设计了PSO 辨识PMSM 参数的适应度函数。该辨识方法不需推导复杂的电机数学…

动态开辟字符串—malloc

该函数包含在头文件<stdlib.h>中. 函数原型 void * malloc (size_t size) ——分配所需的内存空间&#xff0c;并返回一个指向它的指针 size_t size, 该参数的类型是size_t(无符号整型),它表示要开辟的内存块的大小(以字节为单位),它的作用是告诉函数需要动态开辟多少个…

从代码层面熟悉UniAD,开始学习了解端到端整体架构

0. 简介 最近端到端已经是越来越火了&#xff0c;以UniAD为代表的很多工作不断地在不断刷新端到端的指标&#xff0c;比如最近SparseDrive又重新刷新了所有任务的指标。在端到端火热起来之前&#xff0c;成熟的模块化自动驾驶系统被分解为不同的独立任务&#xff0c;例如感知、…

USB 2.0 协议专栏之 USB 2.0 概述(一)

前言&#xff1a;本篇博客为手把手教学的 USB 2.0 协议栈类精品博客&#xff0c;该专栏博客侧重针对 USB 2.0 协议进行讲解。Universal Serial Bus 作为如今最常见的通信接口&#xff0c;被广泛应用于&#xff1a;Keyboard、Mouse、Communication Device Class 和 Mass Storage…

Scrapy + Django爬虫可视化项目实战(一)

目录 一、项目介绍 (一) 项目背景 (二) 项目介绍 二、系统实现 (一) 爬虫 1. 实现步骤 一、爬取字段 二、分析页面 三、具体实现 2. 爬虫结果 系列文章 Python升级打怪—Django入门 Python升级打怪—Scrapy零基础小白入门 实现技术 ScrapyDjangoEcharts 一、项目…

物联网精密空调监控指标解读:松越_TCP7022EX_精密空调

监控易是一款专业的IT和物联网设备监控软件&#xff0c;能够实时监控各类IT资源和物联网设备的运行状态&#xff0c;确保系统的稳定运行。在物联网精密空调领域&#xff0c;监控易对松越_TCP7022EX_精密空调进行了全面的监控&#xff0c;以下是对其监控指标的详细解读。 监控指…

2. Class 文件的组成

16 进制打开class文件 可以通过Notepad下载一个HexEditor插件&#xff0c;下载好该插件后可以以16进制的方式打开class看&#xff0c;打开后我们可以看到如下所示的图片&#xff1a; class 文件的组成 class 文件的组成部分为&#xff1a;魔数&#xff0c;版本号&#xff0c;…

Springboot与SpringSecurity使用(1):介绍、登录验证

一、介绍 Spring 是非常流行和成功的 Java 应用开发框架&#xff0c;Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架&#xff0c;提供了一套 Web 应用安全性的完整解决方案。Web 应用的安全性包括用户认证&#xff08;Authentication&#xff09…

Linux网络:传输层协议TCP(三)滑动窗口及流量控制

目录 一、关于滑动窗口在TCP中的应用 1.1什么是滑动窗口&#xff0c;为什么要有滑动窗口 1.2滑动窗口的实现 1.3滑动窗口针对丢包重传的处理机制 二、流量控制 一、关于滑动窗口在TCP中的应用 1.1什么是滑动窗口&#xff0c;为什么要有滑动窗口 在上一篇博文中博主阐述了…

植物神经紊乱拜拜啦! 6招让你心情美美哒,放松到飞起~

Hey宝贝们&#xff0c;是不是有时候觉得心里的小宇宙&#x1f30c;乱糟糟的&#xff0c;像是有一群小精灵&#x1f9da;‍♀️在跳舞&#xff0c;却偏偏踩不到点上&#xff1f;没错&#xff0c;这可能就是植物神经紊乱在作祟啦&#xff01;别怕&#xff0c;我这就给你支几招&am…

Nginx系列-12 Nginx使用Lua脚本进行JWT校验

背景 本文介绍Nginx中Lua模块使用方式&#xff0c;并结合案例进行介绍。案例介绍通过lua脚本提取HTTP请求头中的token字段&#xff0c;经过JWT校验并提取id和name信息&#xff0c;设置到http请求头中发向后段服务器。 默认情况下&#xff0c;Nginx自身不携带lua模块&#xff0…

Transformer中的Multi-head Attention机制解析——从单一到多元的关注效益最大化

Transformer中的Multi-head Attention机制解析——从单一到多元的关注效益最大化 Multi-head Attention的核心作用 组件/步骤描述多头注意力机制&#xff08;Multi-head Attention&#xff09;Transformer模型中的关键组件&#xff0c;用于处理序列数据功能允许模型同时关注到…

Vue2从基础到实战(指令篇)

Vue中的常用指令&#xff01; 概念&#xff1a;指令&#xff08;Directives&#xff09;是 Vue 提供的带有 v- 前缀 的 特殊 标签属性。 vue 中的指令按照不同的用途可以分为如下 6 大类&#xff1a; 内容渲染指令&#xff08;v-html、v-text&#xff09; 条件渲染指令&…

昇思25天学习打卡营第1天|快速入门-构建基于MNIST数据集的手写数字识别模型

非常感谢华为昇思大模型平台和CSDN邀请体验昇思大模型&#xff01;从今天起&#xff0c;我将以打卡的方式&#xff0c;结合原文搬运和个人思考&#xff0c;分享25天的学习内容与成果。为了提升文章质量和阅读体验&#xff0c;我会将思考部分放在最后&#xff0c;供大家探索讨论…

04 | 深入浅出索引(上)

此系列文章为极客时间课程《MySQL 实战 45 讲》的学习笔记&#xff01; 索引的常见模型 可以提供查询效率的数据结构有很多&#xff0c;常见的有三种&#xff1a;哈希表、有序数组、搜索数。 哈希表是一种以 key-value 形式存储的数据结构。输入一个 key&#xff0c;通过固定…