算法練習(17)-圖的廣度優先遍歷/深度優先遍歷


一、圖的數據結構及表示法

如上圖,由一堆"點"與一堆"邊"構成的數據結構 ,就稱為圖,其中邊上可以有方向(稱為有向圖),也可以無方向(稱為無向圖)。邊上還可以有所謂的權重值。

算法書上,圖的表示方法一般有“鄰接矩陣”等,這里我們用左程雲介紹的一種相對更容易理解的表示法:

圖:

import java.util.List;

public class Graph {

    //點集
    public List<Node> nodes;

    //邊集
    public List<Edge> edges;

    public Graph(List<Node> nodes, List<Edge> edges) {
        this.nodes = nodes;
        this.edges = edges;
    }
}

節點:

import java.util.ArrayList;
import java.util.List;

public class Node {
    public int value;
    //入度(即:有幾個節點連到自己)
    public int in;
    //出度(即:對外連到幾個其它節點)
    public int out;
    //對外連了哪些相鄰節點
    public List<Node> nexts;
    //有幾條從自已出發的邊
    public List<Edge> edges;

    public Node(int val) {
        this.value = val;
        this.in = 0;
        this.out = 0;
        this.nexts = new ArrayList<>();
        this.edges = new ArrayList<>();
    }
}

注:如果為了調試方便輸出,可以加上toString()

//    @Override
//    public String toString() {
//        return "Node{" +
//                "value=" + value +
//                ", in=" + in +
//                ", out=" + out +
//                ", nexts=" + nexts +
//                ", edges=" + edges +
//                '}';
//    }

    @Override
    public String toString() {
        return value + "";
    }

邊:

public class Edge {
    public Node from;
    public Node to;
    public int weight = 0;

    public Edge(int weight, Node from, Node to) {
        this.weight = weight;
        this.from = from;
        this.to = to;
    }
}

最開始那張圖,就可以類似下面的代碼來構建:

public Graph initGraph() {
        Node n1 = new Node(1);
        Node n2 = new Node(2);
        Node n3 = new Node(3);
        Node n4 = new Node(4);
        Node n5 = new Node(5);
        Node n6 = new Node(6);
        Node n7 = new Node(7);

        n1.in = 1;
        n1.out = 1;
        n1.nexts.add(n4);
        n1.edges.add(new Edge(0, n1, n4));

        n6.in = 1;

        n2.in = 1;
        n2.out = 2;
        n2.nexts.add(n1);
        n2.nexts.add(n6);
        n2.edges.add(new Edge(0, n2, n1));
        n2.edges.add(new Edge(0, n2, n6));

        n3.in = 1;
        n3.out = 1;
        n3.nexts.add(n2);
        n3.edges.add(new Edge(0, n3, n2));

        n5.in = 1;
        n5.out = 1;
        n5.nexts.add(n7);
        n5.edges.add(new Edge(0, n5, n7));

        n4.in = 1;
        n4.out = 2;
        n4.nexts.add(n3);
        n4.nexts.add(n5);
        n4.edges.add(new Edge(0, n4, n3));
        n4.edges.add(new Edge(0, n4, n5));


        List<Node> nodes = new ArrayList<>();
        nodes.add(n1);
        nodes.add(n2);
        nodes.add(n3);
        nodes.add(n4);
        nodes.add(n5);
        nodes.add(n6);
        nodes.add(n7);

        List<Edge> edges = new ArrayList<>();
        edges.addAll(n1.edges);
        edges.addAll(n2.edges);
        edges.addAll(n3.edges);
        edges.addAll(n4.edges);
        edges.addAll(n5.edges);
        edges.addAll(n6.edges);
        edges.addAll(n7.edges);

        Graph g = new Graph(nodes, edges);

        return g;
    }

  

二、廣度優先遍歷

思路:從源節點開始(注:假設源節點不是有進無出的終止節點),依次遍歷自己相鄰的節點,這里要注意下,如果圖中有環,不要形成死循環。

    /**
     * breadth-first search
     * @param g
     */
    void bfs(Graph g) {
        if (g == null || g.nodes == null || g.nodes.size() == 0) {
            return;
        }

        Node n = g.nodes.get(0);
        Queue<Node> queue = new LinkedList<>();
        //用於輔助判斷,節點是否已經遍歷過,防止有環情況下,形成死循環
        Set<Node> set = new HashSet<>();
        queue.add(n);
        set.add(n);

        while (!queue.isEmpty()) {
            Node curr = queue.poll();
            System.out.printf(curr.value + " ");
            for (Node next : curr.nexts) {
                if (!set.contains(next)) {
                    queue.add(next);
                    set.add(next);
                }
            }
        }
    }

以本文最開始的圖為例,輸出為:1 4 3 5 2 7 6 

  

三、深度優先遍歷

思路:與廣度優先不同,深度優先要沿着某個節點,盡可能向縱深走,而非優先看自身相鄰節點,這里要換成Stack,而非Queue,詳見下面的代碼

/**
     * depth-first search 深度優先遍歷
     *
     * @param g
     */
    void dfs(Graph g) {
        if (g == null || g.nodes == null || g.nodes.size() == 0) {
            return;
        }

        Node n = g.nodes.get(0);
        Stack<Node> stack = new Stack<>();
        //用於輔助判斷,節點是否已經遍歷過,防止有環情況下,形成死循環
        Set<Node> set = new HashSet<>();
        stack.add(n);
        set.add(n);

        System.out.printf(n.value + " ");
        while (!stack.isEmpty()) {
            //先把自己彈出來
            Node curr = stack.pop();
            for (Node next : curr.nexts) {
                if (!set.contains(next)) {
                    //再把自己及下1個節點壓進去
                    //由於stack是先進后出,
                    //所以彈出的順序就變成了 下一個節點(即:更深層的)先彈出
                    //從而達到了深度優先的效果
                    stack.add(curr);
                    stack.add(next);
                    set.add(next);
                    System.out.printf(next.value + " ");
                    break;
                }
            }
        }
    }

輸出結果:1 4 3 2 6 5 7

 

四、帶權重的遍歷

比如上圖,如果邊上有權重值,假設權重值越大,優先級越高,那么只要把上述的代碼略做調整,在入隊/入棧時,按權重排下序即可

帶權重的廣度優先遍歷:

    /**
     * 帶權重的breadth-first search
     *
     * @param g
     */
    void bfs2(Graph g) {
        if (g == null || g.nodes == null || g.nodes.size() == 0) {
            return;
        }

        Node n = g.nodes.get(0);
        Queue<Node> queue = new LinkedList<>();
        //用於輔助判斷,節點是否已經遍歷過,防止有環情況下,形成死循環
        Set<Node> set = new HashSet<>();
        queue.add(n);
        set.add(n);

        while (!queue.isEmpty()) {
            Node curr = queue.poll();
            System.out.printf(curr.value + " ");
            //根據邊上的權重值排序
            curr.edges.sort((o1, o2) -> o1.weight < o2.weight ? 1 : -1);
            for (Edge next : curr.edges) {
                if (!set.contains(next.to)) {
                    queue.add(next.to);
                    set.add(next.to);
                }
            }
        }
    }

輸出:1 4 5 3 7 2 6

 

帶權重的深度優先遍歷:

/**
     * 帶權重的深度優先遍歷(菩提樹下的楊過 yjmyzz.cnblogs.com)
     * @param g
     */
    void dfs2(Graph g) {
        if (g == null || g.nodes == null || g.nodes.size() == 0) {
            return;
        }

        Node n = g.nodes.get(0);
        Stack<Node> stack = new Stack<>();
        Set<Node> set = new HashSet<>();
        stack.add(n);
        set.add(n);

        System.out.printf(n.value + " ");
        while (!stack.isEmpty()) {
            Node curr = stack.pop();
            //根據邊上的權重值排序
            curr.edges.sort((o1, o2) -> o1.weight < o2.weight ? 1 : -1);
            for (Edge next : curr.edges) {
                if (!set.contains(next.to)) {
                    stack.add(curr);
                    stack.add(next.to);
                    set.add(next.to);
                    System.out.printf(next.to.value + " ");
                    break;
                }
            }
        }
    }

輸出:1 4 5 7 3 2 6

 

五、無向圖的處理

對於無向圖而言,可以看成是有向圖的特例:

如上圖,節點1與節點2構成了1張最簡單的圖,從1可以走到2,從2也可以走到1,可以理解為1與2之間,各有一條指向對方的邊,用代碼表示的話,類似下面這樣:

    public Graph buildGraph() {
        Node n1 = new Node(1);
        Node n2 = new Node(2);

        n1.out = 2;
        n1.in = 2;
        n1.nexts.add(n2);
        n1.edges.add(new Edge(0, n1, n2));

        n2.out = 2;
        n2.in = 2;
        n2.nexts.add(n1);
        n2.edges.add(new Edge(0, n2, n1));

        List<Node> nodes = new ArrayList<>();
        nodes.add(n1);
        nodes.add(n2);

        List<Edge> edges = new ArrayList<>();
        edges.addAll(n1.edges);
        edges.addAll(n2.edges);

        Graph g = new Graph(nodes, edges);
        return g;
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM