數據庫數據轉樹形結構的兩種方式


通常數據庫存儲樹形數據一般采取這種形式:

 

 

 

我們會創建一個對應的實體類

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 這樣的非結構化的數據時,同樣也可以使用第二種方式。

 

代碼下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM