数据结构与算法--图

概述

线性表的每个元素有线性关系,每个数据元素只有一个直接前去和一个直接后继。树的数据元素之间有着明细那的层次关系,并且每层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。这和一对父母可以有很多孩子,但每个孩子却只能有一对父母是一个道理。可现实中,人与人之间关系复杂,不是简单一对一,一对多的关系。这种复杂关系更适合用图来表示。在图结构中,节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。如下图所示:
图结构试例
无向边:Edge (vi,vj)
有向边:也叫弧,Arc。 <vi,vj>
对于无向图 G=(V,E) ,如果边 (v,v)E ,则称定点v和v’互为邻接点(Adjacent),即v和v’相临接。边(v,v’)依附于顶点v,v’,或者说(v,v’)与定点v,v’相关联.定点v的度(Degree)是和v相关联的变的数目,记为TD(v).
blabla…..我去,概念太多了,不写了,自己查书《大话数据结构》

图的存储结构

邻接矩阵

图的邻接矩阵(Adjacency Matrix)春初方式是用两个数组来表示图。一个以为数组存储定点,第二个存贮边或弧的信息

f(x)={1,0,if(vi,vj)Eor<vi,vj>Eother

邻接矩阵
上图是无向图邻接矩阵

有向图邻接矩阵
上图是有向图邻接矩阵
有向网络
上图是有向网络邻接矩阵

邻接表

对于边相对与定点较少的图,邻接矩阵表示很浪费空间
稀疏边的邻接矩阵
稀疏边的邻接矩阵
我们把一种数组与链表想结合的存储方法称为邻接表(Adjacency List).
邻接表是这样子的:

  1. 图中顶点用一个一维数组存储,数组元素还需要存储指向第一个邻接点(vi,vj有边链接,vi,vj就互为邻接点)的指针,一边查询该定点的边信息。
  2. 图中每个顶点vi的所有邻接点构成一个线性表。

邻接表
上图是无向图的邻接表
有向图邻接表
一个有向图的逆邻接表,就是对每个顶点vi都建议一个链接为vi为弧头的表。

十字链表

重新定义顶点表结点结构:

datafirstinfirstout

其中firstin表示入边表头指针,指向该顶点的入边表中第一个节点。firstout表示出边表头指针,指向该顶点的出边表中的第一个节点。
重定义边表结点结构

tailvexheadvexheadlinktaillink

其中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)这条边,需要对邻接表结构中右边表的阴影两个结点进行删除操作边表结点的删除操作
于是我们对便表接待你的结构进行一些改造,也许可以避免刚才的问题。

ivexilinkjvexjlink

其中ivex和jvex是与某条边衣服的两个顶点再顶点表中下表。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。这就是邻接多重表结构。
我们来看看结构示意图的绘制过程,理解了它是怎么链接的,就理解了多重表构造原理了。如下图,作图告诉我们他有四个顶点和5条边,显然,我们就应该先将4个顶点和五条边的边表节点画出来。由于是无向图,所以ivex是0,jvex是1还是翻过来都是无所谓的,不过为了绘图方便,都将ivex值设置得与一旁的顶点下标相同。
多重链接表绘图1
多重链接表绘图1
我们开始连线,如下图。首先连线的①②③④就是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同,这样很好理解。接着,由于顶点v0的(v0,v1)边的邻边有(v0,v3)和(v0,v2)。因此⑤⑥的连线就是满足指向下一条依附于顶点v0的边的目标,注意ilink指向的结点的jvex一定要和它本身的ivex的值相同。同样的道理,连线⑦就是指(v1,v0)这条边,它是相当于顶点v1指向(v1,v2)边后的下一条。v2有三条边依附,所以在③之后就有了⑧⑨。连线⑩的就是顶点v3在连线④之后的下一条边。左图一共有五条边,所以右图有10条连线,完全符合预期。
邻接多重表的绘图2.jpg
邻接多重表的绘图–连线
到这里大家应该看出来了,邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个节点。这样对边的操作就方便多了。如果要删除上图左边(v0,v2)这条边,只需要将右图的⑥⑨的链接指向改为^即可。

边集数组

一图胜千言:
边集数组

图的遍历

深度优先

深度优先(Depth First Search)
如下图,从顶点A开始要走遍所有的图顶点并作上标记,不重复不遗漏。
BFS
有这样一个策略,我们从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

直接上代码:
邻接矩阵的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)。

普里姆(Prim)算法

先构造如下图的邻接矩阵:
pRIM INIT
现在,我们已经有了一个存储结构为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算法,假设 N=(V,E) 是连通网,TE是N上最小生成树中边的集合。算法从 U=u0(u0V),TE={} 开始。重复执行下述操作:在所有 uU,vVU 的边 (u,v)E 找一条代价最小的边 (u0,v0) 并入集合TE,同时 v0 并入U,直到U=V为止。此时TE中必有n-1条边,则 T=(v,TE) 为N的最小生成树。算法时间复杂度是 O(n2) 。具体参见算法导论的分析。

库鲁斯卡儿(Kruskal)

Prim算法是以某顶点为起点,逐步找各顶点最小权值的边来构建。但我们也可以直接以边为目标去构建,因为权值是在边上的,直接找最小权值的边来构建生成树也是很自然的,只不过构建时要考虑是否形成环路而已。此时我们就用到了图的存储结构中的边集数组结构。一下是edge边集数组结构的定义代码:

typedef struct{
    int begin;
    int end;
    int weight;
}

我们将图的邻接矩阵通过程序转化为下图的右图的边集数组,并且对它们按照权值从小到大排序。
Kruskal边集数组
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中,并且还合并了两个连通分量。

最短路径

迪杰斯特拉(Dijkstra)算法

这是一个按路径长度递增的次序产生最短路径的算法。
如下图
Dijstra算法
v0v1 的最短距离,很明显就是v0直接连接到v1。由于v1还与v2,v3,v4连线,所以此时我们同时求得了v0->v1->v2 = 1+3 =4;v0->v1->v3 = 1+7 =8;v0->v1->v4 = 1+5 = 6;
现在,问题来了,从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代码很像,相似处就不说了,它们的区别如下:
1. 前者是求最短路径,后者是求最小生成树
2. 都有一步更新过程前者是 min+G.arc[k][w] < D[w]来判断,也就是新的最短路径+其它任意结点w的值是不是小于以前v0->w的路径长度D[w].是就更新v0到w的路径长度.P[w]记录下w的前驱k。
后者是G.arc[k][j] < lowcost[j]来判断,也就是生成树加入了新成员k,然后其它结点到最小生成树结点集合的最近距离会改变,但有可能改变的只是与k相关的最近距离。我们遍历与k相关
的其它节点的权值,然后这一判断其它结点到K的权值是不是小于以前到最小生成树合集的最近距离。是就更新这个结点到最小生成树的距离,并且在adjvex中记录下这个结点的前驱k。
总结Dijkstra:
Dijkstra算法解决了从某个源点到其余各顶点的最短路径问题。从循环嵌套中可知算法时间复杂度为 O(n2) 。可是我们如果需要知道每个顶点到其余节点的最短路径怎么办?最简单办法就是将每个结点选为源点运行一次DIjkstra算法。此时时间复杂度为 O(n3) 。但下面的Floyd算法,时间复杂度一样,但是代码特别简介。值得一学!

弗洛伊德(Floyd)算法

{未完待续}

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值