算法: 無向圖的深度優先搜索(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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章