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

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

組裝時(shí)只需要組裝成一個(gè) list 返回即可,前端拿到 list 解析。
1. 遞歸方式
原理: 遞歸方法基于“父子關(guān)系”的層級(jí)遍歷,從根節(jié)點(diǎn)開(kāi)始遞歸查找和添加子節(jié)點(diǎn)。適合節(jié)點(diǎn)層次較少的場(chǎng)景。
實(shí)現(xiàn): 利用遞歸的方式進(jìn)行樹(shù)狀結(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ù):本來(lái)這一步是需要從數(shù)據(jù)庫(kù)中查找所有的數(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ù),還是需要過(guò)濾掉最頂層節(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、寫(xiě)方法:
public class Test {
// 假數(shù)據(jù):本來(lái)這一步是需要從數(shù)據(jù)庫(kù)中查找所有的數(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));
// 開(kāi)始構(gòu)建樹(shù)
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)列表中。這樣可以避免遞歸的性能問(wèn)題。
好處: 在 Java 中,使用 Map 來(lái)構(gòu)建樹(shù)結(jié)構(gòu)是一種高效的方法。該方法避免了遞歸的性能問(wèn)題,并且可以快速查找節(jié)點(diǎn),提高構(gòu)建樹(shù)的效率。
實(shí)現(xiàn): 如下:
1、上面的 DeptEntity 類代碼不變
2、寫(xiě)方法:
public class Test {
// 假數(shù)據(jù):本來(lái)這一步是需要從數(shù)據(jù)庫(kù)中查找所有的數(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)建樹(shù)結(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ù):本來(lái)這一步是需要從數(shù)據(jù)庫(kù)中查找所有的數(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)建樹(shù)結(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)建樹(shù)(可能達(dá)到 O(n²))效率更高。
(2)空間復(fù)雜度:使用了 Map<Long, DeptEntity>,其空間復(fù)雜度是 O(n),額外開(kāi)銷較小。
(3)避免遞歸問(wèn)題:采用 Map 直接查找父節(jié)點(diǎn),避免遞歸調(diào)用,避免棧溢出問(wèn)題,特別適用于深度較大的樹(shù)結(jié)構(gòu)。
3、適合場(chǎng)景:
? 數(shù)據(jù)量大,如百萬(wàn)級(jí)別的分類、菜單、組織架構(gòu)等。
? 層級(jí)較深,如文件目錄、權(quán)限管理系統(tǒng)。
? 頻繁查詢,構(gòu)建后可以緩存樹(shù),提高性能。
4、不適合場(chǎng)景:
? 數(shù)據(jù)量特別?。?lt;100),可以直接使用遞歸方法,代碼更簡(jiǎn)潔。
4. 總結(jié)

到此這篇關(guān)于java構(gòu)建樹(shù)形結(jié)構(gòu)的方式及如何組裝樹(shù)狀結(jié)構(gòu)數(shù)據(jù)的文章就介紹到這了,更多相關(guān)java構(gòu)建樹(shù)形結(jié)構(gòu)數(shù)據(jù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot+EasyPOI輕松實(shí)現(xiàn)Excel和Word導(dǎo)出PDF
在企業(yè)級(jí)開(kāi)發(fā)中,將 Excel 和 Word 文檔導(dǎo)出為 PDF 是常見(jiàn)需求,本文將結(jié)合 ??EasyPOI和 ??Aspose 系列工具實(shí)現(xiàn) Excel 和 Word 到 PDF 的轉(zhuǎn)換,需要的可以了解下2025-07-07
Spring學(xué)習(xí)筆記3之消息隊(duì)列(rabbitmq)發(fā)送郵件功能
這篇文章主要介紹了Spring學(xué)習(xí)筆記3之消息隊(duì)列(rabbitmq)發(fā)送郵件功能的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07
Spring?Boot應(yīng)用打WAR包后無(wú)法注冊(cè)到Nacos的問(wèn)題及解決方法
當(dāng)我們將?Spring?Boot?應(yīng)用打包成?WAR?并部署到外部?Tomcat?服務(wù)器時(shí),可能會(huì)遇到服務(wù)無(wú)法注冊(cè)到?Nacos?的情況,其原因主要是應(yīng)用獲取不到正確的服務(wù)器端口,下面給大家介紹Spring?Boot?應(yīng)用打?WAR?包后無(wú)法注冊(cè)到?Nacos的問(wèn)題及解決方法,感興趣的朋友跟隨小編一起看看吧2024-06-06
Java 將一個(gè)字符重復(fù)n遍過(guò)程詳解
這篇文章主要介紹了Java 將一個(gè)字符重復(fù)n遍過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10
如何用Java來(lái)進(jìn)行文件切割和簡(jiǎn)單的內(nèi)容過(guò)濾的實(shí)現(xiàn)
這篇文章主要介紹了如何用Java來(lái)進(jìn)行文件切割和簡(jiǎn)單的內(nèi)容過(guò)濾的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-01-01

