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
相关产品推荐
相关产品推荐

