数据结构知识整理 - 构造图(邻接表和邻接矩阵)

主要内容

  • 图的定义
  • 图的存储结构
  • 邻接矩阵
  • 邻接表
  • 对比邻接矩阵与邻接表
  • 十字链表
  • 邻接多重表

 

前言

在线性表中,数据元素之间仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继;

在树形结构中,数据元素之间有明显的层次关系,并且每一层中的数据元素可能和下一层的多个元素(子结点)相关,但只能和上一层的一个元素(父结点)相关;

在图结构中,结点之间的关系是任意的,可以说树形结构是特殊的图结构。


 

图的定义

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

有向图中,E中的元素形式为表示从顶点x到顶点y的一条有向边,有向边也称作,x为弧尾,y为弧头;

无向图中,E中的元素形式为(x,y),仅表示连接顶点x和顶点y的一条边,效果同(y,x)。

 

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


 

图的存储结构

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


 

邻接矩阵

邻接矩阵(Adjacency Matrix)是表示顶点之间相邻关系的矩阵,存储在二维数组中。

1)在无向图中,若顶点v1与v2有联系,v1与v3没有联系,则在矩阵中,(1,2)和(2,1)的值为1,(1,3)和(3,1)的值为0,对称矩阵

2)在无向网中,若边(v1,v2)的权为7,v1与y3没有联系,则在矩阵中,(1,2)和(2,1)的值为7,(1,3)和(3,1)的值为∞(无限)

3)在有向图中,若v1为弧尾,v2为弧头,v2不指向v1,则在矩阵中,(1,2)的值为1,(2,1)的值为0;

4)有向网的情况可以类推。

*需要注意的是,矩阵和二维数组的“坐标”含义是不同的,矩阵的(1,1)等价于二维数组中的[0,0]

邻接矩阵仅记录着任意两点间的联系,我们还需要一个一维数组来记录每个顶点的信息,有边有顶点,这样才能构成一个完整的图。

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

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


/*邻接矩阵的结构表示*/

#include 
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的出度就是多少,多少个的值为1,v1的入度就是多少。

2)缺点:

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

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

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


 

邻接表

邻接表(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)由上面总结的各自的优缺点可以看出,“我的优点就是你的缺点,and vice versa”。

2)一个图的邻接矩阵唯一,而邻接表不唯一。前面提过,“图中有多少个顶点,就有多少个单链表,每个顶点分别作为每个单链表的头结点,与顶点相连的其余顶点无序地(无特殊的顺序要求)连接在对应的单链表上”,因此每个单链表的链接次序取决于算法以及输入。

3)邻接矩阵和邻接表是图最常用的两种存储结构。(所以“十字链表”和“邻接多重表”暂时就不详细介绍喽_(:з」∠)_)

4)在下一篇将会讲到的“遍历图”中,不论是深度优先搜索还是广度优先搜索,存储结构为邻接矩阵的时间复杂度都为O(n²),存储结构为邻接表的时间复杂度都为O(n+e)


 

十字链表

十字链表(Orthogonal List)是有向图的另一种链式存储结构,可以看作是有向图的邻接表(单链表的结点数-1为出度)逆邻接表(单链表的结点数-1为入度)结合起来的一种链表。

在十字链表中容易找到以vi(vi∈G(V))为弧尾或弧头的弧,因而更容易求顶点的入度和出度。


 

邻接多重表

邻接多重表(Adjacency Multilist)是无向图的另一种链式存储结构。

对无向图而言,邻接表的缺点:每一条边(va,vb)有两个结点,分别在第a和第b个单链表中,这可能会给边的搜索、删除等操作带来了不便。

邻接多重表与邻接表的区别仅在于,同一条边,在邻接表中要用两个结点表示,而在邻接多重表中只需要一个结点

此外,邻接多重表中增加了标志域用以标记该条边是否被搜索过,避免了同一条边的重复搜索。


 

路过的圈毛君:“图形结构分三块讲:构造图、遍历图和应用图。本来想一次写完的,无奈精力有限,要做的事比较多.......而且我也差不多该开始护肝了_(:з」∠)_”

你可能感兴趣的:(数据结构,数据结构,图,邻接表,邻接矩阵)