标签:
线性表的每个元素有线性关系,每个数据元素只有一个直接前去和一个直接后继。树的数据元素之间有着明细那的层次关系,并且每层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。这和一对父母可以有很多孩子,但每个孩子却只能有一对父母是一个道理。可现实中,人与人之间关系复杂,不是简单一对一,一对多的关系。这种复杂关系更适合用图来表示。在图结构中,节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。如下图所示:
无向边:Edge (vi,vj)
有向边:也叫弧,Arc。 <vi,vj>
对于无向图
blabla.....我去,概念太多了,不写了,自己查书《大话数据结构》
图的邻接矩阵(Adjacency Matrix)春初方式是用两个数组来表示图。一个以为数组存储定点,第二个存贮边或弧的信息
上图是有向图邻接矩阵
上图是有向网络邻接矩阵
对于边相对与定点较少的图,邻接矩阵表示很浪费空间
稀疏边的邻接矩阵
我们把一种数组与链表想结合的存储方法称为邻接表(Adjacency List).
邻接表是这样子的:
- 图中顶点用一个一维数组存储,数组元素还需要存储指向第一个邻接点(vi,vj有边链接,vi,vj就互为邻接点)的指针,一边查询该定点的边信息。
- 图中每个顶点vi的所有邻接点构成一个线性表。
上图是无向图的邻接表
一个有向图的逆邻接表,就是对每个顶点vi都建议一个链接为vi为弧头的表。
重新定义顶点表结点结构:
|data|firstin|firstout|
|-------|-------|-------|
其中firstin表示入边表头指针,指向该顶点的入边表中第一个节点。firstout表示出边表头指针,指向该顶点的出边表中的第一个节点。
重定义边表结点结构
|tailvex|headvex|headlink|taillink|
|-------|-------|-------|-------|
其中tailvex是指向弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下标。headlink是指入边表指针域,指向终点相同的下一条边,taillink是指出边表指针域,指向起点相同的下一条边。如果是网,还可以增加一个weight域来存储权值。
如下图,顶点依然存入一个一维数组{v0,v1,v2,v3},实线箭头指针的图示完全与上面有向图邻接表相同。就以顶点v0来说,firstout指向的是出边表中的第一个节点v3.所以v0边表节点的headvex=3,而tailvex其实就是当前顶点v0的下标,由于v0只有一个出边顶点,所以headlink和taillink都是空。
十字链表
我们重点需要解释虚线箭头的含义,它其实就是此图的逆邻接表的表示。对于v0来说,它有两个顶点v1和v2的入边。因此v0的firstin指向顶点v1的边表节点中headvex为0的节点,如上图中的 ①。接着由入边节点的headlink指向下一个边顶点v2,如上图的②。对于顶点v1,它有一个入边顶点v2,所以它的firstin指向顶点v2的边表节点中headvex为1的节点,如图中的③。顶点v2,v3也是同样有一个入边顶点,如图中④和⑤。
十字链表的好处是因为吧邻接表和逆邻接表整合在了一起,这样容易找到以vi为尾的弧,也容易找到以vi为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点外,其实创建图算法的时间复杂度和邻接表示相同的。
如果我们关注的重点是顶点,那么上面的邻接表是不错的存储结构。但如果我们关注边的操作,比如对边删除等操作,就意味着,我们需要找到这条边的两个边表节点进行操作。这其实还是比较麻烦的。比如下图中,如果要删除左图的(v0,v2)这条边,需要对邻接表结构中右边表的阴影两个结点进行删除操作
于是我们对便表接待你的结构进行一些改造,也许可以避免刚才的问题。
|ivex|ilink|jvex|jlink|
|-|
其中ivex和jvex是与某条边衣服的两个顶点再顶点表中下表。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。这就是邻接多重表结构。
我们来看看结构示意图的绘制过程,理解了它是怎么链接的,就理解了多重表构造原理了。如下图,作图告诉我们他有四个顶点和5条边,显然,我们就应该先将4个顶点和五条边的边表节点画出来。由于是无向图,所以ivex是0,jvex是1还是翻过来都是无所谓的,不过为了绘图方便,都将ivex值设置得与一旁的顶点下标相同。
多重链接表绘图1
我们开始连线,如下图。首先连线的①②③④就是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同,这样很好理解。接着,由于顶点v0的(v0,v1)边的邻边有(v0,v3)和(v0,v2)。因此⑤⑥的连线就是满足指向下一条依附于顶点v0的边的目标,注意ilink指向的结点的jvex一定要和它本身的ivex的值相同。同样的道理,连线⑦就是指(v1,v0)这条边,它是相当于顶点v1指向(v1,v2)边后的下一条。v2有三条边依附,所以在③之后就有了⑧⑨。连线⑩的就是顶点v3在连线④之后的下一条边。左图一共有五条边,所以右图有10条连线,完全符合预期。
邻接多重表的绘图--连线
到这里大家应该看出来了,邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个节点。这样对边的操作就方便多了。如果要删除上图左边(v0,v2)这条边,只需要将右图的⑥⑨的链接指向改为^即可。
一图胜千言:
深度优先(Depth First Search)
如下图,从顶点A开始要走遍所有的图顶点并作上标记,不重复不遗漏。
有这样一个策略,我们从A开始,坐上标记后,前面有两条路,分别是B和F,我们给自己定一个原则,在没有碰到重复顶点的情况下,始终是向右手边走(假设我们在图中这样迷宫里),于是走到了B。整个过程,可以参见上面的右图,我们到达B节点,发现三条分支,分别通向顶点C,I,G,右手通行原则,使得我们走到了C顶点,。就这样,我们一直顺着右手通道走,一直走到了F顶点。当我们依然选择右手通道走过去后,发现走回到顶点了,因为我们做了标记,知道已经走过。此时我们退回到顶点F,走向从右数的第二条通道,到了G顶点,他有三条通道,发现B和D都已经是走过的,于是走到H,当我们面对通向H的两条通道D和E时,会发现已经走过了。此时还有很多分支没走,我们原路返回,继续寻找没有访问过的结点。直到返回顶点A,确认已经完成遍历任务,找到所有的九个节点。
其实,就是上面右图那棵树的前序遍历。
邻接矩阵的DFS代码:
//代码没有运行过,纯粹为了记笔记用,如果疏漏,欢迎指正
typdef int Boolean;
Boolean visited[MAX];
//邻接矩阵的深度优先递归算法
void DFS(MGraph,int i){
int j;
visited[i] = TRUE;
printf("%c",G.vexs[i]);
for(j = 0;j < G.numVertexes;j++){
//下一条边存在,且没有被访问,就向下搜索。
if(G.arc[i][j]==1 && !visited[j])
DFS(G,j);
}
}
//邻接矩阵的深度遍历操作
void DFSTraverse(MGraph G){
int i;
for(i = 0;i < G.numVertexes;i++){
visited[i] = FALSE;
}
for(i = 0;i < G.numVertexes; i++){
if(!visited[i])
DFS(G,i)
}
}
图是邻接表结构:其中DFSTraverse函数的代码几乎是相同的,知识在递归函数中因为将数组换成了链表而有不同,代码如下。
void DFS(GraphAdjList GL,int i){
EdgeNode * p;
visited[i] = TRUE;
printf("%c",GL->adjList[i].data);
p = GL->adjList[i].firstedge;
while(p){
if(!visited[p->adjvex])
DFS(GL,p->adjvex);
p = p->next;
}
}
//邻接表的深度遍历操作
void DFSTraverse(GraphAdjList GL){
int i;
for(i = 0;i<GL->numVertexes;i++)
visited[i] = FALSE;
for(i = 0;i<GL->numVertexes;i++){
if(!visited[i])
DFS(GL,i);
}
}
Breadth First Search,有点类似树的层序遍历。见下图:
直接上代码:
邻接矩阵的BFS代码
void BFSTraverse(MGraph G){
int i,j;
Queue Q;
for (i=0;i < G.numVertexes;i++){
visited[i] = FALSE;
}
InitQueue(&Q);
for(i=0;i<G.numVertexes;i++){
if(!visited[i]){
visited[i] = TRUE;
printf("%c",G.vexs[i]);
EnQueue(&Q,i);
while(!QueueEmpty(Q)){
DeQueue(&Q,&i);
for(j=0;j < G.numVertexes;j++){
if(G.arc[i][j] == 1 && !visited[j]){
visited[j] = TRUE;
printf("%c",G.vexs[j]);
EnQueue(&Q,j);
}
}
}
}
}
}
邻接表的BFS算法
void BFSTraverse(GraphAdjList GL){
int i;
EdgeNode * p;
Queue Q;
for (i = 0;i < GL->numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q);
for(i=0;i < GL->numVertexes; i++){
visited[i] = TRUE;
printf("%c",GL->adjList[i].data);
EnQueue(&Q,i);
while(!QueueEmpty(Q)){
DeQueue(&Q,&i);
p = GL->adjList[i].firstedge;
while(p){
if (!visited[p->adjvex]){
visited[p->adjvex] = TRUE;
printf("%c",GL->adjList[p->adjvex].data);
EnQueue(&Q,p->adjvex);
}
p = p->next;
}
}
}
}
一个图的生成树,定义为包含图中全部的顶点,但只有足以构成一棵树的n-1条边(有n个结点)。我们把构造连通网的最小代价生成树称为最小生成树(Minimum Cost Spanning Tree)。
先构造如下图的邻接矩阵:
现在,我们已经有了一个存储结构为MGrapgh的G。
于是Prim算法代码如下:
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535
typedef struct{
VertexType vexs[MAXVEX];
EdgeType arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
int a[9][9] = {
{ 0, 1, 5,INFINITY, INFINITY, INFINITY, INFINITY, INFINITY, INFINITY },
{ 1, 0, 3, 7, 5, INFINITY, INFINITY, INFINITY ,INFINITY },
{ 5, 3, 0, INFINITY, 1, 7, INFINITY, INFINITY, INFINITY },
{ INFINITY, 7, INFINITY, 0, 2, INFINITY, 3, INFINITY, INFINITY },
{ INFINITY, 5, 1, 2, 0, 3, 6, 9, INFINITY },
{ INFINITY, INFINITY, 7, INFINITY, 3, 0, INFINITY, 5, INFINITY },
{INFINITY, INFINITY, INFINITY, 3, 6, INFINITY, 0, 2, 7},
{ INFINITY, INFINITY, INFINITY, INFINITY ,9,5,2,0,4},
{ INFINITY, INFINITY, INFINITY, INFINITY, INFINITY, INFINITY ,7,4,0} };
void CreateMGraph(MGraph *G){
for (int i = 0; i < 8; i++){
G->vexs[i] = ‘0‘ + i;
}
G->numEdges = 16;
G->numVertexes = 9;
for (int i = 0; i < G->numVertexes; i++){
for (int j = 0; j < G->numVertexes; j++)
G->arc[i][j] = a[i][j];
}
}
void MiniSpanTree_Prim(MGraph *G){
int min, i, j, k;
//保存相关顶点下标
int adjvex[MAXVEX];
/*保存相关顶点间边的权值*/
int lowcost[MAXVEX];
lowcost[0] = 0; //初始化第一个权值为0,即v0加入生成树
adjvex[0] = 0; //初始化第一个顶点下标
//将v0以外的值放入lowcost
for (int i = 0; i < G->numVertexes; i++){
lowcost[i] = G->arc[0][i];
adjvex[i] = 0;
}
//每个顶点依次加入生成树
for (int i = 1; i < G->numVertexes; i++){
min = INFINITY;
j = 1; k = 0;
//找到一个lowcost的最小值
while (j < G->numVertexes){
if (lowcost[j] != 0 &&lowcost[j] < min ){
min = lowcost[j];
k = j;
}
j++;
}
//得到最小距离,以及对应的下标k
lowcost[k] = 0;//表示顶点k加入生成树合集
printf("(%d,%d)\n", adjvex[k], k);//adjvex[k]是与顶点k相连的顶点
//更新所有顶点到生成树顶点的距离。当有顶点到达生成树中多个顶点时候,只记录最近的一个
//也就是其它节点到生成树中最近的那个结点的距离
for (j = 1; j < G->numVertexes; j++){
//找最近的那个节点
if (lowcost[j] != 0 && G->arc[k][j] < lowcost[j]){
lowcost[j] = G->arc[k][j];
adjvex[j] = k;
}
}
}
}
void main(void){
MGraph g;
CreateMGraph(&g);
MiniSpanTree_Prim(&g);
while(1);
}
最后一个for循环:
最后这个for循环是,找到顶点k,k成功加入到最小生成树顶点合集中(lowcost对应位置0)。然后,我们寻找其它顶点中, 离这个顶点合集最短的一个顶点。
由于k是新加入合集的,lowcost中还没有记录其它顶点到K的距离,所以我们更新lowcost,将其它顶点到k的距离添加进去。遇到集合之外的顶点同时到达集合内多个顶点的,我们选择最小的距离,并且在adjvex数组中记录下j的最近距离lowcost[j]是指向哪一个节点的(比如adjvex[j]=k,表示顶点j到集合内最短距离边为(j,k),最短距离为lowcost[j]。)这里为什么要记录最短距离呢?因为我们最终求的是集合外一个结点到集合内任意结点的最短距离,所以,当一个结点同时和最小生成树结点集合中多个结点链接时候,我们只保存最小的值,较大的距离值就被抛弃了,反正后面比较的时候较大的值也会被抛弃。
lowcost矩阵存储的都是集合外部节点到生成树节点集合中最近的距离。到底离哪个最近,则对应adjvex[j] = k,表示外部结点j离集合内部节点k最近。
总结一下Prim算法,假设
Prim算法是以某顶点为起点,逐步找各顶点最小权值的边来构建。但我们也可以直接以边为目标去构建,因为权值是在边上的,直接找最小权值的边来构建生成树也是很自然的,只不过构建时要考虑是否形成环路而已。此时我们就用到了图的存储结构中的边集数组结构。一下是edge边集数组结构的定义代码:
typedef struct{
int begin;
int end;
int weight;
}
我们将图的邻接矩阵通过程序转化为下图的右图的边集数组,并且对它们按照权值从小到大排序。
Kruskal代码
void MiniSpanTree_Kruskal(MGraph G){
int i,n,m;
//定义边集数组
Edge edges[MAXEDGE];
//定义一组用来判断边与边是否行成环路
int parent[MAXEDGE];
/*此处省略邻接矩阵G转化为边集数组edges并按照权由小到大排序的代码*/
for(i = 0;i < G.numVertexes; i++)
parent[i] = 0;//初始化数组值为0
for(i = 0;i < G.numEdges;i++){
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if(n!=m){
parent[n] = m;
printf("(%d,%d) %d",edges[i].begin,edges[i].end,edges[i].weight);
/*将此边的结尾顶点法国如下标为起点的parent中*/
/*表示此顶点已经在生成树中*/
}
}
}
//返回这个连通分量的最后一个顶点的下标
int Find(int * parent,int f){
while(parent[f] > 0){
f = parent[f];
}
return f;
}
同一个连通分量中,不管从哪一个vex开始,由begin->end不断“行走”,最后终点都是相同的,由此判断是否在同一连通分量。在边集数据中选择代价最小的边(已经排好序,一次遍历就OK),若该边依附的顶点落在T中不同的连通分量上,则将此边加入到结果集parent中,并且还合并了两个连通分量。
这是一个按路径长度递增的次序产生最短路径的算法。
如下图
从
现在,问题来了,从v0->v2的最短距离是多少?很明显不会是v0->v2的直接连线,而是v0->v1->v2 = 1+ 3 =4。由于V2还和v4 ,v5连线,所以此时我们同时得到了v0->v2->v4=4+1=5,v0->v2->v5=4+7=11.这里v0->v2我们用的是刚才计算出来的最短距离4。此时我们发现v0->v1->v2->v4=5比v0->v1->v4=6小。所以v0->v4最小的距离是5。
当我们求v0->v3的最短距离时,通向v3的三条边,除了v6外,v0->v1->v3=8;v0->v4->v3=7;因此v0->v3的最短距离是7.
好了,这就是大致Dijstra算法的基本步骤。它并不是一下子就求出了v0->8的最短路径,而是一步步求出它们之间顶点的最短路径,过程中都是基于已经求出的最短路径的基础上,求得最远顶点的最短路径,最终得到你想要的结果。
typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535
typedef struct{
VertexType vexs[MAXVEX];
EdgeType arc[MAXVEX][MAXVEX];
int numVertexes, numEdges;
}MGraph;
int a[9][9] = {
{ 0, 1, 5,INFINITY, INFINITY, INFINITY, INFINITY, INFINITY, INFINITY },
{ 1, 0, 3, 7, 5, INFINITY, INFINITY, INFINITY ,INFINITY },
{ 5, 3, 0, INFINITY, 1, 7, INFINITY, INFINITY, INFINITY },
{ INFINITY, 7, INFINITY, 0, 2, INFINITY, 3, INFINITY, INFINITY },
{ INFINITY, 5, 1, 2, 0, 3, 6, 9, INFINITY },
{ INFINITY, INFINITY, 7, INFINITY, 3, 0, INFINITY, 5, INFINITY },
{INFINITY, INFINITY, INFINITY, 3, 6, INFINITY, 0, 2, 7},
{ INFINITY, INFINITY, INFINITY, INFINITY ,9,5,2,0,4},
{ INFINITY, INFINITY, INFINITY, INFINITY, INFINITY, INFINITY ,7,4,0} };
void CreateMGraph(MGraph *G){
for (int i = 0; i < 8; i++){
G->vexs[i] = ‘0‘ + i;
}
G->numEdges = 16;
G->numVertexes = 9;
for (int i = 0; i < G->numVertexes; i++){
for (int j = 0; j < G->numVertexes; j++)
G->arc[i][j] = a[i][j];
}
}
typedef int Patharc[MAXVEX];//用来存储最短路径的下标
typedef int ShortPathTable[MAXVEX];//用于存储到各个点最短路径的权值和,Dijstra,
/*Dijstra算法,求有向网G的v0顶点到其余顶点v最短路径P[v]以及带权长度D[v]*/
/*P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度*/
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc * P,ShortPathTable* D ){
int v, w, k, min;
int final[MAXVEX];/*final[w]=1表示求得顶点v0到v_w的最短路径*/
for (v = 0; v < G.numVertexes; v++){/*初始化数据*/
final[v] = 0; /*全部顶点初始化为未知最短路径状态*/
(*D)[v] = G.arc[0][v]; /*将与v0点有连线的顶点加上权值*/
(*P)[v] = 0; /*初始化路径数组P为0*/
}
(*D)[0] = 0; /*v0-v0路径为0*/
final[0] = 1; /*v0-v0不需要路径*/
/*开始主循环,每次求得v0到某个v顶点的最短路径*/
for (v = 1; v < G.numVertexes; v++){
min = INFINITY;
for (w = 0; w < G.numVertexes; w++){
if (!final[w] && (*D)[w] < min){
k = w;
min = (*D)[w];/*w顶点离v0顶点更近*/
}
}
final[k] = 1; /*将目前找到的最近的顶点置为1*/
for (w = 0; w < G.numVertexes; w++){
if (!final[w] && (min + G.arc[k][w] < (*D)[w])){
/*说明找到了更短的路径,修改D[w]和P[w]*/
(*D)[w] = min + G.arc[k][w]; /*修改当前路径长度*/
(*P)[w] = k;
}
}
}
}
void main(void){
MGraph g;
CreateMGraph(&g);
ShortPathTable d;
Patharc p;
ShortestPath_Dijkstra(g,0,&p,&d);
while (1);
}
上面Dijstra和Prim代码很像,相似处就不说了,它们的区别如下:
{未完待续}
标签:
原文地址:http://www.cnblogs.com/gavin-yue/p/4975531.html