图的基本算法实现
图的基本算法实现主要包括图的存储、图的遍历,求解图的最小生成树,以及求解最短路径等。
图的存储:
对于图的存储有多种方式,最常用的是邻接矩阵存储、邻接表存储,除此外还有十字邻接表存储,邻接多重表存储等;
邻接矩阵存储很简单,对于无权图,用0或1来代表两点是否邻接;如果是是有权图,那就用权值来代表两点邻接,不相邻接的则直接置为无穷大,其数据结构表示如下:
#define MAXV //最大顶点个数
#defind INF 32767 //正无穷
typedef struct
{
}InfoType; //与顶点相关的其它数据
typedef struct
{
int no; //顶点的标号
InfoType info; //与顶点相关的其它数据
}VertexType; //顶点类型
typedef struct
{
int edges[MAXV][MAXV]; //邻接矩阵
int vexnum,arcnum; //点数,边数
VertexType vexs[MAXV]; //端点数组
}MGraph; //图的邻接矩阵表示
邻接表存储是一种顺序分配与链式分配相组合的存储方法,在邻接表里,顺序对图中的顶点v建立一个单链表,每个单链表的结点表示依附于顶点v的边,每个单链表上附设一个表头结点。表头结点由顶点的名或其他信息及指向表结点的指针两部分组成,而表结点由指向下一个表结点的指针,与v相邻接的点在图中的位置,以及其它与边或弧有关的信息三部分组成;注意,表结点其实也就是弧,存储的可以是弧的起点,也可以是弧的终点。其数据结构表示如下:
typedef struct AInfo
{
int arcinfo;
}ArcInfo; //与弧相关的信息,存储的可以是弧的权重,或其它信息
typedef struct ANode
{
int end; //弧的终点
struct ANode* next; //指向下一条弧的指针
ArcInfo info; //与弧相关的信息
}ArcNode; //弧结点,亦即表结点
typedef struct
{
string vertex;
}Vertex; //顶点信息
typedef struct Vnode
{
Vertex data;
ArcNode *firstarc;
}VNode; //表头结点
typedef VNode AdjList[MAXV]; //定义邻接表
typedef struct
{
AdjList adjlist; //邻接表
int n,e; //顶点,边数
}ALGraph; //图的邻接表的表示
十字链表存储以及邻接多重表存储暂不详述。
图的构建:
对图的构建,我们首先采用邻接矩阵存储,然后用邻接矩阵来构建邻接表,算法实现如下:
邻接矩阵构建
void create_matrix_graph(MGraph& mg)
{
int i,j;
cout<<"/nplease input how many of vertexs in the graph:/n";
cin>>mg.vexnum;
for(i=0;i<mg.vexnum;i++)
mg.vexs[i].no=i;
cout<<"/nplease input the graph matrix:/n";
for(i=0;i<mg.vexnum;i++)
for(j=0;j<mg.vexnum;j++)
cin>>mg.edges[i][j];
}
由邻接矩阵转换为邻接表
void Mat2List(const MGraph& mgraph, ALGraph& algraph)
{
int i,j,n=mgraph.vexnum;
algraph.n=n;
for(i=0;i<n;i++)
{
algraph.adjlist[i].firstarc=NULL;
ArcNode *p;
for(j=0;j<n;j++)
{
if(mgraph.edges[i][j]!=0&&mgraph.edges[i][j]!=-1) //-1代表没有两顶点不相邻
{
p=(ArcNode*)malloc(sizeof(ArcNode));
p->next=algraph.adjlist[i].firstarc;
p->end=j;
p->info.arcinfo=mgraph.edges[i][j];
algraph.adjlist[i].firstarc=p; //采用前插法插入
}
}
}
}
再由邻接表转换为邻接矩阵:
void List2Mat(const ALGraph& algraph,MGraph& g)
{
int i,j,n=algraph.n;
g.vexnum=n;
for(i=0;i<g.vexnum;i++)
g.vexs[i].no=i;
for(i=0;i<n;i++)
{
ArcNode* p=algraph.adjlist[i].firstarc;
while(p!=NULL)
{
g.edges[i][p->end]=1;
p=p->next;
}
}
}
邻接表的创建是一个动态申请内存的过程,当我们使用完邻接表,应该手动地去将内存释放,释放实现如下:
void del_List(ALGraph* alg)
{
int i,j,n=alg->n;
for(i=0;i<n;i++)
{
ArcNode *s,*p=alg->adjlist[i].firstarc;
s=p;
while(p!=NULL)
{
delete p;
p=s->next;
s=p;
}
}
}
图的遍历:
图的遍历包括两种形式的遍历,一种是深度优先搜索遍历,一种是广度优先搜索遍历。
深度优先搜索遍历的过程是:从图中某个初始顶点v出发,首先访问该初始顶点v, 然后选择一个与顶点v相邻且没被访问过的顶点w为初始顶点,再从w出发进行深度优先搜索,址到与当前顶点v的所有顶点都被访问过为止。其实现算法如下:
void dfs(ALGraph* alg,int v)
{
ArcNode *p;
visited[v]=true;
cout<<v<<" ";
p=alg->adjlist[v].firstarc;
while(p!=NULL)
{
if(!visited[p->end])
dfs(alg,p->end);
p=p->next;
}
}
广度优先搜索过程稍为复杂了些,它首先访问初始点v, 接着访问v的所有未被访问过的邻接点v0,v1,v2,..., 然后再按照v0,v1,...的次序,访问每个顶点的所有未被访问的邻接点,依次类推,直到完全访问。我们采用队列的方式实现,先将顶层结点压入队列,而后出队,将所有与其邻接的结点入队尾,再出队队首元素,将所有未被访问的结点入队尾,再出队队首元素,如此,直到队列为空。其算法实现过程如下:
void bfs(ALGraph* alg, int v)
{
ArcNode *p;
int queue[MAXV],front=0,rear=0,i,w;
cout<<v<<" ";
visited[v]=true;
rear=(rear+1)%MAXV;
queue[rear]=v;
while(front!=rear)
{
front=(front+1)%MAXV;
w=queue[front];
p=alg->adjlist[w].firstarc;
while(p!=NULL)
{
if(!visited[p->end])
{
cout<<p->end<<" ";
visited[p->end]=true;
rear=(rear+1)%MAXV;
queue[rear]=p->end;
}
p=p->next;
}
}
}
最小生成树的构造:
最小生成树的构造算法一般采用普里姆算法(Prim)或克鲁斯卡尔算法(Kruskal), Prim算法的时间复杂度为O(n^2), 而Kruskalr的时间复杂度与选用的排序算法有关,可以是O(n^2),也可以是O(elog2(e)).
Prim算法是基于将图的顶点分成两部分, 一部分是已经加入候选队列中的点,而另一部分则是未加入的点,假设G=(V,E)是一个具有n个顶点的带权连通无向图,T=(U,TE)是G的最小生成树,其中,U是T的顶点集,TE是T的边集,则由G构造最小生成树的步骤如下:
1)初始化U={v0}, v0到其他顶点的所有边为候选边。
2)重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
从候选边中挑选一个权值最小的边输出,设该边在V-U的顶点是v, 将v加入U中,删除和v关联的边。考查当前V-U中的所有顶点vi, 修改候选边,若(v,vi)的权值小于原来和vi关联的候选边,则用(v,vi)来取代后者作为候选边。其实现算法如下:
void prim(ALGraph* alg,int v)
{
int cost[MAXV][MAXV],lowestcost[MAXV],i,j,k,n=alg->n,min,closest[MAXV];
ArcNode *p;
for(i=0;i<MAXV;i++)
for(j=0;j<MAXV;j++)
{
cost[i][j]=i==j?0:INF;
}
for(i=0;i<n;i++)
{
p=alg->adjlist[i].firstarc;
while(p!=NULL)
{
cost[i][p->end]=p->info.arcinfo;
p=p->next;
}
}
for(i=0;i<n;i++)
{
lowestcost[i]=cost[v][i];
closest[i]=v;
}
for(i=1;i<n;i++)
{
min=INF;
for(j=0;j<n;j++)
{
if(lowestcost[j]!=0&&lowestcost[j]<min)
{
min=lowestcost[j];
k=j;
}
}
cout<<"the weight of edge ("<<closest[k]<<","<<k<<"):"<<min<<"/n";
lowestcost[k]=0;
for(j=0;j<n;j++)
{
if(cost[k][j]!=0&&cost[k][j]<lowestcost[j])
{
lowestcost[j]=cost[k][j];
closest[j]=k;
}
}
}
}
Kruskal算法基于一种最简单的实现,首先将所有的边按权值大小进行排序,首先取权值最小的边,如果其在已选边中不构成回路,增加之,否则,考查下一条边;判断是否是回路的方式也很简单,最初先将每个点置于不同的聚类中,如果待加入的边的两端点所属的聚类不同,说明可以添加,然后修改两端点所属聚类,使其相同;如果待加入的边的两端点所属的聚类相同,说明可以构成回路,该边需要被抛弃。算法实现如下:
void kruskal(ALGraph* alg)
{
struct edge
{
int start;
int end;
int weight;
}edges[MAXV]; //边集
int vset[MAXV],n=alg->n,i,j,k,e,m1,m2,q;//vset代表是判断是否形成回路的数组,e,代表边总数,k代表已选边数,q代表当前所选边,m1代表所选边的开始端点,m2代表所选边的结束端点
ArcNode *p;
bool exist;
e=0;
for(i=0;i<n;i++)
{
p=alg->adjlist[i].firstarc;
while(p!=NULL)
{
exist=false;
for(j=0;j<e;j++)
{
if(edges[j].start==p->end&&edges[j].end==i)
exist=true;
}
if(exist)
{
p=p->next;
continue;
}
edges[e].start=i;
edges[e].end=p->end;
edges[e].weight=p->info.arcinfo;
e++;
p=p->next;
}
}//构建邻接表的边集
for(i=0;i<e;i++)
{
for(j=i;j<e;j++)
{
if(edges[i].weight>edges[j].weight)
{
edge temp=edges[i];
edges[i]=edges[j];
edges[j]=temp;
}
}
}//按权重的大小对邻接表进行排序
for(i=0;i<n;i++)
vset[i]=i;
k=0;
q=0;
while(k<n-1)
{
m1=edges[q].start;
m2=edges[q].end;
if(vset[m1]!=vset[m2])//判断是否属于同一个聚类
{
cout<<"("<<m1<<","<<m2<<"):"<<edges[q].weight<<"/n";
for(i=0;i<n;i++)
if(vset[i]==vset[m2])
vset[i]=vset[m1];
k++;
}
q++;
}
}
求解图的最短路径
求解图的最短路径可以从两个角度着手,一个是着眼于一个顶点,求该顶点到其余各顶点的最短路径;另一个是着眼于两个顶点,求任意两个顶点间的最短路径。这样就有两个算法,一个是狄克斯特拉(Dijkstra)算法,一个是弗洛伊德(Floyd)算法。
Dijkstra算法
Dijkstra的算法的基本思想也是将图中顶点分为两组,一组是已求出的最短路径的顶点集合v1,一组是没有求出的最短路径的顶点集合v2。其先初始化一个顶点总数大小的距离数组,每次都将距离数组中最小距离的点v0加入到v1中,然后更新距离数组,如果经过v0发现路径会更短的话,就把其距离更新,用更小值来代替。对路径的求解,也使用一个数组,不过需要反向求出。其算法实现过程如下:
void dijkstra(ALGraph* alg, int v)
{
int cost[MAXV][MAXV],dist[MAXV],i,j,n=alg->n,current,k,min,path[MAXV]; //cost是距离矩阵,dist是最短距离数组, n代表有多少点,current表示从dist中选取的点,k代表已选取了多少点,min,储存最小值,path代表路径数组
bool flags[MAXV];
ArcNode *p;
for(i=0;i<MAXV;i++)
for(j=0;j<MAXV;j++)
{
cost[i][j]=i==j?0:INF;
}
for(i=0;i<n;i++)
{
p=alg->adjlist[i].firstarc;
while(p!=NULL)
{
cost[i][p->end]=p->info.arcinfo;
p=p->next;
}
} //初始化cost数组
for(i=0;i<n;i++)
path[i]=v; //初始化路径数组
for(i=0;i<n;i++)
flags[i]=false; //判断顶点i的最短距离已经被计算?
flags[v]=true;
for(i=0;i<n;i++) //初始化最短距离数组
dist[i]=cost[v][i];
current=v; //当前选取v点
k=1;
while(k<n)
{
min=INF;
for(i=0;i<n;i++)
{
if(!flags[i]&&dist[i]<min)
{
current=i;
min=dist[i];
} //求出最小的
}
flags[current]=true;
for(i=0;i<n;i++)
if(!flags[i]&&dist[current]+cost[current][i]<dist[i])
{
dist[i]=dist[current]+cost[current][i];
path[i]=current;
} //更新距离数组
k++;
}
int st[MAXV];
int top=-1;
for(i=0;i<n;i++)
{
if(i==v)
continue;
if(dist[i]==INF)
cout<<"the closet dist between "<<v<<" and "<<i<<": no connect "<<"/n";
else
{
cout<<"the closet dist between "<<v<<" and "<<i<<": "<<dist[i]<<"/n";
j=i;
while(j!=v)
{
top++;
st[top]=path[j];
j=path[j];
}
while(top>-1)
{
cout<<st[top]<<"->";
top--;
}
cout<<i<<"/n";
} //输出
}
}
Floyd算法求两点间的最短路径长度,它的基本思想可以用表达式A-1[i][j]=cost[i][j], Ak+1[i][j]=min{Ak[i][j],Ak[i][k]+Ak[k+1][j]}来代表。从顶点vi到顶点vj的最短路径上经过编号为k的顶点,可以分为两段,一段是从顶点vi到顶点vk的最短路径,另一段是从顶点vk+1到顶点vj的最短路径,此时最短路径长度等于这两段路径长度之和,与原先从顶点vi到顶点vj的路径上经过的顶点编号不大于k的最短路径长度,取两者之间最小值,并将路径数组更新,算法实现如下:
void floyd(ALGraph* alg)
{
int cost[MAXV][MAXV],path[MAXV][MAXV],n=alg->n,k,i,j;
ArcNode *p;
for(i=0;i<MAXV;i++)
for(j=0;j<MAXV;j++)
{
cost[i][j]=i==j?0:INF;
}
for(i=0;i<n;i++)
{
p=alg->adjlist[i].firstarc;
while(p!=NULL)
{
cost[i][p->end]=p->info.arcinfo;
p=p->next;
}
}
for(i=0;i<n;i++)
for(j=0;j<n;j++)
path[i][j]=-1;
k=0;
while(k<n)
{
for(i=0;i<n;i++)
for(j=0;j<n;j++)
if(cost[i][k]+cost[k][j]<cost[i][j])
{
cost[i][j]=cost[i][k]+cost[k][j];
path[i][j]=k;
}
k++;
}
int st[MAXV];
int top=-1,q;
for(i=0;i<n;i++)
for(j=0;j<n;j++)
{
cout<<"the closet dist between "<<i<<" and "<<j<<": "<<cost[i][j]<<"/n";
cout<<i<<"->";
q=j;
while(path[i][q]!=-1)
{
top++;
st[top]=path[i][q];
q=path[i][q];
}
while(top>-1)
{
cout<<st[top]<<"->";
top--;
}
cout<<j<<"/n";
}
}
嘿嘿,关于图的这一章终于结束,对于图我已经有个基本的认识了。