資料庫資料轉樹形結構的兩種方式
通常資料庫儲存樹形資料一般採取這種形式:
我們會建立一個對應的實體類
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 voidsetParentId(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 這樣的非結構化的資料時,同樣也可以使用第二種方式。