数据结构-王道2017-第5章 图
1.图的基本概念
1)图的定义
图G由顶点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中定点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合。V={v1,v2,..,vn},用|V|表示图G中顶点的个数,也称为图G的阶,E={(u,v)| u ∈ V,v ∈ V},用|E|表示图G中边的条数。
注意:线性表可以是空表,树可以是空树,但图不可一世空图。就是说图中不能一个顶点也没有,图的顶点集一定非空,但边集E可以为空,此时图中只有顶点没有边
2)简单图:不存在重复边;不存在顶点到自身的边,则称图G为简单图
多重图:图G中两个结点之间的边数多于一条,又允许顶点通过同一条边与自己关联,则G为多重图。多重图的定义和简单图是相对的。
完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图,含有n个顶点的无向完全图又n(n-1)/2条边,含有n个顶点的有向完全图有n(n-1)条有向边
3)连通:在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的,若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图
无向图中的极大连通子图称为连通分量,如果一个图有n个顶点,并且有小于n-1条边,那么此图必是非连通图。
注意:极大连通子图要求该连通子图包含其所有的边;极小连通子图是既要保持图连通,又要使得边数最少的子图。
4)强连通图、强连通分量
在有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称该图为强连通图。有向图的极大强连通子图称为有向图的强连通分量。
5)生成树、生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图。如果图中定点数为n,则它的生成树含有n-1条边。对于生成树而言,看去他的一条边,则会变成非连通图,若加上一条边则会形成回路。在非连通图中,连通分量的生成树构成了非连通图的生成森林。
注意:包含无向图中全部顶点的极小连通子图,只有生成树满足条件,因为砍去生成树的任意一条边,图将不再连通。
6)定点的度、入度出度
每个顶点的度定义为以该顶点为一个端点的边的数目
对于无向图,顶点v的度是指依附于该顶点的边的条数,记为TD(v);无向图的全部顶点的度之和等于边数的两倍,这是因为每条边和两个顶点相关联。
对于有向图,顶点v的度分为入度和出度,入度是以顶点v为终点的有向边的数目,记为ID(v),出度是以顶点v为起点的有向边的数目,记为OD(v),顶点v的度等于出度和入度之和TD(v) = ID(v) +OD(v);
有向图的出度和入度之和相等并且等于边数。
7)边的权和网
在一个图中,每条边都可以标上具有某种含义的值,该数值称为该边的权值。这种边上带权值的图称为带权图,也称为网。
8)稠密图、稀疏图
边数很少的图称为稀疏图,反之,称之为稠密图。一般当图满足|E|<|V|*log|V|时,可以看做稀疏图
9)路径、路径长度和回路
顶点v1到v2的一条路径指的是顶点序列,路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为环或回路。如果一个图有n个顶点,并且有大于n-1条边,这个图一定有环。
10)简单路径、简单回路
在路径序列中,顶点不重复出现的路径称为简单路径、除了第一个和最后一个顶点之外,其余顶点不重复出现的回路称为简单回路。
11)距离
从顶点u出发到顶点v的最短路径若存在,则此路径的长度称作从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷.
12)有向树:有一个顶点的入度为0,其余顶点的入度均为1的有向图称为有向树。
2.图的存储及基本操作
所选存储方式应该适合于欲求解的问题,无论是无向图还是有向图,主要的存储方式都有两种:邻接矩阵和邻接表。前者属于图的顺序存储结构,后者属于图的链接存储结构。
1)邻接矩阵法
稠密图适合使用邻接矩阵的存储表示
2)邻接表法
当图为稀疏图时,使用邻接矩阵会浪费大量的空间。而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
对图G中的每个顶点vi建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边(对于有向图则是以顶点vi为尾的弧),这单链表称为vi的边表(对于有向图,则称为出边表),在邻接表中,存在两种结点:顶点表结点和边表结点。
#define MaxVertexNum 100 typedef struct ArcNode{ int adjvex; //弧指向的邻接点的位置---顶点数组下标 struct ArcNode * next; //指向下一条边的指针 //ElemType data; //网的边权值 }ArcNode; typedef struct VNode{ VertexType data; //顶点信息 ArcNode * first; //指向边表第一个结点 }VNode,AdjList[MaxVertexNum]; typedef struct { AdjList vertices; //邻接表 int vexnum,arcnum; //图的顶点数和弧数 }ALGraph; //ALGraph是以邻接表存储的图类型
a)邻接表特点
如果G为无向图,则所需存储空间为O(|V|+2|E|);如果G为有向图,则所需的存储空间为O(|V|+|E|),无向图中,每条边会出现两次
邻接表中,给定一个顶点很容易查到它的所有邻边,在邻接矩阵中需要扫描一行,时间为O(n),如果要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立即查到,在邻接表中则需要在相应结点中查找另一结点,效率较低。
3)十字链表
十字链表是有向图的一种链式存储结构,在十字链表中,对应于每条弧有一个结点,对应于每个顶点也有一个结点。
#define MaxV 100 //本质上也是邻接表 typedef struct ArcNode{ //边表结点 int tailVex,headVex; //弧的头尾结点 struct ArcNode * hlink,*tlink; //分别指向弧头相同和弧尾相同的结点 // ElemType data; //相关信息 }ArcNode; typedef struct VNode{ VertexType data; //顶点信息 ArcNode *firstin,*firstout; //指向第一条入弧和出弧 }VNode; typedef struct{ VNode xList[MaxV]; //十字链表 int vexnum,arcnum; //图的顶点数和边数 }GLGraph; //以十字链表存储的图类型
在十字链表中,极容易找到vi为尾的弧,也容易找到以vi为头的弧,因而容易求出顶点的出度和入度。
图的十字链表是不唯一的,但一个十字链表表示确定一个图。
4)邻接多重表
邻接多重表是无向图的另一种链式存储结构,与十字链表类似,每一条边用一个结点表示
mark | ivex | ilink | jvex | jlink | info
mark 为标志域,标记该条边是否被搜索过;ivex和jvex分别表示该边依附的两个顶点,ilink指向下一条依附于ivex的边,jlink指向下一条依附于顶点jvex的边,info为指向和边相关的各种信息的指针域。
每一个顶点也用一个结点表示,由如下所示的两个域组成
data | firstedge
每一条边只有一个结点
#define MaxVertexNum 100 typedef struct ArcNode{ bool mark; //访问标记 int ivex,jvex; //分别指向该弧的两个结点 struct ArcNode *ilink,*jlink; //分别指向两个顶点的下一条边 //InfoType info; //相关信息 }ArcNode; typedef struct VNode{ //顶点表结点 VertexType data; //顶点信息 ArcNode * firstedge; //指向第一条依附在该顶点的边 }VNode; typedef struct{ VNode adjmuList[MaxVertexNum]; //邻接表 int vexnum,arcnum; //图的顶点数和弧数 }AMLGraph;
3.图的遍历
图的遍历是指从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中的顶点访问一次且仅访问一次。注意到树是一种特殊的图,所以树的遍历也可以看做是一种特殊的图的遍历。两种算法:广度优先搜索和深度优先搜索。
1) 广度优先搜索(Breadth-First-Search,BFS)
类似于二叉树的层序遍历算法
#define MAX_N 100 bool visited[MAX_N]; void BFSTraverse(Graph G,){ for(int i = 0;i < G.vnum;i++) visited[i] = false; InitQueue(Q); for(int i = 0;i < G.vnum;i++) if(!visited[i]) BFS(G,i); } void BFS(Graph G, int v){ visit(v); visited[v] = true; EnQueue(Q,v); while(!Empty(Q)){ DeQueue(Q,v); //顶点v出列 for(w=neighbor(G,v);w>=0;w=nextNeighbor(G,v,w)){
if(!visited[w]){
visit(w); visited[w] = true; EnQueue(Q,w);
}
}
}
}
性能分析:无论是邻接表还是邻接矩阵的存储方式,BFS算法都需要借助一个辅助队列Q,n个顶点都需入队一次,在最坏的情况下,空间复杂度为O(|V|)
采用邻接表存储时,每个顶点均需搜索一次(或入队一次),故时间复杂度为O(|V|),在搜索任一顶点的邻接点时,每条边至少访问一次,故时间复杂度为O(|E|),算法总的时间复杂度为O(|V|+|E|).当采用邻接表存储时,查找每个顶点的邻接点所需时间为O(|V|),故算法总的时间复杂度为O(|V|^2)。
可以使用BFS算法求解单源最短路径。
给定图的邻接矩阵存储表示是唯一的,故其广度优先生成树也是唯一的,但由于邻接表存储表示不唯一,故其广度优先生成树也是不唯一的。
2)深度优先搜索
类似于树的先序遍历
#define MAX_N 100 bool visited[MAX_N]; void DFSTraverse(Graph G){ for(int i = 0;i < G.vnum;i++){ visited[i] = false; } for(int i =0;i < G.vnum;i++){ //对于每个连通分量进行遍历 if(!visited[i]) DFS(G,i); } } void DFS(Graph G, int v){ visit(v); visited[v] = true; //已经访问 for(w=neighbor(G,v);w >= 0;w=nextneighbor(G,v,w)){ if(!visited[w]) //尚未访问结点 DFS(G,w); } }
注意:同一个图基于邻接矩阵的遍历所得到的DFS序列和BFS序列是唯一的,基于邻接表的遍历所得到的bfs和dfs是不唯一的(因为邻接矩阵唯一,而邻接表表示不唯一)
DFS算法的性能分析:
是一个递归算法,需要借助递归工作栈,空间复杂度为O(|V|);遍历图的过程实质上是对每个顶点查找其邻接点的过程,其耗费的时间取决于采用的存储结构。以邻接矩阵表示,查找每个顶点的邻接点的所需时间为O(|V|),故总的时间复杂度为O(|V|^2).以邻接表表示时,查找所有顶点的邻接点所需时间为O(|E|),访问顶点所需时间为O(|V|),总的时间复杂度为O(|V|+|E|);