大话数据结构读书笔记 5---图

大话数据结构读书笔记 5—图

数据结构 读书笔记


图(Graph)是由顶点的有穷非空集合和顶点之间的边的集合组成,通常表示为:G(V,E),其中G表示一个图,V是图G中顶点的集合,E是图G中边的集合。(顶点集合V有穷非空,边集可以是空的)

线性表中的数据元素叫元素,树中叫结点,在图中数据元素叫顶点

各种图的定义

图按照有无方向分为有向图无向图。无向图由顶点和边构成,有向图由顶点和弧构成。弧有弧尾和弧头之分(若从顶点vi到vj的边有方向,则这条边叫做有向边,也称为弧,用有序对<,>表示(无向的用(vi,vj)表示)),vi称为弧尾,vj称为弧头。)

如果任意两个顶点之间都存在边叫做完全图,有向的叫做有向完全图

无重复的边或顶点到自身的边则叫做简单图。

无向图顶点的边数叫做(记作TD(v),v表示该顶点),有向图顶点分为入度(ID(v),v作为弧头)和出度(OD(v) ,v作为弧尾),入度和出度之和为有向图顶点的度(TD(v)=ID(v)+OD(v))。

图上的边或弧带权的称为

无向图中路径表示图中一个顶点到另一个顶点的序列,有向图中路径也是有向的。路径的长度是路径上的边或弧的数目。

如果顶点之间存在路径,则说明是连通的。路径最终回到起始点,则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向图叫做强连通图

无向图中连通且n个顶点有n-1条边叫做生成树(如果一个图有n个顶点和小于n-1条边,则是非连通图;若有多于n-1条边,则必定构成环。但有n-1条边不一定就是生成树)。有向图中一顶点的入度为0,其余顶点的入度为1的叫做有向树。一个有向图的生成森林由若干棵有向树构成,含有图中全部的顶点

图的储存结构

邻接矩阵(二维数组)

用两个数组表示图。一个一维数组储存图中顶点的信息,一个二维数组储存图中边或弧的信息。

设图G有n个顶点,则邻接矩阵是一个n x n方阵,定义为:

arc[i][j]=1;若(vi,vj)在E中,或在E中;
arc[i][j]=0;其他情况;

无向图的邻接矩阵是一个对称数组,则aij=aji (0<=i,j<=0);

邻接矩阵中:
1 某个顶点的度,就是这个顶点vi在其邻接矩阵中第i行的元素之和。
2 vi的所有邻接点(和vi有边相连的点)其邻接矩阵中第i行中 arc[i][j]为1对应的所有j值;

对于有向图:
其邻接矩阵:
1 对应行之和就是该顶点的出度,对应列之和就是该顶点的入度(邻接矩阵中行从v0到v1。。。依次到vn,列也如此排列);

若是网则:

设图G是网图,有n个顶点,则邻接矩阵是一个n x n 方阵,定义为:
arc[i][j]=Wij;若(vi,vj)在E中,或在E中,Wij表示相应的权;
arc[i][j]=0;若i=j;(即对角线的值为0,因为我们讨论的都是简单图不存在到自身的边或弧)
arc[i][j]=无穷(计算机允许的大于所有边上权值的值);其他情况。

邻接矩阵的储存结构

typedef char VertexType;//顶点类型,由用户定义
typedef int EdgeType;//边上权值类型,由用户定义
#define MAXVEX 100//最大顶点数,由用户定义
#define INFINITY 65535//用65535代表无穷
typedef struct
{
    VertexType vexs[MAXVEX];//顶点表
    EdgeType arc[MAXVEX][MAXVEX];//邻接矩阵,可看作边表
    int numVertexes,numEdges;//图中当前顶点数和边数
}MGraph;

//建立无向网图的邻接矩阵表示
void CreateMGraph(MGraph *G)
{
    int i,j,k,w;
    printf("输入顶点数和边数:\n");
    scanf("%d,%d",&G->numVertexes,&G->numEdges);
    for(i=0;inumVertexves;i++)//读入顶点信息
        scanf(&G->vexs[i]);
    for(i=0;inumVertexves;i++)
        for(j=0;jnumVertexves;j++)
        G->arc[i][j]=INFINITY;//邻接矩阵初始化
    for(k=0;knumEdges;k++)//读入边,建立邻接矩阵
    {
        printf("输入边(vi,vj)上的下标i,下标j和权w");
        scanf("%d,%d,%d",&i,&j,&w);
        G->arc[i][j]=w;
        G->arc[j][i]=G->arc[i][j];//因为是无向图,矩阵对称
    }
}

## 2邻接表
邻接矩阵在边数相对于顶点数较少情况下,会对空间造成极大浪费;
所以我们利用数组和链表相结合的储存方式,这称为邻接表.
所以图中的顶点用一个一维数组储存,然后对图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点个数不定,所以用单链表储存。(无向图称为顶点vi的边表,有向图称为顶点vi的出边表(以顶点为弧尾来储存边表));(有向图的逆邻接表就是以顶点为弧头来储存)

结点定义代码:
typedef char VertexType;
typedef int EdgeType;
typedef struct EdgeNode//边表结点
{
int adjvex;//邻接点域,储存该顶点对应的下标
EdgeType weight;//用于存储权值
struct EdgeNode *next;//链域,指向下一个邻结点
}EdgeNode;

typedef struct VertexNode//顶点表结点
{
    VertexType data;
    EdgeNode *firstedge;//边表头指针
}VertextNode,AdjList[MAXVEX];

typedef struct
{
    AdjList adjList;
    int numVertexes,numEdges;//图中当前顶点数和边数
}GraphAdjList;

//建立图的邻接表结构
void CreateALGraph(GraphAdjList *G)
{
    int i,j,k;
    EdgeNode *e;
    printf("输入顶点数和边数:\n");
    scanf("%d,%d",&G->numVertexes,&G->numEdges);
    for(i=0;inumVertexes;i++)
    {
        scanf("%c",&G->adjList[i].data);
        G->adjList[i].firstedge=NULL;
    }
    for(k=0;knumEdges;k++)
    {
        printf("输入边(vi,vj)上的顶点序号:\n");
        scanf("%d,%d",&i,&j);
        e=(EdgeNode *)malloc(sizeof(EdgeNode));
        e->adjvex=j;//邻接序号为j
        e->next=G->adjList[i].firstedge;//头插法
        G->adjList[i].firstedge=e;

        e=(EdgeNode *)malloc(sizeof(EdgeNode));
        e->adjvex=i;//邻接序号为i
        e->next=G->adjList[j].firstedge;
        G->adjList[j].firstedge=e;
    }
}

十字链表

对于有向图来说,邻接表只能一次关心入度或者出度,如果把邻接表和逆邻接表结合起来就构成了十字链表
十字链表的结构:
顶点表结点:含有 data,firstin,firstout三个域,(firstin表示入边表头指针;firstout表示出边表头指针)
边表结点:含有tailvex,headvex,headlink,taillink 四个域(tailvex是指弧起点在顶点表的下标;headvex是弧终点在顶点表中的下标;headlink是指入边表指针域,指向终点相同的下一条边;taillink是指边表指针域,指向起点相同的下一条边)

邻接多重表

对于无相图的邻接表,如果需要更加关心对边进行操作,则用邻接多重表更好。
邻接多重表结构:
顶点表结点:与邻接表相同;
边表结点:含有ivex,ilink,jvex,jlink四个域 (ivex和jvex是与某条边依附的两个顶点在顶点表中的下标;ilink指向依附于顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边)(注意ilink指向的结点的jvex一定要与它本身的ivex的值相同)

边集数组

边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是储存边的信息,这个边数组每个数据元素由一条边的起点下标,终点下标,和权组成

图的遍历

深度优先遍历

从图中某个顶点v出发,访问此结点,然后从v的未访问的邻接点出发遍历图,直至图中所有和v有路径相通的顶点都被访问到,此时若尚有顶点未被访问,则另选图中一个未曾被访问过的结点,重复上述过程,直至图中所有的顶点都被访问到,这就是深度优先遍历(DFS)

//如果用邻接矩阵的方式,则代码如下
typedef int Boolean;//其值是TURE或者FALSE
Boolean visited[MAX];//访问标志数组
//邻接矩阵深度优先递归算法
void DFS(MGraph G, int i)
{
    int j;
    visited[i]=TRUE;
    printf("%c",G.vexs[i]);//这里采用其他操作
    for(j=0;jadjList[i].data);
    p=GL->adjList[i].firstedge;
    while(p)
    {
        if(!visited[p->adjvex])
            DFS(GL,p->adjvex);
        p=p->next;
    }
}
//邻接表的深度遍历操作
void DFSTraverse(GraphAdiList GL)
{
    int i;
    for(i=0;inumVertexes;i++)
        visited[i]=FALSE;
    for(i=0;i

广度优先遍历

又称广度优先搜索,简称BFS
如果说深度优先遍历类似于树的前序遍历,那广度优先遍历就类似于层序遍历

//邻接矩阵的广度遍历算法
void BFSTraverse(MGraph G)
{
    int i,j;
    Queue Q;
    for(i=0;inumVertexves;i++)
        visited[i]=FALSE;
    InitQueue(&Q)//初始化一辅助用队列
    for(i=0;inumVertexves;i++)
    {
        if(!visited[i])
        {
            visited[i]=TRUE;
            printf("%c",G.vexs[i]);//进行实际的操作
            EnQueue(&Q,i);//将此顶点入队列
            while(!QueueEmpty(Q))//若当前队列不为空
            {
                DeQueue(&Q,&i);//将队中元素出队列,赋值给i
                for(j=0;j

DFS和BFS的时间复杂度是一样的
如果图的顶点和边非常多,不能在短时间遍历完成,则深度优先遍历更适合目标比较明确的情况,广度优先白努力更加适合在不断扩大的范围内找到相对最优解的情况。

最小生成树

我们把构造连通网的最小代价生成树称为最小生成树(最小代价指的是n个顶点,用n-1条边把一个连通图连接起来,并使得权值的和最小)

普里姆(Prim)算法

//普里姆(Prim)算法生成最小生成树
void MiniSpanTree_Prim(MGraph G)
{
    int min,i,j,k;
    int adjvex[MAXVEX];//保存相关顶点下标
    int lowcost[MAXVEX];//保存相关顶点间边的权值
    lowcost[0]=0;//初始化第一个权值为0,即v0加入生成树
    //lowcost的值为0,就表明此下标的顶点已经加入了生成树
    adjvex[0]=0;//初始化第一个顶点的下标为0;
    for(i=1;i

克鲁斯卡尔(Kruskal)算法

需要用到边集数组,并且对边集数组按权值从小到大排序

/Kruskal算法生成最小生成树
void MiniSpanTree-Kruskal(MGraph G)
{
    int i,n,m;
    Edge edges[MAXEDGE];//定义边集数组
    int parent[MAXVEX];//定义一数组用来判断边与边是否形成环路
    //此处省略将邻接矩阵G转换为边集数组egdes并按权值大小从小到大的排列
    for(i=0;i0)//因为parent数组被初始化为0,所以如果不为零(这里由于权值大于0所以等价于大于零),则说明该顶点已经被加入了生成树集合中,所以需要找到其连线最终的尾部的下标
        f=parent[f];//f值变成了尾部下标,若此时parent[f]还是大于零,则继续循环
    return f;
}

时间复杂度O(eloge) (Find函数的时间复杂度为O(loge),外面有一个for循环,所以总的时间复杂度为O(eloge))

最短路径

最短路径是指两顶点之间经过的边数最少的路径
对于网图,是指两顶点之间经过的边上权值之和最少的路径

迪杰斯特拉(Dijkstra)算法

#define MAXVEX 9
#define INFINITY 65535
typedef int Patharc[MAXVEX];//用于存储最短路径下标的数组
typedef int ShortPathTable[MAXVEX];//用于储存到各点最短路径的权值和
//P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和
void ShorttestPath_Dijkstra(MGraph G,int v0,Patharc *p,ShortPathTable *D)
{
    int v,w,k,min;
    int final[MAXVEX];//final[w]=1表示求得顶点v0到vw的最短路径
    for(v=0;v

由P[3]=4表示v3的前驱是v4。。。就这样可以得到v0到v8的最短路径为v0->v1->v2->v4->v3->v6->v7->v8;

弗洛伊德(Floyd)算法

弗洛伊德算法能够一次性求出任意两顶点的最短路径

typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
//Floyd算法,求网图G中各顶点v到其余顶点w最短路径P[v][w]及带权长度D[v][w];
void ShorttestPath_Floyd(MGraph G, Pathmatirx *P,ShortPathTable *D)
{
int v,w,k;
for(v=0;v(*D)[v][k]+(*D)[k][w])//如果经过下标为k顶点的路径比原两点之间的路径更短,则将当前两点之间的权值设为更小的一个         
            {
            (*D)[v][w]=(*D)[v][k]+(*D)[k][w];
            (*P)[v][w]=(*P)[v][k];
            }
        }    
    }    
}    
}

时间复杂度O(n^3)
最终求得的D矩阵的第v0行与Dijkstra求得结果完全相同,第i表示的就是已vi为起点到其余各顶点的最短路径
对于最终得到的P数组,例如要得到从v0到v8的路径,则可以先看P[0][8]的值,然后依次用前面一个数组的值替换P[i][8]的i位置,直到i==8为止,P[i][8]每次的值就是经过的路径

拓扑排序

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网(Actvity On Vertex NetWork)
AOV网中不能存在回路

设G={V,E}是一个具有n个顶点的有向图,V中的顶点序列v1,v2,v3,…,vn,若满足从顶点vi到vj有一条路径,则在顶点序列中vi必在vj之前。则我们称这样的顶点序列是一个拓扑序列。

拓扑排序,就是对一个有向图构造拓扑序列的过程。(如果此网的全部顶点都被输出,则说明是AOV网,否则说明这个网存在回路,不是AOV网,因为一个活动不可能其活动发生前提是它自己本身)

对AOV网进行拓扑排序的基本思路:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。

//拓扑排序算法中涉及的结构代码如下
typedef struct EdgeNode//边表结点
{
    int adjvex;
    int weight;
    struct EdgeNode *next;
}EdgeNode;

typedef struct VertexNode
{
    int in;//顶点的入度,为了遍历时方便判断,减少排序时的复杂度
    int data;
    EdgeNode *firstedge;
}VertexNode,AdjList[MAXVEX];

typedef struct
{
    AdjList adjList;
    int numVertexes,numEdges;
}graphAdjList,*GraphAdjList;


//拓扑排序
Status TopologicalSort(GraphAdjList GL)
{
    EdgeNode *e;
    int i,k,gettop;
    int top=0;//用于栈指针下标
    int count=0;//用于统计输出的顶点的个数
    int *stack;//建栈用于储存入度为0的顶点
    stack=(int *)malloc(GL->numVertexes*sizeof(int));
    for(i=0;inumVertexes;i++)
    if(GL->adjList[i].in==0)
        stack[++top]=i;//将入度为0的顶点入栈
    while(top!=0)
    {
        gettop=stack[top--];//出栈
        printf("%d -> ",GL->adjList[gettop].data);
        count++;
        for(e=GL->adjList[gettop].firstedge;e;e=e->next)
        {//对此顶点弧表进行遍历
            k=e->adjvex;
            if(!(--GL->adjList[k].in))//将k号顶点邻接点的入度减1
                stack[++top]=k;//若入度为0则入栈
        }
    }
    if(countnumVertexes)//如果小于顶点数说明存在环
        return ERROR;
    else
        return OK;
}


时间复杂度O(n+e)(前面扫描顶点表将入度为0的顶点入栈复杂度为O(n),后面while循环对每条弧进行了操作时间复杂度为O(e))

关键路径

在一个表示工程图的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动持续的时间,这种有向图边表示活动的网,称为AOE网(Activity On Edge Network)
正常情况下,AOE网只有一个源点(没有入边的顶点)和一个汇点(没有出边的顶点)
在AOE网中,只有在某顶点代表的事件发生后,从该顶点出发的各个活动才能开始;只有在进入某顶点的各活动都已经结束,该顶点代表的事件才能发生。
路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫关键活动。

关键路径算法

先定义几个参数
int *etv,*ltv;//事件最早发生时间和最迟发生时间数组
int *stack2;//用于存储拓扑序列的栈
int top2;//用于stack2的指针
(由etv和ltv可以计算出活动的最早开始时间和最迟开始时间,当活动的最早开始时间和最迟开始时间相等时,则该活动就是关键活动,所在的弧就是关键路径)

//拓扑排序,用于关键路径计算

Status TopologicalSort(GraphAdjList GL)
{
    EdgeNode *e;
    int i,k,gettop;
    int top=0;
    int count=0;
    int *stack;//建栈将入度为0的顶点入栈
    stack=(int *)malloc(GL->numVertexes*sizeof(int));
    for(i=0;inumVertexes;i++)
        if(0==Gl->adjList[i].in)
        stack[++top]=i;

    top2=0;
    etv=(int *)malloc(GL->numVertexes*sizeof(int));
    for(i=0;inumVertexes;i++)
        etv[i]=0;//初始化为0
    stack=(int *)malloc(GL->numVertexes*sizeof(int));

    while(top!=0)
    {
        gettop=stack[top--];
        count++;

        stack2[++top2]=gettop;//将弹出的顶点序号压入拓扑序列的栈

        for(e=GL->adjList[gettop].firstedge;e;e=e->next)
        {
            k=e->adjvex;
            if(!(--GL->adjList[k].in))
            stack[++top]=k;

            if(etv[gettop]+e->weight>etv[k])//求各顶点事件最早发生时间值,etv[k]的值是其中最大的那一个,因为只有全部活动进行完毕,下一个事件才能发生
                    etv[k]=etv[gettop]+e->weight;
        }
    }
    if(countnumVertexes)
        return ERROR;
    else
        return OK;
}


求事件的最早发生时间etv的过程,就是我们从头至尾找拓扑序列的过程
etv[k]=0; 当k=0时;
etv[k]=max{etv[i]+len};当k!=0时且在P[k]中时(P[k]表示所有到顶点vk的弧的集合)


//求关键路径,GL为有向网,输出GL的各项关键活动
void CriticalPath(GraphAdjList GL)
{
    EdgeNode *e;
    int i,gettop,k,j;
    int ete,lte;//活动最早开始时间和最迟开始时间
    TopologicalSort(GL);//求拓扑序列,计算数组etv和stack2的值
    ltv=(int *)malloc(GL->numVertexes*sizeod(int));//事件最晚发生时间
    for(i=0;inumVertexes;i++)
        ltv[i]=etv[GL->numVertexes-1];//初始化ltv为etv[]中的最大值
    while(top2!=0)
    {
        gettop=stack2[top2--];
        for(e=GL->adjList[gettop].firstedge;e;e=e->next)
        {
            k=e->adjvex;
            if(ltv[k]-e->weightnumVertexes;j++)
    {
        for(e=Gl->adjList[j].firstedge;e;e=e->next)
        {
            k=e->adjvex;
            ete=etv[k];
            lte=ltv[k]-e->weight;
            if(ete==lte)
            printf(" length: %d ",GL->adjList[j].data,GL->adjList[k].data,e->weight);
        }
    }
}

ltv[k]=etv[k];当k=n-1时
ltv[k]=min{ltv[j]-len},当k在S[k]中(S[k]表示所有从顶点vk出发的弧的集合)

图的总结

图的存储结构:
*邻接矩阵
*邻接表->十字链表,邻接多重表
*边集数组
通常稠密图,或者读存数据较多,结构修改较少的图用邻接矩阵更加合适,反之则应该考虑邻接表
最小生成树的两种算法:普里姆(Prim)算法,克鲁斯卡尔(Kruskal)算法
最短路径算法:迪杰斯特拉(Dijkstra)算法,弗洛伊德(Floyd)算法
有向无环图常应用于工程规划中,如果关心工程能否顺利进行,则可以通过拓扑排序的方式,求出拓扑序列或者分析出一个有向图是否存在环;如果关心的整个工程完成所必须的最短时间问题,则可以用求关键路径的算法

你可能感兴趣的:(编程基础)