算法: 无向图的深度优先搜索(dfs)和广度优先搜索(bfs)

更新:DFS和BFS是应用广泛而实现简便的算法,但有2个小点需要稍稍注意一下。
对于DFS来说,可以用递归,也可以用迭代。对于一张较大的图,迭代是优于递归的,因为递归要维护一个函数调用栈。小心stackoverflow喔。
对于BFS来说,实现起来需要注意。很容易把标号为1,2的代码写成3的样子。但这其实是不对的,因为x必须在被取出队列之前mark。

    public void BFS(int v)
    {
        Queue<Integer> que = new LinkedList<>();
        que.offer(v);
        marked[v] = true;// 1
        while(!que.isEmpty())
        {
            int w = que.poll();
            //marked[w] = true; // 2
            for(int x:adj(w))
                if(!marked[x])
                {
                    que.offer(x);
                    marked[x] = true;// 3
                }
        }
    }

深度优先搜索(dfs)

深度优先:从图中某个初始顶点v出发,首先访问初始顶点v,然后选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,直到图中与当前顶点v邻接的所有顶点都被访问过为止。显然,这个遍历过程是个递归过程。
应用:
树的前中后序遍历、连通性问题、寻找从顶点v到顶点w的路径等。(见代码中的pathTo()方法)

性能分析:
深度优先搜索标记与起点连通的所有顶点所需的时间和顶点的度数之和成正比。
使用深度优先搜索得到从给定起点到任意标记顶点的路劲所需的时间与路径的长度成正比。

package Graphs;

import java.util.Stack;

public class DFS {

    private Graph g;// unDirectedGraph
    private int s;// startVertex
    private boolean[] marked;// 索引位置的顶点是否被访问过
    private int[] from;// 在dfs访问顺序中,该顶点的前一个顶点

    public DFS(Graph g, int s) {
        this.g = g;
        this.s = s;
        marked = new boolean[g.V()];
        from = new int[g.V()];
    }

    /**
     * 深度优先搜索无向图g
     * 
     * @param g
     * @param s
     */
    public void dfs() {
        dfs(g, s);
    }


    // dfs的实现代码,从起始节点遍历图g,访问信息记录在marked中,路径信息记录在from中。
    private void dfs(Graph g, int s) {
        marked[s] = true;
        for (int w : g.adj(s))
            if (!marked[w]) {
                from[w] = s;
                dfs(g, w);
            }
    }


    /**
     * 返回定点w是否被访问过,可以用来判断是否有一条从初始节点s到w的路径。
     * 
     * @param w
     * @return
     */
    public boolean hasPathTo(int w) {
        return marked[w];
    }

    /**
     * 用深度优先搜索实现。如果存在从s到w的路径,则返回路径上的节点。如果不存在,返回null
     * 
     * @param w
     * @return
     */
    public Iterable<Integer> pathTo(int w) {
        dfs(g, s);// 先用dfs遍历一下无向图
        if (!marked[w]) // 如果没有被访问过,则路径不存在
            return null;
        Stack<Integer> path = new Stack<>();
        for (int x = w; x != s; x = from[x])// 从from数组倒推前一个顶点,直到回到s
            path.push(x);
        path.push(s);
        return path;
    }

    public static void main(String[] args) {
        Graph g = new Graph(13);
        g.addEdge(0, 5);
        g.addEdge(4, 3);
        g.addEdge(0, 1);
        g.addEdge(9, 12);
        g.addEdge(6, 4);
        g.addEdge(5, 5);
        g.addEdge(0, 2);
        g.addEdge(11, 12);
        g.addEdge(9, 10);
        g.addEdge(0, 6);
        g.addEdge(7, 8);
        g.addEdge(9, 11);
        g.addEdge(5, 3);

        DFS d = new DFS(g, 0);

        Stack<Integer> stack = (Stack<Integer>) d.pathTo(4);
        while (!stack.isEmpty())
            System.out.print(stack.pop() + " ");
        // result: 0 5 3 4
    }

}

广度优先搜索(bfs)

广度优先:某个顶点出发,首先访问这个顶点,然后找出这个结点的所有未被访问的邻接点,访问完后再访问这些结点中第一个邻接点的所有结点,重复此方法,直到所有结点都被访问完为止。

应用:
树的层序遍历、网络爬虫、寻找最短路径等

性能分析:
对于从s可达的任意顶点v,广度优先搜索都能找到一条从s到v的最短路径。(没有其他从s到v的路径所含的边比这条路径更少)
广度优先搜索所需的时间在最坏情况下和V+E成正比。

编程的时候需要注意的一点是,顶点要在被放入队列之前mark,否则的话可能得不到最短路径。

package Graphs;

import java.util.Stack;
import list.MyQuene;

public class BFS {

    private Graph g;
    private int s;
    private boolean[] marked;
    private int[] from;

    public BFS(Graph g, int s) {
        this.g = g;
        this.s = s;
        marked = new boolean[g.V()];
        from = new int[g.V()];
    }

    /**
     * 广度优先遍历图g
     */
    public void bfs() {
        bfs(g, s);
    }

    public void bfs(Graph g, int s) {
        MyQuene<Integer> quene = new MyQuene<>();
        // 要在放入队列之前mark,否则得到的不是最短路径。
        //因为这样的话,一个节点虽然进入了队列,但有可能被再次放入队列。也就是说被访问了2次
        marked[s] = true;
        quene.enQuene(s);// 初始顶点加入队列
        while (!quene.isEmpty()) {
            int v = quene.deQuene();
            // marked[v] = true;//将弹出队列的顶点设置为已访问过
            for (int w : g.adj(v)) {
                if (!marked[w]) {
                    from[w] = v;
                    marked[w] = true;
                    quene.enQuene(w);// 将v的未被访问的邻接顶点加入队列

                }
            }
        }
    }

    public boolean hasPathTo(int w) {
        return marked[w];
    }

    /**
     * 用广度优先搜索实现。如果存在从s到w的路径,则返回路径上的节点。如果不存在,返回null
     * 
     * @param w
     * @return
     */
    public Iterable<Integer> pathTo(int w) {
        bfs(g, s);// 先用bfs遍历一下无向图
        if (!marked[w]) // 如果没有被访问过,则路径不存在
            return null;
        Stack<Integer> path = new Stack<>();
        for (int x = w; x != s; x = from[x])// 从from数组倒推前一个顶点,直到回到s
            path.push(x);
        path.push(s);
        return path;
    }

    public static void main(String[] args) {
        Graph g = new Graph(13);
        g.addEdge(0, 5);
        g.addEdge(4, 3);
        g.addEdge(0, 1);
        g.addEdge(9, 12);
        g.addEdge(6, 4);
        g.addEdge(5, 5);
        g.addEdge(0, 2);
        g.addEdge(11, 12);
        g.addEdge(9, 10);
        g.addEdge(0, 6);
        g.addEdge(7, 8);
        g.addEdge(9, 11);
        g.addEdge(5, 3);

        BFS b = new BFS(g, 0);

        Stack<Integer> stack = (Stack<Integer>) b.pathTo(4);
        while (!stack.isEmpty())
            System.out.print(stack.pop() + " ");
        // result: 0 6 4 
    }
}

总结

深度优先使用递归(可以看成隐式的栈)实现,广度优先显示地使用队列实现。
图的深度优先搜索和广度优先搜索是两种简单却十分重要的算法,在很多领域都有重要作用,也是很多其他操作的基础。

发布了28 篇原创文章 · 获赞 4 · 访问量 6万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章