1. 程式人生 > 實用技巧 >資料庫資料轉樹形結構的兩種方式

資料庫資料轉樹形結構的兩種方式

通常資料庫儲存樹形資料一般採取這種形式:

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

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

程式碼下載