Graph
图相关概念
-
图是一种比线性表和树更为复杂的数据结构;在线性表中,数据元素之间仅存在线性关系,每个而数据元素只有一个直接前驱和一个直接后继;在树形结构中,数据元素之间有着明显的层次关系,并且每一层中的数据元素可能和下一层的多个元素相关但只能和上一层中的一个元素相关;而在图结构中,结点之间的关系可以是任意的,任意两个数据元素之间都可能相关
-
图(Graph) G 由两个集合 V 和 E 组成,即为 G = ( V,E),其中 V 是顶点的有穷非空集, E 是边的有穷集合。有向图中,顶点对 <x,y> 是有序的,在无向图中,顶点对 (x,y) 是无序的
-
图的基本术语
①子图
②完全有向、无向图:拥有 n(n-1)/2 条边的无向图和拥有 n(n-1) 条边的有向图
③稀疏图和稠密图:边的个数
④权和网:边上的数值称作权,带权的图称作网
⑤邻接点:无向图中存在边的两个顶点互为邻接点
⑥度、入度、出度
⑦路径和路径长度:有向图中的路径也是有向的
⑧回路或环:第一个顶点和最后一个顶点相同的路径称为环
⑨简单路径、简单回路:序列中顶点不重复出现的路径称为简单路径,除了第一个和最后一个顶点之外,其余顶点都不重复出现的回路称为简单回路或简单环
图的表示 / 图的存储结构
因为图的结构比较复杂,任意两个顶点之间都存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序存储结构,但可以借助二维数组来表示元素之间的关系,即邻接矩阵表示法;另一方面,由于图的任意两个顶点之间都可能存在关系,因此用链式存储表示图是件很正常的事,链式存储包括邻接表、十字链表和邻接多重表
- 邻接矩阵表示法
邻接矩阵是表示顶点间相邻关系的矩阵, A[i][j] = 1 (存在 <vi,vj> 或者 (vi,vj)),反之 A[i][j] = 0
无向图的邻接矩阵关于对角线对称,且图中无环的话,则对角线均为 0
/* 图 / 网 的邻接矩阵表示 */
#define MaxInt 2110000
typedef struct
{
char vexs[100]; /* 顶点表存储顶点信息 */
int arcs[100][100]; /* 邻接矩阵存储顶点间相邻关系 */
int vexnum,arcnum; /* 图的点数和边数 */
}AMGraph;
算法步骤
①输入总顶点数和总边数
②依次输入顶点信息存入顶点表中
③初始化邻接矩阵,使得每个权值初始化为 0(网 则初始化为极大值 MaxInt )
④构造邻接矩阵;依次输入每条边依附的顶点和其权值,确定两个顶点在图中的位置后,使邻接矩阵中的相应边赋予相应的权值,同时使其对称边赋予相同的权值(无向图)
/* 采用邻接矩阵表示法,创建无向网 G */
void CreateAMGraph(AMGraph &G)
{
scanf("%d %d",&G.vexnum,&arcnum); /* 输入总顶点数和总边数 */
for(int i=0;i<G.vexnum;i++)
scanf("%c",&G.vexs[i]); /* 依次输入各顶点的信息 */
for(int i=0;i < G.vexnum;i++)
for(int j=0;j < G.vexnum;j++) /* 初始化邻接矩阵边的权值 */
G.arcs[i][j] = MaxInt;
/* 构造邻接矩阵 */
for(int k=0;k < G.arcnum;k++)
{
sancf("%c %c %d",&v1,&v2,&weight); /* 输入边和边的权值 */
int i = LocateVex(G,v1); int j = LocateVex(G,v2); /* 依据数值定位边的两顶点下标 */
G.arcs[i][j] = G.arcs[j][i] = w;
}
}
邻接矩阵表示法优缺点
①便于判断两个顶点之间是否存在边
②便于计算各个顶点的度:对于无向图,第 i 行或第 i 列元素之和就是顶点 i 的度;对于有向图,第 i 行元素之和就是顶点 i 的出度,第 i 列元素之和就是顶点 i 的入度
③不利于增加和删除顶点
④不便于统计边的数目:需扫描邻接矩阵的所有元素,时间复杂度为 O(n2)
⑤空间复杂度高,虽说对于无向图可以采用压缩存储的方法(仅存储下/上三角),但无论如何,空间复杂度均为 O(n2),对于稀疏图而言极其浪费空间
- 邻接表表示法
邻接表是图的一种链式存储结构;在邻接表中,对图的每一个顶点 vi 建立一个单链表,把与 vi 相邻接的顶点放在这个链表中
邻接表中的每个单链表的第一个结点存放有关顶点的信息,把这一结点看成链表的表头结点,其余结点存放有关边的信息,这样邻接表便由两部分组成:表头结点表和边表
(1)表头结点表:是所有表头结点以顺序结构的形式存储,以便可以随机访问任一顶点的边链表;表头结点包括数据域和链域两部分,数据域 data 用于存储顶点 vi 的相关信息,链域用于指向链表的第一个结点(即第一个邻接点)
(2)边表:是由表示图中顶点间关系的边链表组成;包括邻接点域、数据域和链域三个部分,邻接点域指示与该顶点 vi 邻接的点在图中的位置,数据域存储与边相关的信息,如权值,链域指示与顶点 vi 邻接的下一个结点
在无向图的邻接表中,顶点 vi 的度恰为第 i 个链表中的结点个数;而有向图中,第 i 个链表的结点个数只是顶点 vi 的出度,为求入度,必须遍历整个邻接表,在所有链表中,其邻接点域的值为 i 的结点的个数就是顶点 vi 的入度
/* 图的邻接表存储表示 */
/* 边结点 */
typedef struct ArcNode
{
int adjvex; /* 邻接顶点的位置 */
struct ArcNode * nextarc; /* 指示下一邻接结点 */
OtherInfo info; /* 边信息(权) */
}ArcNode;
/* 头结点 */
typedef struct VNode
{
char data;
ArcNode *firstarc;
}VNode,AdjList[100]; /* AdjList为邻接表类型 */
/* 图 */
typedef struct
{
AdjList vertices;
int vexnum,arcnum; /* 图的当前顶点数和边数 */
}ALGraph;
算法步骤
①输入总顶点数和总边数
②依次输入顶点信息存入各表头结点中,并初始化每个表头结点的指针域为 NULL
③创建邻接表:依次输入每条边依附的两个顶点,确定这两个顶点的序号 i 和 j 后,将此边结点分别插入 vi 和 vj 对应的两个边链表头部
/* 采用邻接表法,创建无向图 */
void Create ALGraph( ALGraph &G )
{
scanf("%d %d",&vexnum,&arcnum);
/* 输入各结点信息,构造邻接表表头结点表 */
for(int i=0;i<G.vexnum;i++)
{
scanf("%c",&G.vertices[i].data);
G.vertice[i].firstarc = NULL;
}
/* 输入各边,构造邻接表边表 */
for(int k=0;k<G.arcnum;k++)
{
scanf("%c %c",&v1,&v2);
int i = LocateVex(G,v1); int j = LocateVex(G,v2); /* 依据数值定位边的两顶点下标 */
/* 新生成结点 p1 作为表头结点 Vi 的邻接结点插入其边链表头部 */
ArcNode p1 = (*struct ArcNode)malloc(sizeof(struct ArcNode));
p1.adjvex = j;
p1->nextarc = G.vertice[i].firstarc; G.vertice[i].firstarc = p1;
/* p2同理 */
ArcNode p2 = (*struct ArcNode)malloc(sizeof(struct ArcNode));
p2.adjvex = i;
p2->nextarc = G.vertice[j].firstarc; G.vertice[j].firstarc = p1;
}
}
邻接表表示法优缺点
①便于增加和删除顶点
②便于统计边的数目:按顶点表顺序扫描所有边表可得边的数目,时间复杂度为 O(n+e)
③空间效率高
④不便于判断顶点之间是否存在:要判定 vi 和 vj 之间是否有边,需要扫描第 i 个边表,最坏情况下要耗费 O(n) 时间
⑤不便于计算有向图中各个顶点的度:在无向图的邻接表中,顶点 vi 的度恰为第 i 个链表中的结点个数;而有向图中,第 i 个链表的结点个数只是顶点 vi 的出度,为求入度,必须遍历各顶点的边表,在所有链表中,其邻接点域的值为 i 的结点的个数就是顶点 vi 的入度;若有向图采用逆邻接表,则求入度容易,出度难
- 十字链表(Orthogonal List)
有向图的另一种链式存储结构。该结构可以看成是将有向图的邻接表和逆邻接表结合起来得到的,在十字链表中,对应于有向图中每一条弧都有一个结点,对应于每个定顶点也有一个结点
- 邻接多重表
无向图的一种存储方式:邻接多重表是邻接表的改进,它把边的两个顶点存放在边表结点中,所有依附于同一个顶点的边串联在同一链表中,由于每条边依附于两个顶点,则每个边表结点同时链接在两个链表中。用于解决无向图存储时同一条边要存储两遍的问题