java構(gòu)建樹形結(jié)構(gòu)的方式及如何組裝樹狀結(jié)構(gòu)數(shù)據(jù)
思考思路
我在考慮如何在Java中構(gòu)建樹狀數(shù)據(jù)結(jié)構(gòu):常見的方法包括遞歸、父子引用和使用節(jié)點(diǎn)列表等??梢酝ㄟ^創(chuàng)建樹節(jié)點(diǎn)類來表示樹形結(jié)構(gòu),例如,可以定義“樹節(jié)點(diǎn)”類為:class Node { T data; List<Node> children; Node parent; }
。
我將提供詳細(xì)分析,看看哪些方法在實(shí)際項(xiàng)目中更常見,如何構(gòu)建包含父子關(guān)系和操作樹的數(shù)據(jù)結(jié)構(gòu)。
這個(gè)問題的答案可能會(huì)包括多個(gè)常見的實(shí)現(xiàn)方式。
在 Java 中構(gòu)建樹狀結(jié)構(gòu),常見方法包括:1. 遞歸:從父子關(guān)系數(shù)據(jù)遞歸構(gòu)建樹;2. 使用 Map/HashMap:存儲(chǔ)節(jié)點(diǎn)并根據(jù)父子關(guān)系鏈接它們。我可以提供代碼示例,如定義TreeNode 類如下:
public class TreeNode<T> { private T data; private List<TreeNode<T>> children = new ArrayList<>(); public TreeNode(T data) { this.data = data; } public void addChild(TreeNode<T> child) { children.add(child); } }
實(shí)現(xiàn):Java構(gòu)建樹狀結(jié)構(gòu)數(shù)據(jù)
目的: 在 Java 中構(gòu)建樹狀結(jié)構(gòu)數(shù)據(jù)是一個(gè)非常常見的需求,通常用于展示分層數(shù)據(jù)(如:組織架構(gòu)、菜單、文件目錄等),下面詳細(xì)介紹幾種常見的方法、代碼案例及實(shí)際項(xiàng)目中的實(shí)踐經(jīng)驗(yàn)。比如實(shí)現(xiàn)這樣的分層效果:
這些數(shù)據(jù)都在數(shù)據(jù)庫,所以查數(shù)據(jù)庫:查出所有的數(shù)據(jù),然后再組裝返回給前端解析即可,當(dāng)然數(shù)據(jù)表中應(yīng)該設(shè)計(jì)成這樣:
組裝時(shí)只需要組裝成一個(gè) list 返回即可,前端拿到 list 解析。
1. 遞歸方式
原理: 遞歸方法基于“父子關(guān)系”的層級(jí)遍歷,從根節(jié)點(diǎn)開始遞歸查找和添加子節(jié)點(diǎn)。適合節(jié)點(diǎn)層次較少的場(chǎng)景。
實(shí)現(xiàn): 利用遞歸的方式進(jìn)行樹狀結(jié)構(gòu)數(shù)據(jù)的組裝,代碼如下:
@Data @NoArgsConstructor public class DeptEntity { private Long deptId; private Long parentId; private String deptName; private List<DeptEntity> children; public DeptEntity(Long deptId, Long parentId, String deptName) { this.deptId = deptId; this.parentId = parentId; this.deptName = deptName; } }
public class Test { // 假數(shù)據(jù):本來這一步是需要從數(shù)據(jù)庫中查找所有的數(shù)據(jù),然后再組裝,為了方便,這里弄成假數(shù)據(jù) public static List<DeptEntity> getData() { List<DeptEntity> list = new ArrayList<>(); list.add(new DeptEntity(100L, 0L, "若依科技")); list.add(new DeptEntity(101L, 100L, "深圳總公司")); list.add(new DeptEntity(102L, 100L, "長(zhǎng)沙分公司")); list.add(new DeptEntity(103L, 101L, "研發(fā)部門")); list.add(new DeptEntity(104L, 101L, "市場(chǎng)部門")); list.add(new DeptEntity(105L, 102L, "運(yùn)維部門")); list.add(new DeptEntity(106L, 102L, "財(cái)務(wù)部門")); return list; } public static void main(String[] args) { recursionBuildTree(); } // 方法1:遞歸 public static void recursionBuildTree() { // 獲取假數(shù)據(jù) List<DeptEntity> data = getData(); // 留住最頂層節(jié)點(diǎn) List<DeptEntity> collect = data.stream() .filter(d -> d.getParentId() == 0L) .collect(Collectors.toList()); //首先設(shè)置最頂層子節(jié)點(diǎn) for (DeptEntity deptEntity : collect) { deptEntity.setChildren(recursionBuildChildren(deptEntity.getDeptId(), data)); } System.out.println(collect); } // 設(shè)置子節(jié)點(diǎn) public static List<DeptEntity> recursionBuildChildren(Long deptId, List<DeptEntity> data) { // 保存子節(jié)點(diǎn) List<DeptEntity> children = new ArrayList<>(); // 尋找子節(jié)點(diǎn) for (DeptEntity deptEntity : data) { // 因?yàn)閐ata是最原始數(shù)據(jù),還是需要過濾掉最頂層節(jié)點(diǎn) if (deptEntity.getParentId() == 0L) { continue; } if (deptEntity.getParentId().equals(deptId)) { children.add(deptEntity); } } //遞歸遍歷子節(jié)點(diǎn) for (DeptEntity deptEntity : children) { deptEntity.setChildren(recursionBuildChildren(deptEntity.getDeptId(), data)); } return children; } }
2. 利用Stream流
1、上面的 DeptEntity
類代碼不變
2、寫方法:
public class Test { // 假數(shù)據(jù):本來這一步是需要從數(shù)據(jù)庫中查找所有的數(shù)據(jù),然后再組裝,為了方便,這里弄成假數(shù)據(jù) public static List<DeptEntity> getData() { List<DeptEntity> list = new ArrayList<>(); list.add(new DeptEntity(100L, 0L, "若依科技")); list.add(new DeptEntity(101L, 100L, "深圳總公司")); list.add(new DeptEntity(102L, 100L, "長(zhǎng)沙分公司")); list.add(new DeptEntity(103L, 101L, "研發(fā)部門")); list.add(new DeptEntity(104L, 101L, "市場(chǎng)部門")); list.add(new DeptEntity(105L, 102L, "運(yùn)維部門")); list.add(new DeptEntity(106L, 102L, "財(cái)務(wù)部門")); return list; } public static void main(String[] args) { streamBuildTree(); } // 方法2:Stream流 public static void streamBuildTree() { List<DeptEntity> data = getData(); // 按照parentId分組,將數(shù)據(jù)轉(zhuǎn)為Map<parentId, 屬于這個(gè)parentId的數(shù)據(jù)的一個(gè)集合> Map<Long, List<DeptEntity>> collect = data.stream() .collect(Collectors.groupingBy(DeptEntity::getParentId)); // 開始構(gòu)建樹 List<DeptEntity> list = streamBuildChildren(0L, collect); System.out.println(list); } // 設(shè)置子節(jié)點(diǎn) public static List<DeptEntity> streamBuildChildren(Long parentId, Map<Long, List<DeptEntity>> collect) { return Optional.ofNullable(collect.get(parentId)) .orElse(new ArrayList<>()) // 避免報(bào)空指針異常 .stream() .peek(child -> child.setChildren(streamBuildChildren(child.getDeptId(), collect))) .collect(Collectors.toList()); } }
3. 基于Map(推薦)
原理: 首先將所有節(jié)點(diǎn)存入一個(gè)哈希表(Map),鍵為節(jié)點(diǎn)的 id。然后遍歷所有節(jié)點(diǎn),根據(jù)其 parentId 找到對(duì)應(yīng)父節(jié)點(diǎn)并加入到父節(jié)點(diǎn)的子節(jié)點(diǎn)列表中。這樣可以避免遞歸的性能問題。
好處: 在 Java 中,使用 Map 來構(gòu)建樹結(jié)構(gòu)是一種高效的方法。該方法避免了遞歸的性能問題,并且可以快速查找節(jié)點(diǎn),提高構(gòu)建樹的效率。
實(shí)現(xiàn): 如下:
1、上面的 DeptEntity
類代碼不變
2、寫方法:
public class Test { // 假數(shù)據(jù):本來這一步是需要從數(shù)據(jù)庫中查找所有的數(shù)據(jù),然后再組裝,為了方便,這里弄成假數(shù)據(jù) public static List<DeptEntity> getData() { List<DeptEntity> list = new ArrayList<>(); list.add(new DeptEntity(100L, 0L, "若依科技")); list.add(new DeptEntity(101L, 100L, "深圳總公司")); list.add(new DeptEntity(102L, 100L, "長(zhǎng)沙分公司")); list.add(new DeptEntity(103L, 101L, "研發(fā)部門")); list.add(new DeptEntity(104L, 101L, "市場(chǎng)部門")); list.add(new DeptEntity(105L, 102L, "運(yùn)維部門")); list.add(new DeptEntity(106L, 102L, "財(cái)務(wù)部門")); return list; } public static void main(String[] args) { mapBuildTree(); } // 方法3:基于Map 1 public static void mapBuildTree() { // 獲取假數(shù)據(jù) List<DeptEntity> data = getData(); // 存放根節(jié)點(diǎn) List<DeptEntity> roots = new ArrayList<>(); // 存放所有子節(jié)點(diǎn) Map<Long, List<DeptEntity>> childMap = new HashMap<>(); // 1.初始化childMap,確保不會(huì)出現(xiàn)null for (DeptEntity deptEntity : data) { childMap.put(deptEntity.getDeptId(), new ArrayList<>()); } // 2.構(gòu)建樹結(jié)構(gòu) for (DeptEntity deptEntity : data) { // 根節(jié)點(diǎn) if (deptEntity.getParentId() == null || deptEntity.getParentId() == 0) { roots.add(deptEntity); } else { // 非根節(jié)點(diǎn),加入到父節(jié)點(diǎn)的children列表 childMap.get(deptEntity.getParentId()).add(deptEntity); } } // 3.統(tǒng)一賦值children for (DeptEntity deptEntity : data) { deptEntity.setChildren(childMap.get(deptEntity.getDeptId())); } System.out.println(roots); } }
基于 Map 的方式還有一個(gè),如下:
public class DeptEntity { private Long deptId; private Long parentId; private String deptName; private List<DeptEntity> children = new ArrayList<>(); // 構(gòu)造方法 public DeptEntity(Long deptId, Long parentId, String deptName) { this.deptId = deptId; this.parentId = parentId; this.deptName = deptName; } // Getter & Setter public Long getDeptId() { return deptId; } public Long getParentId() { return parentId; } public String getDeptName() { return deptName; } public List<DeptEntity> getChildren() { return children; } // 加一個(gè)這個(gè)方法 public void addChild(DeptEntity child) { this.children.add(child); } }
public class Test { // 假數(shù)據(jù):本來這一步是需要從數(shù)據(jù)庫中查找所有的數(shù)據(jù),然后再組裝,為了方便,這里弄成假數(shù)據(jù) public static List<DeptEntity> getData() { List<DeptEntity> list = new ArrayList<>(); list.add(new DeptEntity(100L, 0L, "若依科技")); list.add(new DeptEntity(101L, 100L, "深圳總公司")); list.add(new DeptEntity(102L, 100L, "長(zhǎng)沙分公司")); list.add(new DeptEntity(103L, 101L, "研發(fā)部門")); list.add(new DeptEntity(104L, 101L, "市場(chǎng)部門")); list.add(new DeptEntity(105L, 102L, "運(yùn)維部門")); list.add(new DeptEntity(106L, 102L, "財(cái)務(wù)部門")); return list; } public static void main(String[] args) { mapBuildTree(); } // 方法3:基于Map 2 public static void mapBuildTree() { // 獲取假數(shù)據(jù) List<DeptEntity> data = getData(); List<DeptEntity> roots = new ArrayList<>(); Map<Long, DeptEntity> childMap = new HashMap<>(); // 1.將所有節(jié)點(diǎn)存入Map中 for (DeptEntity deptEntity : data) { childMap.put(deptEntity.getDeptId(), deptEntity); } // 2.構(gòu)建樹結(jié)構(gòu) for (DeptEntity deptEntity : data) { // 根節(jié)點(diǎn) if (deptEntity.getParentId() == null || deptEntity.getParentId() == 0) { roots.add(deptEntity); } else { // 非根節(jié)點(diǎn),加入到父節(jié)點(diǎn)的children列表 DeptEntity parent = map.get(deptEntity.getParentId()); if (parent != null) { parent.addChild(deptEntity); } } } System.out.println(roots); } }
3.1 基于Map方式分析
1、代碼分析:
- 先將所有節(jié)點(diǎn)存入
Map<Long, DeptEntity>
,便于快速查找。 - 遍歷 data:
1、如果 parentId 為 null 或 0,表示它是根節(jié)點(diǎn),加入 roots 列表。
2、否則,在 map 中查找 parentId 對(duì)應(yīng)的父節(jié)點(diǎn),并將該節(jié)點(diǎn)加入父節(jié)點(diǎn)的 children 列表。
2、優(yōu)勢(shì)分析:
(1)時(shí)間復(fù)雜度:遍歷列表 data 兩次,因此時(shí)間復(fù)雜度為 O(n),相比遞歸構(gòu)建樹(可能達(dá)到 O(n²))效率更高。
(2)空間復(fù)雜度:使用了 Map<Long, DeptEntity>
,其空間復(fù)雜度是 O(n),額外開銷較小。
(3)避免遞歸問題:采用 Map 直接查找父節(jié)點(diǎn),避免遞歸調(diào)用,避免棧溢出問題,特別適用于深度較大的樹結(jié)構(gòu)。
3、適合場(chǎng)景:
? 數(shù)據(jù)量大,如百萬級(jí)別的分類、菜單、組織架構(gòu)等。
? 層級(jí)較深,如文件目錄、權(quán)限管理系統(tǒng)。
? 頻繁查詢,構(gòu)建后可以緩存樹,提高性能。
4、不適合場(chǎng)景:
? 數(shù)據(jù)量特別?。?lt;100),可以直接使用遞歸方法,代碼更簡(jiǎn)潔。
4. 總結(jié)
到此這篇關(guān)于java構(gòu)建樹形結(jié)構(gòu)的方式及如何組裝樹狀結(jié)構(gòu)數(shù)據(jù)的文章就介紹到這了,更多相關(guān)java構(gòu)建樹形結(jié)構(gòu)數(shù)據(jù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring MVC接口防數(shù)據(jù)篡改和重復(fù)提交
這篇文章主要為大家詳細(xì)介紹了Spring MVC接口防數(shù)據(jù)篡改和重復(fù)提交,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08Springboot 1.5.7整合Kafka-client代碼示例
這篇文章主要介紹了Springboot 1.5.7整合Kafka-client代碼示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-1010個(gè)經(jīng)典的Java main方法面試題
這篇文章主要為大家分享了10個(gè)經(jīng)典的Java main方法面試題,與其說是Java面試題,其實(shí)也是Java的一些最基礎(chǔ)知識(shí)問題,感興趣的小伙伴們可以參考一下2016-01-01java代碼關(guān)閉tomcat程序及出現(xiàn)問題解析
這篇文章主要介紹了java代碼關(guān)閉tomcat程序 及出現(xiàn)問題解析,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2019-05-05Java之Pattern.compile函數(shù)用法詳解
這篇文章主要介紹了Java之Pattern.compile函數(shù)用法詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Springboot?HTTP如何調(diào)用其他服務(wù)
這篇文章主要介紹了Springboot?HTTP如何調(diào)用其他服務(wù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01IntelliJ?IDEA設(shè)置JVM運(yùn)行參數(shù)的圖文介紹
這篇文章主要介紹了IntelliJ?IDEA設(shè)置JVM運(yùn)行參數(shù)的方法,包括配置方式及優(yōu)先級(jí),本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04Dubbo?LoadBalance基于權(quán)重的隨機(jī)負(fù)載均衡算法提高服務(wù)性能
這篇文章主要為大家介紹了Dubbo?LoadBalance基于權(quán)重的隨機(jī)負(fù)載均衡算法提高服務(wù)性能詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2023-10-10