数据结构(五)图---图的存储结构5种

一:图的抽象数据类型

ADT 图(Graph)

Data
    顶点的有穷非空集合和边的集合

Operation
    CreateGraph(*G,V,VR):按照顶点集V和边弧集VR的定义构造图G
    DestroyGraph(*G):图G存在则销毁
    LocateVex(G,u):若图G中存在顶点u,则返回图中位置
    GetVex(G,v):返回图中顶点v的值
    PutVex(G,v,value):将图G中顶点v赋值给value
    FirstAdjVex(G,*v):返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点则返回空
    NextAdjVex(G,v,*w):返回顶点v相对于顶点w的下一个邻接顶点,若w是v的最后一个邻接点则返回空
    InsertVex(*G,v):在图G中增加新顶点v
    DeleteVex(*G,v):删除图G中顶点v及其相关的弧
    InsertArc(*G,v,w):在图G中添加弧<v,w>,若G是无向图,还需要添加对称弧<w,v>
    DeleteArc(*G,v,w):在图G中删除弧<v,w>,若G是无向图,则需要删除对称弧<w,v>
    DFSTraverse(G):对图G中进行深度优先遍历,在遍历过程对每个顶点调用
    HFSTraverse(G):对图G中进行广度优先遍历,在遍历过程对每个顶点调用
endADT

二:图的存储结构讨论

对于线性表来说,是一对一的关系,所以用数组或者链表均可以简单存放。
对于树结构是一对多的关系,所以我们要将数组和链表的特性结合在一起才能更好的存放。
对于图来说,是多对多的情况,另外图上的任意一个顶点都可以被看做是第一个顶点,任一顶点的邻接点之间也不存在次序关系
如下图:实际是一个图结构,只不过顶点位置不同。

由于图的结构复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。
内存物理位置是线性的,图的元素关系是平面的。
虽然我们可以向树结构中说到的那样使用多重链表,但是我们需要先确定最大的度,然后按照这个度最大的顶点设计结点结构,若是每个顶点的度数相差较大,就会造成大量的存储单元浪费。

三:图的存储结构(1)---邻接矩阵

考虑到图是由顶点和边(弧)两部分组成的,合在一起是比较困难的,那就很自然的考虑到分为两个结构来分别存储
顶点因为不区分大小,主次,所以用一个一维数组来存储时不错的选择。
而边或弧由于是顶点与顶点之间的关系,所以我们最好使用二维数组来存储
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

(一)无向图

其中1表示两个顶点之间存在关系,0表示无关,不存在顶点间的边。
对称矩阵:就是n阶矩阵满足a[i][j]=a[j][i](0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的源与左下角相对应的元都是相等的。
根据这个矩阵,我们可以很容易的知道图中的信息。
1.我们要判定容易两顶点是否有边无边就非常容易了。
2.我们要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。比如上图顶点v1的度就是1+0+1+0=2
3.求顶点vi的所有邻接点就是将矩阵第i行元素扫描一遍,arc[i][j]为1就是邻接点

(二)有向图

对于上面的无向图,二维对称矩阵似乎浪费了一半的空间。若是存放有向图,会更大程度利用起来空间

其中顶点数组是一样的和无向图,弧数组也是一个矩阵,但因为是有向图,所以这个矩阵并不对称:例如v1->v0有弧,arc[1][0]=1,而v0到v1没有弧,所以arc[0][1]=0。
另外有向图,要考虑入度和出度,顶点v1的入度为1,正好是第v1列的各数之和,顶点v1的出度为2,正好是第v2行的各数之和

(三)网

每条边上带有权的图就叫做网

这里‘∞’表示一个计算机允许的,大于所有边上权值的值

(四)实现无向网图创建

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXVEX 100    //最大顶点数
#define INFINITY 65535    //用65535表示∞

typedef char VertexType;    //顶点类型,字符型A,B,C,D...
typedef int EdgeType;    //边上权值类型10,15,...

typedef struct
{
    VertexType vers[MAXVEX];    //顶点表
    EdgeType arc[MAXVEX][MAXVEX];    //邻接矩阵,可看作边表
    int numVertexes, numEdges;    //图中当前的顶点数和边数
}MGraph;

void CreateMGraph(MGraph* G)
{
    int i, j, k, w;
    printf("please input number of vertex and edge:\n");
    scanf("%d,%d", &G->numVertexes, &G->numEdges);    //输入顶点数和边数
    getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes; i++)    //读入顶点信息,建立顶点表
        scanf("%c", &G->vers[i]);
    getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes;i++)
        for (j = 0; j < G->numVertexes;j++)
            G->arc[i][j] = INFINITY;    //邻接矩阵初始化

    for (k = 0; k < G->numEdges;k++)    //读入numEdges条边,建立邻接矩阵
    {
        printf("input edge(vi,vj) row(i),col(j),weight(w):\n");
        scanf("%d,%d,%d", &i, &j, &w);    //输入边(vi,vj),以及上面的权值
        getchar();    //可以获取回车符
        G->arc[i][j] = w;
        G->arc[j][i] = G->arc[i][j];    //因为是无向图,所有是对称矩阵
    }
}

int main()
{
    MGraph MG;
    CreateMGraph(&MG);

    system("pause");
    return 0;
}
n个顶点,e条边创建无向网图,时间复杂度是O(n+n^2+e),初始化时耗费了O(n^2)

 

四:图的存储结构(2)---邻接表

上面的邻接矩阵是一种不错的图存储结构,便于理解,但是当我们的边数相对于顶点较少的图,这种结构是存在对存储空间的极大的浪费。

我们可以考虑使用链表来动态分配空间,避免使用数组一次分配而造成空间浪费问题。
同树中,孩子表示法,我们将结点存放入数组,对结点的孩子进行链式存储,不管有多少孩子,都不会存在空间浪费。这种思路也适用于图的存储。我们把这种数组与链表相结合的存储方法称为邻接表

邻接表处理办法

1.图中顶点用一个一维数组。当然,顶点也可以用单链表来存储,不过数组可以较容易的读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息
2.图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表

(一)无向图

这样的结构,对于我们要获得图的相关信息也是很方便。比如:
我们要获取某个顶点的度,就要去查找这个顶点的边表中结点的个数。
我们要判断vi到vj是否存在边,只需要测试vi的边表链表中是否存在结点vj的下标j即可。
我们若是要获取顶点的所有邻接点,就是对此顶点的边表进行遍历。

(二)有向图

有向图由于有方向,我们是以顶点为弧尾来存储边表的,这样很容易就可以得到每个顶点的出度。但是由于有时也需要确定顶点的入度或以顶点作为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个链接为vi为弧头的表

(三)带权值的网图

我们可以在边表结点定义中再增加一个weight数据域,存储权值信息即可

(四)实现无向网图

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXVEX 100    //最大顶点数

typedef char VertexType;    //顶点类型,字符型A,B,C,D...
typedef int EdgeType;    //边上权值类型10,15,...

typedef struct EdgeNode    //边表结点
{
    int adjvex;            //邻接点域,存放该顶点对应的下标
    int weight;            //用于存放权值,对于非网图可以不需要
    struct EdgeNode* next;    //链域,指向下一个邻接点
}EdgeNode;

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

typedef struct
{
    AdjList adjList;    //邻接表数组
    int numVertexes, numEdges;    //图中所存储的顶点数和边数
}GraphAdjList;

void CreateALGraph(GraphAdjList* G)
{
    int i, j ,k,w;
    EdgeNode *e;
    printf("please input number of vertex and edge:\n");
    scanf("%d,%d",&G->numVertexes,&G->numEdges);    //输入顶点数和边数
    getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes;i++)    //输入顶点信息
    {
        scanf("%c", &G->adjList[i].data);    //输入顶点信息
        G->adjList[i].firstedge = NULL;    //将边表置为空
    }
    getchar();    //可以获取回车符
    for (k = 0; k < G->numEdges;k++)
    {
        printf("input edge(vi,vj) vertexs series and the weight:\n");
        scanf("%d,%d,%d", &i, &j,&w);
        getchar();
        //由于是无向图,对称矩阵,当我们设置边以后,需要在两个地方设置结点
        e = (EdgeNode *)malloc(sizeof(EdgeNode));
        //使用头插法将数据插入(主要是头插法方便),我们插入不需要考虑顺序,因为链表结点都是与数组顶点相连接的
        e->adjvex = j;
        e->next = G->adjList[i].firstedge;    
        e->weight = w;
        G->adjList[i].firstedge = e;

        e = (EdgeNode*)malloc(sizeof(EdgeNode));
        e->adjvex = i;
        e->next = G->adjList[j].firstedge;
        e->weight = w;
        G->adjList[j].firstedge = e;
    }
}

int main()
{
    GraphAdjList gl;
    CreateALGraph(&gl);
    system("pause");
    return 0;
}

注意:上面的两种存储结构是针对顶点,下面的三种存储结构是针对边

五:图的存储结构(3)---十字链表

我们想要知道出度方向的顶点,可以使用邻接表,我们要了解入度就需要使用逆邻接表。但是我们既想要了解入度有想要了解出度,那么我们该如何处理?
这时就需要使用到有向图的一种存储方法:十字链表

顶点表结点结构

firstin表示入边表头指针,指向该顶点的入边表中第一个结点。
firstout表示出边表头指针,指向该顶点的出边表中第一个结点。

边表结点结构

其中tailvex是指弧起点顶点表的下标,headvex是指弧终点顶点表的下标。
headlink是指入边表指针域,指向终点相同的下一条边taillink是指边表指针域,指向起点相同的下一条边。
如果是网,我们还要在其中加入权值域,来存储权值

我们可以看做横向是出度,竖向是入度
顶点的出度和入度。除了结构复杂一点外,其实创建图算法的时间复杂度和邻接表是相同的,因此很好的应用在有向图中。
注意:整张图的出度和入度是一致的(不是某个顶点,而是这张图)

代码实现

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXVEX 100    //最大顶点数

typedef char VertexType;    //顶点类型,字符型A,B,C,D...
typedef int EdgeType;    //边上权值类型10,15,...

typedef struct OLNode    //十字链表结点
{
    int tailvex, headvex;    //表示两个顶点下标,构成了一条边。tailvex是箭头尾,弧尾,headvex是箭头头,是弧头
    int weight;            //用于存放权值,对于非网图可以不需要
    struct OLNode* taillink;    //十字链域,指向下一个邻接点,是出度方向
    struct OLNode* headlink;    //十字链域,指向下一个邻接点,是入度方向
}OLNode;

typedef struct VertexNode   //顶点表结点
{
    VertexType data;    //顶点域,存储顶点信息
    OLNode* firstin;    //边表头指针,入度
    OLNode* firstout;    //边表头指针,出度
}VertexNode, CrossList[MAXVEX];

typedef struct
{
    CrossList adjList;    //邻接表数组
    int numVertexes, numEdges;    //图中所存储的顶点数和边数(出度指针和入度指针是和边数是一致的)
}GraphCrossList;

void CreateCrossGraph(GraphCrossList* G)
{
    int i, j ,k,w;
    OLNode *e;
    printf("please input number of vertex and edge:\n");
    scanf("%d,%d",&G->numVertexes,&G->numEdges);    //输入顶点数和边数
    getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes;i++)    //输入顶点信息
    {
        scanf("%c", &G->adjList[i].data);    //输入顶点信息
        G->adjList[i].firstin = NULL;    //将边表置为空
        G->adjList[i].firstout = NULL;    //将边表置为空
    }
    getchar();    //可以获取回车符

    //先循环获取出度结点,出度数和入度数是一致的
    for (k = 0; k < G->numEdges;k++)
    {
        printf("Out-->input edge(vi,vj) vertexs series and the weight:\n");
        scanf("%d,%d,%d", &i, &j,&w);
        getchar();
        //创建十字链表结点
        e = (OLNode *)malloc(sizeof(OLNode));
        //使用头插法将数据插入(主要是头插法方便),我们插入不需要考虑顺序,因为链表结点都是与数组顶点相连接的
        e->tailvex = i;
        e->headvex = j;
        e->weight = w;
        e->taillink = G->adjList[i].firstout;
        G->adjList[i].firstout = e;
    }

    //再循环获取入度结点,在上面的出度结点建立了所有结点的基础上,我们再建立入度指针指向
    //例如我们要获取v0的入度v1,我们先输入入度点0,再输入出度点1,不需要权值,权值在上面对有向图都赋值了
    //我们进行第二个指针v0的入度v2,我们使用相同输入,但是开始使用头插法修改链表firstin指向
    for (k = 0; k < G->numEdges; k++)
    {
        printf("In-->input edge(vi,vj) vertexs series:\n");
        scanf("%d,%d", &i, &j);
        getchar();
        //根据上面的输入,我们将当前的顶点域firstin指向我们获取的新的入度域
        //先找入度域
        e = G->adjList[j].firstout;
        while (e->headvex!=i)
            e = e->taillink;    //循环十字链表结点,获取我们要的入度结点
        //使用头插法插入入度指针域
        e->headlink = G->adjList[i].firstin;
        G->adjList[i].firstin = e;
    }
}

int main()
{
    GraphCrossList gl;
    CreateCrossGraph(&gl);
    gl;
    system("pause");
    return 0;
}

六:图的存储结构(4)---邻接多重表

邻接多重表是对无向图的存储结构的优化

问题:

对于无向图的邻接表,我们更加关注的重点是顶点,那么是不错的选择,但是我们要是关注的是边的操作。
比如:删除一条边,那么我们的操作将变得复杂,我们需要找到这条边的两个顶点,方便去其链表中删除所表示的边。稍微有点麻烦。

改进:

 

我们只出现无向图中对应条数的边表结点,其他的结构,我们全部由指针来联系,所以当我们想要删除一条边时,就只需要删除对应的边表结点。指向她的指针会置为空,他自己产生的指针会消失。就完成了对边的操作。

例如上图,我们若是使用邻接表:是要10个顶点结点去表示5条边,而我们若是使用邻接多重表,只需要5个边结点即可。删除一条边就不存在重复操作

定义

邻接多重表结构

其中ivex和jvex是指某条边依附的两个顶点在顶点表中的下标ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。
如上图有4个顶点和5条边,先将边表结点画出来。由于是无向图,所以ivex,jvex正反过来都可以,为了绘图方便,都将ivex值设置的与一旁的顶点下标相同

1.将边表结点画出来

2.开始连线

首先连线的(1)(2)(3)(4)是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同。

接着,由于顶点v0的(v0,v1)边的邻边有(v0,v3)和(v0,v2)。因此(5)(6)的连线就是满足指向下一条依附于顶点v0的边的目标,注意ilink指向的结点的jvex(ivex)一定要与它本身的ivex的值相同
注意ilink指向的结点的jvex(ivex)一定要与它本身的ivex的值相同。而且为了方便,我们和jvex相同,那么全部指向都要与之一样

同理,连线(7)就是指(v1,v0)这条边,它是相当于顶点v1指向(v1,v2)边后的下一条。

v2有三条边依附,所以(3)之后就有了(8)(9)。

连线(10)就是顶点v3在连线(4)之后的下一条边

左图一共有5条边,所以右图有10条连线,完全符合预期。

总结

邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个边表结点表示,而在邻接多重表中只有一个结点。这样对边的操作就方便多了,
若要删除左图的(v0,v2)这条边,只需要将右图的(
6)(9)的链接指向改为^即可。

代码实现前的思考:

MMP,怎么就想不出来用什么方法去存储这些边呢?顶点和边不对应呀....吐血....,去网上找找其他关于邻接多重表的信息吧

数据结构之图(2-2)【邻接多重表】适用于无向图

无向图的邻接多重表存储结构

这两篇文章来了思路,将表的结构修改一下,我们只需要保证邻接多重表的根本,就是创建边表,而不是创建顶点表,因为顶点表是边表的两倍,在删除时会导致重复操作,而我们的边表只需要创建对应边数的结点,删除某条边,就删除对应结点即可。不需要重复操作。

 

 

上面这种表示方法,但是好像没说清楚为啥这么排列。
于是决定测试将对应顶点的邻接边全部放入顶点后面

步骤一:先排序第一个顶点的所有邻接边

步骤二:我们再排序第二个顶点的所有邻接边

步骤三:我们接着排序第三个顶点的所有邻接边

步骤四:开始排序最后一个顶点所有邻接边

 

 通过上面排序,我们可以获取所有邻接点和邻接边

 按照上面来实现代码

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXVEX 100    //最大顶点数

typedef char VertexType;    //顶点类型,字符型A,B,C,D...
typedef int EdgeType;    //边上权值类型10,15,...

typedef struct ENode    //十字链表结点
{
    int ivex, jvex;    //表示两个顶点下标,构成了一条边。
    int weight;            //用于存放权值,对于非网图可以不需要
    struct ENode* ilink;    //邻接多重表,指向下一个邻接点,
    struct ENode* jlink;    //邻接多重表,指向下一个邻接点,
}ENode;

typedef struct VertexNode   //顶点表结点
{
    VertexType data;    //顶点域,存储顶点信息
    ENode* firstedge;    //边表头指针
}VertexNode, AMLList[MAXVEX];

typedef struct
{
    AMLList adjList;    //邻接多重表数组
    int numVertexes, numEdges;    //图中所存储的顶点数和边数
}AMLGraphList;

//根据ivex找到第几行,我们去前面几行查找,jvex获取下标
ENode* GetNode(AMLGraphList* G,int ivex,int jvex)
{
    int i;
    ENode *e;
    for (i = ivex-1; i >=0;i--)
    {
        e = G->adjList[i].firstedge;
        while (e)
        {
            if (e->jvex==jvex)
            {
                return e;
            }
            e = e->ilink;
        }
    }

    return NULL;
}

void CreateAMLGraph(AMLGraphList* G)
{
    int i, j, k, w,flag;
    ENode *e;
    ENode *tempNode;
    printf("please input number of vertex and edge:\n");
    scanf("%d,%d", &G->numVertexes, &G->numEdges);    //输入顶点数和边数
    getchar();    //可以获取回车符
    for (i = 0; i < G->numVertexes; i++)    //输入顶点信息
    {
        scanf("%c", &G->adjList[i].data);    //输入顶点信息
        G->adjList[i].firstedge = NULL;    //将边表置为空
    }
    getchar();    //可以获取回车符

    //先循环获取边信息。创建所有边信息,放在对应的顶点后面,
    for (k = 0; k < G->numEdges; k++)
    {
        printf("Out-->input edge(vi,vj) vertexs series and the weight:\n");
        scanf("%d,%d,%d", &i, &j, &w);
        getchar();
        //创建邻接多重表结点
        e = (ENode *)malloc(sizeof(ENode));
        //使用头插法将数据插入(主要是头插法方便),我们插入不需要考虑顺序,因为链表结点都是与数组顶点相连接的
        e->ivex = i;
        e->jvex = j;
        e->weight = w;
        e->ilink = G->adjList[i].firstedge;
        e->jlink = NULL;
        G->adjList[i].firstedge = e;
    }

    //开始连接多张表之间的关系,并且判断ilink和jlink,从第二个顶点开始
    for (i = 1; i < G->numVertexes;i++)
    {
        e = G->adjList[i].firstedge;
        if (e)
        {
            flag = 0;    //用来标识是不是最后一个结点
            while (e&&!flag)    //需要将最后一个单独处理
            {
                if (e->ilink == NULL)
                {
                    tempNode = GetNode(G, i, e->ivex);
                    e->ilink = tempNode;
                    flag = 1;
                }
                tempNode = GetNode(G, i, e->jvex);
                e->jlink = tempNode;

                e = e->ilink;
            }
            //e = GetNode(G, i, e->jvex);    //处理最后一个
        }
        else
            G->adjList[i].firstedge = GetNode(G, i, i);
    }
}

int main()
{
    AMLGraphList gl;
    CreateAMLGraph(&gl);
    gl;
    system("pause");
    return 0;
}

七:图的存储结构(5)---边集数组

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

结构

表现

如上图所示,边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。
因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。

 代码实现

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXVEX 100    //最大顶点数

typedef char VertexType;    //顶点类型,字符型A,B,C,D...
typedef int EdgeType;    //边上权值类型10,15,...

typedef struct VertexNode   //顶点表结点
{
    VertexType data;    //顶点域,存储顶点信息
}VertexNode, VertexList[MAXVEX];

typedef struct EdgeNode   //边集表结点
{
    int begin, end, weight;
}EdgeNode, EdgeList[MAXVEX];

typedef struct
{
    VertexList vexList;    //顶点表
    EdgeList edgeList;    //边集表
    int numVertexes, numEdges;    //图中所存储的顶点数和边数
}EdgeGraphList;


void CreateEdgeGraph(EdgeGraphList* G)
{
    int i, j ,k,w;
    printf("please input number of vertex and edge:\n");
    scanf("%d,%d",&G->numVertexes,&G->numEdges);    //输入顶点数和边数
    getchar();    //可以获取回车符

    //获取顶点数组信息
    for (i = 0; i < G->numVertexes;i++)    //输入顶点信息
    {
        scanf("%c", &G->vexList[i].data);    //输入顶点信息
    }

    //获取边数组
    for (k = 0; k < G->numEdges;k++)
    {
        printf("input edge(vi,vj) vertexs series and the weight:\n");
        scanf("%d,%d,%d", &i, &j,&w);
        getchar();
        //由于是无向图,对称矩阵,当我们设置边以后,需要在两个地方设置结点
        G->edgeList[k].begin = i;
        G->edgeList[k].end = j;
        G->edgeList[k].weight = w;
    }
}

int main()
{
    EdgeGraphList gl;
    CreateEdgeGraph(&gl);
    gl;
    system("pause");
    return 0;
}

 

posted @ 2018-08-15 14:08  山上有风景  阅读(11297)  评论(2编辑  收藏  举报