[從今天開始修煉數據結構]圖的最小生成樹 —— 最清楚易懂的Prim算法和kruskal算法講解和實現

接上文,研究了一下算法以後,發現大話數據結構的代碼風格更適合與前文中鄰接矩陣的定義相關聯,因此硬着頭皮把大話中的最小生成樹用本身的話整理了一下,但願你們可以看懂。算法

  1、最小生成樹數組

    1,問題網絡

      最小生成樹要解決的是帶權圖 即 網 結構的問題,就是n個頂點,用n-1條邊把一個連通圖鏈接起來,而且使得權值的和最小。能夠普遍應用在修路建橋、管線運輸、快遞等各中網絡方面。咱們把構造連通圖的最小代價生成樹成爲最小生成樹。數據結構

      最小生成樹有兩個算法 普里姆算法和克魯斯卡爾算法ide

    2,普里姆算法函數

      (1)普里姆算法的思路是,從一個入口進入,找到距離入口最近的下一個結點;如今你有了兩個結點,找到分別與這兩個結點連通的點中,權值最小的;如今你有了三個結點,分別找到與這三個結點聯通的點中,權值最小的;……this

      從思路能夠看出,這就是一個切分問題。將一副加權圖中的點進行區分,它的橫切邊中權值最小的必然屬於子圖的最小生成樹。(橫切邊,指鏈接兩個部分的頂點的邊)也就是將已經找到的點,和沒找到的點進行區分。spa

      解決思路就是貪心算法,使用切分定理找到最小生成樹的一條邊,而且不斷重複直到找到最小生成樹的全部邊。3d

      (2)實現思路code

       下面咱們就來一步一步地實現普里姆算法。爲了方便討論,咱們先規定一些事情。a,只考慮連通圖,不考慮有不連通的子圖的狀況。b,只考慮每條邊的權值都不一樣的狀況,如有權值相同,會致使生成樹不惟一

        c,Vi的角標對應了其在vertex[]中存儲的角標。 d,這裏咱們從V0爲入口進入。e,這裏咱們使用的是上一篇文章實現的鄰接矩陣存儲的圖結構。

      首先,咱們拿到一張網。

      

 

     咱們從V0進入,找到與V0相連的邊,鄰接點和權值。咱們須要容器來存儲,由於是鄰接矩陣存儲的,因此咱們這裏用一維數組來存儲這些元素,咱們仿照書中的代碼風格,定義一個adjvex[numVertex]和一個lowcost[numVertex]。這兩個數組的含義和具體用法咱們後面再說,如今你只須要知道它們是用來橫切邊,鄰接點和權值的。adjvex[]的值被初始爲0,由於咱們從V0開始進入。lowcost[]的值被初始化爲INFINITY,由於咱們要經過這個數組找到最小權值,因此要初始化爲一個不可能的大值,以方便後續比較,但這裏的INFINITE 不須要手動設置,由於edges中已經將沒有邊的位置設置爲了I,因此只須要在拷貝權值時同事將I也拷貝過來。

    如今咱們的切分只區分了V0和其餘點,橫切邊權值有10,11,分別對應鄰接點V1,V5,咱們將V1,V5的對應權值按照其在vertex[]中存儲的角標1,5來存入lowcost,將與V1,V5對應的鄰接點V0的角標0存入adjvex[]中(其實全部頂點的鄰接點對應角標都被初始化爲0)。因此咱們如今獲得

    adjvex[]  = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 };
    lowcost[] = { 0 ,10 , I , I , I , 11 , I , I , I };

  這裏你們要理解這兩個數組的含義,我再仔細解釋一下。lowcost中存有非I元素的位置的下標,表示的是,橫切邊所鏈接的,沒有被連入生成樹的一端的頂點在vertex[]中保存的下標,在這裏也等於Vi的下標,也能夠理解爲「剛剛被發現的結點的下標」。

  而後adjvex[]中保存了非0元素的下標,與lowcost的中的下標是同樣的意義,只不過這裏由於被劃分的點是0因此數組中沒有非0元素。

  lowcost中存有的非I元素,表示的是,這個位置對應的頂點與現有最小生成樹的橫切邊中權值最小的一條邊的權值。

  adjvex中存有的非0元素,是一個頂點在vertex[]中的下標,表示的是該角標index對應的vertex[index]與adjvex[index]這兩個頂點之間的權值最小,該權值是lowcost[index]。

  這樣你們應該可以對大話數據結構中這一部分有完整的理解了。   

  咱們如今從lowcost[]中找到最小的權值,而後將該權值對應的頂點加入最小生成樹。加入的方式是將該權值置爲0,而且將該新結點的鄰接點在adjvex中對應的角標置爲該新結點的index。

  對應如今的狀況,就是把V1加入生成樹,把兩個數組調整以下:

    adjvex[]  = { 0 , 0 , 1 , 0 , 0 , 0 , 1 , 0 , 1 };
    lowcost[] = { I , 0 , 18, I , I ,11 , 16, I , 12};

 

 

接下來咱們有四條待選邊,lowcost中找到最小的非零權值是11,對應index爲5,去vertex[5]找到V5,因此此時V5加入生成樹,將lowcost[5]置爲0,V5的鄰接點加入adjvex和lowcost,以下

    adjvex[]  = { 0 , 0 , 1 , 0 , 5 , 0 , 1 , 0 , 1 };
    lowcost[] = { I , 0 , 18, I , 26, 0 , 16, I , 12};

 

 反覆重複上面的動做v-1次,此時就加入了v-1條邊,獲得了最小生成樹。

代碼實現以下:

    public void MiniSpanTree_Prim(){

        int[] adjvex = new int[numVertex];
        int[] lowcost = new int[numVertex];

        adjvex[0] = 0;
        lowcost[0] = 0;
        /*
        for (int i = 1; i < numVertex; i++){
            lowcost[i] = INFINITY;
        }

         */

        for (int i = 1; i < numVertex; i++)
        {
            lowcost[i] = edges[0][i];
            adjvex[0] = 0;
        }

        for (int i = 1; i < numVertex; i++){
            int min = INFINITY;
            int k = 0;
            for (int j = 1; j < numVertex; j++){
                if (lowcost[j] != 0 && lowcost[j] < min){
                  min = lowcost[j];
                  k = j;
                }
            }
            System.out.printf("(%d,%d,w = %d)加入生成樹",adjvex[k],k,min);
            System.out.println();

            lowcost[k] = 0;
            for (int j = 1; j < numVertex; j++){
                if (lowcost[j] != 0 && lowcost[j] > edges[k][j]){
                    lowcost[j] = edges[k][j];
                    adjvex[j] = k;
                }
            }
        }
    }

  2,克魯斯卡爾(Kruskal)算法

  若是說普里姆算法是面向頂點進行運算,那麼克魯斯卡爾算法就是面向邊來進行運算的。

  (1)思路:克魯斯卡爾算法的思路是,在離散的頂點集中,不斷尋找權值最小的邊,若是加入該邊不會在點集中生成環,則加入這條邊,直到得到最小生成樹。

      因此咱們的問題就是兩個,第一個:將邊按照權值排序  第二個:判斷加入一條邊以後會不會生成環

    將邊按照權值排序很容易,這裏咱們用邊集數組

    那麼如何判斷環呢? 克魯斯卡爾判斷環的依據是,在一棵樹結構中,若是對其中兩個結點添加一條本來不存在的邊,那麼必定會產生一個環。而一棵樹中,根節點是惟一肯定的。咱們將添加邊抽象爲創建一棵樹,並用數組parent[numVertex]存儲這棵樹的結構,下標index與結點在vertex[]中的位置vertex[index]相同,parent[index]是這個結點在這棵樹中的父親結點。每次添加一條邊,就是在擴大森林中某一棵樹,當森林所有連成一棵樹,則獲得了最小生成樹。

  (2)具體步驟:咱們這裏使用與普里姆相同的例子

 

其邊集數組排序後爲

 

 咱們遍歷邊集數組,首先拿到edges[0],分別判斷4和7是否擁有相同的最大頂點,方法是進入parent[]中查詢它們所在的樹的根結點是否相同。由於是第一次查詢,因此結果都是0,即它們不是同一棵樹,能夠連線。連線時,將parent[4]置7或者將parent[7]置4均可以,這裏咱們選擇前者。

 

 

 下面拿到edges[1],查詢parent[2],parent[8]得均爲0,則不是同一棵樹,能夠連線。咱們將parent[2]置8

 

 下面是edges[2],查詢0,1,能夠連線。

 

 接下來edges[3],查詢0,5,此時V0的父是V1,V1對應的parent[1]中存儲的0表示V1是這棵樹的父,parent[5]=0,即V0和V5不是同一棵樹,能夠連線。將parent[1]置爲5

 

 

接下來edges[4], 查詢1,8,不在同一棵樹,此時1所在樹的根是5,將1和8連線,此時樹合併應該將根節點5的parent[5]置爲8.如今上圖的兩個棵樹合併了

 

 接下來是edges[5],查詢3,7,不在同一子樹,連線。

 

 

 接下來是edges[6],查詢1,6,不在同一子樹,連線。

 

 

 接下來是edges[7]查詢5,6,發現它們的根節點都是8,在同一棵子樹,因此不連線。

下面我就再也不重複了,總之這樣循環檢測,能夠獲得最終的最小生成子樹。(注!最小生成子樹和咱們上面用來判斷是否連通的樹是不一樣的!parent數組也並非惟一的!由於在構造判斷樹的時候,無論把誰作父,誰作子,均可以構建樹,並不影響判斷環的結果)

 

 

   (3)代碼實現

   

    /*
        定義邊結構的內部類。
     */
    private class Edge implements Comparable<Edge> {
        private int begin;
        private int end;
        private int weight;

        private Edge(int begin, int end, int weight){
            this.begin = begin;
            this.end = end;
            this.weight = weight;
        }

        @Override
        public int compareTo(Edge e) {
            return this.weight - e.weight;
        }

        public int getBegin() {
            return begin;
        }

        public void setBegin(int begin) {
            this.begin = begin;
        }

        public int getEnd() {
            return end;
        }

        public void setEnd(int end) {
            this.end = end;
        }

        public int getWeight() {
            return weight;
        }

        public void setWeight(int weight) {
            this.weight = weight;
        }
    }
    /**
     * 獲得排序好的邊集數組,用ArrayList存儲
     * @return
     */
    public ArrayList<Edge> getOrderedEdges() {
        ArrayList<Edge> edgeList = new ArrayList<>();
        for (int row = 0; row < numVertex; row++){
            for (int col = row; col < numVertex; col++){
                if(edges[row][col] != 0 && edges[row][col] != INFINITY){
                    edgeList.add(new Edge(row, col, edges[row][col]));
                }
            }
        }
        Collections.sort(edgeList);
        return edgeList;
    }

    /**
     * 克魯斯卡爾算法
     */
    public void MiniSpanTree_Kruskal(){
        ArrayList<Edge> edgeList = getOrderedEdges();
        int[] parent = new int[numVertex];
        for (int i = 0; i < numVertex; i++){
            parent[i] = 0;
        }

        for (int i = 0; i < edgeList.size(); i++){
            int m = findRoot(edgeList.get(i).getBegin(), parent);
            int n = findRoot(edgeList.get(i).getEnd(), parent);
            if (m != n){
                link(edgeList.get(i), parent, m, n);
            }
        }


    }

    /*
        鏈接兩點,而且設置parent數組
     */
    private void link(Edge edge, int[] parent, int m, int n) {
        System.out.printf("(%d,%d),weight = %d 加入最小生成樹", edge.getBegin(), edge.getEnd(), edge.getWeight());
        System.out.println();

        parent[m] = n;
    }

    /*
    找到本子樹的根節點
     */
    private int findRoot(int root, int[] parent) {
        while (parent[root] > 0){
            root = parent[root];
        }
        return root;
    }

總結:克魯斯卡爾的FindRoot函數的時間複雜度由邊數e決定,時間複雜度爲O(loge),而外面有一個for循環e次,因此克魯斯卡爾的時間複雜度是O(eloge)

  對立兩個算法,克魯斯卡爾主要針對邊展開,邊少時時間效率很高,對於稀疏圖有很大優點,;而普里姆算法對於稠密圖會更好一些。

相關文章
相關標籤/搜索