zoukankan      html  css  js  c++  java
  • 数据库数据转树形结构的两种方式

    通常数据库存储树形数据一般采取这种形式:

    我们会创建一个对应的实体类

    package cn.kanyun.build_tree;
    
    import java.util.List;
    
    /**
     * 节点类
     * 部分字段添加transient关键字是为了,在Json序列化时不序列化该字段
     * 
     * @author KANYUN
     *
     */
    public class Node {
    
        private Long id;
    
        private Long parentId;
    
        private String name;
    
        private transient String parentName;
    
        private transient boolean isDir;
    
        private transient String path;
    
        private List<Node> children;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public Long getParentId() {
            return parentId;
        }
    
        public void setParentId(Long parentId) {
            this.parentId = parentId;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getParentName() {
            return parentName;
        }
    
        public void setParentName(String parentName) {
            this.parentName = parentName;
        }
    
        public boolean isDir() {
            return isDir;
        }
    
        public void setDir(boolean isDir) {
            this.isDir = isDir;
        }
    
        public String getPath() {
            return path;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    
        public List<Node> getChildren() {
            return children;
        }
    
        public void setChildren(List<Node> children) {
            this.children = children;
        }
    
        @Override
        public String toString() {
            return "Node [id=" + id + ", parentId=" + parentId + ", name=" + name + "]";
        }
        
        
    
    }

    第一种处理方式:递归

    package cn.kanyun.build_tree;
    
    import java.sql.SQLException;
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicBoolean;
    import java.util.concurrent.atomic.AtomicInteger;
    
    import com.google.gson.Gson;
    
    import cn.hutool.db.Db;
    import cn.hutool.db.Entity;
    import cn.hutool.db.sql.Condition;
    
    /**
     * 递归构建树 深度优先遍历(DFS)
     * 
     * @author KANYUN
     *
     */
    public class Recursion2Tree {
    
        /**
         * 定义根节点
         */
        static Node root = new Node();
    
        /**
         * 所有的节点数据
         */
        static List<Node> nodeList = new ArrayList();
    
        public static void main(String[] args) throws Exception {
            // TODO Auto-generated method stub
            long startTime = System.currentTimeMillis();
            Recursion2Tree tree = new Recursion2Tree();
    
            // 从数据库中获取数据,并进行类型转换开始
            List<Entity> result = Db.use().query("SELECT * FROM daasfolder_copy1");
    
            for (Entity entity : result) {
                Node node = new Node();
                node.setId(entity.getLong("id"));
                node.setParentId(entity.getLong("parentid"));
                node.setPath(entity.getStr("path"));
                node.setName(entity.getStr("name"));
                nodeList.add(node);
            }
            // 从数据库中获取数据,并进行类型转换结束
    
            // 初始化根节点的children
            root.setChildren(new ArrayList<Node>());
            // 构建根节点
            tree.buildRoot(nodeList);
            // 递归子节点
            tree.buildChildren();
    
            // 完成打印
            Gson gson = new Gson();
            System.out.println(gson.toJson(root.getChildren()));
    
            System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
        }
    
        /**
         * 构建顶级树,即找到根节点下的数据
         * 
         * @param nodeList
         */
        private void buildRoot(List<Node> nodeList) {
            Iterator<Node> iterator = nodeList.iterator();
            while (iterator.hasNext()) {
                Node node = iterator.next();
                if (node.getParentId() == 0) {
                    // 找到根节点下的数据,将其添加到root下,并将该节点从所有的节点列表中移除
                    root.getChildren().add(node);
                    iterator.remove();
                }
            }
    
        }
    
        /**
         * @return void
         * @throws Exception
         * @Author 赵迎旭
         * @Description 构建子节点
         * @Date 14:48 2020/9/18
         * @Param []
         **/
        private void buildChildren() throws Exception {
            // 如果元数据没有被删除完,说明还有数据没有挂到相应的节点上,则继续循环
            while (nodeList.size() > 0) {
                Iterator<Node> iterator = nodeList.iterator();
                build: while (iterator.hasNext()) {
                    Node node = iterator.next();
                    // 是否找到父节点,(注意这里使用的是原子类型,因为原子类型是引用类型)
                    AtomicBoolean isFind = new AtomicBoolean(false);
                    // 从根节点下的所有一级子节点开始递归遍历DFS
                    for (Node pNode : root.getChildren()) {
                        recursion(node, pNode, iterator, isFind);
                        if (isFind.get()) {
                            continue build;
                        }
                    }
    
                    // 如果该node在上面的递归中没有找到父节点
                    // 出现这种问题一般是两个原因:
                    // 1.就是数据的顺序是乱的,即当前遍历的节点的父节点还没有挂到树上 处理方法:跳过该Node继续遍历
                    // 2.当前节点的父节点,不存在(除非当前节点是根节点下的节点) 处理方法:抛出异常
                    if (!isFind.get()) {
                        // 则看剩下的Node集合中是否存在该node的父节点
                        for (Node pNode : nodeList) {
                            if (pNode.getId().equals(node.getParentId())) {
                                // 如果存在则继续外层遍历循环
                                continue build;
                            }
                        }
                        // 否则抛出异常
                        throw new Exception("当前Node节点找不到父节点:" + node.toString());
                    }
                }
            }
    
        }
    
        /**
         * @return boolean
         * @Description 递归添加
         * @Date 14:49 2020/9/18
         * @Param [bean, beanList]
         **/
        private void recursion(Node node, Node pNode, Iterator<Node> iterator, AtomicBoolean isFind) {
            Long id = pNode.getId();
            Long parent_id = node.getParentId();
            if (parent_id.equals(id)) {
                if (pNode.getChildren() == null) {
                    List<Node> children = new ArrayList<>();
                    pNode.setChildren(children);
                }
                pNode.getChildren().add(node);
                iterator.remove();
                isFind.set(true);
                ;
                return;
            }
    
            if (pNode.getChildren() != null) {
                for (Node currentPNode : pNode.getChildren()) {
                    recursion(node, currentPNode, iterator, isFind);
                }
            }
    
        }
    
    }

    可见递归构造树形数据分两步:

    1.构建根节点下的所有一级子节点

    2.未挂载的节点开始循环遍历递归 尝试挂载到根节点下的一级子节点下

    第二种方式:

    我们尝试更改一下数据库的结构,增加每个节点的路径,如图所示:

    那么我们就可以得到另一种处理树形结构的方法:

    package cn.kanyun.build_tree;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    import com.google.gson.Gson;
    
    import cn.hutool.db.Db;
    import cn.hutool.db.Entity;
    
    /**
     * 循环构建树 广度优先遍历(BFS)
     * 
     * @author KANYUN
     *
     */
    public class FlatPath2Tree {
    
        /**
         * 同一层级的数据放在Map中,层数为key。需要注意的是这里的层数从 0 开始 不断地 自增 中间是不会出现断序的,即 key 一定 是 1,2,3,4
         * 而不是 1,2,4 如果出现了断续,则说明数据是存在问题,即脏数据问题
         */
        static Map<Integer, List<Node>> levelMap = new HashMap<Integer, List<Node>>();
    
        /**
         * 定义根节点
         */
        static Node root = new Node();
    
        public static void main(String[] args) throws Exception {
    
            long startTime = System.currentTimeMillis();
            FlatPath2Tree tree = new FlatPath2Tree();
    
            // 从数据库中获取数据,并进行类型转换开始
            List<Entity> result = Db.use().query("SELECT * FROM daasfolder_copy1");
            List<Node> nodeList = new ArrayList();
            for (Entity entity : result) {
                Node node = new Node();
                node.setId(entity.getLong("id"));
                node.setParentId(entity.getLong("parentid"));
                node.setPath(entity.getStr("path"));
                nodeList.add(node);
            }
            // 从数据库中获取数据,并进行类型转换结束
    
            // 数据预处理
            tree.preNodeHandler(nodeList);
            // 构建树
            tree.buildTree();
    
            // 完成打印
            Gson gson = new Gson();
            System.out.println(gson.toJson(root.getChildren()));
    
            System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
        }
    
        /**
         * 数据预处理,分析Node节点的层数,判断是否是目录(其实这个判断不一定要像程序中写的那么复杂,有时候数据库里会有相应的字段标识是否是目录)
         * 得到父节点的名字
         * 
         * @param nodes
         */
        private void preNodeHandler(List<Node> nodes) {
    
            for (Node node : nodes) {
                // 这里使用了split的一个重载方法,因为 "test/".split("/") 默认返回的数组长度是1,省略了最后的空值,详情查阅split的重载方法
                String[] pathInfoList = node.getPath().split("/", -1);
                // 判断是否是目录,split的结果返回是数组,其数组长度肯定大于等于1的,直接判断数组的最后一个元素是否为空即可
                boolean isDir = pathInfoList[pathInfoList.length - 1].equals("");
                // 如果是目录标题为length - 2,否则目录标题为length - 1
                String title = isDir ? pathInfoList[pathInfoList.length - 2] : pathInfoList[pathInfoList.length - 1];
                // 判断有几级目录,如果是目录 -2 ,非 目录 -1
                int level = isDir ? pathInfoList.length - 2 : pathInfoList.length - 1;
                // 获取父目录,先判断level是否为0,如果为0 说明父目录是根目录,接着再判断路径是否是目录
                String parentName = level == 0 ? "/"
                        : isDir ? pathInfoList[pathInfoList.length - 3] : pathInfoList[pathInfoList.length - 2];
    
                // System.out.println("当前遍历目录的层级为:" + level);
                node.setName(title);
                node.setDir(isDir);
                node.setParentName(parentName);
                if (isDir) {
                    // 如果是目录初始化children
                    List<Node> children = new ArrayList();
                    node.setChildren(children);
                }
                // 将该Node放到Map中去
                List<Node> nodeLevel = levelMap.get(level);
                if (nodeLevel == null) {
                    nodeLevel = new ArrayList<>();
                    levelMap.put(level, nodeLevel);
                }
                nodeLevel.add(node);
            }
        }
    
        /**
         * 最终处理树,即处理层级,封装数据
         * 
         * @throws Exception
         */
        public void buildTree() throws Exception {
            root.setChildren(new ArrayList<Node>());
            int maxLevel = levelMap.size();
            System.out.println("maxLevel:" + maxLevel);
            // Set<Integer> keys = levelMap.keySet();
            // for (Integer level : keys) {
            // System.out.println(level);
            // }
            // 需要注意的是,这里是顺序遍历,即首先得到操作的肯定是根节点下的数据,BFS 广度优先遍历,对树一层一层的扫
            for (int level = 0; level < maxLevel; level++) {
                List<Node> nodeLevel = levelMap.get(level);
                for (Node node : nodeLevel) {
                    // 得到当前节点的兄弟节点列表
                    List<Node> siblingNodes = this.getSiblingNodes(node, level, root);
                    // 将当前节点加入到该列表中
                    siblingNodes.add(node);
                }
            }
    
        }
    
        /**
         * 得到当前节点的兄弟节点列表
         * 
         * @param node
         * @param level
         * @param root
         * @return
         * @throws Exception
         */
        private List<Node> getSiblingNodes(Node node, int level, Node root) throws Exception {
            String patName = node.getParentName();
            List<Node> cutNode = new ArrayList();
            if (level == 0) {
                // 当层级为0时,说明是根节点的数据
                cutNode = root.getChildren();
            } else {
                // 当层级不为0时,说明有父目录.此时先找到父目录,从levelMap中找到父目录列表,再遍历到底哪个是父目录
                List<Node> parentNodeList = levelMap.get(level - 1);
                for (Node parentNode : parentNodeList) {
                    // 需要注意的是这里是进行的字符串的判断,name的判断,那么会不会存在name重复的问题呢?其实是有一定概率重复的,如下面的例子
                    // 北京市->丰台区->长辛店镇->朱家坟
                    // 郑州市->金水区->长辛店镇->朱家坟
                    // 长辛店镇是不会挂错节点的,因为还有一个父节点的名字做保证,但是到朱家坟就不一样的了,他们的父节点名称是一样的,那么很有可能会挂错
                    // 如果能保证名称不会出现这个问题,那么这代码是可用的,如果不能保证,还会需要进行适量的更改,主要是从Node类的path属性入手,将其改为ID进行组装
                    // 如果解决这个问题?就是 Node类中的path属性使用节点id进行拼接,id是不会重复的,所以就不会出现这个问题了
                    if (parentNode.isDir() && parentNode.getName().equals(patName)) {
                        return parentNode.getChildren();
                    }
                }
                throw new Exception("当前Node节点找不到父节点:" + node.toString());
            }
            return cutNode;
    
        }
    
    }

    可以看到这种方式处理树形结构分4步:

    1.计算每个节点的所在层级和该节点对应的父节点:如 /a/b/c 那么c就在第三层 c的父节点是b

    2.将层数相同的节点放在同一个结合中,存储在Map集合中,层数作为key,节点结合做为value

    3.此时Map中的key 为 1,2,3... 按key的大小取出Map中的数据 ,那么你就知道了,第一次取出第一层的数据,也就是根节点的第一级数据

    4.取出数据之后怎么照他的父级节点呢?其实很简单,比如当前节点的层数是3,那么他的父级节点一定是2,所以我们从Map中找2层的数据,然后对比当前节点的父名称,父级的名称是否一致得到了到底要挂载到哪个父节点下

    对比:

    我们看到第一种处理方式它是如何构建数据的呢?假如有一个数据想要加入树,那么就需要从根节点遍历,然后再找根节点下的子节点,依次递归下去,这种形式叫深度优先(DFS),它的对比方式依赖于id和pid

    而第二种方式则不同,它先收集当前树的层级,并保存每个层级的数据到集合中去,假如有一个数据想要加入树,先看当前数据的层级,然后直接找到它父节点所在的层级,再对比找到对应的父节点,这种形式在广度优先(BFS),它的对比方式可以依赖于id和pid也可以单纯依赖path的数据

    所以可以很明显的看出BFS的效率是更高的,因为它避免了许多无谓的递归判断,而DFS由于每次都需要从根节点开始判断,因此注定效率不会太高,但是DFS的优点是什么呢?DFS的代码简单而容易理解。而BFS则需要计算每个节点的层级,这一块逻辑稍显复杂。

    因此当已有递归在面对大量且层级较深的数据时效率低下时,可以尝试使用第二种方式来处理树形结构。

    同样如果你得到的 数据 是诸如: /a/b1/c1 , /a/b2/c2 , /a/b3/c3 这样的非结构化的数据时,同样也可以使用第二种方式。

    代码下载

  • 相关阅读:
    C++11特性
    DBC文件小结
    关于宏定义
    CentOS 6.5下Zabbix的安装配置
    CentOS下搭建LAMP环境详解
    VS2010中汉字拷贝到Word出现乱码问题解决
    DLL注入
    数组赋值
    CDC的StretchBlt函数载入位图时图片失真问题
    2019年下半年Web前端开发初级理论考试
  • 原文地址:https://www.cnblogs.com/kanyun/p/14261155.html
Copyright © 2011-2022 走看看