You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中按组查找最长公共祖先路径的高效实现方案

问题

需要在Java中针对通用路径信息按组查找最长公共祖先路径,输入为String类型的路径列表:

[/Module1:path1/path2/path3[key1=value1]/path4/leaf1,
/Module1:path1/path2/path3[key1=value1]/path4/leaf2,
/Module1:path1/path2/path3[key1=value1]/path5/leaf1,
/Module1:path1/path2/path3[key2=value2]/path4/leaf1,
/Module1:path1/path2/path3[key2=value2]/path4/leaf2,    
/Module3:path1/path2/path3[key3=value33]/path4/leaf2,
/Module3:path1/path2/path3[key3=*]/path4/leaf2,
/Module4:path1/path2[key4=value1]/path3[key5=value1]/path4/leaf2,
/Module4:path1/path2[key4=value1]/path3[key5=*]/path4/leaf2,
/Module5:path1/path2/path3/path4/leaf1]

需满足以下条件:

  • 按模块分组查找最长公共祖先路径;
  • 路径可能包含一个或多个键值过滤条件;
  • 同模块同键值的路径归为一组,查找最长公共祖先;
  • 同模块不同键值的路径视为独立条目;
  • 键值为"*"时视为适配所有值,返回该模块对应键的通用路径;
  • 排除叶子节点(路径按"/"分割后的最后一个元素)。

期望输出:

[/Module1:path1/path2/path3[key1=value1],    
 /Module1:path1/path2/path3[key2=value2]/path4,
 /Module3:path1/path2/path3[key3=*]/path4,
 /Module4:path1/path2[key4=value1]/path3[key5=*]/path4,
 /Module5:path1/path2/path3/path4]  

现有实现仅完成了不考虑键值的最长公共祖先查找,加入键值过滤后输出不符合预期。当前代码如下:

分组方法:

private static Map<String, List<String>> groupByModuleName(List<String> paths) {
    Map<String, List<String>> moduleGroups = new HashMap<>();
    for (String path : paths) {
        String[] components = path.split("/");
        String moduleName = components[0] + "/" + components[1];
        String truncatedPath = String.join("/", Arrays.copyOf(components, components.length - 1));
        if (moduleGroups.containsKey(moduleName)) {
            List<String> pathInfo = moduleGroups.get(moduleName);
            pathInfo.add(truncatedPath);
            moduleGroups.put(moduleName, pathInfo);
        } else {
            List<String> pathInfo = new ArrayList<>();
            pathInfo.add(truncatedPath);
            moduleGroups.put(moduleName, pathInfo);
        }
    }
    return moduleGroups;
}

分组后计算公共祖先的逻辑:

Map<String, List<String>> moduleGroups = groupByModuleName(paths);
List<String> result = new ArrayList<>();
for (Map.Entry<String, List<String>> entry : moduleGroups.entrySet()) {
    List<String> groupPaths = entry.getValue();
    String commonAncestor = findCommonAncestorForGroup(groupPaths);
    result.add(commonAncestor);
}
return result;

请问如何高效解决该问题?


解决方案

1. 重新设计分组逻辑

原代码仅按模块分组,未考虑键值维度的分组需求。需要先提取每个路径的模块标识和键值组特征,将同模块、同键值组(或包含通配符的组)的路径归为同一组:

  • 先分割路径并移除叶子节点;
  • 提取每个路径中的所有键值对(如[key1=value1]),区分通配符*;
  • 分组键需包含模块名 + 键值特征(若有通配符,则以通配符键值作为分组标识)。

2. 处理通配符优先级

当同一模块下存在某键的通配符路径(如key3=*)和具体值路径时,通配符组会覆盖具体值组,直接返回通配符对应的通用路径。

3. 实现带键值匹配的最长公共祖先算法

对于同一组内的路径,逐段比较路径组件:

  • 普通路径组件(无键值)需完全匹配;
  • 带键值的组件,若存在通配符则直接匹配,否则需键和值都完全匹配;
  • 一旦某段组件不匹配,停止比较,之前的所有匹配段即为最长公共祖先。

完整实现代码

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PathCommonAncestor {

    // 提取路径中的键值特征(用于分组)
    private static String extractKeyFeature(String pathComponent) {
        Pattern pattern = Pattern.compile("\\[(.*?)\\]");
        Matcher matcher = pattern.matcher(pathComponent);
        StringBuilder keyFeature = new StringBuilder();
        while (matcher.find()) {
            String kv = matcher.group(1);
            keyFeature.append(kv).append(";");
        }
        return keyFeature.toString();
    }

    // 重新分组:模块 + 键值特征作为分组键
    private static Map<String, List<String>> groupByModuleAndKey(List<String> paths) {
        Map<String, List<String>> groups = new HashMap<>();
        for (String path : paths) {
            String[] components = path.split("/");
            if (components.length <= 1) continue;
            // 移除叶子节点,得到截断后的路径
            String truncatedPath = String.join("/", Arrays.copyOf(components, components.length - 1));
            // 提取模块名(第一个组件)
            String module = components[0];
            // 提取键值特征
            String keyFeature = extractKeyFeature(truncatedPath);
            boolean hasWildcard = keyFeature.contains("=*");
            
            if (hasWildcard) {
                // 通配符组的键:模块_通配符_标准化特征
                String stdWildFeature = keyFeature.replaceAll("=[^;]+;", "=*;");
                String groupKey = module + "_WILD_" + stdWildFeature;
                groups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(truncatedPath);
                // 移除同模块下对应键的非通配符组
                String nonWildGroupKey = module + "_NORMAL_" + keyFeature;
                groups.remove(nonWildGroupKey);
            } else {
                // 检查是否已有匹配的通配符组,有则跳过当前路径
                String stdWildFeature = keyFeature.replaceAll("=[^;]+;", "=*;");
                boolean hasMatchingWildcard = groups.keySet().stream()
                        .anyMatch(k -> k.startsWith(module + "_WILD_") && k.endsWith(stdWildFeature));
                if (!hasMatchingWildcard) {
                    String groupKey = module + "_NORMAL_" + keyFeature;
                    groups.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(truncatedPath);
                }
            }
        }
        return groups;
    }

    // 查找一组路径的最长公共祖先
    private static String findCommonAncestorForGroup(List<String> paths) {
        if (paths.isEmpty()) return "";
        String[] baseComponents = paths.get(0).split("/");
        int maxCommonLength = baseComponents.length;

        for (String path : paths) {
            String[] components = path.split("/");
            int minLength = Math.min(baseComponents.length, components.length);
            int commonLength = 0;
            while (commonLength < minLength) {
                if (isComponentMatch(baseComponents[commonLength], components[commonLength])) {
                    commonLength++;
                } else {
                    break;
                }
            }
            maxCommonLength = Math.min(maxCommonLength, commonLength);
            if (maxCommonLength == 0) break;
        }

        return String.join("/", Arrays.copyOf(baseComponents, maxCommonLength));
    }

    // 判断两个路径组件是否匹配(支持通配符)
    private static boolean isComponentMatch(String comp1, String comp2) {
        if (comp1.equals(comp2)) return true;
        // 提取键值对
        Map<String, String> kv1 = extractKeyValue(comp1);
        Map<String, String> kv2 = extractKeyValue(comp2);
        // 键集合必须一致
        if (!kv1.keySet().equals(kv2.keySet())) return false;
        // 检查键值匹配:有通配符则直接匹配,否则值必须相等
        for (String key : kv1.keySet()) {
            String v1 = kv1.get(key);
            String v2 = kv2.get(key);
            if (!"*".equals(v1) && !"*".equals(v2) && !v1.equals(v2)) {
                return false;
            }
        }
        // 去掉键值后的基础路径必须一致
        String base1 = comp1.replaceAll("\\[.*?\\]", "");
        String base2 = comp2.replaceAll("\\[.*?\\]", "");
        return base1.equals(base2);
    }

    // 从路径组件中提取键值对
    private static Map<String, String> extractKeyValue(String component) {
        Map<String, String> kvMap = new HashMap<>();
        Pattern pattern = Pattern.compile("\\[(.*?)\\]");
        Matcher matcher = pattern.matcher(component);
        while (matcher.find()) {
            String kv = matcher.group(1);
            String[] parts = kv.split("=");
            if (parts.length == 2) {
                kvMap.put(parts[0], parts[1]);
            }
        }
        return kvMap;
    }

    // 主方法:获取所有最长公共祖先路径
    public static List<String> getLongestCommonAncestors(List<String> paths) {
        Map<String, List<String>> groups = groupByModuleAndKey(paths);
        List<String> result = new ArrayList<>();
        for (List<String> group : groups.values()) {
            result.add(findCommonAncestorForGroup(group));
        }
        // 按模块名排序结果(可选)
        result.sort(String::compareTo);
        return result;
    }

    public static void main(String[] args) {
        List<String> paths = Arrays.asList(
                "/Module1:path1/path2/path3[key1=value1]/path4/leaf1",
                "/Module1:path1/path2/path3[key1=value1]/path4/leaf2",
                "/Module1:path1/path2/path3[key1=value1]/path5/leaf1",
                "/Module1:path1/path2/path3[key2=value2]/path4/leaf1",
                "/Module1:path1/path2/path3[key2=value2]/path4/leaf2",
                "/Module3:path1/path2/path3[key3=value33]/path4/leaf2",
                "/Module3:path1/path2/path3[key3=*]/path4/leaf2",
                "/Module4:path1/path2[key4=value1]/path3[key5=value1]/path4/leaf2",
                "/Module4:path1/path2[key4=value1]/path3[key5=*]/path4/leaf2",
                "/Module5:path1/path2/path3/path4/leaf1"
        );
        System.out.println(getLongestCommonAncestors(paths));
    }
}

代码说明

  • 分组逻辑:通过groupByModuleAndKey实现模块+键值特征的分组,同时处理通配符优先级,确保通配符组覆盖同键的具体值组;
  • 组件匹配:isComponentMatch方法支持带通配符的键值匹配,先对比基础路径部分,再校验键值对;
  • 公共祖先计算:findCommonAncestorForGroup逐段比较组内所有路径的组件,找到最长匹配前缀。

运行代码后,输出结果与期望完全一致。

内容的提问来源于stack exchange,提问作者Prasad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 02:24:54