图是比线性表和树更为复杂的数据结构。在线性表中,每个数据元素至多只有一个直接前驱和一个直接后继。在树中,各个节点都处在一定的层次上,并且每一层上的结点可以和其下一层中的多个结点相关联。所以可以把图看作是线性表和树的推广,也就是说线性表和树实际上都是一种特殊的图。图结构可以描述各种复杂的数据结构,在计算机科学、信息、控制、运筹学、通信网络、工程等许多领域都有着广泛的应用,如通信线路、交通航线、工序进度计划等。
目录
① 图G是由两个集合V和E组成的,记为G=(V,E)。其中,V是图G中数据元素的非空有穷集合,这些数据元素称为顶点;E是图G的V中顶点偶对的有限集合,这些顶点偶对称为边。在图G中,E集可以是为空,若E为空,则图G中只有顶点没有边。
② 若图中边是有方向的,称此图为有向图,用尖括号括起来的顶点偶对表示,并称为弧或有向边。若图中的边是无方向的,称此图为无向图,用括号括起的顶点偶对表示,通常称为边。
① 通常,在图中用n表示顶点的数目,e表示边或弧的数目。在n个顶点的无向图G中,e的取值范围是0~n(n-1)/2,若图G中有n(n-1)/2条边则称该无向图为无向完全图。在有n个顶点的有向图G中,e的取值范围是0~n(n-1),若图G中有n(n-1)条边则称该有向图为有向完全图。
② 有时图的边或弧附有相关的数值,这种数值称为权。这些权可以表示一个顶点到另一个顶点的距离或时间耗费、开销等。每条边或弧都带权的图称为网。
③ 无向图中,若顶点x与y之间有边(x,y),则x,y互为邻接点,边(x,y)与顶点x和y相关联。无向图中顶点x的度是与x相关联的边的数目,记为TD(x)。有向图中,若
是一条弧,则称顶点v邻接到顶点w,顶点w邻接自顶点v。有向图中顶点v的入度是以顶点v为终点的弧的数目,记为ID(v),顶点v的出度是以顶点v为始点的弧的数目,记为OD(v),顶点v的度记为TD(v)=ID(v)+OD(v)。
④ 路径序列中顶点不重复出现的路径称为简单路径。路径中第一个顶点和最后一个顶点相同的路径称为回路或环。简单路径中第一个顶点和最后一个顶点相同的路径称为简单回路。
⑤ 在无向图中,如果顶点x到顶点y有路径,则称x和y是连通的。如果无向图中任意两个顶点都是连通的,则称该无向图为连通图;在有向图中,如果顶点v到顶点w之间存在路径,顶点w到顶点v之间也存在路径,则称v和w是强连通的。如果有向图中任意两个顶点都是强连通的,则称该有向图为强连通图。
⑥ 非连通图含有连通分量,即该图中的极大连通子图;非强连通图含有强连通分量,即该图中的极大强连通子图。
⑦ 一个连通图的生成树,是含有该连通图全部顶点的一个极小连通子图。若连通图G的顶点个数为n,则G的生成树的顶点个数也为n,边数为n-1,边数多于n-1就不是极小连通子图,少于n-1则不连通了。但是有n个顶点,n-1条边的图并不一定都是生成树。
⑧ 设G=(V,E)是一个图,G'=(V',E')也是一个图,如果V'是V的子集,E'是E的子集,且E'中的边仅与V'中的顶点相关联,则称G'为G的子图。
图的结构比较复杂,表示图的存储结构有多种形式。常用图的存储结构有邻接矩阵、邻接表、十字链表、邻接多重表等方法。在实际应用中,应根据具体的图和需要进行的操作,设计适合的存储结构。
(1)邻接矩阵是表示顶点之间相邻关系的矩阵。
设G=(V,E)是具有n个顶点的图,则G的邻接矩阵是有如下性质的n阶矩阵:
A[i,j]=1,若(Vi,Vj)或
是E(G)中的边 A[i,j]=0,若(Vi,Vj)或
不是E(G)中的边
(2)以二维数组表示有n个顶点的图时,需存放n个顶点信息和n^2个边或弧信息的存储量。
若考虑无向图邻接矩阵的对称性,则可采用压缩存储方式只存入矩阵的下三角(或上三角)元素。显然,邻接矩阵表示法的空间复杂度S(n)=O(n^2)。
图的邻接矩阵存储结构定义如下:
- #define N 20
- typedef struct{
- int vexnum,arcnum; //图的顶点数,边数
- VexType vexs[N]; //图的顶点
- int arcs[N][N]; //图的邻接矩阵
- }MGraph;
(3)从图的邻接矩阵表示法中,计算每个顶点的度非常方便。
在无向图中每个顶点的度数就等于邻接矩阵中与该顶点相应行或列中非零元素的个数。在有向图中,顶点的出度是邻接矩阵中与该顶点对应行的非零元素的个数,顶点的入度是邻接矩阵中与该顶点对应列的非零元素的个数。
(4)创建一个以邻接矩阵为存储表示的图的基本步骤:
① 输入顶点数和边数
② 输入顶点信息
③ 输入边的信息
(5)创建图邻接矩阵的算法
- void CreateMGraph(MGraph *g){
- int i,j;
- char v;
- scanf("%d %d",&g->vexnum,&g->arcnum);
- getchar();
- i=0;
- for(i=0;i
vexnum;i++){ //输入顶点信息 - v=getchar();
- g->vexs[i]=v;
- }
- for(i=0;i
vexnum;i++){ //邻接矩阵初始化 - for(j=0;j
vexnum;j++){ - g->arcs[i][j]=0;
- }
- }
- for(i=0;i
arcnum;i++){ //输入边的信息 - scanf("%d,%d",&i,&j);
- g->arcs[i][j]=1;
- }
- }
(1)邻接表是图的一种顺序存储和链式存储相结合的存储结构。
在邻接表中,对图中的每一个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点Vi的边(对于有向图是以顶点Vi为尾的弧)。顶点Vi存储在表头结点中,以顺序存储结构存储所有顶点信息。采用邻接表存储图以便于随机访问任一顶点及其链表。
(2)图的邻接表表示法采用两种不同的顶点结构;顶点表结点和链表结点。
顶点表结点由两个域组成,其中顶点域vertex存储顶点信息;next域指向该顶点的单链表。链表结点由两个域组成,其中邻接点域adjvex存储邻接点在顶点中的序号;next域指向下一个邻接边。
(3)在无向图的邻接表中,顶点Vi的度即为第i个链表中的结点数;在有向图中,第i个链表中的结点数为顶点Vi的出度
为求顶点Vi的入度,必须遍历整个邻接表,即所有链表中其邻接点域的值为i的结点个数是顶点Vi的入度。为了便于确定顶点的入度或以顶点Vi为头的弧,可以建立一个有向图的逆邻接表,即对每个顶点Vi建立一个链表以Vi为弧头的表。
(4)对于有向图,也可以在顶点表结点中增加一个入度域in,存储该顶点的入度。
这样的存储结构对于有向图的邻接表可以方便获得顶点的入度,其缺点是当图中的弧发生变化时,相应的入度也必须修改。
(5)图的邻接表存储结构定义:
- typedef struct EdgeNode{ //邻接链表结点
- int adjvex; //顶点序号
- struct EdgeNode * next; //指向下一个结点
- }EdgeNode;
- typedef struct VNode{ //顶点表结点
- VexType data; //顶点信息
- int in; //有向图的顶点入度或无向图顶点度
- struct EdgeNode *link; //指向邻接链表指针
- }VNode;
- typedef struct ALgraph{ //图的邻接表
- int vexnum,arcnum; //顶点数,弧数
- VNode adjlist[N];
- }ALGraph;
(6)邻接表的建立需要根据输入的边的信息创建邻接链表
邻接链表的建立和单链表的建立过程基本类似,采用前插法将边的顶点直接插入表结点后。
下面以有向图为例给出图的邻接链表创建算法:
- void CreateALGraph(ALGraph *g){
- int i,j;
- char v;
- EdgeNode *s;
- i=0;
- scanf("%d,%d",&g->vexnum,&g->arcnum); //输入顶点数和边弧数
- getchar();
- for(i=0;i
vexnum;i++){ - v=getchar();
- g->adjlist[i].data=v; //读入顶点信息
- g->adjlist[i].in=0; //初始化入度为0
- g->adjlist[i].link=NULL; //初始化指向顶点邻接链表指针为空
- }
- for(i=0;i
vexnum;i++){ - scanf("%d,%d",&i,&j);
- s=(EdgeNode *)malloc(sizeof(EdgeNode)); //创建邻接链表表结点
- s->adjvex=j;
- s->next=g->adjlist[i].link; //结点插入i相应单链表
- g->adjlist[i].link=s;
- g->adjlist[j].in++; //顶点j的入度加1
- }
- }
图的遍历是从某个顶点出发,对图中所有顶点仅访问一次。若给定的图是连通的,则从图的任一顶点出发沿着边可以访问到该图的所有顶点。然而,图的遍历比树的遍历复杂很多,这是因为图中的任一顶点都可能和其余顶点相邻接,故在访问了某个顶点之后,可能沿着某条回路又回到了该顶点。为了避免重复访问同一个顶点,必须记住每个顶点是否被访问过。
(1)深度优先搜索遍历类似树的先根遍历。假设给定图G初态是所有顶点均未曾访问过,在图中任选一个顶点Vi为初始出发点,则深度优先搜索可定义为,首先访问出发点Vi,从Vi的每一个未被访问过的邻接点出发,深度优先搜索图G,直至图G中所有与Vi相通的顶点都被访问过,若图中还有未被访问的顶点,选择一个未被访问的顶点作为出发点,继续深度优先搜索。
(2)图的深度优先搜索是一个递归的过程。对于一个连通的图来说,从任一个顶点出发,执行一次深度优先搜索可以遍历图中所有顶点;而对于非连通的图来说,需要进行多次深度优先搜索才可以遍历图中所有顶点。
(3)遍历过程中,为了区分顶点是否被访问过,需要设置一个访问标志数组visit[],若顶点i未被访问,则visit[i]为0,否则visit[i]为1。
- int visit[N]; //定义辅助数组记录顶点的访问状态
- void DFS(int i,MGraph *g){
- int j;
- printf("%c",g->vexs[i]);
- visit[i]=1;
- for(j=0;j
vexnum;j++) - if((g->arcs[i][j]==1)&&(!visit[j]))
- DFS(j,g);
- }
- void TDFS(MGraph *g){ //深度优先遍历整个图算法
- int i;
- for(i=0;i
vexnum;i++) - if(visit[i]!=1)
- DFS(i,g);
- }
- void DFSL(int i,ALGraph *g){
- int j;
- EdgeNode *p;
- printf("%c",g->adjlist[i].data);
- visit[i]=1;
- p=g->adjlist[i].link;
- while(p){
- j=p->adjvex;
- if((!visit[j])){
- DFSL(j,g);
- }
- p=p->next;
- }
- }
- void TDFSL(ALGraph *g){
- int i;
- for(i=0;i
vexnum;i++){ - if(visit[i]==0){
- DFSL(i,g);
- }
- }
- }
(1)广度优先搜索遍历类似于树的按层次遍历。设图G的初始状态是所有顶点均未访问过,在G中任选一顶点Vi为初始出发点,则广度优先搜索的基本思想是,首先访问出发点Vi,接着依次访问Vi的所有邻接点W1,W2,W3,…,Wt;然后,再依次访问与W1,W2,…,Wt邻接的所有未曾访问的顶点,直至图G中所有与Vi相通的顶点都被访问;若图中还有未被访问的顶点,选择一个未被访问的顶点作为出发点,继续广度优先搜索
(2)广度搜索过程是一种基于层次遍历的搜索过程,类似于二叉树的层次遍历,以某个顶点为出发点,访问其所有未被访问邻接点,下一步的搜索是以刚刚被访问过的顶点为出发点继续广度搜索,而上一层刚被访问的顶点可能不止一个,需要暂存这些顶点,且下次访问要保证这些顶点的先后访问次序,因此广度优先搜索需要借助队列来暂存那些刚被访问过的顶点。广度优先搜索不需要回溯,不是递归过程。
广度优先遍历算法如下:
- int visited[N];
- void BFS(int k,MGraph *g){
- int i,j;
- SqQueue qlist,*q;
- q=&qlist;
- q->rear=0;
- q->front=0;
- printf("%c",g->vexs[k]);
- visited[k]=1;
- q->data[q->rear]=k;
- q->rear=(q->rear+1)%MAX;
- while(q->rear!=q->front){
- i=q->data[q->front];
- q->front=(q->front+1)%MAX;
- for(j=0;j
arcnum;j++){ - if((g->arcs[i][j]==1)&&(!visited[j])){
- printf("%c",g->vexs[j]);
- visited[j]=1;
- q->data[q->rear]=j;
- q->rear=(q->rear+1)%MAX;
- }
- }
- }
- }
- void TBFS(MGraph *g){
- int i;
- for(i=0;i
vexnum;i++){ - if(visited[i]!=1){
- BFS(i,g);
- }
- }
- }