8.26--9.2
第六章 图
目录
6.1 图的基本概念
一、图的定义
图G由顶点集f和边集E组成,记为G = (V,E),V(G)表示图G中顶点的有限非空集;
E(G)表示图中顶点之间的关系(边)的集合,用|V|表示图中顶点的个数,
E={(u,v)|u∈V,v∈V},|E|表示图中边的条数
注:图不可以为空(顶点不能为空,但是边集可以为空)
1.有向图
<v,w>成为从v到w的弧,也成v邻接到w,v是弧头,w是弧尾
2.无向图
G=(V,E) V = {1,2,3,4}
E = {(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)}
3.简单图、多重图
简单图:不存在重复边,不存在顶点到自身的边;图6.1中(a)(b)
(重复边是指从一个顶点到另一个顶点存在多个方向相同的边)
多重图:图中某两个顶点之间的边数>1又允许顶点通过一条边和自身关联
4.完全图:
对于无向图:任意两个顶点之间都存在边;边总数:n(n-1)/2
(无向图中|E|的取值范围:0~n(n-1)/2 )
对于有向图:有n(n-1)条弧的有向图成为有向完全图;在有向完全图中任意两个顶点之间都存在方向相反的两条弧。
5.子图
设有两个图G=(V,E)和G'=(V,E),若V是V的子集,且E是E的子集,则称G'是G的子圈。
若有满足V(G')=V(G)的子图G',则称其为G的生成子图。图6.1中G,为G的子图。
注意:并非V和E的任何子集都能构成G的子图,因为这样的子集可能不是图,
即E的子集中的某些边关联的顶点可能不在这个V的子集中。
6.连通,连通图,连通分量
无向图中,从顶点v到顶点w有路径存在,说明这两个点连通。
若图G中任意两个顶点都连通,则称图G为连通图;否则是非连通图。 无向图中的极大连通子图称为连通分量。
假设一个图顶点为n;若边数<n-1则该图一定是非连通的。
7.强连通图,强连通分量
在有向图中,若有一对顶点从v到w与从w到v都有路径,成为这两个点强连通;
若图中任意一对顶点都是强连通的,则称该图为强连通图;
有向图中的极大强连通子图成为有向图的强连通分量。
有向强连通情况下边最少为n,构成一个环路
8.生成树、生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图。 去掉一条边就会变成非连通图,加上一条边就会形成一个回路。
若顶点个数为n,则其生成树有n-1条边。
注:区分极大连通子图和极小连通子图。极大连通子图是无向图的连通分量。
极大即要求该连通子图包含其所有的边;极小连通子图是既要保持图连通又要使得边数最少的子图
9.顶点的度、入度、出度
无向图中 顶点的度指依附于该顶点的边数TD(v);无向图全部顶点的度数和=边数的2倍
有向图中 顶点的入度指以该点为终点的边数ID(v);出度指以该点为起点的边数OD(v)
有向图全部顶点的入度和=出度和=边数
10.边的权和网
11.稠密图,稀疏图
当图G满足|E| < |V|log|V|时将G视为稀疏图
12.路径、路径长度、回路
13.简单路径、简单回路
14.距离
从顶点u触发到顶点v的最短路径,若存在,则此路径的长度为从u到v的距离,若不存在路径,则记该距离为无穷
15.有向树
一个顶底入度为0,其余顶点的入度均为1的有向图。
在具有n个顶点e条边的无向图中有 ∑TD(vi)=2e,无向图中所有顶点度数之和=2倍的边数
课后题:
6.2 图的存储与基本操作
6.2.1 邻接矩阵法
用一个一维数组存储图中顶点信息,用二维数组存储图中边的信息(邻接矩阵)
结点数为n的邻接矩阵A是n*n的,G顶点编号为v1,v2……若(vi,vj)∈E,则A[i][j] = 1,否则为0
对于带权图而言,若顶点V,.和巧之间有边相连,则邻接矩阵中对应项存放着该边对应的权值
(本来想写个完整实现的图的所有功能的,但是发现要好多时间,而且好像没必要,放弃了)
//邻接矩阵
#include<stdio.h>
#include<stdlib.h>
#define maxsize 10 //顶点最大数为100
typedef char VertexType; //顶点数据类型
typedef int EdgeTpye; //权值类型
typedef struct{
char Vex[maxsize]; //顶点表
int Edge[maxsize][maxsize]; //边表
int vexnum,arcnum; //图当前顶点数和弧数
}Mgraph;
void Init(Mgraph &G){
printf(" ");
for(int i=0;i<maxsize;i++){
G.Vex[i] = 1;
printf("%c ", G.Vex[i]);
}printf("\n");
for(int i=0;i<maxsize;i++){
printf("%c ", G.Vex[i]);
for(int j=0;j<maxsize;j++){
G.Edge[i][j] = -1;
printf("%d ", G.Edge[i][j]);
}
printf("\n");
}
G.vexnum = G.arcnum = 0;
}
bool change(Mgraph &G){
int row=0;
int column = 0;
int weight = 0;
if(row>=maxsize || column >= maxsize){
return false;
}
printf("请输入要修改权值的行、列:");
scanf("%d", &row);
scanf("%d", &column);
printf("\n请输入要修改的权值:");
scanf("%d", &weight);
G.Edge[row][column] = G.Edge[column][row] = weight;
return true;
}
/*typedef struct{
char Vex[maxsize]; //顶点表
int Edge[maxsize][maxsize]; //边表
int vexnum,arcnum; //图当前顶点数和弧数
}Mgraph; */
bool addVex(Mgraph &G){//添加顶点
char V;
int num;
printf("请输入要添加的结点个数:");
scanf("%d", &num);
while(num>0){
if(G.vexnum >=maxsize){
return false;
}
printf("请输入要创建的顶点名:");
scanf(" %c", &V);
for(int i=0;i<G.vexnum;i++){
if(V == G.Vex[i]){
printf("名称重复");
return false;
}
}
G.Vex[G.vexnum] = V;
G.vexnum++;
for(int i=0;i<G.vexnum;i++){
for(int j=0;j<G.vexnum;j++){
G.Edge[i][j] = 0;
}
}
num--;
}
return true;
}
bool removeVex(Mgraph &G){//删除顶点
char V;
printf("请输入要删除的顶点:");
scanf(" %c", &V);
for(int i=0;i<G.vexnum;i++){
if(V == G.Vex[i]){//如果找到了对应顶点 ,则用后面的顶点覆盖这个位置的点
for(int j=i;j<G.vexnum-i;j++){
G.Vex[j] = G.Vex[j+1];
}
//同时还要删除对应的行列
break;//已经找到目标结点,中断外层循环
}
else{
if(i == G.vexnum-1)//如果找到了最后一个点还不是,则返回false
printf("未找到指定顶点");
return false;
}
}
return true;
}
bool addEdge(Mgraph &G){
char V1,V2;
printf("\n检查两个顶点之间是否存在边\n");
printf("请输入要检查的两个顶点:");
scanf(" %c %c", &V1, &V2);
//首先检查两个顶点是否存在
for(int i=0;i<G.vexnum;i++){
if(V1==G.vexnum){
break;
}
}
//其次检查两个顶点之间是否存在边(对应位置的值是0还是1)
// 如果不存在,则将值修改为1
}
int main(){
Mgraph G;
Init(G);
printf("\n");
//添加顶点
if(addVex(G)){
printf("添加成功\n");
}else{printf("添加失败\n");
}
//删除顶点
if(removeVex(G)){
printf("删除成功\n");
} else{printf("删除失败\n");
}
printf(" ");
for(int i=0;i<G.vexnum;i++){
printf("%c ", G.Vex[i]);
}printf("\n");
for(int i=0;i<G.vexnum;i++){
printf("%c ", G.Vex[i]);
for(int j=0;j<G.vexnum;j++){
printf("%d ", G.Edge[i][j]);
}
printf("\n");
}
return 0;
}
邻接矩阵存储的特点:
①无向图的邻接矩阵一定是对称矩阵且唯一,可以只存储上或下三角矩阵
②对于无向图,邻接矩阵第i行或第i列的非0元素个数正好是点i的度TD(vi)
③对于有向图,邻接矩阵第i行非0元素个数是点i的出度OD(vi);;;第i列非0元素个数是入度ID(vi)
④用邻接矩阵存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
⑤稠密图适合邻接矩阵存储表示
⑥
注:①邻接矩阵表示法的空间复杂度为O(n^2),其中n为图的顶点数|V|.
②当邻接矩阵的元素仅表示相应边是否存在时,EdgeType可采用值为0和1的枚举类型
③无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩可采用压缩存储
④在简单应用中,可直接用二维数组作为图的邻接矩阵,顶点信息等均可省略
6.2.2 邻接表法
邻接表:为图G中每个顶点vi建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边,对于有向图是以顶点vi为尾的弧。这个单链表称为顶点vi的边表。(有向图中称为出边表)
边表的头指针和顶点的数据信息采用顺序存储(顶点表),在邻接表中存在两种结点:顶点表结点和边表结点。
邻接表存储方法的特点:
① 若G为无向图,所需的存储空间为O(|V|+2|E|);
若G为有向图,所需的存储空间为O(|V|+|E|);(每条边在无向图邻接表中出现了两次)
②稀疏图采用邻接表可以节省很大空间
③邻接表中,给定一顶点,很同意找到其所有邻边,只需要读取其邻接表;在邻接矩阵中该操作要扫描一整行,花费的时间为O(n)
但若要确定给定的两个顶点之间是否有边,在邻接矩阵中很容易查询到。而在邻接表中要在相应结点对应的便飙中查找另一个结点,效率很低
④在有向图邻接表中求一个给定顶点的出度只需要计算器邻接表中结点个数
但要求其入度要遍历全部的邻接表。(可采用逆邻接表的方法来加快求入度)
⑤图的邻接表不唯一,在每个顶点对应的单链表中各边结点的连接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。
#define maxsize 10 //顶点最大数
typedef struct ArcNode{ //边表结点
int adjvex; //该弧指向的顶点的位置
struct ArcNode *next; //指向下一条弧的指针
//InfoType info; //网的边权值
}ArcNode;
typedef struct VNode{
VertexType data; //顶点表结点
ArcNode *first; //顶点信息
}VNode,AdjList[maxsize]; //指向第一条依附该顶点的弧的指针
typedef struct{
AdjList vertices; //邻接表
int vexnum, arcnum; //图的顶点数和弧数
}ALGraph; //ALGraph是以邻接表存储的图类型
6.2.3 十字链表(有向图
十字链表是有向图的一种链式存储结构,在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点,结点的结构:
顶点结点之间顺序存储
在十字链表 中找到以Vi为尾的弧,与找到以Vi为头的弧都很容易,所以也容易求得顶点的入度和出度,十字链表表示不唯一,但一个十字链表唯一确定一个图
找入度:从该顶点结点的firstin开始找,再进入弧结点的hlink
找出度:从该顶点结点的firstout开始找,再进入弧结点的tlink
6.2.4 邻接多重表(无向图)
邻接表容易求顶点和边的信息,但是不易求两个顶点之间是否存在边。
每条边用一个结点表示:
每个顶点也用一个结点表示:
邻接多重表各种基本操作的实现与邻接表类似
邻接多重表与邻接表的区别:同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。
6.2.5 图的基本操作
1.Adjacent(G,x,y);
最好时间复杂度O(1):该顶点的下一个边结点即为要搜索的边
最坏时间复杂度O(|V|):该顶点的最后一个边界点是要搜索的边O(|V| - 1)
2.Neighbors(G, x); //列出图G与顶点x邻接的边
无向图:
最好时间复杂度O(1):该顶点只有一个边(邻接表)
最坏时间复杂度O(|V|):该顶点的边有V - 1条(邻接表);遍历整行n个顶点(邻接矩阵)
有向图:
最好时间复杂度O(1):出边:该顶点只有一个出边(邻接表)
最坏时间复杂度:遍历整行n个结点O(|V|)(邻接矩阵);入边:遍历全部边结点O(|E|)(邻接表)
3.InsertVertex(G, x); //在图中插入顶点x
时间复杂度为O(1):有向图、无向图、邻接矩阵、邻接表都一样(新顶点刚开始不连任何边)
4.DeleteVertex(G, x); //在图中删除顶点x
无向图:
最好时间复杂度O(1):该顶点没有相邻的边(邻接表)
最坏时间复杂度:删除该顶点的行和列O(|V|)(邻接矩阵);该顶点邻接了尽可能多的边,则需要遍历其他所有边O(|E|)(邻接表)
有向图:
邻接矩阵:删除行列O(|V|)
邻接表:
删出边:O(1)没有出边,O(|V|)
删入边:O(|E|)遍历所有边
5.AddEdge(G, x, y); //添加<x, y>或者(x, y)
邻接矩阵:O(1)顺序表随机存储特性
邻接表:O(1)头插法,O(|V|)尾插法
6.FirstNeighhbor(G, x); //求图G中顶点x的第一个邻接顶点
无向图:
邻接矩阵:遍历行,最好时间复杂度第一个O(1),最坏时间复杂度最后一个或不存在O(|V|)
邻接表:顶点结点连着的第一个边结点O(1)
有向图:
邻接矩阵:出边行,入边列 O(1) ~ O(n)
邻接表:
入边O(1),指定节点的第一个边结点
出边O(1),遍历所有边O(1) ~ O(|E|)
7.NextNeighbor(G, x, y); //顶点y是图G中顶点x的一个邻接点,返回除y外的下一个邻接点
6.3 图的遍历
6.3.1 广度优先搜索(BFS)
基本思想:先访问图的顶点v,然后从v出发,一次访问v的各个未访问的邻接顶点,然后从这些邻接顶点出发依次访问它们各自的邻接顶点。
![](https://i-blog.csdnimg.cn/blog_migrate/8d9ddbaecaa7dc4db59176404e19fa9a.png)
//BFS
vool visited[maxsize]; //访问标记数组,用于标记顶点是否被访问过
void BFStraverse(Graph G){
for(int i=0;i<G.vexnum;++i){
visited[i] = false; //访问标记数组初始化
}
InitQueue(Q); //初始化辅助队列Q
for(int i=0;i<G.vexnum;++i){
if(!visited[i]){ //对每个连通分量调用一次BFS
BFS(G,i); //如果vi未被访问过,从vi开始执行BFS
}
}
}
void BFS(Graph G,int v){//从顶点v出发
visit(G); //访问初试顶点v
visited[v] = true; //对v做已访问标记
Enqueue(Q,v); //顶点v入队Q,
while(!isEmpty(Q)){
DeQueue(Q,v); //顶点v出队
for(w = FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
//检查v所有的邻接点
if(!visited[w]){ //w是v的未访问的邻接节点
visit(w); //访问顶点w
visited[w] = true; //对w做已访问标记
EnQueue(Q,w); //w入队
}
}
}
}
算法性能分析:
在邻接表与邻接矩阵存储方式下BFS都需要辅助队列Q,n个顶点都需要入队依次,最坏情况下时间复杂度为O(|V|);
采用邻接存储方式时:,算法总的时间复 杂度为O(|V|+|E|)。
每个顶点均需搜索一次(或入队一次),故时间复杂度为O(|V|),搜索任意一个顶点的邻接点时,每条边至少访问一次,故时间复杂度为O(|E|)
采用邻接矩阵存储时,算法总时间复杂度为O(|V|^2);查找每个顶点的邻接点所需时间为O(|V|)
邻接矩阵唯一→广度优先遍历序列唯一
邻接表不唯一→广度优先遍历序列不唯一
广度优先生成树:
广度遍历过程中会的到广度优先生成树
注: 同一个图邻接矩阵是唯一的,所以广度优先生成树也唯一
邻接表不唯一,所以其广度优先生成树也不唯一
6.3.2 深度优先生成树(DFS)
思想:
从起始顶点v出发,访问与v邻接且未被访问的任意一个顶点vi,若它还有邻接且未被访问的任意一个顶点,则继续向下访问,直到不能继续往下时。
退回至最近一个被访问结点w,若w还有邻接且未被访问的结点,则从此结点开始向下访问,否则继续退回
注:基于邻接矩阵得到的DFS和BFS序列唯一,基于邻接表得到的不唯一
2.算法性能分析
DFS是一个递归算法,需要递归工作栈辅助,空间复杂度为O(|V|).
邻接矩阵: 总时间复杂度为O(|V|^2) ,因为查找每个顶点的邻接点所需时间都是O(|V|)
邻接表:总时间复杂度O(|V|+|E|), 查找所有顶点的邻接表所需时间为O(|E|),访问顶点所需时间为O(|V|)
2.DFS生成树和生成森林
DFS只有对连通图调用时才会产生生成树,否则会产生生成森林
6.4图的应用
6.4.1 最小生成树
对于一个带权连通无向图G=(V,E),设R为G所有生成树的集合,T为R中边权值最小的那个生成树,则T称为G的最小生成树(MST)
最小生成树性质:
(1)最小生成树不唯一
(2)最小生成树的边的权值之和唯一,
(3)最小生成树的边数 = 顶点数-1
(4)假设G = (V,E)是一个带权连通无向图,U是顶点集V的一个非空子集,若(u,v)是一条具有Uzi最小权值的边,则必存在一棵包含边(u,v)的最小生成树。
prim算法和kruskal算法都基于贪心算法的策略
1.prim算法
prim算法构造最小生成树:从图中任意定点开始,加入树T,此时树中只含有一个顶点,之后U型安泽一个与当前T中顶点集合距离最近(权值最小)的顶点和相应的边加入T,每次操作后T中的顶点数和边数都+1
Prim算法时间复杂度为O(|V|^2),不依赖于边集|E|因此适用于边稠密图的最小生成树
2.kruskal 算法
按权值递增次序选择合适的边构成最小生成树
每次选择代价最小的一条边,使该边的两个顶点相通,但是,如果原本就相通的两个顶点的边就不选,直到所有顶点相通
共执行e轮,每轮判断两个结点是否属于同一个结点
算法性能:
通常在kruskal算法中采用堆来存放边的集合,因此每次选择最小权值的边只需要O(|E|log 2 |E|)的时间。适用于边稀疏而点较多的情况
6.4.2 最短路径
两类:单源最短路径(迪杰斯特拉算法);求每对顶点之间的最短路径(弗洛伊德算法)
1.迪杰斯特拉(Dijkstra算法)求单源最短路径问题(只有一个源头)
点优先;贪心算法(只选当前最优,不考虑全局)
适用于:带权图、无权图(无负权值)
第一次先选一个起点,从起点出发,看有哪些点与起点直接相连,如果有,则填入权值,没有则填无穷;第二次选一个已经找到最短路径的点,将其与起点一同考虑,如果可以通过这两个点找到其他点的最短路径,则也把找到最短路径的点纳入,一同考虑,直到找到所有点的最短路径。
循环遍历所有结点,找到还没确定最短路径且dist值最小的顶点Vi,令final[i] = true.
检查所有邻接自Vi的顶点,如果其final值为false(未找到最短路径),则更新dist和path信息。
重复上述操作直到所有final值全为true
算法时间复杂度:O(|V|^2)
2.弗洛伊德算法(Floyd)(带权图,无权图)
边优先;采用动态规划思想,进行递推
算法时间复杂度O(|V|^3)
3.对比:
6.4.3 有向无环图描述表达式
有向无环图(DAG图)一个有向图中不存在环
常用语描述含有公共子式的表达式:((a+b)*(b*(c+d)*e)*((c+d)*e)
6.4.4拓扑排序
AOV网:用DAG图表示一个工程,其顶点表示活动,有向边<V,W>表示活动V必须先于活动W进行
将这种有向图成为顶点表示活动的网络,记为AOV网
AOV网中任何活动都不能以它自己作为自己的前驱或后继,只能V是W的直接前驱,W是V的直接后继,这种关系具有传递性
拓扑排序:由一个DAG图的顶点组成的序列,当且仅当满足下列条件:
①每个顶点只出现一次②若顶点A在序列中排在顶点B之前,则不存在从顶点B到A的路径
对AOV网进行拓扑排序的步骤:
①从AOV网中选择一个没有前驱的顶点并输出
②从网中删除该顶点和所有以它为起点的有向边
![](https://i-blog.csdnimg.cn/blog_migrate/2a29189726cda4ad40fba9ffddabca9e.png)
bool TopologicalSort(Grapg G){
InitStack(S); //初始化栈,用于存储入度为0的顶点
int i;
for(i=0;i<G.vexnum;i++)
{
if(indegree[i]==0){
Push(S,i); //将所有入度为0的顶点进栈
}
}
int count = 0; //计数,记录当前已经输出的顶点数
while(!IsEmpty(S)){ //栈不为空,则说明存在入度为0的顶点
Pop(S,i); //栈顶元素出栈
print[count++]=i; //输出顶点i
for(p=G.vertices[i].firstarc;p;p=p->nextarc){
//将所有顶点i指向的顶点入度全部减1,并且将入度减为0的顶点入栈
v=p->adjvex;
if(!(--indegree[v])){
Push(S,v);}
}//while
if(count<G.vexnum){
return false; //排序失败,图中有回路
}else
return true; //排序成功
}
逆拓扑序列:
①从AOV网中选择一个没有后继(出度为0)的顶点并输出
②从网中删除该顶点和所有以它为终点的有向边
③重复①②直到当前的AOV网为空