實現一個通用的樹形結構構建工具

文章目錄

  • 1. 前言
  • 2. 樹結構
  • 3. 具體實現邏輯
    • 3.1 TreeNode
    • 3.2 TreeUtils
    • 3.3 例子
  • 4. 小結


1. 前言

樹結構的生成在項目中應該都比較常見,比如部門結構樹的生成,目錄結構樹的生成,但是大家有沒有想過,如果在一個項目中有多個樹結構,那么每一個都要定義一個生成方法顯然是比較麻煩的,所以我們就想寫一個通用的生成樹方法,下面就來看下如何來寫。


2. 樹結構

在這里插入圖片描述
看上面的圖,每一個節點都會有三個屬性

  • parentId 表示父節點 ID,根節點的父結點 ID = null
  • id 表示當前節點 ID,這個 ID 用來標識一個節點
  • children 是當前節點的子節點

那么上面來介紹完基本的幾個屬性,下面就來看下具體的實現了。


3. 具體實現邏輯

3.1 TreeNode

TreeNode 是公共節點,就是頂層父類,里面的屬性就是上面圖中的三個。

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class TreeNode<T, V> {private T parentId;private T id;private List<TreeNode<T, V>> children;public TreeNode(T parentId, T id) {this.parentId = parentId;this.id = id;}public void addChild(TreeNode<T, V> treeNode){if(children == null){children = new ArrayList<>();}children.add(treeNode);}}

TreeNode 里面的 id 都是用的范型,其中 T 就是 id 的類型,因為這個 id 有可能是 Long、Int、String … 類型,不一定是 Long。另一個 V 就是具體的節點類型。

使用范型的好處就是擴展性高,不需要把屬性寫死。


3.2 TreeUtils

這個是工具類,專門實現樹的構建以及一些其他的方法,下面一個一個來看。首先是創建樹的方法:

/*** 構建一棵樹** @param flatList* @param <T>* @param <V>* @return*/
public static <T, V extends TreeNode<T, V>> List<V> buildTree(List<V> flatList) {if (flatList == null || flatList.isEmpty()) {return null;}Map<T, TreeNode<T, V>> nodeMap = new HashMap<>();for (TreeNode<T, V> node : flatList) {nodeMap.put(node.getId(), node);}// 查找根節點List<V> rootList = new ArrayList<>();for (V node : flatList) {// 如果父節點為空,就是一個根節點if (node.getParentId() == null) {rootList.add(node);} else {// 父節點不為空,就是子節點TreeNode<T, V> parent = nodeMap.get(node.getParentId());if (parent != null) {parent.addChild(node);} else {rootList.add(node);}}}return rootList;
}

整體時間復雜度:O(n),創建的時候傳入節點集合,然后返回根節點集合。里面的邏輯是首先放到一個 nodeMap 中,然后遍歷傳入的集合,根據 parentId 進行不同的處理。邏輯不難,看注釋即可。但是創建樹的時候,有時候我們希望根據某個順序對樹進行排序,比如同一層的我想根據名字或者 id 進行排序,順序或者倒序都可以,那么就可以使用下面的方法。

/**
* 構建一棵排序樹
*
* @param flatList
* @param comparator
* @param <T>
* @param <V>
* @return
*/
public static <T, V extends TreeNode<T, V>> List<V> buildTreeWithCompare(List<V> flatList, Comparator<V> comparator) {if (flatList == null || flatList.isEmpty()) {return Collections.emptyList(); // 返回空列表而不是null,這通常是一個更好的實踐}// 子節點分組Map<T, List<V>> childGroup = flatList.stream().filter(v -> v.getParentId() != null).collect(Collectors.groupingBy(V::getParentId));// 找出父節點List<V> roots = flatList.stream().filter(v -> v.getParentId() == null).sorted(comparator) // 根據提供的比較器對根節點進行排序.collect(Collectors.toList());// 構建樹for (V root : roots) {buildTreeRecursive(root, childGroup, comparator);}return roots;
}private static <T, V extends TreeNode<T, V>> void buildTreeRecursive(V parent, Map<T, List<V>> childGroup, Comparator<V> comparator) {List<V> children = childGroup.get(parent.getId());if (children != null) {// 對子節點進行排序children.sort(comparator);// 將排序后的子節點添加到父節點中children.forEach(parent::addChild);// 遞歸對子節點繼續處理children.forEach(child -> buildTreeRecursive(child, childGroup, comparator));}
}

這里面是使用的遞歸,其實也可以使用層次遍歷的方式來寫,或者直接用第一個 buildTree 方法來往里面套也行。

上面這兩個是關鍵的方法,那么下面再給出一些其他的非必要方法,比如查詢節點數。下面這個方法就是獲取以 root 為根的數的節點數。

/*** 查詢以 root 為根的樹的節點數** @param root* @param <T>* @param <V>* @return*/
private static <T, V extends TreeNode<T, V>> long findTreeNodeCount(TreeNode<T, V> root) {if (root == null) {return 0;}long res = 1;List<TreeNode<T, V>> children = root.getChildren();if (children == null || children.isEmpty()) {return res;}for (TreeNode<T, V> child : children) {res += findTreeNodeCount(child);}return res;
}

上面是傳入一個根節點,獲取這棵樹的節點數,而下面的就是傳入一個集合來分別獲取節點數,里面也是調用了上面的 findTreeNodeCount 方法去獲取。

/*** 查詢給定集合的節點數** @param nodes 根節點集合* @param <T>* @param <V>* @return*/
public static <T, V extends TreeNode<T, V>> HashMap<V, Long> findTreeNodeCount(List<V> nodes) {if (nodes == null || nodes.isEmpty()) {return new HashMap<>(); // 返回空列表而不是null,這通常是一個更好的實踐}HashMap<V, Long> map = new HashMap<>();for (V root : nodes) {map.put(root,  findTreeNodeCount(root));}return map;
}

下面再給一下獲取數的深度的方法。

// 查找樹的最大深度
private static <T, V extends TreeNode<T, V>> int getMaxDepthV(TreeNode<T, V> root) {if (root == null || root.getChildren() == null || root.getChildren().isEmpty()) {return 1;}return 1 + root.getChildren().stream().mapToInt(TreeUtils::getMaxDepthV).max().getAsInt();
}public static <T, V extends TreeNode<T, V>> int getMaxDepth(V root) {return getMaxDepthV(root);
}

最后,我們拿到一棵樹之后,肯定有時候會希望在里面查找一些具有特定屬性的節點,比如某個節點名字是不是以 xx 開頭 … ,這時候就可以用下面的方法。

// 查找所有具有特定屬性的節點
public static <T, V extends TreeNode<T, V>> List<V> findAllNodesByProperty(TreeNode<T, V> root, Function<V, Boolean> predicate) {if (root == null) {return Collections.emptyList();}List<V> result = new ArrayList<>();// 符合屬性值if (predicate.apply((V) root)) {result.add((V) root);}if (root.getChildren() == null || root.getChildren().isEmpty()) {return result;}for (TreeNode<T, V> child : root.getChildren()) {result.addAll(findAllNodesByProperty(child, predicate));}return result;
}

好了,方法就這么多了,其他方法如果你感興趣也可以繼續補充下去,那么這些方法是怎么用的呢?范型的好處要怎么體現呢?下面就來看個例子。


3.3 例子

首先我們有一個部門類,里面包括部門的名字,然后我需要對這個部門集合來構建一棵部門樹。

@Data
@ToString
@NoArgsConstructor
public class Department extends TreeNode<String, Department> {private String name;public Department(String id, String parentId, String name){super(parentId, id);this.name = name;}}

構建的方法如下:

public class Main {public static void main(String[] args) {List<Department> flatList = new ArrayList<>();flatList.add(new Department("1", null, "Sales"));flatList.add( new Department("2", "1", "East Sales"));flatList.add( new Department("3", "1","West Sales"));flatList.add( new Department("4", "2","East Sales Team 1"));flatList.add( new Department("5", "2","East Sales Team 2"));flatList.add( new Department("6", "3","West Sales Team 1"));List<Department> departments = TreeUtils.buildTreeWithCompare(flatList, (o1, o2) -> {return o2.getName().compareTo(o1.getName());});Department root = departments.get(0);List<Department> nodes = TreeUtils.findAllNodesByProperty(root, department -> department.getName().startsWith("East"));System.out.println(nodes);System.out.println(TreeUtils.getMaxDepth(root));System.out.println(TreeUtils.findTreeNodeCount(nodes));}}

可以看下 buildTreeWithCompare 的輸出:
在這里插入圖片描述
其他的輸出如下:

[Department(name=East Sales), Department(name=East Sales Team 2), Department(name=East Sales Team 1)]
3
{Department(name=East Sales)=3, Department(name=East Sales Team 2)=1, Department(name=East Sales Team 1)=1}

4. 小結

工具類就寫好了,從例子就可以看出范型的好處了,用了范型之后只要實現類繼承了 TreeNode,就可以直接用 TreeUtils 里面的方法,并且返回的還是具體的實現類,而不是 TreeNode。





如有錯誤,歡迎指出!!!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/64971.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/64971.shtml
英文地址,請注明出處:http://en.pswp.cn/web/64971.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

day30-awk進階

awk模式種類 awk的模式分為這幾種 正則表達式 基本正則擴展正則比較表達式范圍表達式特殊模式 BEGINEND awk比較運算符&#xff08;語法&#xff09; 關系運算符解釋示例<小于x<y<小于等于x<y等于xy!不等于x!y>大于等于x>y>大于x>y~匹配正則x~/正則…

大語言模型(LLM)一般訓練過程

大語言模型(LLM)一般訓練過程 數據收集與預處理 收集:從多種來源收集海量文本數據,如互聯網的新聞文章、博客、論壇,以及書籍、學術論文、社交媒體等,以涵蓋豐富的語言表達和知識領域。例如,訓練一個通用型的LLM時,可能會收集數十億甚至上百億字的文本數據.清洗:去除…

數據庫新建用戶后(Host:%),報錯:localhost無法連接

存在問題 在給數據庫&#xff08;MySQL、MariaDB等&#xff09;創建了新的用戶名&#xff08;eg&#xff1a;maxscale&#xff09;后&#xff0c;無法使用新用戶名登錄&#xff0c;并報如下錯誤&#xff1a;ERROR 1045 (28000): Access denied for user maxscalelocalhost (us…

2024年大型語言模型(LLMs)的發展回顧

2024年對大型語言模型&#xff08;LLMs&#xff09;來說是充滿變革的一年。以下是對過去一年中LLMs領域的關鍵進展和主題的總結。 GPT-4的壁壘被打破 去年&#xff0c;我們還在討論如何構建超越GPT-4的模型。如今&#xff0c;已有18個組織擁有在Chatbot Arena排行榜上超越原…

數據挖掘——支持向量機分類器

數據挖掘——支持向量機分類器 支持向量機最小間隔面推導基于軟間隔的C-SVM非線性SVM與核變換常用核函數 支持向量機 根據統計學習理論&#xff0c;學習機器的實際風險由經驗風險值和置信范圍值兩部分組成。而基于經驗風險最小化準則的學習方法只強調了訓練樣本的經驗風險最小…

檢索增強生成

概述 檢索增強生成&#xff08;Retrieval-Augmented Generation&#xff0c;RAG&#xff09;是一種將信息檢索與語言模型相結合的技術。由Facebook AI Research于2020年提出&#xff0c;它把數據庫的優勢與語言模型的優勢相結合。它能讓模型從外部知識庫中檢索信息&#xff0c…

在 SQL 中,區分 聚合列 和 非聚合列(nonaggregated column)

文章目錄 1. 什么是聚合列&#xff1f;2. 什么是非聚合列&#xff1f;3. 在 GROUP BY 查詢中的非聚合列問題示例解決方案 4. 為什么 only_full_group_by 要求非聚合列出現在 GROUP BY 中&#xff1f;5. 如何判斷一個列是聚合列還是非聚合列&#xff1f;6. 總結 在 SQL 中&#…

ETL處理工具Kettle入門

1. Kettle簡介 Kettle&#xff08;現已更名為Pentaho Data Integration&#xff0c;簡稱PDI&#xff09;是一個開源的ETL工具&#xff0c;能夠進行數據的抽取&#xff08;Extract&#xff09;、轉換&#xff08;Transform&#xff09;和加載&#xff08;Load&#xff09;。它是…

petalinux2017.4對linux4.9.0打實時補丁

準備工作&#xff1a; 1.windows&#xff1a;安裝vivado 2017.4&#xff0c;xilinx sdk 2017.4 2.ubuntu16.04&#xff1a;安裝petalinux 2017 3.黑金ax7020&#xff0c;sd卡 一、準備linux內核的操作系統 1.1 Petalinux配置 Petalinux使用教程-CSDN博客非常詳細&#xf…

Maven 教程之 pom.xml 詳解

Maven 教程之 pom.xml 詳解 pom.xml 簡介 什么是 pom POM 是 Project Object Model 的縮寫,即項目對象模型。 pom.xml 就是 maven 的配置文件,用以描述項目的各種信息。 pom 配置一覽 <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi

Golang的緩存一致性策略

Golang的緩存一致性策略 一致性哈希算法 在Golang中&#xff0c;緩存一致性策略通常使用一致性哈希算法來實現。一致性哈希算法能夠有效地解決緩存節點的動態擴容、縮容時數據重新分布的問題&#xff0c;同時能夠保證數據訪問的均衡性。 一致性哈希算法的核心思想是將節點的哈希…

【機器學習:一、機器學習簡介】

機器學習是當前人工智能領域的重要分支&#xff0c;其目標是通過算法從數據中提取模式和知識&#xff0c;并進行預測或決策。以下從 機器學習概述、有監督學習 和 無監督學習 三個方面進行介紹。 機器學習概述 機器學習定義 機器學習&#xff08;Machine Learning&#xff0…

藍橋杯JAVA--003

需求 2.代碼 public class RegularExpressionMatching {public boolean isMatch(String s, String p) {if (p.isEmpty()) {return s.isEmpty();}boolean firstMatch !s.isEmpty() && (s.charAt(0) p.charAt(0) || p.charAt(0) .);if (p.length() > 2 && p…

被催更了,2025元旦源碼繼續免費送

“時間從來不會停下&#xff0c;它只會匆匆流逝。抓住每一刻&#xff0c;我們才不會辜負自己。” 聯系作者免費領&#x1f496;源&#x1f496;碼。 三聯支持&#xff1a;點贊&#x1f44d;收藏??留言&#x1f4dd;歡迎留言討論 更多內容敬請期待。如有需要源碼可以聯系作者免…

WebRTC的線程事件處理

1. 不同平臺下處理事件的API&#xff1a; Linux系統下&#xff0c;處理事件的API是epoll或者select&#xff1b;Windows系統下&#xff0c;處理事件的API是WSAEventSelect&#xff0c;完全端口&#xff1b;Mac系統下&#xff0c;kqueue 2. WebRTC下的事件處理類&#xff1a; …

關于Zotero

1、文獻數據庫&#xff1a; Zotero的安裝 Zotero安裝使用_zotero只能安裝在c盤嗎-CSDN博客 2、如何使用zotero插件 我剛下載的時候就結合使用的是下面的這兩個博主的分享&#xff0c;感覺暫時是足夠的。 Zotero入&#x1f6aa;基礎 - 小紅書 Green Frog申請easyscholar密鑰…

企業三要素如何用PHP實現調用

一、什么是企業三要素&#xff1f; 企業三要素即傳入的企業名稱、法人名稱、社會統一信用代碼或注冊號&#xff0c;校驗此三項是否一致。 二、具體怎么樣通過PHP實現接口調用&#xff1f; 下面我們以阿里云為例&#xff0c;通過PHP示例代碼進行調用&#xff0c;參考如下&…

Go 語言中強大的配置管理庫—Viper

Viper 是 Go 語言中強大的配置管理庫&#xff0c;廣泛用于云原生和微服務開發中。它支持多種配置文件格式&#xff08;如 YAML、JSON、TOML 等&#xff09;、環境變量、命令行參數以及遠程配置管理。 Viper 的主要功能 1. 支持多種格式的配置文件&#xff1a; ? YAML、JSON…

鴻蒙-封裝loading動畫

import { AnimatorOptions, AnimatorResult } from "kit.ArkUI" export enum SpinImageType { RedLoading, WhiteLoading } Component export struct SpinImage { Prop type?: SpinImageType Prop url?: string State animatedValue: number 0 …

今日復盤103周五(189)

1、早上&#xff0c;看了一下二手書里的十種主要游戲類型的相關內容。 其實收獲不大&#xff0c;主要是引發思考。 2、白天&#xff0c;持續多日的模式1的白模原型關卡結束&#xff0c;開始轉做準正式資源的關卡&#xff0c; 但進度低于預期。 并不是改改參數那么簡單輕松&a…