無向圖


簡介

在現實生活中,有許多應用場景會包含很多點以及點點之間的連接,而這些應用場景我們都可以用即將要學習的圖這種數據結構去解決。

地圖:

我們生活中經常使用的地圖,基本上是由城市以及連接城市的道路組成,如果我們把城市看做是一個一個的點,把道路看做是一條一條的連接,那么地圖就是我們將要學習的圖這種數據結構。

image-20210827085946863

電路圖:

下面是一個我們生活中經常見到的集成電路板,它其實就是由一個一個觸點組成,並把觸點與觸點之間通過線進行連接,這也是我們即將要學習的圖這種數據結構的應用場景

image-20210827090045022

圖的定義及分類:

定義:圖是由一組頂點和一組能夠將兩個頂點相連的邊組成的

image-20210827090227891

特殊的圖:

  1. 自環:即一條連接一個頂點和其自身的邊;
  2. 平行邊:連接同一對頂點的兩條邊;

image-20210827090320099

圖的分類:

按照連接兩個頂點的邊的不同,可以把圖分為以下兩種:

無向圖:邊僅僅連接兩個頂點,沒有其他含義;

有向圖:邊不僅連接兩個頂點,並且具有方向;

無向圖

圖的相關術語

相鄰頂點:當兩個頂點通過一條邊相連時,我們稱這兩個頂點是相鄰的,並且稱這條邊依附於這兩個頂點。

度:某個頂點的度就是依附於該頂點的邊的個數

子圖:是一幅圖的所有邊的子集(包含這些邊依附的頂點)組成的圖;

路徑:是由邊順序連接的一系列的頂點組成

環:是一條至少含有一條邊且終點和起點相同的路徑

image-20210827090614782

連通圖:如果圖中任意一個頂點都存在一條路徑到達另外一個頂點,那么這幅圖就稱之為連通圖

連通子圖:一個非連通圖由若干連通的部分組成,每一個連通的部分都可以稱為該圖的連通子圖

image-20210827090919004

圖的存儲結構

要表示一幅圖,只需要表示清楚以下兩部分內容即可:

  1. 圖中所有的頂點;
  2. 所有連接頂點的邊;

常見的圖的存儲結構有兩種:鄰接矩陣和鄰接表

鄰接矩陣

  1. 使用一個V*V的二維數組int[V][V] adj,把索引的值看做是頂點;
  2. 如果頂點v和頂點w相連,我們只需要將adj[v][w]和adj[w][v]的值設置為1,否則設置為0即可。
image-20210827091303228

很明顯,鄰接矩陣這種存儲方式的空間復雜度是V^2的,如果我們處理的問題規模比較大的話,內存空間極有可能不夠用。

鄰接表

1.使用一個大小為V的數組 Queue[V] adj,把索引看做是頂點;

2.每個索引處adj[v]存儲了一個隊列,該隊列中存儲的是所有與該頂點相鄰的其他頂點

image-20210827092511130

很明顯,鄰接表的空間並不是是線性級別的,所以后面我們一直采用鄰接表這種存儲形式來表示圖。

代碼實現

圖API設計:

類名 Graph
構造方法 Graph(int V):創建一個包含V個頂點但不包含邊的圖
成員方法 1.public int V():獲取圖中頂點的數量
2.public int E():獲取圖中邊的數量
3.public void addEdge(int v,int w):向圖中添加一條邊 v-w
4.public Queue adj(int v):獲取和頂點v相鄰的所有頂點
成員變量 1.private final int V: 記錄頂點數量
2.private int E: 記錄邊數量
3.private Queue[] adj: 鄰接表
/**
 * @author wen.jie
 * @date 2021/8/27 10:43
 * 無向圖
 */
public class Graph {

    //頂點數目
    final int V;
    //邊的數目
    int E;
    //鄰接表
    Queue<Integer>[] adj;

    //初始化
    public Graph(int v) {
        this.V = v;
        this.E = 0;
        this.adj = new Queue[v];
        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<>();
        }
    }

    public int V(){
        return V;
    }

    public int E(){
        return E;
    }

    //向圖中添加一條邊 v-w
    public void addEdge(int v, int w) {
        adj[v].enqueue(w);
        adj[w].enqueue(v);
        E++;
    }

    //獲取和頂點v相鄰的所有頂點
    public Queue<Integer> adj(int v) {
        return adj[v];
    }

}

圖的搜索

在很多情況下,我們需要遍歷圖,得到圖的一些性質,例如,找出圖中與指定的頂點相連的所有頂點,或者判定某個頂點與指定頂點是否相通,是非常常見的需求。

有關圖的搜索,最經典的算法有深度優先搜索和廣度優先搜索,接下來我們分別講解這兩種搜索算法。

深度優先搜索

所謂的深度優先搜索,指的是在搜索時,如果遇到一個結點既有子結點,又有兄弟結點,那么先找子結點,然后找 兄弟結點。

image-20210827105724784

很明顯,在由於邊是沒有方向的,所以,如果4和5頂點相連,那么4會出現在5的相鄰鏈表中,5也會出現在4的相鄰鏈表中,那么為了不對頂點進行重復搜索,應該要有相應的標記來表示當前頂點有沒有搜索過,可以使用一個布爾類型的數組 boolean[V] marked,索引代表頂點,值代表當前頂點是否已經搜索,如果已經搜索,標記為true, 如果沒有搜索,標記為false;

api設計:

類名 DepthFirstSearch
構造方法 DepthFirstSearch(Graph G,int s):構造深度優先搜索對象,使用深度優先搜索找出G圖中s頂點 的所有相通頂點
成員方法 1.private void dfs(Graph G, int v):使用深度優先搜索找出G圖中v頂點的所有相通頂點
2.public boolean marked(int w):判斷w頂點與s頂點是否相通
3.public int count():獲取與頂點s相通的所有頂點的總數
成員變量 1.private boolean[] marked: 索引代表頂點,值表示當前頂點是否已經被搜索
2.private int count:記錄有多少個頂點與s頂點相通

代碼實現:

/**
 * dfs:深度優先搜索
 * @author wen.jie
 * @date 2021/8/27 11:08
 */
public class DepthFirstSearch {
    //索引代表頂點,值代表當前頂點是否已經被搜索
    private boolean[] marked;
    //記錄有多少頂點與s頂點相通
    private int count;

    public DepthFirstSearch(Graph G, int s) {
        this.marked = new boolean[G.V()];
        dfs(G, s);
    }

    //深度優先搜索找出G圖中v頂點的所有相通頂點
    private void dfs(Graph G, int v) {
        //標為已搜索
        marked[v] = true;
        for (int w : G.adj(v)) {
            if (!marked(w))
                dfs(G, w);
        }
        count++;
    }

    //判斷w頂點與s頂點是否相通
    public boolean marked(int w){
        return marked[w];
    }

    //獲取與頂點s相通的所有頂點的總數
    public int count() {
        return this.count;
    }
}

構造如下的圖,並測試:

image-20210827133736915
    private Graph graph = new Graph(13);
    {
        graph.addEdge(0, 5);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(0, 6);
        graph.addEdge(6, 4);
        graph.addEdge(4, 3);
        graph.addEdge(4, 5);
        graph.addEdge(5, 3);
        graph.addEdge(7, 8);
        graph.addEdge(9, 10);
        graph.addEdge(9, 11);
        graph.addEdge(9, 12);
        graph.addEdge(11, 12);
    }


    @Test
    public void test1() {
        DepthFirstSearch dfs = new DepthFirstSearch(graph, 0);
        int count = dfs.count();
        System.out.println("與起點0相通的頂點數量為:"+ count);
        System.out.println(dfs.marked(5));
        System.out.println(dfs.marked(7));
    }

image-20210827133749911

廣度優先搜索

所謂的廣度優先搜索,指的是在搜索時,如果遇到一個結點既有子結點,又有兄弟結點,那么先找兄弟結點,然后找子結點。

image-20210827133904737

api設計:

類名 BreadthFirstSearch
構造方法 BreadthFirstSearch(Graph G,int s):構造廣度優先搜索對象,使用廣度優先搜索找出G圖中s頂點的所有相鄰頂點
構造方法 1.private void bfs(Graph G, int v):使用廣度優先搜索找出G圖中v頂點的所有相鄰頂點
2.public boolean marked(int w):判斷w頂點與s頂點是否相通
3.public int count():獲取與頂點s相通的所有頂點的總數
成員變量 1.private boolean[] marked: 索引代表頂點,值表示當前頂點是否已經被搜索
2.private int count:記錄有多少個頂點與s頂點相通
3.private Queue waitSearch: 用來存儲待搜索鄰接表的點

代碼實現:

/**
 * @author wen.jie
 * @date 2021/8/27 13:43
 * bfs:廣度優先遍歷
 */
public class BreadthFirstSearch {

    //索引代表頂點,值代表當前頂點是否已經被搜索
    private boolean[] marked;
    //記錄有多少頂點與s頂點相通
    private int count;
    //用來存儲待搜索鄰接表的點
    private Queue<Integer> waitSearch;

    public BreadthFirstSearch(Graph G, int s) {
        this.marked = new boolean[G.V()];
        this.waitSearch = new Queue<>();
        bfs(G, s);
    }

    //廣度優先搜索找出G圖中v頂點的所有相通頂點
    private void bfs(Graph G, int v) {
        count++;
        marked[v] = true;
        //入隊列,待搜索
        waitSearch.enqueue(v);
        while (!waitSearch.isEmpty()) {
            //出隊列
            Integer wait = waitSearch.dequeue();
            for (Integer w : G.adj(wait)) {
                if (!marked[w]) {
                    waitSearch.enqueue(w);
                    marked[w] = true;
                    count++;
                }
            }
        }
    }

    //判斷w頂點與s頂點是否相通
    public boolean marked(int w){
        return marked[w];
    }

    //獲取與頂點s相通的所有頂點的總數
    public int count() {
        return this.count;
    }

}

測試:

    @Test
    public void test2() {
        BreadthFirstSearch bfs = new BreadthFirstSearch(graph, 0);
        int count = bfs.count();
        System.out.println("與起點0相通的頂點數量為:"+ count);
        System.out.println(bfs.marked(5));
        System.out.println(bfs.marked(7));
    }

路徑查找

在實際生活中,地圖是我們經常使用的一種工具,通常我們會用它進行導航,輸入一個出發城市,輸入一個目的地城市,就可以把路線規划好,而在規划好的這個路線上,會路過很多中間的城市。這類問題翻譯成專業問題就是: 從s頂點到v頂點是否存在一條路徑?如果存在,請找出這條路徑。

image-20210827141605916

例如在上圖上查找頂點0到頂點4的路徑用紅色標識出來,那么我們可以把該路徑表示為 0-2-3-4。

深度優先遍歷查找路徑

類名 DepthFirstPaths
構造方法 DepthFirstPaths(Graph G,int s):構造深度優先搜索對象,使用深度優先搜索找出G圖中起點為 s的所有路徑
構造方法 1.private void dfs(Graph G, int v):使用深度優先搜索找出G圖中v頂點的所有相鄰頂點
2.public boolean hasPathTo(int v):判斷v頂點與s頂點是否存在路徑
3.public Stack pathTo(int v):找出從起點s到頂點v的路徑(就是該路徑經過的頂點)
成員變量 1.private boolean[] marked: 索引代表頂點,值表示當前頂點是否已經被搜索
2.private int s:起點
3.private int[] edgeTo:索引代表頂點,值代表從起點s到當前頂點路徑上的最后一個頂點

思路:

我們實現路徑查找,最基本的操作還是得遍歷並搜索圖,所以,我們的實現暫且基於深度優先搜索來完成。其搜索的過程是比較簡單的。我們添加了edgeTo[]整型數組,這個整型數組會記錄從每個頂點回到起點s的路徑。 如果我們把頂點設定為0,那么它的搜索可以表示為下圖:

image-20210827144138869

image-20210827144322793

image-20210827144202369

代碼實現如下:

public class DepthFirstPaths {
    // 索引代表頂點,值表示當前頂點是否已經被搜索
    private boolean[] marked;
    //起點
    private int s;
    //索引代表頂點,值代表從起點s到當前頂點路徑上的最后一個頂點
    private Integer[] edgeTo;

    public DepthFirstPaths(Graph G,int s){
        this.marked = new boolean[G.V()];
        this.s = s;
        this.edgeTo = new Integer[G.V()];
        dfs(G, s);
    }

    private void dfs(Graph G, int v){
        marked[v] = true;
        for (Integer w : G.adj(v)) {
            if(!marked[w]) {
                //到達頂點w的路徑上的最后一個頂點是v
                edgeTo[w] = v;
                dfs(G, w);
            }
        }
    }

    //判斷v頂點與s頂點是否存在路徑
    public boolean hasPathTo(int v){
        return marked[v];
    }

    //找出從起點s到頂點v的路徑(就是該路徑經過的頂點)
    public Stack<Integer> pathTo(int v){

        if (!hasPathTo(v))
            return null;
        Stack<Integer> path = new Stack<>();
        path.push(v);
        while (edgeTo[v] != null) {
            int p = edgeTo[v];
            path.push(p);
            v = p;
        }
        return path;
    }
}

測試:

    private Graph graph = new Graph(6);
    {
        graph.addEdge(0,2);
        graph.addEdge(2,1);
        graph.addEdge(2,3);
        graph.addEdge(0,1);
        graph.addEdge(0,5);
        graph.addEdge(3,5);
        graph.addEdge(3,4);
        graph.addEdge(2,4);
    }

    @Test
    public void test() {
        DepthFirstPaths paths = new DepthFirstPaths(graph, 0);
        Stack<Integer> path = paths.pathTo(4);
        for (Integer integer : path) {
            System.out.println(integer);
        }
    }

image-20210827144359874

廣度優先遍歷查找路徑

深度優先搜索得到的路徑不僅取決於圖的結構,還取決於圖的表示和遞歸調用的性質。

對於找出最短路徑的那條,我們可以使用廣度優先遍歷(BFS)。

結合上面的深度優先遍歷查找路徑和前面廣度優先搜索代碼,不難得出廣度優先遍歷查找路徑的代碼:

/**
 * @author wen.jie
 * @date 2021/8/27 14:52
 */
public class BreadthFirstPaths {

    //索引代表頂點,值代表當前頂點是否已經被搜索
    private boolean[] marked;
    //用來存儲待搜索鄰接表的點
    private Queue<Integer> waitSearch;
    //索引代表頂點,值代表從起點s到當前頂點路徑上的最后一個頂點
    private Integer[] edgeTo;

    public BreadthFirstPaths(Graph G, int s) {
        this.marked = new boolean[G.V()];
        this.waitSearch = new Queue<>();
        this.edgeTo = new Integer[G.V()];
        bfs(G, s);
    }

    //深度優先搜索找出G圖中v頂點的所有相通頂點
    private void bfs(Graph G, int v) {
        //標記起點
        marked[v] = true;
        //入隊列,待搜索
        waitSearch.enqueue(v);
        while (!waitSearch.isEmpty()) {
            //出隊列
            Integer wait = waitSearch.dequeue();
            for (Integer w : G.adj(wait)) {
                if (!marked[w]) { //對於每個未被標記的相鄰頂點
                    edgeTo[w] = wait; //保存最短路徑的最后一條邊
                    marked[w] = true; //標記,因為最短路徑已知
                    waitSearch.enqueue(w); //入隊列
                }
            }
        }

    }

    //判斷w頂點與s頂點是否相通
    public boolean hasPathTo(int w){
        return marked[w];
    }

    //找出從起點s到頂點v的路徑(就是該路徑經過的頂點)
    public Stack<Integer> pathTo(int v){

        if (!hasPathTo(v))
            return null;
        Stack<Integer> path = new Stack<>();
        path.push(v);
        while (edgeTo[v] != null) {
            int p = edgeTo[v];
            path.push(p);
            v = p;
        }
        return path;
    }
}

測試:

        BreadthFirstPaths paths = new BreadthFirstPaths(graph, 1);
        Stack<Integer> path = paths.pathTo(3);
        for (Integer integer : path) {
            System.out.println(integer);
        }

下面是廣度優先遍歷的處理樣圖:

image-20210827152307120

image-20210827152323781

image-20210827152338774

命題:對於從s可達的任意頂點v,廣度優先搜索都能找到一條從s到v的最短路徑(沒有其他從s到v的路徑所含的邊比這條路徑更少)

證明:由歸納易得隊列總是包含零個或多個到起點的距離為k的頂點,之后是零個或多個到起點的舉例為k+1的頂點,其中k為整數,起始值為0。這意味着頂點是按照它們和s的距離的順序加入隊列或者離開隊列的。從頂點v加入隊列到它離開隊列之前,不可能找出到v的更短的路徑,而在v離開隊列之后發現的所有能夠到達v的路徑不可能短於v在樹中的路徑長度。

本篇所有代碼均已上傳至:https://gitee.com/wj204811/algorithm


免責聲明!

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



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