定义
图是由有穷非空顶点集合和顶点之间关系——边(或者弧)的集合组成,其形式化定义为:
G=(V,E)
V={ vi | vi ∈ dataobject}
E={ (vi, vj) | vi, vj ∈V 且P(vi, vj) }
其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合,集合E中P(vi,vj)表示顶点vi和顶点vj之间有一条直接连线。
2.图的基本术语
(1)无向边、有向边
(2)无向图、有向图
(3)边、弧、弧头、弧尾
(4)无向完全图:任意两个顶点间都存在边,共有n(n-1)/2条边
(5)有向完全图:共有n(n-1)条边
(6)稠密图、稀疏图:相对而言
(7)顶点的度、入度、出度:TD(v) = ID(v) + OD(v)
(8) 权:与边或弧相关的数据信息
(9) 网图:边或弧上带权的图
(10) 路径、路径长度:路径上边的数目
(11) 回路、简单路径、简单回路:第一个顶点和最后一个顶点相同的路径成为回路或环;序列中顶点不重复出现的路径称为简单路径
(12) 子图
(13) 连通、连通图、连通分量:在无向图中,如果一个顶点vi到另一个顶点vj(i != j)有路径,则称顶点vi和vj是连通的;如果图中任意两个顶点间都有路径存在,则称该图是连通图;如果是非连通无向图,则非连通无向图的极大连通子图称为连通分量
(14) 强连通图、强连通分量:在有向图中,若图中任意一对顶点 vi和vj(i != j)均有从顶点vi到顶点vj的路径,也有从vi到vj的路径,则称该有向图为强连通图。如果是非强连通无向图,则非强连通无向图的极大连通子图称为强连通分量
(15) 生成树:连通图G的生成树,是包含G的全部n个顶点的一个极小连通子图,该极小连通子图必定包含且仅包含G的 (n-1)条边。对于生成树而言,如果在其中任意添加一条属于原图中的边,必定会产生回路。如果在生成树中减少任意一条边,则必然成为非连通的。生成树极小指连通所有点数的边数最少
(16) 生成森林:如果是一张非连通图,必然会有若干连通分量,由每个连通分量都能得到一个极小连通子图,即一个生成树。这些生成树就组成了一个非连通图的生成森林
存储结构
1. 邻接矩阵
数组表示法,用一维数组存储图中顶点信息,用二维数组(矩阵)存储图中各顶点之间的邻接关系。
矩阵的元素值为:
A
[
i
]
[
j
]
=
{
1
,
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
是
E
中
的
边
0
,
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
不
是
E
中
的
边
A[i][j] =\begin{cases} 1, (v_i, v_j)或<v_i,v_j>是E中的边\\0, (v_i, v_j)或<v_i,v_j>不是E中的边 \end{cases}
A[i][j]={1,(vi,vj)或<vi,vj>是E中的边0,(vi,vj)或<vi,vj>不是E中的边
若G是带权图(网),则邻接矩阵可以定义为:
A
[
i
]
[
j
]
=
{
w
i
j
,
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
是
E
中
的
边
∞
,
(
v
i
,
v
j
)
或
<
v
i
,
v
j
>
不
是
E
中
的
边
A[i][j] =\begin{cases} w_{ij}, (v_i, v_j)或<v_i,v_j>是E中的边\\\infty, (v_i, v_j)或<v_i,v_j>不是E中的边 \end{cases}
A[i][j]={wij,(vi,vj)或<vi,vj>是E中的边∞,(vi,vj)或<vi,vj>不是E中的边
邻接矩阵存储方法特点:
(1)无向图的邻接矩阵必是一个对称矩阵,存储邻接矩阵时可采用压缩存储方式,只需存放上(或下)三角矩阵的元素即可;
(2)对于无向图(网),邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数是第i个顶点的度TD(vi)。
(3) 对于有向图(网),邻接矩阵的第i行非零元素(或非∞元素)的个数是第i个顶点的出度OD(vi)。
(4)对于有向图(网),邻接矩阵的第i列非零元素(或非∞元素)的个数是第i个顶点的入度ID(vi)。
(5)用邻接矩阵方法存储图(网),根据顶点序号检查矩阵对应值是否非零(或非∞元素)即可确定图(网)中任意两个顶点之间是否有边相连;若想确定图中共有多少条边,则必须按行或按列对矩阵中每个元素进行检测,看是否是非零(或非∞)元素,若是则计数一次。
具体结构描述:
#define MaxVerNum 100 /*最大顶点数设为100*/
typedef char VerType; /*顶点类型设为字符型*/
typedef int ArcType; /*边的权值设为整型*/
typedef struct {
VerType vexs[MaxVerNum]; /*顶点表*/
ArcType arcs[MaxVerNum][MaxVerNum]; /*邻接矩阵*/
int vexnum, arcnum; /*顶点数和边数*/
}MGragh; /*MGraph是以邻接矩阵存储的图类型*/
建立有向图G的邻接矩阵:
void CreateMGraph(MGraph *G){ /*建立有向图G的邻接矩阵*/
int i,j,k,w;
char ch;
scanf("%d,%d",&(G->vexnum),&(G->arcnum)); /*输入顶点数和边数*/
for (i=0;i<G-> vexnum;i++)
scanf("%c",&(G->vexs[i])); /*输入顶点信息*/
for (i=0;i<G-> vexnum;i++)
for (j=0;j<G-> vexnum;j++)
G->arcs[i][j]=0; /*初始化邻接矩阵*/
for (k=0;k<G-> arcnum;k++){
scanf("%d,%d",&i,&j); /*输入每条边对应的两个顶点的序号(输入格式为:i,j)*/
G->arcs[i][j]=1; /*若加入G->arcs[j][i]=1,则为无向图的邻接矩阵存储建立*/
}
}
用邻接矩阵来存储图时,算法时间复杂度只与图中顶点数n相关,与边数e无关,故比较适合于稠密图。
邻接矩阵表示法空间需求一般为(n+n2)个空间,其中n代表顶点信息所占空间,n2代表邻接矩阵所占空间。
如果是建立一张带权图,则只需要将以上算法稍加改动,初始化邻接矩阵:当i=j时,令G->arcs[i][j]=0;当(i != j)时,令G->arcs[i][j]=∞;当i、j间存在边(弧)时,将G->arcs[i][j]=1;改为G->arcs[i][j]=权值即可。
2. 邻接表
邻接表(Adjacency List) 是一种将顺序存储与链式存储相结合的存储方法。
边表:为图中每个顶点都建立一个单链表,即对于图G中的每个顶点vi,将vi的所有邻接点vj都链在一个单链表里,该单链表称为顶点vi的邻接表。【链式存储结构】
顶点表:将所有顶点的邻接表表头集中放到一个一维数组中,两者一起就构成了图的邻接表结构。【顺序存储结构】
带权图(网),边表中还需要保存每条边(弧)的权值:
#define MaxVerNum 100 /*最大顶点数为100*/
typedef struct node{ /*边表结点*/
int adjvex; /*邻接点域*/
struct node *nextadj; /*指向下一个邻接点的指针域*/
/*若要表示边信息,则应增加一个数据域infotype info*/
}EdgeNode;
typedef struct vnode{ /*顶点表结点*/
VerType vertex; /*顶点域*/
EdgeNode *firstedge; /*边表头指针*/
}VerNode;
typedef VerNode AdjList[MaxVerNum];
typedef struct{
AdjList adjlist; /*邻接表*/
int vexnum, arcnum; /*顶点数和边数*/
}ALGraph;
void CreateALGraph(ALGraph *G){ /*建立有向图的邻接表*/
int i,j,k;
EdgeNode *s; /*边表结点指针*/
scanf("%d,%d", &(G->vexnum), &(G->arcnum)); /*输入顶点数和边数*/
for(i = 0; i < G->vexnum; i++){
scanf("%c", &(G->adjlist[i].vertex)); /*输入顶点信息*/
G->adjlist[i].firstedge = NULL;
}
for(k = 0; k < G->arcnum; k++){
scanf("%d,%d", &i, &j); /*输入边的信息*/
s = (E dgeNode*)malloc(sizeof(EdgeNode));
s->adjvex = j;
s->nextadj = G->adjlist[i].firstedge; /*使用头插法*/
G->adjlist[i].firstedge=s;
}
}
若无向图中有n个顶点、e条边,每条边在邻接表中会出现两次,故该邻接表需n个头结点和2e个表结点。在边稀疏(e<<n(n-1)/2)的情况下,邻接表方式比邻接矩阵节省存储空间。
在无向图的邻接表中,第i个链表中的结点数量即为顶点vi的度TD(vi);在有向图中,第i个链表中的结点个数代表的是顶点vi的出度OD(vi);若想求得vi的入度ID(vi),则必须遍历整个邻接表,找到所有链表中其邻接点域adjvex的值为i的结点的个数代表的就是就是顶点vi的入度,明显效率较低。
为了便于确定顶点的入度,可以建立一张逆邻接表,即对每个顶点vi 建立一个以vi为弧头的邻接点的链表,则第i个链表中的结点个数代表的就是顶点vi的入度ID(vi)。
3. 图的存储结构比较
(1) 存储表示的唯一性
当图中每个顶点的序号确定后,邻接矩阵表示法将是唯一的;而邻接表的表示法则不是唯一的,因为各边表结点的链接次序取决于建立邻接表的算法和边的输入次序。
(2) 空间复杂度
设图中顶点个数为n,边的数量为e,那么邻接矩阵的空间需求为O(n2),适合于边相对较多的稠密图;邻接表的空间需求为O(n+e),针对于边相对较少的稀疏图。
(3) 时间复杂度
求边的数目:邻接矩阵方式下必须检测整个矩阵,耗费的时间是O(n2);邻接表存储方式下只对每个边表的结点个数计数即可求得e,所耗费的时间是O(e+n),当e < n2时,采用邻接表更节省时间;
判定<vi,vj>是否是图的一条边:邻接矩阵方式下只需判定矩阵中的第i行第j列上的元素是否为零即可,时间复杂度为O(1);邻接表存储方式下需扫描第i个边表,最坏情况下时间复杂度为O(n)。
图的遍历
图的遍历是图的一种基本操作,指从图中的任一顶点出发,对图中的所有顶点访问一次且仅访问一次。
操作时要注意如下问题:
(1) 在图结构中,不能规定谁是首结点,图中可以从任意一个顶点出发来进行遍历操作;
(2) 如果是非连通图,那么从一个顶点出发,只能够访问到它所在的连通分量上的所有顶点,并不能访问完该图的所有结点,因此,遍历过程中需考虑要如何选取下一个出发点来访问图中其余的连通分量;
(3) 在图结构中,可能存在回路,那么在一个顶点被访问之后,很有可能沿回路又回到该顶点,由于不允许重复访问,需考虑如何在遍历操作中来避免此种情况的发生;
(4) 在图结构中,顶点间的关系复杂,一个顶点可以和其它多个顶点相连,当这个顶点被访问过后,如何选取下一个要访问的顶点。
1. 深度优先搜索
Depth_First Search,简记为DFS。从图中某个未被访问过的顶点v出发,首先访问该顶点,然后从v的所有未被访问的邻接点中选择某一个邻接点w访问,再从w的所有未被访问的邻接点中选择某一个邻接点x访问,依此类推,直至图中所有和v有路径相通的顶点都被访问过为止;若此时图中仍有顶点未被访问,则另选图中一个未曾被访问的顶点作为起始点,重复上述过程,直至图中所有顶点都被访问过为止。
原理:递归和栈。
遍历序列:
V1,V2,V4,V5
V1,V2,V4,V8
V1,V3,V6,V7
在遍历过程中一定要区分该顶点是否已被访问过,如果已被访问过就不可再次访问,解决方法为附设一访问标志数组visited[0…n-1]。
从图的某一点v出发,递归地进行深度优先搜索的算法:
void DFS(Graph G, int v ){ /*从第v个点出发递归深度优先遍历图G*/
visited[v] = TRUE;
Visit(v); /*访问第一个顶点*/
for(w = FirstAdjVex(G, v); w ; w = NextAdjVex(G ,v , w))
if (!visited[w]) DFS(G , w); /*对v中尚未访问的邻接点w进行递归调用DFS*/
}
以邻接表为存储结构对图G进行深度优先搜索:
void DFSAL(ALGraph *G , int j){ /*以j为出发点对邻接表存储的图G进行DFS搜索*/
EdgeNode *p;
visited[j] = TRUE; /*标记vj已访问*/
p = G->adjlist[j].firstedge; /*取vj边表的头指针*/
while(p){ /*依次搜索vj的邻接点va,va=p->adjvex*/
if(!visited[p->adjvex]) /*p->adjvex为结点下标*/
DFSAL(G, p->adjvex);
p = p->nextadj;
}
}
void DFSTraverseAL(ALGraph *G){ /*深度优先搜索以邻接表存储的图*/
int i;
for(i = 0; i < G->vexnum; i++)
visited[i] = FALSE; /*访问标志数组初始化*/
for(i = 0; i < G->vexnum; i++)
if(!visited[i]) DFSAL(G, i);
}
当用邻接矩阵来存储图的时侯,查找每个顶点的邻接点所需时间共为O(n2) ,其中n为顶点数;当以邻接表来存储图的时侯,查找每个顶点的邻接点所需时间共为O(e),其中e为无向图中边的数量或有向图中弧的数量,找到每个顶点所需时间共为O(n),其总的时间复杂度为O(n+e) 。
2. 广度优先搜索
Breadth_First Search,简记为BFS。从图中某顶点v出发,访问了顶点v后,再依次访问v的所有未曾访问过的邻接点,然后再分别从这些邻接点出发依次访问它们的所有未曾访问过的邻接点,遵循“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问的原则,至图中所有已被访问的顶点的邻接点都被访问到。若还有顶点未被访问,则另选一个未曾被访问的顶点作为新的出发点,重复上述过程,直至图中所有顶点都被访问到为止。
原理:非递归和队列。
遍历序列:
V1
V2,V3
V3,V4,V5
V4,V5,V6,V7
V5,V6,V7,V8
从图的某一顶点v出发,非递归地进行广度优先搜索:
void BFSTraver(Graph G , Status(*visit)(int v)){ /*按广度优先搜索非递归遍历图G,使用辅助队列Q和访问标志数组visited*/
for(v = 0; v < G.vexnum; ++v)
visited[v] = FALSE; /*访问标志数组初始化*/
Init_Que(Q); /*置空队列Q*/
if(!visited[v]){ /*v尚未访问*/
In_Que (Q,v); /*v入队列*/
while (!Empty_Que (Q)){
Out_Que(Q,u); /*队头元素出队并置为u*/
visited[u] = TRUE;
visit(u); /*访问u*/
for(w = FirstAdjVex(G,u); w; w = NextAdjVex(G,u,w))
if(!visited[w]) In_Que(Q,w);
}
}
}
void BFSM(MGraph *G, int m){
int i, j;
c_SeqQue Q;
Init_SeqQue (&Q);
printf("visit vertex:V%c\n",G->vexs[m]); /*访问出发点Vm*/
visited[m] = TRUE;
In_SeqQue(&Q , m); /*出发点Vm入队列*/
while(!Empty_SeqQue(&Q)){
i = Out_SeqQue(&Q); /*Vi出队列*/
for(j = 0;j < G->vexnum; j++) /*依次搜索Vi的邻接点Vj*/
if(G->arcs[i][j] == 1 && !visited[j]){ /*若Vj未访问*/
printf("visit vertex:V%c\n", G->vexs[j]); /*访问Vj */
visited[j] = TRUE;
In_SeqQue (&Q,j); /*访问过的Vj入队列*/
}
}
}
void BFSTraverM(MGraph *G){ /*广度优先遍历以邻接矩阵存储的图G*/
int i;
for(i = 0; i < G->vexnum; i++)
visited[i] = FALSE; /*访问标志向量初始化*/
for (i = 0; i < G->vexnum; i++)
if(!visited[i]) BFSM(G, i); /* vi未访问过,从vi开始BFS搜索*/
}
当以邻接矩阵来存储图的时侯,每个顶点入队所需时间共为O(n),查找每个顶点的邻接点所需时间共为O(n2),其总的时间复杂度为O(n2);
当以邻接表来存储图的时侯,找到每个顶点所需时间共为O(n),查找每个顶点的邻接点所需时间共为O(e),其总的时间复杂度为O(n+e) 。
广度优先搜索和深度优先搜索不同之处在于搜索策略不同导致对顶点访问的顺序不同。