1. 程式人生 > >演算法: 無向圖的深度優先搜尋(dfs)和廣度優先搜尋(bfs)

演算法: 無向圖的深度優先搜尋(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 
    }
}

總結

深度優先使用遞迴(可以看成隱式的棧)實現,廣度優先顯示地使用佇列實現。
圖的深度優先搜尋和廣度優先搜尋是兩種簡單卻十分重要的演算法,在很多領域都有重要作用,也是很多其他操作的基礎。