Java數據結構和算法(十五)——無權無向圖

前面咱們介紹了樹這種數據結構,樹是由n(n>0)個有限節點經過鏈接它們的組成一個具備層次關係的集合,把它叫作「樹」是由於它看起來像一棵倒掛的樹,包括二叉樹、紅黑樹、2-3-4樹、堆等各類不一樣的樹,有對這幾種樹不瞭解的能夠參考我前面幾篇博客。而本篇博客咱們將介紹另一種數據結構——圖,圖也是計算機程序設計中最經常使用的數據結構之一,從數學意義上講,樹是圖的一種,你們能夠對比着學習。算法

一、圖的定義

  咱們知道,前面討論的數據結構都有一個框架,而這個框架是由相應的算法實現的,好比二叉樹搜索樹,左子樹上全部結點的值均小於它的根結點的值,右子樹全部結點的值均大於它的根節點的值,相似這種形狀使得它容易搜索數據和插入數據,樹的邊表示了從一個節點到另外一個節點的快捷方式。數組

  而圖一般有個固定的形狀,這是由物理或抽象的問題所決定的。好比圖中節點表示城市,而邊可能表示城市間的班機航線。以下圖是美國加利福利亞簡化的高速公路網:數據結構

  

  ①、鄰接:

  若是兩個頂點被同一條邊鏈接,就稱這兩個頂點是鄰接的,如上圖 I 和 G 就是鄰接的,而 I 和 F 就不是。有時候也將和某個指定頂點鄰接的頂點叫作它的鄰居,好比頂點 G 的鄰居是 I、H、F。框架

  ②、路徑:

  路徑是邊的序列,好比從頂點B到頂點J的路徑爲 BAEJ,固然還有別的路徑 BCDJ,BACDJ等等。學習

  ③、連通圖和非連通圖:

  若是至少有一條路徑能夠鏈接起全部的頂點,那麼這個圖稱做連通的;若是假如存在從某個頂點不能到達另一個頂點,則稱爲非聯通的。測試

  

  ④、有向圖和無向圖:

  若是圖中的邊沒有方向,能夠從任意一邊到達另外一邊,則稱爲無向圖;好比雙向高速公路,A城市到B城市能夠開車從A駛向B,也能夠開車從B城市駛向A城市。可是若是隻能從A城市駛向B城市的圖,那麼則稱爲有向圖。this

  ⑤、有權圖和無權圖:

  圖中的邊被賦予一個權值,權值是一個數字,它能表明兩個頂點間的物理距離,或者從一個頂點到另外一個頂點的時間,這種圖被稱爲有權圖;反之邊沒有賦值的則稱爲無權圖。spa

  本篇博客咱們討論的是無權無向圖。.net

二、在程序中表示圖

  咱們知道圖是由頂點和邊組成,那麼在計算機中,怎麼來模擬頂點和邊?設計

  ①、頂點:

  在大多數狀況下,頂點表示某個真實世界的對象,這個對象必須用數據項來描述。好比在一個飛機航線模擬程序中,頂點表示城市,那麼它須要存儲城市的名字、海拔高度、地理位置和其它相關信息,所以一般用一個頂點類的對象來表示一個頂點,這裏咱們僅僅在頂點中存儲了一個字母來標識頂點,同時還有一個標誌位,用來判斷該頂點有沒有被訪問過(用於後面的搜索)。

1

2

3

4

5

6

7

8

9

10

11

12

13

/**

 * 頂點類

 * @author vae

 */

public class Vertex {

    public char label;

    public boolean wasVisited;

     

    public Vertex(char label){

        this.label = label;

        wasVisited = false;

    }

}

  頂點對象能放在數組中,而後用下標指示,也能夠放在鏈表或其它數據結構中,不論使用什麼結構,存儲只是爲了使用方便,這與邊如何鏈接點是沒有關係的。

  ②、邊:

  在前面講解各類樹的數據結構時,大多數樹都是每一個節點包含它的子節點的引用,好比紅黑樹、二叉樹。也有用數組表示樹,樹組中節點的位置決定了它和其它節點的關係,好比堆就是用數組表示。

  然而圖並不像樹,圖沒有固定的結構,圖的每一個頂點能夠與任意多個頂點相連,爲了模擬這種自由形式的組織結構,用以下兩種方式表示圖:鄰接矩陣和鄰接表(若是一條邊鏈接兩個頂點,那麼這兩個頂點就是鄰接的)

  

 

  鄰接矩陣:

  鄰接矩陣是一個二維數組,數據項表示兩點間是否存在邊,若是圖中有 N 個頂點,鄰接矩陣就是 N*N 的數組。上圖用鄰接矩陣表示以下:

  

  1表示有邊,0表示沒有邊,也能夠用布爾變量true和false來表示。頂點與自身相連用 0 表示,因此這個矩陣從左上角到右上角的對角線全是 0 。

  注意:這個矩陣的上三角是下三角的鏡像,兩個三角包含了相同的信息,這個冗餘信息看似低效,可是在大多數計算機中,創造一個三角形數組比較困難,因此只好接受這個冗餘,這也要求在程序處理中,當咱們增長一條邊時,好比更新鄰接矩陣的兩部分,而不是一部分。

  鄰接表:

  鄰接表是一個鏈表數組(或者是鏈表的鏈表),每一個單獨的鏈表表示了有哪些頂點與當前頂點鄰接。

    

三、搜索 

  在圖中實現最基本的操做之一就是搜索從一個指定頂點能夠到達哪些頂點,好比從武漢出發的高鐵能夠到達哪些城市,一些城市能夠直達,一些城市不能直達。如今有一份全國高鐵模擬圖,要從某個城市(頂點)開始,沿着鐵軌(邊)移動到其餘城市(頂點),有兩種方法能夠用來搜索圖:深度優先搜索(DFS)和廣度優先搜索(BFS)。它們最終都會到達全部連通的頂點,深度優先搜索經過棧來實現,而廣度優先搜索經過隊列來實現,不一樣的實現機制致使不一樣的搜索方式。

  ①、深度優先搜索(DFS)

  深度優先搜索算法有以下規則:

  規則1:若是可能,訪問一個鄰接的未訪問頂點,標記它,並將它放入棧中。

  規則2:當不能執行規則 1 時,若是棧不爲空,就從棧中彈出一個頂點。

  規則3:若是不能執行規則 1 和規則 2 時,就完成了整個搜索過程。

  

  對於上圖,應用深度優先搜索以下:假設選取 A 頂點爲起始點,而且按照字母優先順序進行訪問,那麼應用規則 1 ,接下來訪問頂點 B,而後標記它,並將它放入棧中;再次應用規則 1,接下來訪問頂點 F,再次應用規則 1,訪問頂點 H。咱們這時候發現,沒有 H 頂點的鄰接點了,這時候應用規則 2,從棧中彈出 H,這時候回到了頂點 F,可是咱們發現 F 也除了 H 也沒有與之鄰接且未訪問的頂點了,那麼再彈出 F,這時候回到頂點 B,同理規則 1 應用不了,應用規則 2,彈出 B,這時候棧中只有頂點 A了,而後 A 還有未訪問的鄰接點,全部接下來訪問頂點 C,可是 C又是這條線的終點,因此從棧中彈出它,再次回到 A,接着訪問 D,G,I,最後也回到了 A,而後訪問 E,可是最後又回到了頂點 A,這時候咱們發現 A沒有未訪問的鄰接點了,因此也把它彈出棧。如今棧中已無頂點,因而應用規則 3,完成了整個搜索過程。

  深度優先搜索在於可以找到與某一頂點鄰接且沒有訪問過的頂點。這裏以鄰接矩陣爲例,找到頂點所在的行,從第一列開始向後尋找值爲1的列;列號是鄰接頂點的號碼,檢查這個頂點是否未訪問過,若是是這樣,那麼這就是要訪問的下一個頂點,若是該行沒有頂點既等於1(鄰接)且又是未訪問的,那麼與指定點相鄰接的頂點就所有訪問過了(後面會用算法實現)。

  ②、廣度優先搜索(BFS)

  深度優先搜索要儘量的遠離起始點,而廣度優先搜索則要儘量的靠近起始點,它首先訪問起始頂點的全部鄰接點,而後再訪問較遠的區域,這種搜索不能用棧實現,而是用隊列實現。

  規則1:訪問下一個未訪問的鄰接點(若是存在),這個頂點必須是當前頂點的鄰接點,標記它,並把它插入到隊列中。

  規則2:若是已經沒有未訪問的鄰接點而不能執行規則 1 時,那麼從隊列列頭取出一個頂點(若是存在),並使其成爲當前頂點。

  規則3:若是由於隊列爲空而不能執行規則 2,則搜索結束。

  對於上面的圖,應用廣度優先搜索:以A爲起始點,首先訪問全部與 A 相鄰的頂點,並在訪問的同時將其插入隊列中,如今已經訪問了 A,B,C,D和E。這時隊列(從頭至尾)包含 BCDE,已經沒有未訪問的且與頂點 A 鄰接的頂點了,因此從隊列中取出B,尋找與B鄰接的頂點,這時找到F,因此把F插入到隊列中。已經沒有未訪問且與B鄰接的頂點了,因此從隊列列頭取出C,它沒有未訪問的鄰接點。所以取出 D 並訪問 G,D也沒有未訪問的鄰接點了,因此取出E,如今隊列中有 FG,在取出 F,訪問 H,而後取出 G,訪問 I,如今隊列中有 HI,當取出他們時,發現沒有其它爲訪問的頂點了,這時隊列爲空,搜索結束。

  ③、程序實現

  實現深度優先搜索的棧 StackX.class

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

package com.ys.graph;

 

public class StackX {

    private final int SIZE = 20;

    private int[] st;

    private int top;

     

    public StackX(){

        st = new int[SIZE];

        top = -1;

    }

     

    public void push(int j){

        st[++top] = j;

    }

     

    public int pop(){

        return st[top--];

    }

     

    public int peek(){

        return st[top];

    }

     

    public boolean isEmpty(){

        return (top == -1);

    }

 

}

  實現廣度優先搜索的隊列Queue.class

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

package com.ys.graph;

 

public class Queue {

    private final int SIZE = 20;

    private int[] queArray;

    private int front;

    private int rear;

     

    public Queue(){

        queArray = new int[SIZE];

        front = 0;

        rear = -1;

    }

     

    public void insert(int j) {

        if(rear == SIZE-1) {

            rear = -1;

        }

        queArray[++rear] = j;

    }

     

    public int remove() {

        int temp = queArray[front++];

        if(front == SIZE) {

            front = 0;

        }

        return temp;

    }

     

    public boolean isEmpty() {

        return (rear+1 == front || front+SIZE-1 == rear);

    }

}

  圖代碼 Graph.class

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

package com.ys.graph;

 

public class Graph {

    private final int MAX_VERTS = 20;//表示頂點的個數

    private Vertex vertexList[];//用來存儲頂點的數組

    private int adjMat[][];//用鄰接矩陣來存儲 邊,數組元素0表示沒有邊界,1表示有邊界

    private int nVerts;//頂點個數

    private StackX theStack;//用棧實現深度優先搜索

    private Queue queue;//用隊列實現廣度優先搜索

    /**

     * 頂點類

     * @author vae

     */

    class Vertex {

        public char label;

        public boolean wasVisited;

         

        public Vertex(char label){

            this.label = label;

            wasVisited = false;

        }

    }

     

    public Graph(){

        vertexList = new Vertex[MAX_VERTS];

        adjMat = new int[MAX_VERTS][MAX_VERTS];

        nVerts = 0;//初始化頂點個數爲0

        //初始化鄰接矩陣全部元素都爲0,即全部頂點都沒有邊

        for(int i = 0; i < MAX_VERTS; i++) {

            for(int j = 0; j < MAX_VERTS; j++) {

                adjMat[i][j] = 0;

            }

        }

        theStack = new StackX();

        queue = new Queue();

    }

     

    //將頂點添加到數組中,是否訪問標誌置爲wasVisited=false(未訪問)

    public void addVertex(char lab) {

        vertexList[nVerts++] = new Vertex(lab);

    }

     

    //注意用鄰接矩陣表示邊,是對稱的,兩部分都要賦值

    public void addEdge(int start, int end) {

        adjMat[start][end] = 1;

        adjMat[end][start] = 1;

    }

     

    //打印某個頂點表示的值

    public void displayVertex(int v) {

        System.out.print(vertexList[v].label);

    }

    /**深度優先搜索算法:

     * 一、用peek()方法檢查棧頂的頂點

     * 二、用getAdjUnvisitedVertex()方法找到當前棧頂點鄰接且未被訪問的頂點

     * 三、第二步方法返回值不等於-1則找到下一個未訪問的鄰接頂點,訪問這個頂點,併入棧

     *    若是第二步方法返回值等於 -1,則沒有找到,出棧

     */

    public void depthFirstSearch() {

        //從第一個頂點開始訪問

        vertexList[0].wasVisited = true//訪問以後標記爲true

        displayVertex(0);//打印訪問的第一個頂點

        theStack.push(0);//將第一個頂點放入棧中

         

        while(!theStack.isEmpty()) {

            //找到棧當前頂點鄰接且未被訪問的頂點

            int v = getAdjUnvisitedVertex(theStack.peek());

            if(v == -1) {   //若是當前頂點值爲-1,則表示沒有鄰接且未被訪問頂點,那麼出棧頂點

                theStack.pop();

            }else //不然訪問下一個鄰接頂點

                vertexList[v].wasVisited = true;

                displayVertex(v);

                theStack.push(v);

            }

        }

         

        //棧訪問完畢,重置全部標記位wasVisited=false

        for(int i = 0; i < nVerts; i++) {

            vertexList[i].wasVisited = false;

        }

    }

     

    //找到與某一頂點鄰接且未被訪問的頂點

    public int getAdjUnvisitedVertex(int v) {

        for(int i = 0; i < nVerts; i++) {

            //v頂點與i頂點相鄰(鄰接矩陣值爲1)且未被訪問 wasVisited==false

            if(adjMat[v][i] == 1 && vertexList[i].wasVisited == false) {

                return i;

            }

        }

        return -1;

    }

     

    /**

     * 廣度優先搜索算法:

     * 一、用remove()方法檢查棧頂的頂點

     * 二、試圖找到這個頂點還未訪問的鄰節點

     * 三、 若是沒有找到,該頂點出列

     * 四、 若是找到這樣的頂點,訪問這個頂點,並把它放入隊列中

     */

    public void breadthFirstSearch(){

        vertexList[0].wasVisited = true;

        displayVertex(0);

        queue.insert(0);

        int v2;

         

        while(!queue.isEmpty()) {

            int v1 = queue.remove();

            while((v2 = getAdjUnvisitedVertex(v1)) != -1) {

                vertexList[v2].wasVisited = true;

                displayVertex(v2);

                queue.insert(v2);

            }

        }

         

        //搜索完畢,初始化,以便於下次搜索

        for(int i = 0; i < nVerts; i++) {

            vertexList[i].wasVisited = false;

        }

    }

     

    public static void main(String[] args) {

        Graph graph = new Graph();

        graph.addVertex('A');

        graph.addVertex('B');

        graph.addVertex('C');

        graph.addVertex('D');

        graph.addVertex('E');

         

        graph.addEdge(01);//AB

        graph.addEdge(12);//BC

        graph.addEdge(03);//AD

        graph.addEdge(34);//DE

         

        System.out.println("深度優先搜索算法 :");

        graph.depthFirstSearch();//ABCDE

         

        System.out.println();

        System.out.println("----------------------");

         

        System.out.println("廣度優先搜索算法 :");

        graph.breadthFirstSearch();//ABDCE

    }

}

  測試結果:

  

四、最小生成樹

   對於圖的操做,還有一個最經常使用的就是找到最小生成樹,最小生成樹就是用最少的邊鏈接全部頂點。對於給定的一組頂點,可能有不少種最小生成樹,可是最小生成樹的邊的數量 E 老是比頂點 V 的數量小1,即:

  V = E + 1

  這裏不用關心邊的長度,不是找最短的路徑(會在帶權圖中講解),而是找最少數量的邊,能夠基於深度優先搜索和廣度優先搜索來實現。

  好比基於深度優先搜索,咱們記錄走過的邊,就能夠建立一個最小生成樹。由於DFS 訪問全部頂點,但只訪問一次,它絕對不會兩次訪問同一個頂點,但她看到某條邊將到達一個已訪問的頂點,它就不會走這條邊,它歷來不遍歷那些不可能的邊,所以,DFS 算法走過整個圖的路徑一定是最小生成樹。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

//基於深度優先搜索找到最小生成樹

public void mst(){

    vertexList[0].wasVisited = true;

    theStack.push(0);

     

    while(!theStack.isEmpty()){

        int currentVertex = theStack.peek();

        int v = getAdjUnvisitedVertex(currentVertex);

        if(v == -1){

            theStack.pop();

        }else{

            vertexList[v].wasVisited = true;

            theStack.push(v);

             

            displayVertex(currentVertex);

            displayVertex(v);

            System.out.print(" ");

        }

    }

     

    //搜索完畢,初始化,以便於下次搜索

    for(int i = 0; i < nVerts; i++) {

        vertexList[i].wasVisited = false;

    }

}

相關文章
相關標籤/搜索