Java构建Tree并实现节点名称模糊查询
乐于学习分享… 大家加油努力
package com.tom.backtrack;import lombok.Data;
import lombok.Getter;import java.util.ArrayList;
import java.util.List;
import java.util.Objects;/*** 树节点** @author zx* @date 2025-05-27 19:51*/
@Data
public class TreeNode {private Long id;private Long parentId;private String name;private List<TreeNode> children;/*** 是否根节点*/@Getterprivate Boolean rootNode;/*** 是否叶子节点**/@Getterprivate Boolean leafNode;/*** 设置子节点数据,设置为protected禁止外部调用**/public void setChildren(List<TreeNode> children) {this.children = children;this.rootNode = Objects.equals(getParentId(), 0L);this.leafNode = children == null || children.isEmpty();}// 模糊搜索方法,返回符合条件的节点列表public List<TreeNode> fuzzySearch(String query) {List<TreeNode> results = new ArrayList<>(); // 存储搜索结果fuzzySearchHelper(this, query, results); // 调用搜索辅助方法return results;}// 判断节点是否有子节点的方法private boolean hasLeaf(TreeNode node) {for (TreeNode child : node.children) {if (child.children.isEmpty()) { // 如果存在叶子节点,返回truereturn true;}}return false; // 否则返回false}// 判断节点的子节点是否存在符合条件的节点的方法private boolean hasMatchingChild(TreeNode node, String query) {for (TreeNode child : node.children) {if (child.name.contains(query) || hasMatchingChild(child, query)) {return true; // 如果子节点的名称包含查询字符串,或者子节点的子节点存在符合条件的节点,则返回true}}return false; // 否则返回false}// 递归搜索辅助方法private void fuzzySearchHelper(TreeNode node, String query, List<TreeNode> results) {// 如果当前节点的值包含查询字符串,并且至少有一个子节点也符合查询条件,则将其添加到结果列表中if (node.name.contains(query) || hasMatchingChild(node, query)) {results.add(node);}// 递归搜索子节点List<TreeNode> removableChildren = new ArrayList<>();for (TreeNode child : node.children) {fuzzySearchHelper(child, query, results);if (!hasLeaf(child)) {removableChildren.add(child);}}// 删除没有树叶的子节点for (TreeNode child : removableChildren) {node.children.remove(child);}}
}
package com.tom.backtrack;import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;/*** <h1>树工具类</h1>* <pre>* 1.该工具类支持: 构建树结构、模糊查询* 2.优化建议:* 1.扁平化处理:如果只需要匹配结果而不需要保持树结构,可以先将所有节点展平为列表再进行过滤* 2.性能优化:* - 对于大数据量场景,可结合缓存机制或异步加载* - 使用 equalsIgnoreCase() 支持不区分大小写的模糊匹配* 3.高级模糊匹配:使用正则表达式或第三方库如 Apache Commons Text 的模糊匹配功能* 3.注意事项:* 1.若需返回完整树结构但仅展示匹配路径,可在递归中剪枝保留匹配路径* 2.若数据来源于数据库,推荐在 SQL 层面使用 LIKE '%keyword%' 进行模糊查询以减少内存开销(多次 SQL 查询 + 应用层拼接)** 案例:* 场景假设:* tree_node 的表,用于存储树形结构数据:* CREATE TABLE tree_node (* id BIGINT PRIMARY KEY,* parent_id BIGINT, -- 父节点ID,顶级节点为0或NULL* name VARCHAR(255) -- 节点名称* );* 根据某个关键词模糊查询节点名称,并获取其所有父节点路径或子树结构。* 实现方式一:多次 SQL 查询 + 应用层拼接(推荐)* 步骤:* - 先模糊查询匹配的节点* - 递归向上查父节点,直到根节点* - 应用层构建树结构* 示例代码:* 1.模糊查询匹配节点* SELECT * FROM tree_node WHERE name LIKE '%keyword%';* 2.根据匹配节点 ID,逐级向上查找父节点* -- 假设匹配到 id = 5 的节点* SELECT * FROM tree_node WHERE id = 5;* SELECT * FROM tree_node WHERE id = (SELECT parent_id FROM tree_node WHERE id = 5);* -- 依次类推,直到 parent_id 为 NULL 或 0** 通过java、python等代码逻辑循环执行上述语句,构建出完整路径。* 实现方式二:使用存储过程模拟递归查询(适用于固定层级)* 创建一个存储过程,递归查找所有子节点:* DELIMITER $$** CREATE PROCEDURE search_tree(IN keyword VARCHAR(255))* BEGIN* CREATE TEMPORARY TABLE IF NOT EXISTS temp_result (* id BIGINT,* parent_id BIGINT,* name VARCHAR(255)* );** TRUNCATE TABLE temp_result;** -- 插入初始匹配节点* INSERT INTO temp_result (id, parent_id, name)* SELECT id, parent_id, name FROM tree_node WHERE name LIKE CONCAT('%', keyword, '%');** -- 循环插入父节点* WHILE ROW_COUNT() > 0 DO* INSERT INTO temp_result (id, parent_id, name)* SELECT t.id, t.parent_id, t.name* FROM tree_node t* INNER JOIN temp_result tr ON t.id = tr.parent_id* WHERE t.id NOT IN (SELECT id FROM temp_result);* END WHILE;** -- 返回结果* SELECT * FROM temp_result ORDER BY id;** DROP TEMPORARY TABLE IF EXISTS temp_result;* END$$** DELIMITER ;** 调用方式:* CALL search_tree('1-2-1');* 注意:* - MySQL 5.7 不支持 CTE,建议优先在应用层处理树结构。* - 如果可以升级到 MySQL 8.0,可以直接使用递归查询(CTE):* WITH RECURSIVE cte AS (* SELECT * FROM tree_node WHERE name LIKE '%keyword%'* UNION ALL* SELECT t.* FROM tree_node t INNER JOIN cte c ON t.id = c.parent_id* )** SELECT * FROM cte;** </pre>* @author zx* @date 2025-05-27 20:05*/
public class TreeUtil {public static List<TreeNode> buildTree(List<TreeNode> treeNodeList) {if (treeNodeList == null || treeNodeList.size() == 0) {return treeNodeList;}// 2.根据父节点进行分组Map<Long, List<TreeNode>> groups = treeNodeList.stream().collect(Collectors.groupingBy(TreeNode::getParentId));return treeNodeList.stream().filter(Objects::nonNull).peek(pnd -> {List<TreeNode> ts = groups.get(pnd.getId());pnd.setChildren(ts);}).filter(TreeNode::getRootNode).collect(Collectors.toList());}public static List<TreeNode> fuzzySearch(List<TreeNode> treeNodes, String keyword) {List<TreeNode> result = new ArrayList<>();for (TreeNode node : treeNodes) {traverseAndCollect(node, keyword, result);}return result;}private static boolean traverseAndCollect(TreeNode node, String keyword, List<TreeNode> result) {boolean isMatched = false;//模糊查询---这里使用正则表达式,临时使用字符串包含方法if (node.getName().contains(keyword)) {isMatched = true;}if (node.getChildren() != null && !node.getChildren().isEmpty()) {List<TreeNode> matchedChildren = new ArrayList<>();for (TreeNode child : node.getChildren()) {if (traverseAndCollect(child, keyword, result)) {matchedChildren.add(child);isMatched = true;}}// 可选:只保留匹配的子节点node.setChildren(matchedChildren);}if (isMatched) {result.add(node);}return isMatched;}public static List<TreeNode> buildTreeNodeData() {List<TreeNode> treeNodeList = new ArrayList<>();TreeNode treeNode1 = new TreeNode();treeNode1.setId(1L);treeNode1.setParentId(0L);treeNode1.setName("1");treeNodeList.add(treeNode1);TreeNode treeNode2 = new TreeNode();treeNode2.setId(2L);treeNode2.setParentId(0L);treeNode2.setName("2");treeNodeList.add(treeNode2);TreeNode treeNode3 = new TreeNode();treeNode3.setId(3L);treeNode3.setParentId(1L);treeNode3.setName("1-1");treeNodeList.add(treeNode3);TreeNode treeNode4 = new TreeNode();treeNode4.setId(4L);treeNode4.setParentId(1L);treeNode4.setName("1-2");treeNodeList.add(treeNode4);TreeNode treeNode5 = new TreeNode();treeNode5.setId(5L);treeNode5.setParentId(4L);treeNode5.setName("1-2-1");treeNodeList.add(treeNode5);TreeNode treeNode6 = new TreeNode();treeNode6.setId(6L);treeNode6.setParentId(2L);treeNode6.setName("2-1");treeNodeList.add(treeNode6);TreeNode treeNode7 = new TreeNode();treeNode7.setId(7L);treeNode7.setParentId(2L);treeNode7.setName("2-2");treeNodeList.add(treeNode7);TreeNode treeNode8 = new TreeNode();treeNode8.setId(8L);treeNode8.setParentId(6L);treeNode8.setName("2-2-1");treeNodeList.add(treeNode8);return treeNodeList;}public static void main(String[] args) {List<TreeNode> treeNodeList = buildTree(buildTreeNodeData());List<TreeNode> newTreeNodeList = new ArrayList<>();newTreeNodeList.addAll(treeNodeList);System.out.println(newTreeNodeList);String keyword = "1-2-1";List<TreeNode> searchResultTreeNodeList = buildTree(fuzzySearch(treeNodeList, keyword));System.out.println(searchResultTreeNodeList);}
}