数据库数据转树形结构的两种方式

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

 

 

 

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

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 这样的非结构化的数据时,同样也可以使用第二种方式。

 

代码下载

posted @ 2021-01-11 11:02  陈无问  阅读(2817)  评论(0编辑  收藏  举报