图的定义

图(Graph)G由两个集合V和G组成,记作G = (V,G)。其中V是各顶点(结点)的有穷非空集合,V中的任意两个顶点配对后作为集合E的元素,顶点偶对亦称为边。

  • 有向图中,E中的元素形式为<x,y>,表示从顶点x到顶点y的一条有向边,有向边也称作弧,x为弧尾,y为弧头;
  • 无向图中,E中的元素形式为(x,y),仅表示连接顶点x和顶点y的一条边,效果同(y,x)。

在实际应用中,每条边可以标上具有某种含义的数值,该数值称为边上的,这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图又称作

图的存储结构

由于图的任意两个顶点之间都可能存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序存储结构,但我们可以用二维数组(矩阵)来表示元素之间的关系——邻接矩阵。除此之外还有链式存储结构,包括邻接表、十字链表和邻接多重表。其中邻接矩阵和邻接表最常用

邻接矩阵

定义和性质

图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息
设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
在这里插入图片描述
看一个实例,下图左就是一个无向图:
在这里插入图片描述
从上面可以看出,无向图的边数组是一个对称矩阵。所谓对称矩阵就是n阶矩阵的元满足aij = aji。即从矩阵的左上角到右下角的主对角线为轴,右上角的元和左下角相对应的元全都是相等的。

从这个矩阵中,很容易知道图中的信息:

(1)要判断任意两顶点是否有边无边就很容易了,判断邻接矩阵中元素值a[i][j]是0是1即可;
(2)要知道某个顶点的度,其实就是 这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点;

有向图讲究入度出度,顶点vi的入度为1,正好是第i列各数之和。顶点vi的出度为2,即第i行的各数之和
若图G是网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
在这里插入图片描述

代码实现

清楚邻接矩阵的使用方法后,我们可以轻松写出以它为基础的图的结构形式。

为了避免概念的混淆,之后我会把图称作邻接矩阵图(AMGraph)

 
/*邻接矩阵的结构表示*/
 
#include <iostream>
using namespace std;
 
#define MaxInt 32767       /*表示∞(无限)*/
#define MaxVNum 100        /*表示图中最多可以包含的顶点数*/
 
typedef char Vextype;      /*将顶点的数据类型设为字符型,如果你喜欢也可以把它设成更复杂的结构体*/
typedef int Arctype;       /*将边的权值设为整型*/
 
typedef struct
{
    Vextype vexs[MaxVNum];              /*用来保存顶点信息的一维数组*/
    Arctype arcs[MaxVNum][MaxVNum];     /*邻接矩阵*/
    int vexnum, arenum;                 /*记录图的顶点数和边数*/
} AMGraph;                              /*将结构体命名为AMGraph*/

写出邻接矩阵图的结构形式后,可以开始往里面放东西了。

下面以“无向网”为例写一下邻接矩阵表示法的使用方法:

<思路>

(1)数一下无向网有多少个顶点(vexnum),多少条边(arcnum),然后将数值输入。当然,你也可以凭空造一个网,在先前定义的MaxVNum内赋值就行。

(2)在一维数组vex内输入顶点信息,一个循环完成。

(3)初始化邻接矩阵,将每个权值初始化为MaxInt。为什么呢?因为矩阵里除了若干个权值外,剩余的都是MaxInt,不可能先把权值填好,再来一个个把没有权值的地方填上MaxInt,这样效率极低而且繁琐。

(4)构造邻接矩阵。依次输入每条边依附的两个顶点v1和v2以及其权值w并赋值。因为输入的v1、v2有可能不在0~MaxInt的范围内,而且矩阵和二维数组的“坐标”含义是不同的,即矩阵的(1,1)等价于二维数组中的[0,0],所以需要一个函数LocateVex()帮助判断、转化并确定“坐标”。

int CreatUDN(AMGraph &G)      /*UDN意为Omnidirectional Net,无向网*/
{                             /*上面<思路>中写了的东西下面就不再啰嗦喽*/
    int i, j, w;
    cin>>G.vexnum>>G.arcnum;
    
    for(i = 0; i < G.vexnum; i++) cin>>G.vexs[i];
 
    for(i = 0; i < G.vexnum; i++)
        for(j = 0; j < G.vexnum; j++)
            G.arcs[i][j] = MaxInt;
 
    for(int k = 0; k < G.arcnum; k++)
    {
        cin>>v1>>v2>>w;
        i = LocateVex(G, v1); j = LocateVex(G, v2);
        G.arc[i][j] = G.arc[j][i] = w;
    }
 
    return 0;
}
    

优缺点

1)优点:

a. 便于判断两个顶点是否有联系。确定顶点后再确定矩阵上的相应位置是否非0或非MaxInt即可。

b.便于计算各个顶点的度。其实也不用计算,数就完事了。对于无向图,多少个(1,n)的值为1,v1的度就是多少;对于有向图,多少个<1,n>的值为1,v1的出度就是多少,多少个<n,1>的值为1,v1的入度就是多少。

2)缺点:

a. 不便于增加和删除顶点。非链式结构的通病。

b. 不便于统计边的数目,需要遍历邻接矩阵的所有元素,时间复杂度为O(n²)。无向图遍历完后还要除以2。

c. 空间复杂度高。非链式结构的另一通病,稀疏图(边或弧数较少)尤其浪费空间。

邻接表

定义和性质

邻接矩阵是不错的一种图存储结构,但是,对于边数相对顶点较少的图,这种结构存在对存储空间的极大浪费。因此,找到一种数组与链表相结合的存储方法称为邻接表。

邻接表的处理方法是这样的:

(1)图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过,数组可以较容易的读取顶点的信息,更加方便。
(2)图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。

例如,下图就是一个无向图的邻接表的结构:
在这里插入图片描述
从图中可以看出,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。如下图所示。
在这里插入图片描述

代码实现

邻接表(Adjacency List)是图的一种链式结构。图中有多少个顶点,就有多少个单链表,每个顶点分别作为每个单链表的头结点,与顶点相连的其余顶点无序地(无特殊的顺序要求)连接在对应的单链表上。

顶点间的关系蕴含在若干个单链表中,因此邻接表图(ALGraph)的结构体只需要包含{每个单链表的头结点(即每个顶点) 以及 顶点数和边数}。

为了方便管理头结点,我们可以把它们放到一维数组ALs[MaxVNum]中,且该一维数组的功能包含着邻接矩阵图中vexs[MaxVNum]的功能。

*下面将头结点代表的顶点称作起始顶点

再把注意力放到单链表上,由于起始顶点已经确定,所以与起始顶点相连的每一个顶点都分别对应着一条边,因此我们可以把单链表上的其余结点看作边结点

边结点需要包含的信息有{边依附的另一个顶点编号(因为顶点放在一维数组ALs[MaxVNum]中,所以顶点v1的编号为0,如此类推)、指向下一个边结点的指针 以及 边上的权}。

头结点需要包含的信息有{起始顶点的信息指向下一个边结点的指针}

依据上面的思路,我们一共需要创建三个结构体。

/*邻接表的结构表示*/
 
typedef struct ArcNode         /*边结点*/
{
    int anothervex;            /*另一个顶点的编号*/
    ArcNode *nextarcnode;      /*指向下一边结点的指针*/
    int weight;                /*权*/
};
 
typedef struct VexNode         /*头结点*/
{
    VexType data;              /*起始顶点的信息*/
    ArcNode *nextarcnode;      /*指向下一边结点的指针*/
};
 
typedef struct ALGraph         /*邻接表图*/
{
    VexNode ALs[MaxInt];       /*头结点数组*/
    int vexnum, arcnum;        /*顶点数、边数*/
};

写出邻接表图的结构形式后,可以开始往里面放东西了。

下面以“无向图”为例写一下邻接表表示法的使用方法:

<思路>

(1)输入无向图的顶点数和边数;

(2)在一维数组ALs[MaxVNum]中输入起始顶点的信息,并使指针域初始化为NULL,完成各单链表的初始化;

(3)构造邻接表。输入起始顶点va和相邻顶点va的值,以及(va,vb)的权w。

(4)new一个临时边结点tempnode,使tempnode中“另一顶点的编号”为vb的编号,并使其指针指向头结点指针原本指向的“空间”,再令头结点指针指向tempnode,完成tempnode的插入,即完成相邻顶点的连接。

(5)因为这个是无向图,所以我们需要对称地完成起始顶点为vb的单链表的插入。

(6)在达到边数arcnum前循环执行步骤(3)(4)(5)。

int CreatUDG(ALGraph &G)
{                                /*下面的代码已经按照<思路>步骤分块喽*/
    cin>>G.vexnum>>G.arcnum;
 
    int i, j;    
    for(i =0; i < G.vexnum; i++)
    {
        cin>>G.ALs[i].data;
        G.ALs[i].nextarcnode = NULL;
    }
 
    for(int k = 0; k < G.arcnum; k++)
    {
        cin>>va>>vb>>w;
        i = LocateVex(G, va); j = LocateVex(G, vb);
 
        tempnode1 = new ArcNode;
        tempnode1->anothervex = j;
        tempnode1->nextarcnode = G.ALs[i].nextarcnode;
        G.ALs[i].nextarcnode = tempnode1;
 
        tempnode2 = new ArcNode;
        tempnode2->anothervex = i;
        tempnode2->nextarcnode = G.ALs[j].nextarcnode;
        G.ALs[j].nextarcnode = tempnode2;
    }
 
    return 0;
}

优缺点

1)优点:

a. 便于增加和删除顶点。

b. 便于统计边的数量,时间复杂度为O(n+e)。

c. 空间效率高。

2)缺点:

a. 不便于判断顶点间是否有联系。

b. 不便于计算有向图各个顶点的入度,需要遍历其余所有起始顶点的单链表。

总结

  1. 对于一个具有n个顶点e条边的无向图,它的邻接表表示有n个顶点表结点2e个边表结点
  2. 对于一个具有n个顶点e条边的有向图,它的邻接表表示有n个顶点表结点e个边表结点
  3. 如果图中边的数目远远小于n2称作稀疏图,这是用邻接表表示比用邻接矩阵表示节省空间;
  4. 如果图中边的数目接近于n2,对于无向图接近于n*(n-1)称作稠密图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐