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

为何实现BFS与DFS时前者远快于后者?求优化方案

DFS与BFS查找有限长度无环路径的性能差异分析及DFS优化方案

性能差异的核心原因

你的测试中k=1时BFS比DFS快几个数量级,核心问题出在DFS实现里的vertexList.indexOf(u)操作,以及递归的额外开销:

  1. 线性查找的巨大开销:vertexList.indexOf(u)是O(n)的线性扫描操作,你的图有19万个顶点,每次调用这个方法都要遍历近20万元素。DFS的递归过程中,标记访问、判断邻接点是否访问、回溯取消标记这三个步骤都要调用这个方法,每个邻接点的递归都会触发多次线性扫描,而出度平均105,累计的时间开销直接爆炸。
  2. 递归栈的额外开销:DFS采用递归实现,Java的递归调用涉及栈帧的创建和销毁,当调用次数较多时,这部分开销会被放大。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 20:44:52