图的定义与存储结构

知识框架

图的基本概念

在线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继。在树形结构中,数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。

一、图的定义

图(Graph)是由顶点的有穷非空集合V(G)和顶点之间边的集合E(G)组成,通常表示为: G=(V,E),其中,G表示个图,V是图G中顶点的集合,E是图G中边的集合。若V={v1,v2,...,vn} ,则用∣V∣表示图G中顶点的个数,也称图G的阶,E={(u,v)∣u∈V,v∈V},用∣E∣表示图G中边的条数。

注意:线性表可以是空表,树可以是空树,但图不可以是空图。就是说,图中不能一个顶点也没有,图的顶点集V一定非空,但边集E可以为空,此时图中只有顶点而没有边。

图的基本概念和术语

有向图

若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v, w>,其中v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自v。

图(a)所示的有向图G1可表示为

G1=(V1,E1)

V1={1,2,3}

E1={<1,2>,<2,1>,<2,3>}

有向图:每条边都是有方向的

无向图:每条边都是无方向的

完全图:任意两个点都有一条边相连

稀疏图:有很少边或弧的图

稠密图:有较多边或弧的图

网:边/弧相连的两个顶点之间的关系 存在(vi,vj),则称vi和vj互为邻接点;存在<vi,vj>,则称vi邻接到vj,vj邻接于vi

关联(依附):边/弧与顶点之间的关系。 存在(vi,vj)/<vi,vj>,则称该边/弧关联于vi和vj

顶点的度:与该顶点相关联的边的数目,记为TD(v)

在有向图中,顶点的度等于该顶点的入度与出度之和

v的入度:是以v为终点的有向边的条数,记作ID(v)

v的出度:是以v为始点的有向边的条数,记作OD(v)

路径: 接续的边构成的顶点序列

路径长度:路径上边或弧的数目/权值之和

回路(环):第一个顶点和最后一个顶点相同的路径

简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径

简单回路(简单环): 除路径起点和终点相同外,其余顶点均不相同的路径

连通图(强连通图):在无(有)向图G=(V,{E})中,若对任何两个顶点v,u 都存在从v到u的路径,则称G是连通图(强连通图)

权与网:图中边弧所具有的相关数称为权,表明从一个顶点到另一个顶点的距离或消耗。带权的图称为网

子图:设有两个图G=(V,{E}),G1=(V1,{E1}),若V1是V的子集,且E1是E的子集,则称G1是G的子图。

连通分量(强连通分量):无向图G的极大连通子图称为G的连通分量,极大连通子图的意思是该子图是G连通子图,将G的任何不在该子图的顶点加入,子图不再连通

极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边子图不再连通

生成树:包含无向图G所有顶点的极小连通子图

生成森林:对非连通图,由各个连通分量的生成树的集合

邻接矩阵

图的邻接矩阵(Adjacency Matrix) 存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

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

图是一个无向图和它的邻接矩阵:

1.无向图的邻接矩阵一定是一个对称矩阵(即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的)。 因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。

2.对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度TD(vi)。比如顶点v1的度就是1+0+1+0=2。

求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,A[i][j]为 1就是邻接点。

下图是有向图和它的邻接矩阵:

1.主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称。

2.有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之和。顶点v1的出度为2,即第v1行的各数之和。

3.与无向图同样的办法,判断顶点vi到vj是否存在弧,只需要查找矩阵中A[i][j] 是否为1即可。

对于带权图而言,若顶点vi和vj之间有边相连,则邻接矩阵中对应项存放着该边对应的权值

下图是有向网图和它的邻接矩阵:

通过以上对无向图、有向图和网的描述,可定义出邻接矩阵的存储结构:

邻接矩阵存储

#define MaxVertexNum 100    //顶点数目的最大值
typedef char VertexType;    //顶点的数据类型
typedef int EdgeType;    //带权图中边上权值的数据类型
typedef struct{
    VertexType Vex[MaxVertexNum];    //顶点表
    EdgeType Edge[MaxVertexNum][MaxVertexNum];    //邻接矩阵,边表
    int vexnum, arcnum;    //图的当前顶点数和弧树
}MGraph;

注意:

①在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可省略)。

②当邻接矩阵中的元素仅表示相应的边是否存在时,EdgeType可定义为值为0和1的枚举类型。

③无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储。

④邻接矩阵表示法的空间复杂度为O(n2), 其中n为图的顶点数∣V∣。

⑤ 用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。

⑥ 稠密图适合使用邻接矩阵的存储表示。

采用邻接矩阵表示法创建无向图(有向图)

算法思想:

1.输入总顶点数和总边数

2.依次输入点的信息存入顶点表中

3.初始化邻接矩阵,使每个权值初始化为极大值

4.构造邻接矩阵

Status CreateUDN(AMGraph &G){//采用邻接矩阵表示法,创建无向网G
    cout<<"\n请输入顶点个数:"; 
    cin>>G.vexnum;
    cout<<"请输入边的个数:";
    cin>>G.arcnum;
    cout<<"请输入顶点名称:"<<endl;//输入总顶点数,总边数 
    for(int i=0;i<G.vexnum;i++) cin>>G.vexs[i];//依次输入点的信息 
    for(int i=0;i<G.vexnum;i++){//初始化邻接矩阵 
        for(int j=0;j<G.vexnum;j++){
            G.arcs[i][j]=MaxInt;//边的权值均置为极大值 
        }
    }
    cout<<"请输入边连接的顶点:"<<endl; 
    for(int k=0;k<G.arcnum;k++){//构造邻接矩阵 
        cin>>v1>>v2>>W;//输入一条边所依附的顶点及边的权值 
        i=LocateVex(G,v1);
        j=LocateVex(G,v2);//确定v1v2在G中的位置 
        G.arcs[i][j]=w;//边<v1,v2>的权值置为w 
        G.arcs[j][i]=G.arcs[i][j];//置<v1,v2>的对称边<v2,v1>的权值为w (这句话在建立有向图时注释掉) 
    } 
    return OK;
} 

邻接表

当一个图为稀疏图时(边数相对顶点较少),使用邻接矩阵法显然要浪费大量的存储空间,如下图所示:

而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。

所谓邻接表,是指对图G中的每个顶点vi建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边(对于有向图则是以顶点vi为尾的弧),这个单链表就称为顶点vi的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点,如下图所示。

顶点表结点由顶点域(data)和指向第一条邻接边的指针(firstarc) 构成,边表(邻接表)结点由邻接点域(adjvex)和指向下一条邻接边的指针域(nextarc) 构成。

无向图的邻接表的实例如下图所示。

顶点表结点由顶点域(data)和指向第一条邻接边的指针(firstarc) 构成,边表(邻接表)结点由邻接点域(adjvex)和指向下一条邻接边的指针域(nextarc) 构成。

无向图的邻接表的实例如下图所示。

对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。

图的邻接表存储方法具有以下特点

若G为无向图,则所需的存储空间为O(∣V∣+2∣E∣);若G为有向图,则所需的存储空间O(∣V∣+∣E∣)。前者的倍数2是由于无向图中,每条边在邻接表中出现了两次

对于稀疏图,采用邻接表表示将极大地节省存储空间。

在邻接表中,给定一顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为O(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低。

在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数;但求其顶点的入度则需要遍历全部的邻接表。因此,也有人采用逆邻接表的存储方式来加速求解给定顶点的入度。当然,这实际上与邻接表存储方式是类似的。

图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。

邻接表储存

#define MAXVEX 100    //图中顶点数目的最大值
type char VertexType;    //顶点类型应由用户定义
typedef int EdgeType;    //边上的权值类型应由用户定义
/*边表结点*/
typedef struct EdgeNode{
    int adjvex;    //该弧所指向的顶点的下标或者位置
    //EdgeType weight;    //权值,对于非网图可以不需要
    struct EdgeNode *next;    //指向下一个邻接点
    //OtherInfo info; //和边相关的信息 
}EdgeNode;

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

/*邻接表*/
typedef struct{
    AdjList vertices;//vertices为vertex的复数 
    int numVertexes, numEdges;    //图中当前顶点数和边数
}ALGraph;

邻接表建立无向图

Status CreateUDG(ALGraph &G)
{
    cout<<"\n请输入顶点个数:"; 
    cin>>G.vexnum;
    cout<<"请输入边的个数:";
    cin>>G.arcnum;/输入总顶点,总边数 
    cout<<"请输入顶点名称:"<<endl;/
    for (int i=0;i<G.vexnum;i++)//输入各点,构造表头结点表 
    {
        cin>>G.vertices[i].data;//输入顶点值 
        G.vertices[i].firstarc=NULL;//初始化表头结点的指针域 
    }
    cout<<"请输入边连接的顶点:"<<endl; 
    for (int k=0;k<G.arcnum;k++)//输入各条边,构造邻接表 
    {
        VerTexType v1,v2;
        cin>>v1>>v2;//输入一条边依附的两个顶点 
        int i=LocateVexALG(G,v1);
        int j=LocateVexALG(G,v2);
        ArcNode *p1,*p2;
        p1=new ArcNode;//生成一个新的边结点*p1 
        p1->adjvex=j;//邻接点序号为j 
        p1->nextarc=G.vertices[i].firstarc;
        G.vertices[i].firstarc=p1;//将新结点*p1插入顶点vi的边表头部 
        p2=new ArcNode;//生成另一个对称的新的边结点*p2 
        p2->adjvex=i;//邻接点序号为i 
        p2->nextarc=G.vertices[j].firstarc;
        G.vertices[j].firstarc=p2;//将新结点*p2插入顶点vj的边表头部
        //建立有向网不用最后四部 
    }
    return OK;
}

十字链表(主要用于有向图)

缺点:求结点的度难

十字链表(Orthogonal List)。

我们重新定义顶点表结点结构如下表所示。

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

重新定义的边表结点结构如下表所示。

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

如下图所示,顶点依然是存入一个一维数组{V0,V1,V2,V3},实线箭头指针的图示完全与的邻接表的结构相同。就以顶点V0来说,firstout 指向的是出边表中的第一个结点V3。所以V0边表结点的headvex=3,而tailvex就是当前顶点V0的下标0,由于V0只有一个出边顶点,所以headlink和taillink都是空。

我们重点需要来解释虚线箭头的含义,它其实就是此图的逆邻接表的表示。对于V0来说,它有两个顶点V1和V2的入边。因此V0的firstin指向顶点V1的边表结点中headvex为0的结点,如上图右图中的①。接着由入边结点的headlink指向下一个入边顶点V2,如图中的②。对于顶点V1,它有一个入边顶点V2,所以它的firstin指向顶点V2的边表结点中headvex为1的结点,如图中的③。顶点V2和V3也是同样有一个入边顶点,如图中④和⑤。

十字链表的好处就是因为把邻接表和逆邻接表整合在了一起, 这样既容易找到以V1为尾的弧,也容易找到以V1为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图的应用中,十字链表是非常好的数据结构模型。

邻接多重表(主要用于无向图)

缺点:每条边都要储存两边

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

在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率较低。比如下图中,若要删除左图的(V0,V2)这条边,需要对邻接表结构中右边表的阴影两个结点进行删除操作,显然这是比较烦琐的。

重新定义的边表结点结构如下表所示。

其中ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink 指向依附顶点ivex的下一条边,jlink 指向依附顶点jvex的下一条边。这就是邻接多重表结构。

每个顶点也用一一个结点表示,它由如下所示的两个域组成。

其中,data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。

我们来看结构示意图的绘制过程,理解了它是如何连线的,也就理解邻接多重表构造原理了。如下图7所示,左图告诉我们它有4个顶点和5条边,显然,我们就应该先将4个顶点和5条边的边表结点画出来。

我们开始连线,如图,首先连线的①②③④就是将顶点的firstedge指向一条边,顶点下标要与ivex的值相同,这很好理解。接着,由于顶点V0的(V0,V1)边的邻边有(V0,V3)和(V0,V2)。 因此⑤⑥的连线就是满足指向下一条依附于顶点V0的边的目标,注意ilink指向的结点的jvex一定要和它本身的ivex的值相同。同样的道理,连线⑦就是指(V1,V0)这条边,它是相当于顶点V1指向(V1,V2)边后的下一条。V2有三条边依附,所以在③之后就有了⑧⑨。连线④的就是顶点V3在连线④之后的下一条边。 左图一共有5条边,所以右图有10条连线,完全符合预期。

到这里,可以明显的看出,邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。 这样对边的操作就方便多了,若要删除左图的(V0,V2)这条边,只需要将右图的⑥⑨的链接指向改为NULL即可。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值