最近在復習數據結構和算法的的內容,棧和隊列的思想是比較深刻,借於許多高級語言都有相應的框架實現了棧和隊列鏈表等,所以對於這一類,我們只需要了解其思想,在真正操作時,也會顯得比較簡單。但是還有一類數據結構是稍顯復雜的,在高級語言的程序里面並沒有相應的框架,比如樹和圖。樹一般可用節點結構體來封裝一個節點,但是圖,圖的話就不容易表示了,因為圖是無序的,每個節點與其他節點都有任意的連通性。但是基於使用圖的操作目的而言,一般有:搜索(遍歷)、最小生成樹、尋找節點之間的最小路徑等。其目的都是為了存儲點對之間的連通性,以及通路的代價,為此,我們可以根據我們的使用目的對其進行抽象為:鄰接表、鄰接矩陣、十字鏈表。
連通圖的最小生成樹
最小生成樹其實在計算機網絡里面也有應用:在有線Lan中,為避免交換機之間的連線形成環路,而最終會導致“兜圈子”,從而引起“廣播風暴”的現象,Lan中交換機的配置就采用了最小生成樹的算法,來避免形成環路。下面介紹兩種連通圖的最小生成樹算法,普里姆算法(Prim)和克魯斯卡算法(Kruskal),他們在時空消耗上面,各有優劣。但是這里也順便說,Prim和Kruskal算法都是具是貪心算法的類比,都是從局部最優最后到全局最優的。
(Prim)普里姆算法
其思想是:
1.有兩個集合V,S . S代表已經被識別的最小生成樹路徑上的節點集合,V代表所有節點的集合,V-S 就是剩余未被識別的節點的集合。
2.程序開始時,指定v0 加入S中,使得{v0} = S .
3.在V-S 集合中尋找到下一個節點vi,使得vi 到 S的距離最短。(vi到S的距離是指,vi到S集合中任意一點的距離;當兩點直接相連時為連通,否則距離為無窮)。將vi 加入到集合S中。
4.不斷運行步驟三,直到S集合包含了所有節點。
由上就是普里姆算法,其思想非常簡單,每次都是去取尋找離已識別集合最短的路徑,這樣局部最優導致全局最優。該算法的時間復雜度為O(n2).
下面給出完整的C++代碼實現:
#include <iostream>
#include<vector>
#include<algorithm>
#include<set>
#include<string.h>
#define N 6
#define MAX_INT 999999
using namespace std;
// 邊的結構體
typedef struct{
int x;
int y;
int cost;
} Tpath;
//連通圖的鄰接矩陣
int g [N][N] = {{-1,6,1,5,-1,-1},{6,-1,5,-1,3,-1},{1,5,-1,5,6,4},{5,-1,5,-1,-1,2},{-1,3,6,-1,-1,6},{-1,-1,4,2,6,-1}};
void gprim();
//main
int main(int argc, char** argv) {
gprim();
return 0;
}
//運行prim算法
void gprim(){
vector<Tpath> p; //記錄邊
vector<int>u; //集合S
u.push_back(0); //將V0加入到S中
int node1= 0,node2 = 0,cost = 0;
int i = 0;
vector<int>::iterator it;
for(u.size(); u.size() < N ;){
// get the lowcost path
node1 = -1;
node2 = -1;
cost = MAX_INT ;
for(it = u.begin() ;it != u.end() ; it++){ // 從V-S集合里面尋找到離S集合最lowcost的節點和對應的邊。將其記錄下來為 為cost,node1,node2
int k = (*it);
for(i = 0; i < N ;i++){
if(i == (*it))continue;
if(g[k][i] >= 0 && (find(u.begin(),u.end(),i) == u.end()) && g[k][i] < cost){
node1 = k;
node2 = i;
cost = g[k][i];
}
}
}
// 將該節點加入到S中 並記錄下路徑path
Tpath path;
path.cost = cost;
path.x = node1;
path.y = node2;
p.push_back(path);
u.push_back(node2);
}
//輸出
vector<Tpath>::iterator itO;
for(itO = p.begin() ; itO != p.end() ;itO++){
printf("(%d ,%d) cost: %d\n",itO->x+1,itO->y+1,itO->cost);
}
}
(Kruskal)克魯斯卡算法
其思想是:
1.引入節點的連通分量的概念:即一個節點與其他哪些節點相連通。
2.程序開始時,每個節點的連通分量就是自己。有集合E,SE,S。E為圖中邊的集合,SE為圖中已經被識別的邊的集合。SE開始為{},S為已識別點的集合。
3.從E-SE中選擇一條邊(vi,vj),其邊的兩個頂點時是vi,vj:該邊的距離是所有E-S中距離最短的。同時,vi的連通分量中不包含vj,vj的連通分量中不包含vi。將(vi,vj)加入到SE中,將vi,vj
加入到S中,同時將vi的連通分量加入vj中,將vj的連通分量加入到vi中。
4.持續運行步驟3,直到S集合包含了所有節點。
由上就是克魯斯卡算法。分析其算法可知,其時間復雜度度為n(logn) , n 為連通圖中邊的個數。為什么是O(n(logn))呢?其實很簡單,克魯斯卡的算法中每次都是找的E-SE中最短的邊,這里可以使用排序算法對所有的邊進行排序(O(nlogn)),然后再執行算法步驟2-4時,就可以依次取出來(O(n))。而這里最大的時間消耗是排序,所以是O(nlogn)。
Kruskal的個人實現:
#include <iostream>
#include<vector>
#include<algorithm>
#include<set>
#include<string.h>
#define N 6
#define MAX_INT 999999
using namespace std;
// 邊的結構體
typedef struct{
int x;
int y;
int cost;
} Tpath;
//連通圖的鄰接矩陣
int g [N][N] = {{-1,6,1,5,-1,-1},{6,-1,5,-1,3,-1},{1,5,-1,5,6,4},{5,-1,5,-1,-1,2},{-1,3,6,-1,-1,6},{-1,-1,4,2,6,-1}};
//increase sort
bool cmp(const Tpath &p1, const Tpath &p2){
return p1.cost < p2.cost;
}
void gkruskal();
//main
int main(int argc, char** argv) {
gkruskal();
return 0;
}
void gkruskal(){
vector<Tpath> t;
int i = 0 ,j = 0;
//將所有的邊生成一個一個的結構體節點
for(i ; i < N ; i++){
for(j = i+1;j <N ;j++){
if(g[i][j] < 0) continue;
Tpath p ;
p.x = i;
p.y = j;
p.cost = g[i][j];
t.push_back(p);
}
}
//按邊的距離升序排序
sort(t.begin(),t.end() ,cmp);
vector<Tpath>::iterator it;
//為每個節點Vi設置連通分量
vector< set<int> > sets;
for(i = 0; i < N ;i++){
set<int> v;
v.insert(i);
sets.push_back(v);
}
vector<Tpath> p;
i = 0;
//執行算法,掃描升序邊集合
for(;p.size() < N -1 ; ){
set<int> x = sets[t[i].x];
set<int> y = sets[t[i].y];
set<int>::iterator it;
//如果該邊的兩個頂點Vi ,Vj 各自的連通分量不包含對方,就將改變加入到路徑集合SE中
if(x.find(t[i].y) == x.end()){
p.push_back(t[i]);
set<int>::iterator xi ;
//同時將Vj的連通分量加入到Vi的連通分量重
for(xi = sets[t[i].x].begin() ; xi != sets[t[i].x].end(); xi++){
if((*xi) == t[i].x)continue;
sets[(*xi)].insert(y.begin(),y.end());
}
//同時將Vi的連通分量加入到Vj的連通分量重
for(xi = sets[t[i].y].begin() ; xi != sets[t[i].y].end(); xi++){
if((*xi) == t[i].y)continue;
sets[(*xi)].insert(x.begin(),x.end());
}
sets[t[i].x].insert(y.begin(),y.end());
sets[t[i].y].insert(x.begin(),x.end());
}
++i; //掃描下一條邊
}
//輸出最小生成樹的 邊對
for(it = p.begin() ; it != p.end();it++){
cout<<"("<< it->x +1 <<","<<it->y + 1<<")"<<"cost :"<<it->cost<<endl;
}
}
上面就是兩個比較簡單,但是比較經典的連通圖最小生成樹的算法。Prim算法時間復雜度略高,但是空間消耗較少;而Kruskal的算法呢,時間復雜度低,但需要為每個節點設置連通分量的存儲空間,因此空間復雜度略高。總之看了這些算法之后,總是對計算機的算法設計有股莫名的傾佩和向往啊!。。。
參考書本:
數據結構(c語言版) 清華大學出版社
計算機算法設計與分析
