圖(一):無向圖的深度優先遍歷、廣度優先遍歷及連通份量

無向圖:

一些關於圖的定義:前端

圖是由一組頂點和一組可以將兩個頂點相連的邊組成。java

連通圖:若是從任意一個頂點都存在一條路徑到達另外一個任意頂點,就稱爲連通圖,一個非連通圖由若干連通的部分組成,都稱爲極大連通子圖。node

無向圖:即鏈接兩個頂點的邊是沒有方向的。數組

 

無向圖的數據結構:數據結構

使用鄰接表來表示圖:this

 

如上圖所示,使用一個鏈表數組來表示圖,其中數組的索引表示全部的頂點,每一個數組中存放的鏈表表示全部與此頂點相連的頂點,也能夠理解爲邊。spa

 

數據結構以下:code

先看鏈表:component

public class LinkedList<T> {
    private Integer N = 0;
    private Node<T> root;
    
    //鏈表的結點
    @SuppressWarnings("hiding")
    class Node<T> {
        T value;
        Node<T> next;
        
        public Node(T t,Node<T> node) {
            this.value = t;
            this.next = node;
        }
    }
    
    //添加一個結點
    public void add(Node<T> node) {
        if(root == null)
            root = node;
        else {
            Node<T> temp = root;
            root = node;
            root.next = temp;
        }
        N++;
    }
    
    public void add(T t) {
        Node<T> node = new Node<T>(t,null);
        if(root == null)
            root = node;
        else {
            Node<T> temp = root;
            root = node;
            root.next = temp;
        }
        N++;
    }
    
    //鏈表的節點數
    public Integer size() {
        return N;
    }
    
    public Node<T> getRoot() {
        return root;
    }
    
    
    //遍歷鏈表
    public void traverse() {
        Node<T> node = root;
        while(node != null) {
            System.out.print(node.value + " ");
            node = node.next;
        }
    }
    
    
    //獲取鏈表上的全部節點的值,返回一個數組
    public Integer[] getAllValue(LinkedList<T> linkedList) {
        if(linkedList.root == null)
            return null;
        
        Integer[] result = new Integer[linkedList.size()];
        Node<T> node = linkedList.root;
        Integer i = 0;
        while(node != null) {
            result[i++] = (Integer)node.value;
            node = node.next;
        }
        return result;
    }
}

 

圖的數據結構:blog

public class UndiGraphBase {
    //頂點數目
    private Integer V;
    
    //邊的數目
    private Integer E;
    
    //鄰接表
    public LinkedList<Integer>[] adj;
    
    //建立一個含有V個頂點但不含有邊的圖
    @SuppressWarnings("unchecked")
    void graph(Integer V) {
        this.V = V;
        this.E = 0;
        adj = (LinkedList<Integer>[]) new LinkedList[V];
        for(int v=0;v<V;v++) {
            adj[v] = new LinkedList<Integer>();
        }
    }
    
    public Integer V() {
        return V;
    }
    
    public Integer E() {
        return E;
    }
    
    
    //添加一條邊
    public void addEdge(Integer v,Integer w) {
        adj[v].add(w);
        adj[w].add(v);
        E++;
    }
    
    //遍歷
    public void traverse() {
        for(int i=0;i<V;i++) {
            System.out.print(i + ": ");
            adj[i].traverse();
            System.out.println();
        }
    }
    
    
    //獲取某個頂點全部相鄰的頂點
    public Integer[] getAllValueByIndex(Integer i) {
        return adj[i].getAllValue(adj[i]);
    }
}

 

只要靜下心來看,這些代碼都比較簡單,而且都有註釋,就很少說了。

 

深度優先遍歷

深度優先遍歷相似於一我的走迷宮:

 

如圖所示,從起點開始選擇一條邊走到下一個頂點,沒到一個頂點便標記此頂點已到達。

當來到一個標記過的頂點時回退到上一個頂點,再選擇一條沒有到達過的頂點。

當回退到的路口已沒有可走的通道時繼續回退。

 

先給出一個已經構建好的圖,以後的代碼都創建在這個圖的實例上:

 

public class Instance {
    public static UndiGraphBase getInstance() {
        UndiGraphBase undiGraphBase = new UndiGraphBase();
        undiGraphBase.graph(13);
        undiGraphBase.addEdge(0, 5);
        undiGraphBase.addEdge(4, 3);
        undiGraphBase.addEdge(0, 1);
        undiGraphBase.addEdge(9, 12);
        undiGraphBase.addEdge(6, 4);
        undiGraphBase.addEdge(5, 4);
        undiGraphBase.addEdge(0, 2);
        undiGraphBase.addEdge(11, 12);
        undiGraphBase.addEdge(9, 10);
        undiGraphBase.addEdge(0, 6);
        undiGraphBase.addEdge(7, 8);
        undiGraphBase.addEdge(9, 11);
        undiGraphBase.addEdge(5, 3);
        return undiGraphBase;
    }
}

 

 

 

該實例表示的圖以下:

 

深度遍歷的順序以下圖所示:

 

數組marked表示索引所表明的頂點是否被訪問過

數組edgeTO[]表示到達索引所表明的頂點的上一個頂點,經過對此數組的處理便可獲得路徑,便可以用循環,也能夠用棧,由於知道每一個頂點的上一個頂點,因此能夠獲得路徑

 

遍歷的代碼以下:

public class DepthFirstPath {
    //全部頂點組成的數組,若是起點到某頂點可達,則爲true,不然爲false
    private boolean[] marked;
    
    //起點
    private final Integer startV;
    
    //用數組來表示路徑樹,表示起點到可達頂點的路徑。保存的是到達此頂點的上一個頂點
    private Integer[] edgeTo;
    
    public DepthFirstPath(UndiGraphBase G,Integer startV) {
        marked = new boolean[G.V()];
        edgeTo = new Integer[G.V()];
        this.startV = startV;
        dfs(G,startV);
    }
    
    public void dfs(UndiGraphBase G,Integer v) {
        marked[v] = true;
        Integer[] allNodeValues = G.getAllValueByIndex(v);
        if(allNodeValues == null)
            return;
        
        for(int i=0;i<allNodeValues.length;i++) {
            if((!marked[allNodeValues[i]])) {
                edgeTo[allNodeValues[i]] = v;
                dfs(G,allNodeValues[i]);
            }
        }
    }
    
    
    //獲取頂點到v的路徑
    public void getPath(UndiGraphBase G,Integer v) {
        if(!hasPathTo(v))
            return;
        
        //存儲路徑
        Stack<Integer> path = new Stack<Integer>();
        for(int i=v;i!=startV;i = edgeTo[i])
            path.push(i);
        
        //加入起點
        path.push(startV);
        
        //打印棧(即起點startV到v的路徑)
        System.out.println(path.toString());
    }
    
    //從起點startV是否有路徑通往v(便是否可達)
    public boolean hasPathTo(Integer v) {
        return marked[v];
    }
    
    public static void main(String[] args) {
        UndiGraphBase G = Instance.getInstance();
        DepthFirstPath depthFirstPath = new DepthFirstPath(G,0);
        depthFirstPath.getPath(G, 3);
    }
}

 

其實整個代碼是比較好懂的,主要是edgeto數組須要理解一下。

 

 

廣度優先遍歷

廣度優先遍歷其實比深度優先遍歷好理解,即從起點開始,遍歷全部與起點連通的頂點,再遍歷與這些頂點連通的頂點,即先搜索距離起點距離爲1的頂點,再遍歷與起點距離爲2的頂點.....

 

 

其中經edgeTo數組的意義與深度優先中edgeTo數組同樣。

而distTo[]數組則表示與起點的距離,圖中使用該數組只是爲了便於理解,代碼中能夠不用,由於距離能夠經過遍歷edgeTO[]數組獲得

 

廣度優先遍歷的代碼:

 

public class BreadthFirstPath {
    //到達該頂點的最短路徑是否已知
    private boolean[] marked;
    
    //到達該頂點的已知路徑上的最後一個頂點
    private Integer[] edgeTo;
    
    //起點
    private final Integer start;
    
    public BreadthFirstPath(UndiGraphBase G,Integer start) {
        marked = new boolean[G.V()];
        edgeTo = new Integer[G.V()];
        this.start = start;
        bfs(G,start);
    }
    
    private void bfs(UndiGraphBase G,Integer start) {
        Queue<Integer> queue = new java.util.LinkedList<Integer>();
        //標記起點
        marked[start] = true;
        queue.add(start);
        
        while(!queue.isEmpty()) {
            //從隊列中刪除下一頂點
            Integer v = queue.remove();
            
            Integer[] allNodeValues = G.getAllValueByIndex(v);
            for(int i=0;i<allNodeValues.length;i++) {
                //對於每一個未被標記的頂點
                if(!marked[allNodeValues[i]]) {
                    //保存最短路徑的最後一個頂點
                    edgeTo[allNodeValues[i]] = v;
                    //標記它,由於最短路徑已知
                    marked[allNodeValues[i]] = true;
                    queue.add(allNodeValues[i]);
                }
            }
        }
    }
    
    public boolean hasPathTo(Integer v) {
        return marked[v];
    }
    
    //獲取頂點到v的路徑
    public void getPath(UndiGraphBase G,Integer v) {
        if(!hasPathTo(v))
            return;
        
        //存儲路徑
        Stack<Integer> path = new Stack<Integer>();
        for(int i=v;i!=start;i = edgeTo[i])
            path.push(i);
        
        //加入起點
        path.push(start);
        
        //打印棧(即起點startV到v的路徑)
        System.out.println(path.toString());
    }
    
    public static void main(String[] args) {
        UndiGraphBase G = Instance.getInstance();
        BreadthFirstPath breadthFirstPath = new BreadthFirstPath(G,0);
        breadthFirstPath.getPath(G, 3);
    }
}

 

 

 

 

使用深度優先遍歷獲取圖中的全部連通份量

 

public class ConnectedComponent {
    //頂點是否聯通
    private boolean[] marked;
    
    //聯通份量標識符
    private Integer[] id;
    
    //聯通份量數目
    private Integer count = 0;
    
    public ConnectedComponent(UndiGraphBase G) {
        marked = new boolean[G.V()];
        id = new Integer[G.V()];
        for(int i=0;i<G.V();i++) {
            if(!marked[i]) {
                dfs(G,i);
                count++;
            }
        }
    }
    
    public void dfs(UndiGraphBase G,Integer v) {
        marked[v] = true;
        id[v] = count;
        
        Integer[] allNodeValues = G.getAllValueByIndex(v);
        for(int i=0;i<allNodeValues.length;i++) {
            if((!marked[allNodeValues[i]]))
                dfs(G,allNodeValues[i]);
        }
    }
    
    //判斷v和w是否聯通
    public boolean connected(Integer v,Integer w) {
        return id[v] == id[w];
    }
    
    //連通份量數
    public Integer count() {
        return count;
    }
    
    //v頂點所在的聯通份量的標識符(0-count()-1)
    public Integer id(Integer v) {
        return id[v];
    }
    
    public Integer[] getID() {
        return id;
    }
    
    public static void main(String[] args) {
        UndiGraphBase G = Instance.getInstance();
        ConnectedComponent cc = new ConnectedComponent(G);
        
        Integer m = cc.count();
        System.out.println(m + " components");
        
        Integer[] id = cc.getID();
        
        for(int j=0;j<m;j++) {
            for(int i=0;i<id.length;i++) {
                if(id[i] == j) {
                    System.out.print(i + " ");
                }
            }
            System.out.println();
        }

    }
}

 

 

 

 

其中marked數組表示索引所表明的頂點是否已經被訪問

Id數組表示索引所表明的頂點屬於哪一個連通份量,假如一個圖總共有n個份量,那麼各個連通份量分別用0,1,2....n-1表示,id數組的值便是該頂點屬於第幾個連通份量。

 

 

廣度優先遍歷與深度優先遍歷的區別

深度優先遍歷不斷深刻圖中並在棧中保存了全部分叉的頂點;廣度優先遍歷則像扇面通常掃描圖,用一個隊列保存訪問過的最前端的頂點。深度遍歷的方式是尋找離起點更遠的頂點,只在碰到死衚衕時才訪問進出的頂點,廣度遍歷則會首先覆蓋起點附近的頂點,只在臨近的頂點都被訪問完後,纔去訪問更遠的頂點。

深度優先遍歷的路徑一般較長而曲折,廣度優先遍歷的路徑一般短而直接。

 

關於廣度優先遍歷和深度優先遍歷的使用場景與效率以下圖所示:

相關文章
相關標籤/搜索