1. 程式人生 > >[從今天開始修煉資料結構]圖

[從今天開始修煉資料結構]圖

我們之前介紹了線性關係的線性表,層次關係的樹形結構,下面我們來介紹結點之間關係任意的結構,圖。
一、相關概念

  1,圖是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示為G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。

  2,各種圖定義

   若兩頂點之間的邊沒有方向,則稱這條邊為無向邊Edge,用無序偶對(v1,v2)來表示。如果圖中任意兩個頂點之間的邊都是無向邊,則稱該圖為無向圖。

   若從頂點v1到v2的邊有方向,則稱這條邊為有向邊,也稱為弧(Arc),表示為有序偶<v1,v2>,稱v1為弧尾,v2為弧頭。若圖中任意兩個頂點之間的邊都是有向邊,則稱該圖為有向圖。

  注意無向邊用(),有向邊用<>

  圖按照邊或弧的多少分為稀疏圖和稠密圖,但劃分邊界比較模糊。任意兩個頂點之間都存在邊叫完全圖,有向的叫有向完全圖。若無重複邊或頂點回到自身的邊的叫做簡單圖。

  圖上邊或弧帶權則稱為網。

  3,圖的頂點與邊之間的關係

  圖中頂點之間有鄰接點Adjacent的概念,v和v‘相鄰接,邊(v,v')依附於頂點v和v’,或者說邊(v,v')與頂點v和v’相關聯。頂點的度Degree是與v相關聯的邊的數目,記作TD(v)。

  有向圖中有入度ID(v)和出度OD(V)的概念.

  圖中的頂點間存在路徑則說明是連通的,如果路徑最終回到起始點則成為環,不重複的路徑稱為簡單路徑。頂點不重複出現的迴路,叫做簡單迴路或簡單環。

  若任意兩點之間都是連通的,則成為連通圖,有向則是強連通圖。圖中有子圖,若子圖極大連通則稱該子圖為連通分量,有向的則稱為強連通分量。

  無向圖是連通圖且n個頂點有n-1條邊則叫做生成樹。有向圖中一頂點入度為0,其他頂點入度為1的叫做有向樹,一個有向圖可以分解為若干有向樹構成的生成森林。

二、圖的抽象資料型別

  

 

 

 三、圖的儲存結構

  圖結構比較複雜,任意兩個頂點之間都可能存在聯絡,因此無法以資料元素在記憶體中的物理位置來表示元素間的關係;而多重連結串列的方式又有操作的不便,因此對於圖來說,它實現物理儲存是個難題,下面我們來看前輩們已經提供的五種不同的儲存結構

  1,鄰接矩陣

  圖的鄰接矩陣儲存方式是用兩個陣列來表示圖。一個一維陣列儲存圖中頂點資訊,一個二維陣列(稱為鄰接矩陣)儲存圖中的邊或弧的資訊。若無向圖中存在這條邊,或有向圖中存在這條弧,則矩陣中的該位置置為1,否則置0.如下

 

 

 

 

 

 

 

 

   鄰接矩陣是如何實現圖的建立的呢? 程式碼如下

/*
頂點的包裝類
 */
public class Vertex<T>{
    private T data;
    public Vertex(T data){
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

 

/*
鄰接矩陣實現圖的建立
 */


public class MGraph<T> {
    private Vertex<T>[] vertexs;
    private int[][] edges;
    private int numVertex;          //頂點的實際數量
    private int maxNumVertex;       //頂點的最大數量
    private int INFINITY = 65535;

    public MGraph(int maxNumVertex){
        this.maxNumVertex = maxNumVertex;
        this.vertexs = (Vertex<T>[]) new Vertex[maxNumVertex];
        this.edges = new int[maxNumVertex][maxNumVertex];
        for (int i = 0; i < numVertex; i++){
            for (int j = 0; j < numVertex; j++){
                edges[i][j] = INFINITY;
            }
        }
        for (int i = 0; i < numVertex; i++) {
            edges[i][i] = 0;
        }
    }

    public int getNumVertex(){
        return numVertex;
    }

    public int getMaxNumVertex(){
        return maxNumVertex;
    }

    public boolean isFull(){
        return numVertex == maxNumVertex;
    }

    public void addVertex(T data){
        if(isFull()){
            throw new RuntimeException("圖滿了");
        }
        Vertex<T> v = new Vertex<T>(data);
        vertexs[numVertex++] = v;
    }

    /**
     * 刪除圖中與data相等的頂點
     * @param data 要刪除的頂點的data值
     * @return 返回刪除了幾個頂點
     */
    public int removeVertex(T data){
        int flag = 0;
        for (int i = 0; i < numVertex; i++){
            if (vertexs[i].getData().equals(data)){
                for (int j = i; j < numVertex - 1; j++){
                    vertexs[j] = vertexs[j + 1];
                }
                //刪除矩陣的第 i 行
                for (int row = i; row < numVertex - 1; row++){
                    for (int col = 0; col < numVertex; col++){
                        edges[col][row] = edges[col][row + 1];
                    }
                }
                //刪除矩陣的第 i 列
                for (int row = 0; row < numVertex; row++){
                    for (int col = i; col < numVertex - 1; col++){
                        edges[col][row] = edges[col + 1][row];
                    }
                }
                numVertex--;
                flag++;
            }
        }
        return flag;
    }

    private int getIndexOfData(T data){
        int i = 0;
        while (!vertexs[i].getData().equals(data)){
            i++;
        }
        return i;
    }

    /**
     * 若為無向圖,data的順序隨意;若為有向圖,則新增的邊是data1指向data2
     * @param data1 弧尾
     * @param data2 弧頭
     * @param weight 權值
     */
    public void addEdge(T data1, T data2, int weight){
        int index1 = getIndexOfData(data1);
        int index2 = getIndexOfData(data2);
        edges[index1][index2] = weight;
    }

    public void removeEdge(T data1, T data2){
        int index1 = getIndexOfData(data1);
        int index2 = getIndexOfData(data2);
        edges[index1][index2] = INFINITY;
    }

    public void printMatrix(){
        for (int row = 0; row < numVertex; row++){
            for (int col = 0; col < numVertex; col++){
                System.out.print(edges[row][col] + " ");
            }
            System.out.println();
        }
    }
}

  鄰接矩陣可以解決圖的物理儲存,但我們也發現,對於邊相對頂點來說較少的圖,這種結構是存在對儲存空間的極大浪費的。如何解決呢?看下面

  2,鄰接表

  我們在前面提到過,順序儲存結構存在預先分配記憶體可能造成空間浪費的問題,於是引出了鏈式儲存結構。我們用類似於前面樹結構中孩子表示法的方式,陣列與連結串列相結合的儲存方法稱為鄰接表。

  處理方法:頂點用一維陣列儲存;每個頂點的所有鄰接點構成一個線性表,用單鏈表儲存。有向圖稱為頂點v的邊表;無向圖稱為頂點v作為弧尾的出邊表。

  

 

 注:若是有向圖,鄰接表結構是類似的。由於有向圖有方向,我們是以頂點為弧尾來儲存邊表的,這樣很容易得到每個頂點的出度。但也有是為了便於確定頂點的入度,我們可以建立一個有向圖的逆鄰接表,即對每個頂點v1都建立一個連結為v1為弧頭的表。

   若是帶權值的網圖,可以在邊表結點的定義中再增加一個weight的資料域,儲存權值資訊即可。

  下面是鄰接表儲存圖結構的程式碼實現

public class VertexL<T> {
    private T data;
    private EdgeL firstEdge;

    public VertexL(T data)
    {
        this.data = data;
    }

    public void setFirstEdge(EdgeL e){
        this.firstEdge = e;
    }

    public EdgeL getFirstEdge(){
        return firstEdge;
    }
    public T getData(){
        return data;
    }
}
public class EdgeL {
    private int adjvex; //儲存鄰接點對應的下標
    private EdgeL nextEdge;  //儲存

    public EdgeL(int adjvex){
        this.adjvex = adjvex;
    }

    public EdgeL(int adjvex, EdgeL e){
        this.adjvex = adjvex;
        this.nextEdge = e;
    }

    public EdgeL getNextEdge(){
        return nextEdge;
    }

    public void setNextEdge(EdgeL e){
        this.nextEdge = e;
    }

    public int getAdjvex(){
        return adjvex;
    }
}
/*
    無向圖(無權值)的鄰接表儲存。
 */
public class GraphAdjList <T>{
    private VertexL<T>[] vertexs;
    private int numVertex;
    private int maxNumVertex;

    public GraphAdjList(int maxNumVertex){
        this.maxNumVertex = maxNumVertex;
        this.vertexs =(VertexL<T>[]) new VertexL[maxNumVertex];
        numVertex = 0;
    }

    public boolean isFull(){
        return numVertex == maxNumVertex;
    }

    private int getNumVertex(){
        return numVertex;
    }
    /**
     * 新增頂點
     * @param data
     */
    public void addVertex(T data){
        if (isFull()){
            throw new IndexOutOfBoundsException();
        }
        VertexL<T> v = new VertexL<T>(data);
        vertexs[numVertex++] = v;
    }

    /**
     * 新增邊
     * @param data1
     * @param data2
     */
    public void addEdge(T data1, T data2){
        int indexOfData1 = getIndex(data1);
        int indexOfData2 = getIndex(data2);

        if (vertexs[indexOfData1].getFirstEdge() == null){
            vertexs[indexOfData1].setFirstEdge(new EdgeL(indexOfData2));
        }else {
            vertexs[indexOfData1].getFirstEdge().setNextEdge(new EdgeL(indexOfData2, vertexs[indexOfData1].getFirstEdge().getNextEdge()));
        }

        if (vertexs[indexOfData2].getFirstEdge() == null){
            vertexs[indexOfData2].setFirstEdge(new EdgeL(indexOfData1));
        }else {
            vertexs[indexOfData2].getFirstEdge().setNextEdge(new EdgeL(indexOfData1, vertexs[indexOfData1].getFirstEdge().getNextEdge()));
        }
    }


    private int getIndex(T data){
        int i = 0;
        for (; i < numVertex; i++){
            if (data.equals(vertexs[i].getData())){
                break;
            }

        }
        if (!data.equals(vertexs[i].getData()) && i == numVertex){
            throw new NullPointerException();
        }
        return i;
    }

    /**
     * 刪除邊
     * @param data1
     * @param data2
     */
    public void removeEdge(T data1, T data2){
        int indexOfData1 = getIndex(data1);
        int indexOfData2 = getIndex(data2);

        VertexL v = vertexs[indexOfData1];
        EdgeL e = v.getFirstEdge();
        if (e.getAdjvex() == indexOfData2){
            if (v.getFirstEdge().getNextEdge() == null) {
                v.setFirstEdge(null);
            }else {
                v.setFirstEdge(e.getNextEdge());
            }
        }else {
            while (e.getNextEdge().getAdjvex() != indexOfData2){
                e = e.getNextEdge();
            }
            if (e.getNextEdge().getNextEdge() != null) {
                e.setNextEdge(e.getNextEdge().getNextEdge());
            }else {
                e.setNextEdge(null);
            }
        }
    }

    /**
     * 刪除頂點
     * @param data
     */
    public void removeVertex(T data){
        int index = getIndex(data);
        for (int i = 0; i < numVertex; i++){
            if (i == index){
                continue;
            }
            removeEdge(vertexs[i].getData(), data);
        }
        for (int i = index; i < numVertex - 1; i++){
            vertexs[i] = vertexs[i + 1];
        }
    }

 

  3,十字連結串列

  對於有向圖來說,鄰接表只關心了出度問題,想了解入度就必須遍歷整個圖才能知道;反之,逆鄰接表解決了入度問題,卻不瞭解出度的情況。那麼能不能把鄰接表和逆鄰接表結合一下呢?

  這就是下面要講的儲存方式:十字連結串列。

  我們既然要結合鄰接表和逆鄰接表,就要先把頂點域融合一下如下

  firstin表示入邊表表頭指標,指向該頂點入邊表的第一個結點;  firstout表示出邊表表頭指標,指向該頂點出邊表的第一個結點。

  下面我們來把邊表結點結構也融合一下

  其中tailvex是指弧起點在頂點表的下標 ; headvex是指弧終點在頂點表中的下標。   

 

 

  headlink是入邊表指標域,指向同一個弧頭的弧;taillink是出邊表指標域,指向同一個弧尾的弧。 從新的邊表結點的域可以看出來,每一個邊表結點既承擔了作為入邊表的職責,也承擔了作為出邊表結點的職責。

  例如下面這個例子

 

 

 圖中虛線箭頭的含義就是此圖的逆鄰接表的表示。我們可以簡單的理解為,比如第一行的邊結點,就是表示從0指向3的有向弧,所以它一定是由0直接指向,並且由3虛線指向的。

再如圖中唯一連續指向的從V0指向邊10,再指向邊20,可以發現弧頭為0的都在同一列,弧尾同的都在同一行;由於V0有兩個入度,所以虛線連續指向兩個邊結點。

十字連結串列的好處是因為結合了鄰接表和逆鄰接表,既容易找到入度,也容易找到出度。除了結構複雜一點,它建立圖演算法的時間複雜度是和鄰接表相同的。因此在有向圖中,十字連結串列是非常好的資料結構。

程式碼實現如下: 

/*
    十字連結串列實現的圖結構的弧定義
 */
public class EdgeOL {
    private int tail;
    private int head;

    public EdgeOL(int tail, int head) {
        this.tail = tail;
        this.head = head;
    }
}


/*
    十字連結串列實現的圖結構的頂點定義
 */
public class VertexOL<T> {
    private T data;
    private EdgeOL firstIn;
    private EdgeOL firstOut;

    public VertexOL(T data) {
        this.data = data;
    }
}
/*
    圖結構的十字連結串列實現
 */
public class GraphOrthogonalList<T> {
    private VertexOL<T>[] vertexs;
    private int numVertex;
    private int maxNumVertex;

    public GraphOrthogonalList(int maxNumVertex){
        this.maxNumVertex = maxNumVertex;
        vertexs = (VertexOL<T>[])new VertexOL[maxNumVertex];
    }

    public boolean isFull(){
        return numVertex == maxNumVertex;
    }

    /**
     * 新增新頂點
     * @param data 新頂點的資料域
     */
    public void addVertex(T data){
        if (isFull()){
            return;
        }
        VertexOL<T> v = new VertexOL<>(data);
        vertexs[numVertex++] = v;
    }

    public void addEdge(int tail, int head){
        EdgeOL e = new EdgeOL(tail, head);
        //頭插法,形成十字連結串列
        e.setTailLink(vertexs[tail].getFirstOut());
        vertexs[tail].setFirstOut(e);
        e.setHeadLink(vertexs[head].getFirstIn());
        vertexs[head].setFirstIn(e);
    }

    /**
     * 刪除一個邊結點
     * @param tail
     * @param head
     */
    public void removeEdge(int tail, int head){
        removeFromTailList(tail, head);
        removeFromHeadList(tail, head);
    }

    /**
     * 從鄰接表中刪除一個邊結點
     * @param tail
     * @param head
     */
    private void removeFromTailList(int tail, int head){
        EdgeOL e = vertexs[tail].getFirstOut();
        //從tailLink中刪除它
        if (e != null && e.getHeadVex() == head){
            //如果e是第一個但不是最後一個結點,刪除它
            if (e.getTailLink() != null){
                vertexs[tail].setFirstOut(e.getTailLink());
            }else {
                //如果e是第一個也是最後一個結點,刪除它
                vertexs[tail].setFirstOut(null);
            }
        }else if (e != null){
            //如果e不是第一個結點,那麼遍歷連結串列找到要刪除的邊結點的上一個結點!!
            while (e.getTailLink() != null && e.getTailLink().getHeadVex() != head){
                e = e.getTailLink();
            }
            if (e.getHeadVex() != head){
                //throw new NullPointerException();
                //這裡不能拋異常,因為後面要遍歷刪除邊,拋異常會使程式終止
                return;
            }else {
                e.setTailLink(e.getTailLink().getTailLink());
            }
        }
    }

    /**
     * 從逆鄰接表中刪除一個邊結點
     * @param tail
     * @param head
     */
    private void removeFromHeadList(int tail, int head){
        //從headLink中刪除它
        EdgeOL e = vertexs[head].getFirstOut();
        if (e != null && e.getTailVex() == tail){
            //如果e1是第一個但不是最後一個結點,刪除它
            if (e.getHeadLink() != null){
                vertexs[head].setFirstIn(e.getHeadLink());
            }else {
                //如果e1是第一個也是最後一個結點,刪除它
                vertexs[head].setFirstIn(null);
            }
        }else if (e != null){
            //如果e1不是第一個結點,那麼遍歷連結串列找到要刪除的邊結點的上一個結點!!

            while (e.getHeadLink() != null
                    && e.getHeadLink().getTailVex() != tail){
                e = e.getHeadLink();
            }
            if (e.getTailVex() != tail){
                //throw new NullPointerException();
                return;
            }else {
                e.setHeadLink(e.getHeadLink().getHeadLink());
            }
        }
    }

    /**
     * 刪除index角標的頂點
     * @param index
     */
    public void removeVertex(int index){
        if (index >= numVertex){
            throw new NullPointerException();
        }

        //刪除與該頂點有關的所有邊
        for (int i = numVertex - 1; i > 0; i--){
            removeEdge(index, i);
            removeEdge(i, index);
        }

        //刪除該結點
        for (int i = index; i < numVertex - 1; i++){
            vertexs[i] = vertexs[i + 1];
        }
        numVertex--;
    }
}

  4,鄰接多重表

  上面的三種結構看似已經解決了所有問題,但在編寫程式碼的時候才能體會到,插入頂點,插入邊時非常方便,但刪除時很麻煩。如何解決呢? 有時又要對已訪問的邊做標記,又怎麼做呢? 

  下面我們來看面向無向圖的鄰接多重表。

   我們把鄰接表中的邊表結點的結構進行改造如下

   

 

   其中ivex和jvex是與某條邊依附的兩個頂點在頂點表中的下標。ilink指向依附頂點ivex的下一條邊,jlink指向依附頂點jvex的下一條邊。這就是鄰接多重表。

      注意:ilink指向的結點的jvex和它本身的ivex值相同

   下面舉例

 

 若要刪除左圖(v0 , v2)這條邊,僅需讓6 , 9這兩個連結改為^即可,刪除方便了很多。

上面這種方法是《大話資料結構》中的畫法,它 “貌似” 限制了ivex和jvex的順序,使得在程式碼實現上難以思考。我找到了下面這個視訊,是一個很好的鄰接多重表的解釋,(咖哩英語警告)。它沒有限制ivex必須指向相同的jvex,沒有限制ivex和jvex的順序,也沒有限制陣列中的頂點結點只能指向一個邊結點,更靈活,更好理解,程式碼也更容易實現。

https://www.youtube.com/watch?v=f2z1n6atBsc

  下面是視訊中畫法的程式碼實現。

/*
鄰接多重表的頂點定義
 */
public class VertexAM<T> {
    private T data;
    private EdgeAM firstEdge;

    public VertexAM(T data){
        this.data = data;
    }
    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public EdgeAM getFirstEdge() {
        return firstEdge;
    }

    public void setFirstEdge(EdgeAM firstEdge) {
        this.firstEdge = firstEdge;
    }
}
/*
鄰接多重表的邊結點定義
 */
public class EdgeAM {
    private int ivex;
    private int jvex;
    private EdgeAM ilink;
    private EdgeAM jlink;

    public EdgeAM(int ivex, int jvex){
        this.ivex = ivex;
        this.jvex = jvex;
    }

    public int getIvex() {
        return ivex;
    }

    public void setIvex(int ivex) {
        this.ivex = ivex;
    }

    public int getJvex() {
        return jvex;
    }

    public void setJvex(int jvex) {
        this.jvex = jvex;
    }

    public EdgeAM getIlink() {
        return ilink;
    }

    public void setIlink(EdgeAM ilink) {
        this.ilink = ilink;
    }

    public EdgeAM getJlink() {
        return jlink;
    }

    public void setJlink(EdgeAM jlink) {
        this.jlink = jlink;
    }
}
/*
鄰接多重表的程式碼實現
 */
public class GraphAM <T>{
    private VertexAM<T>[] vertexs;
    private int numVertex;
    private int maxNumVertex;

    public GraphAM(int maxNumVertex){
        this.maxNumVertex = maxNumVertex;
        this.vertexs = (VertexAM<T>[])new VertexAM[maxNumVertex];
        numVertex = 0;
    }

    public boolean isFull(){
        return numVertex == maxNumVertex;
    }

    public void addVertex(T data){
        if (isFull()){
            return;
        }
        vertexs[numVertex++] = new VertexAM<>(data);
    }

    /**
     * 將新結點連在iLink連結串列的鏈尾和jLink連結串列的鏈尾
     * @param ivex
     * @param jvex
     */
    public void addEdge(int ivex, int jvex){
        EdgeAM e = new EdgeAM(ivex, jvex);
        if (vertexs[ivex].getFirstEdge() == null) {
            vertexs[ivex].setFirstEdge(e);
        } else if (vertexs[jvex].getFirstEdge() == null){
            vertexs[jvex].setFirstEdge(e);
        } else {
            EdgeAM ptr = vertexs[ivex].getFirstEdge();
            while (ptr.getIlink() != null){
                ptr = ptr.getIlink();
            }
            ptr.setIlink(e);

            ptr = vertexs[jvex].getFirstEdge();
            while (ptr.getJlink() != null){
                ptr = ptr.getJlink();
            }
            ptr.setJlink(e);
        }
    }

    /**
     * 刪除邊,如果邊結點直連頂點結點,則直接刪除;若不直連,先找到邊結點的上一個邊結點,然後將上一個邊結點的link域置空。
     * @param ivex
     * @param jvex
     */
    public void removeEdge(int ivex, int jvex) {
        if (vertexs[ivex].getFirstEdge() != null && vertexs[ivex].getFirstEdge().getJvex() == jvex) {
            vertexs[ivex].setFirstEdge(null);
        } else if (vertexs[jvex].getFirstEdge() != null && vertexs[jvex].getFirstEdge().getIvex() == ivex) {
            vertexs[jvex].setFirstEdge(null);
        } else {
            removeFromLink(ivex, jvex);

            removeFromLink(jvex, ivex);
        }
    }

    private void removeFromLink(int ivex, int jvex){
        EdgeAM ptr = vertexs[ivex].getFirstEdge();
        if (ptr == null){
            return;
        }
        while (ptr.getIlink() != null && ptr.getIlink().getJvex() != jvex) {
            ptr = ptr.getIlink();
        }
        if (ptr.getIlink() == null){
            return;
        }else {
            ptr.setIlink(null);
        }
    }

    /**
     * 先刪除與本頂點相連的所有邊,然後再刪除頂點
     * @param index
     */
    public void removeVertex(int index){
        for (int i = 0; i < numVertex; i++){
            removeEdge(i, index);
            removeEdge(index, i);
        }

        for (int i = index; i < numVertex - 1; i++){
            vertexs[i] = vertexs[i + 1];
        }
        numVertex--;
    }
}

  5,邊集陣列

   邊集陣列由兩個一維陣列構成,一個儲存頂點的資訊;另一個儲存邊的資訊,這個邊陣列每個資料元素由一條邊的起點下標(begin)、終點下標(end)和權(weight)組成。

    邊集陣列的效率並不高,它適合對邊依次進行處理的操作,而不適合對頂點進行相關的操作。邊集陣列的應用將在後面的克魯斯卡爾(Kruskal)演算法中有介紹。

 

 四、圖的遍歷

  圖的遍歷是和樹的遍歷類似,我們希望從圖中某一頂點出發仿遍圖中其餘頂點,且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)

  1,深度優先遍歷(Depth_First_Search DFS) 

  

 

 我們以上圖為例,假設我們從A出發,只要沒碰到訪問過的結點,就一直往右手邊走,先到B,再到C,再到D,E,F,此時再往右走就碰到A了,所以我們往左邊走,到G,往右為D,D訪問過了,所以往左走到H,這時H的左右D和E都已經訪問過了,到了死衚衕。

但是此時圖中還有I結點沒有訪問過,所以我們從H沿原路後退經過G,F,E,D,C,此時從C往左手邊走訪問了I。這就是深度優先遍歷的思路:從圖中某個頂點v出發,只要它存在沒有被訪問過的鄰接點,就進入該鄰接點,然後以該點進行深度優先遍歷。

仔細觀察大家會感受到,深度優先遍歷其實很像棧/遞迴。所以想到用遞迴來實現深度優先遍歷。注意實現過程中不必拘泥於上面解釋圖片時的一直往右走這個說法,因為圖的儲存只有畫出圖片對人的觀察來說才有左和右的意義。程式碼如下

    /*
        對於使用鄰接矩陣儲存的圖的深度優先遍歷
    */

//標識結點是否被訪問過 private boolean[] visited; //深度優先遍歷操作入口 public void DFSTraverse(){ this.visited = new boolean[getNumVertex()]; for (boolean bool : visited){ bool = false; } //若該頂點沒被訪問過,則從該頂點為起點深度優先遍歷,若為連通圖,則只會執行一次DFS for (int i = 0; i < getNumVertex(); i++){ DFS(i); } } //深度優先遍歷演算法 private void DFS(int i) { visited[i] = true; System.out.println(vertexs[i].getData()); for (int j = 0; j < numVertex; j++){ if ( !visited[j] && (edges[i][j] != 0 || edges[i][j] != INFINITY)){ DFS(j); } } }

  

    //標識結點是否被訪問過
    private boolean[] visited;
    //深度優先遍歷操作入口
    public void DFSTraverse(){
        this.visited = new boolean[getNumVertex()];
        for (boolean bool : visited){
            bool = false;
        }
        //若該頂點沒被訪問過,則從該頂點為起點深度優先遍歷,若為連通圖,則只會執行一次DFS
        for (int i = 0; i < getNumVertex(); i++){
            if (!visited[i]) {
                DFS(i);
            }
        }
    }
    //深度優先遍歷演算法
    private void DFS(int i) {
        visited[i] = true;
        System.out.println(vertexs[i].getData());
        if (!(vertexs[i].getFirstEdge() == null)) {
            EdgeL e = vertexs[i].getFirstEdge();  //取到鄰接表的第一個元素
            while (e != null) {      //鄰接表不為空
                if (!visited[e.getAdjvex()]) {     //如果該元素沒有被訪問過,則以該元素為起點再次進行深度優先遍歷;如果訪問過,則取到鄰接表的下一個結點(可以理解為往右走走不通,變成往左走)
                    DFS(e.getAdjvex());
                }
                e = e.getNextEdge();
            }
        }
    }

 

上面是兩種儲存方式的深度優先遍歷的遞迴寫法,我們可以看出鄰接矩陣的DFS的時間複雜度是O(n2)而鄰接表的DFS是O(n+e)所以當點多邊少的稀疏圖時,鄰接表結構在深度優先遍歷上的時間效率大大提高。

  眾所周知,遞迴演算法在資料量過大時容易引起棧溢位等問題,所以下面我們來看一下DFS的非遞迴寫法

  如下是鄰接矩陣,DFS非遞迴寫法(ArrayStack是我在前面關於棧的部落格中實現的一個簡單鏈棧demo)

    //標識結點是否被訪問過
    private boolean[] visited_2;

    /**
     * 利用棧來實現,如果該頂點被訪問,則壓棧;
     * 當走到死衚衕,查詢棧頂元素是否有其他未被訪問的鄰接點,如果有,則訪問它,並壓棧,如果沒有,則將棧頂元素彈棧,直到棧為空。
     */
    public void DFSTraverse_2(){
        this.visited = new boolean[numVertex];
        for (int i = 0; i < numVertex; i++){
            visited[i] = false;
        }

        ArrayStack<Integer> s = new ArrayStack<Integer>();
        int i = 0;
        visit(i);
        s.push(i);
        while (!s.isEmpty()){
            int j = 0;
            int top = s.getTop();
            for (; j < numVertex; j++){
                if ( !visited[j] && (edges[top][j] != 0 || edges[top][j] != INFINITY)){
                    visit(j);
                    visited[j] = true;
                    s.push(j);
                    break;
                }
            }
            if (j == numVertex){
                s.pop();
            }
        }
    }

    private void visit(int i){
        System.out.println(vertexs[i].getData());
        visited[i] = true;
    }

  以下是鄰接表DFS非遞迴寫法。

   //標識結點是否被訪問過
    private boolean[] visited_2;
    //深度優先遍歷操作入口
    public void DFSTraverse_2(){
        this.visited = new boolean[getNumVertex()];
        for (int i = 0; i < numVertex; i++){
            visited[i] = false;
        }
        for (int i = 0; i < numVertex; i++) {
            ArrayStack<Integer> s = new ArrayStack<>();
            visit(i);
            s.push(i);
            while (!s.isEmpty()) {
                int topIndex = s.getTopData();

                EdgeL p = vertexs[topIndex].getFirstEdge();
                //遍歷鄰接表,直到找到鄰接表中沒有被訪問過的結點
                while (p != null) {
                    if (!visited[p.getAdjvex()]) {
                        visit(p.getAdjvex());
                        s.push(p.getAdjvex());
                    } else if(p.getNextEdge() != null && visited[p.getNextEdge().getAdjvex()] == false){
                        p = p.getNextEdge();
                    }
                }
                if (p == null) {
                    s.pop();
                }
            }
        }
    }

    public void visit(int i){
        System.out.println(vertexs[i].getData());
        visited[i] = true;
    }

  2,廣度優先遍歷(Breadth_First_Search  BFS) 

  如果說圖的深度優先遍歷類似於樹的前序遍歷,那麼廣度優先遍歷就類似於樹的層序遍歷。

 

 

 我們把上左圖調整一下位置,形成有層間關係的類似樹的結構。然後以佇列的形式,當一個元素被遍歷,則將它出隊的同時,將它的未被遍歷的鄰接結點入隊,直到佇列中的全部元素都被遍歷。

程式碼如下

/*
  鄰接矩陣儲存的圖的廣度優先遍歷
*/ 
    public void BFSTraverse(){
        ArrayDeque<Integer> queue = new ArrayDeque<>();
        this.visited = new boolean[getNumVertex()];
        for (int i = 0; i < numVertex; i++){
            visited[i] = false;
        }
        for(int i = 0; i < numVertex; i++){
            if (!visited[i]) {
                queue.add(i);
            }
            while (!queue.isEmpty()){
                int row = queue.remove();
                visit(row);
                for (int j = 0; j < numVertex; j++){
                    //如果存在這條邊,且這條邊的鄰接點沒有被訪問過,且鄰接點不在佇列中,則將該鄰接點入隊
                    if ((edges[row][j] != 0 && edges[row][j] != INFINITY) && !visited[j] && !queue.contains(j)){
                        queue.add(j);
                    }
                }
            }
        }
    }

    private void visit(int i){
        System.out.println(vertexs[i].getData());
        visited[i] = true;
    }
}
/*
    鄰接表儲存的圖的廣度優先遍歷
*/
    public void BFSTraverse(){
        ArrayDeque<Integer> queue = new ArrayDeque<>();
        this.visited = new boolean[getNumVertex()];
        for (int i = 0; i < getNumVertex(); i++){
            visited[i] = false;
        }

        for (int i = 0; i < getNumVertex(); i++){
            if (!visited[i]) {
                queue.add(i);
            }
            while (!queue.isEmpty()){
                int node = queue.remove();
                visit(node);
                EdgeL e = vertexs[node].getFirstEdge();
                if (e != null && !queue.contains(e.getAdjvex()) && !visited[e.getAdjvex()]) {
                    queue.add(e.getAdjvex());

                    while (e != null && e.getNextEdge() != null && !queue.contains(e.getAdjvex()) && !visited[e.getAdjvex()]) {
                        queue.add(e.getAdjvex());
                        e = e.getNextEdge();
                    }
                }
            }
        }
    }

    public void visit(int i){
        System.out.println(vertexs[i].getData());
        visited[i] = true;
    }

對比發現,DFS和BFS在時間複雜度上是一樣的,僅僅是訪問次序不同。深度優先更適合目標明確,以找到目標為目的的情況;廣度優先更適合在不斷擴大遍歷範圍時找到相對最優解的情況。

下面應該是最小生成樹這一部分,但是研究了一天,發現大話資料結構這本書的圖這部分寫的實在是爛,難以下嚥,所以後面將再起一篇部落格,來記錄後續部分,將會以Sedgewick版《演算法》的風格來敘述。如果有人看到部落格對後續有期待,請等我幾天