为何实现BFS与DFS时前者远快于后者?求优化方案
DFS与BFS查找有限长度无环路径的性能差异分析及DFS优化方案
性能差异的核心原因
你的测试中k=1时BFS比DFS快几个数量级,核心问题出在DFS实现里的vertexList.indexOf(u)操作,以及递归的额外开销:
- 线性查找的巨大开销:
vertexList.indexOf(u)是O(n)的线性扫描操作,你的图有19万个顶点,每次调用这个方法都要遍历近20万元素。DFS的递归过程中,标记访问、判断邻接点是否访问、回溯取消标记这三个步骤都要调用这个方法,每个邻接点的递归都会触发多次线性扫描,而出度平均105,累计的时间开销直接爆炸。 - 递归栈的额外开销:DFS采用递归实现,Java的递归调用涉及栈帧的创建和销毁,当调用次数较多时,这部分开销会被放大。
- BFS的天然优势:k=1时,BFS中的
path.contains(next)操作只需要检查最多2个元素(路径长度为1时,path里只有起点和当前节点),实际是O(1)的开销;同时BFS是迭代实现,没有递归栈的额外消耗,整体操作效率极高。
DFS的优化方案
1. 给顶点分配唯一整数ID,替换线性查找
最关键的优化是用哈希表维护顶点到唯一ID的映射,把O(n)的indexOf操作改成O(1)的哈希查找:
修改AdjGraph类,添加顶点ID映射:
public class AdjGraph { private int V; private int E; private List<Vertex> vertexList; private Map<Vertex, List<Edge>> vertexAdj; // 新增:顶点到唯一ID的映射 private Map<Vertex, Integer> vertexIdMap; public AdjGraph() { this.V = 0; this.E = 0; this.vertexList = new ArrayList<>(); this.vertexAdj = new HashMap<>(); this.vertexIdMap = new HashMap<>(); } public void addVertex(Vertex v) { this.vertexList.add(v); this.vertexAdj.put(v, new ArrayList<>()); // 分配自增ID this.vertexIdMap.put(v, this.V); this.V++; } // ... 其他原有方法不变 // 优化后的DFS实现 public Map<Integer, List<List<Vertex>>> findAllPathsUpToLengthByDFS(Vertex start, int k) { boolean[] visited = new boolean[vertexList.size()]; List<Vertex> path = new ArrayList<>(); Map<Integer, List<List<Vertex>>> allPaths = new HashMap<>(); findAllPathsUpToLengthByDFSUtil(start, k, visited, path, allPaths); return allPaths; } private void findAllPathsUpToLengthByDFSUtil(Vertex u, int k, boolean[] visited, List<Vertex> path, Map<Integer, List<List<Vertex>>> allPaths) { // 用O(1)的哈希查找代替O(n)的indexOf int uId = vertexIdMap.get(u); visited[uId] = true; path.add(u); int pathLength = path.size() - 1; if (pathLength > k) { visited[uId] = false; path.remove(path.size() - 1); return; } if (pathLength <= k) { allPaths.computeIfAbsent(pathLength, x -> new ArrayList<>()).add(new ArrayList<>(path)); } for (Edge edge : vertexAdj.get(u)) { Vertex v = edge.getEndVertex(); int vId = vertexIdMap.get(v); if (!visited[vId]) { findAllPathsUpToLengthByDFSUtil(v, k, visited, path, allPaths); } } path.remove(path.size() - 1); visited[uId] = false; } }
2. 替换递归为迭代式DFS
递归在调用次数较多时会有栈开销和栈溢出风险,改成迭代式DFS可以进一步提升性能:
public Map<Integer, List<List<Vertex>>> findAllPathsUpToLengthByDFS_Iterative(Vertex start, int k) { Map<Integer, List<List<Vertex>>> allPaths = new HashMap<>(); // 栈元素:当前顶点、访问标记数组副本、当前路径、当前路径长度 Stack<Object[]> stack = new Stack<>(); int startId = vertexIdMap.get(start); boolean[] initialVisited = new boolean[V]; initialVisited[startId] = true; List<Vertex> initialPath = new ArrayList<>(); initialPath.add(start); stack.push(new Object[]{start, initialVisited, initialPath, 0}); // 先添加起点路径(长度0) allPaths.computeIfAbsent(0, x -> new ArrayList<>()).add(new ArrayList<>(initialPath)); while (!stack.isEmpty()) { Object[] elem = stack.pop(); Vertex u = (Vertex) elem[0]; boolean[] visited = (boolean[]) elem[1]; List<Vertex> path = (List<Vertex>) elem[2]; int pathLength = (int) elem[3]; if (pathLength >= k) { continue; } for (Edge edge : vertexAdj.get(u)) { Vertex v = edge.getEndVertex(); int vId = vertexIdMap.get(v); if (!visited[vId]) { // 复制访问标记数组 boolean[] newVisited = Arrays.copyOf(visited, visited.length); newVisited[vId] = true; List<Vertex> newPath = new ArrayList<>(path); newPath.add(v); int newPathLength = pathLength + 1; // 添加当前路径到结果 allPaths.computeIfAbsent(newPathLength, x -> new ArrayList<>()).add(new ArrayList<>(newPath)); // 入栈继续探索(注意迭代DFS要倒序遍历邻接点,保证顺序和递归一致,可选) stack.push(new Object[]{v, newVisited, newPath, newPathLength}); } } } return allPaths; }
3. 其他细节优化
- 提前终止:在递归/迭代中,当路径长度达到k时,直接停止继续探索,避免不必要的操作。
- 路径存储优化:如果不需要保留完整路径的副本,可以考虑用更轻量的结构(比如数组)存储路径,减少ArrayList的复制开销,但这取决于业务需求。
验证效果
优化后的DFS,vertexIdMap.get(u)是O(1)操作,彻底消除了原有的线性查找开销,在k=1的场景下,性能会和BFS接近甚至持平。
内容的提问来源于stack exchange,提问作者ivygrowing
相关产品推荐
相关产品推荐

