Java构建Tree并实现节点名称模糊查询

乐于学习分享... 大家加油努力

java 复制代码
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;

    /**
     * 是否根节点
     */
    @Getter
    private Boolean rootNode;

    /**
     * 是否叶子节点
     **/
    @Getter
    private 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()) { // 如果存在叶子节点,返回true
                return 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);
        }
    }
}
java 复制代码
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);
    }
}
相关推荐
Dcs几秒前
深入理解短链服务:原理、设计与实现全解析
java
新程快咖员4 分钟前
IDEA插件MPVP(maven)更新2.2.x版本啦,实操带你体验快速查询maven依赖版本!
java·后端
南汐以墨12 分钟前
Mybatis:灵活掌控SQL艺术
java·数据库·sql
天天摸鱼的java工程师22 分钟前
ConcurrentHashMap 原理与实战:从底层实现到高并发场景应用
java·后端
天天摸鱼的java工程师29 分钟前
Java 多线程与线程安全:volatile、锁与同步机制全解析
java·后端
超人小子31 分钟前
c++之字符串
java·c语言·c++
黄雪超34 分钟前
JVM——JVM运行时数据区的内部机制是怎样的?
java·开发语言·jvm
别惹CC1 小时前
Spring Boot 3 整合 MQ 构建聊天消息存储系统
java·spring boot·后端
还是鼠鼠1 小时前
Maven-生命周期
java·开发语言·后端·maven
Java技术小馆2 小时前
VitalInsight智能体检报告解读
java·架构