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

我們會創建一個對應的實體類
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 這樣的非結構化的數據時,同樣也可以使用第二種方式。
