java構(gòu)建樹形結(jié)構(gòu)的方式及如何組裝樹狀結(jié)構(gòu)數(shù)據(jù)
思考思路
我在考慮如何在Java中構(gòu)建樹狀數(shù)據(jù)結(jié)構(gòu):常見的方法包括遞歸、父子引用和使用節(jié)點列表等??梢酝ㄟ^創(chuàng)建樹節(jié)點類來表示樹形結(jié)構(gòu),例如,可以定義“樹節(jié)點”類為:class Node { T data; List<Node> children; Node parent; }。
我將提供詳細(xì)分析,看看哪些方法在實際項目中更常見,如何構(gòu)建包含父子關(guān)系和操作樹的數(shù)據(jù)結(jié)構(gòu)。
這個問題的答案可能會包括多個常見的實現(xiàn)方式。
在 Java 中構(gòu)建樹狀結(jié)構(gòu),常見方法包括:1. 遞歸:從父子關(guān)系數(shù)據(jù)遞歸構(gòu)建樹;2. 使用 Map/HashMap:存儲節(jié)點并根據(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);
}
}
實現(xiàn):Java構(gòu)建樹狀結(jié)構(gòu)數(shù)據(jù)
目的: 在 Java 中構(gòu)建樹狀結(jié)構(gòu)數(shù)據(jù)是一個非常常見的需求,通常用于展示分層數(shù)據(jù)(如:組織架構(gòu)、菜單、文件目錄等),下面詳細(xì)介紹幾種常見的方法、代碼案例及實際項目中的實踐經(jīng)驗。比如實現(xiàn)這樣的分層效果:

這些數(shù)據(jù)都在數(shù)據(jù)庫,所以查數(shù)據(jù)庫:查出所有的數(shù)據(jù),然后再組裝返回給前端解析即可,當(dāng)然數(shù)據(jù)表中應(yīng)該設(shè)計成這樣:

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

