图的定义和基本术语
图:G=( V , E ) Graph=(Vertex,Edge)
V:顶点(数据元素)的有穷非空集合;
E:边的有穷集合。
无向图:每条边都是无方向的
有向图:每条边都是有方向的

完全图:任意两个顶点都有一条边相连

稀疏图:有很少边或弧的图(e<n logn)
稠密图:有较多边或弧的图
网:边或弧带权的图
邻接:有边或弧相连的两个顶点之间的关系
存在(v1,v2),则称v1和v2互为邻接点;
存在<v1,v2>,则称v1邻接到v2,v2邻接于v1。
关联(依附):边或弧与顶点之间的关系;存在(v1,v2)或<v1,v2>,则称该边或弧关联于v1和v2
顶点的度:与该顶点相关联的边的数目,记为TD(v)
在有向图中,顶点的度等于该顶点的入度与出度之和;
顶点v的入度是以v为终点的有向边的条数,记作ID(v);
顶点v的出度是以v为始点的有向边的条数,记作OD(v)。
路径:接续的边构成的顶点序列
路径长度:路径上的边或弧的数目或权值之和
回路(环):第一个顶点和最后一个顶点相同的路径
简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的回路

连通图(强连通图):在无(有)向图G=(V,E)中,若对任何两个顶点v、u都存在从v到u的路径,则称G是连通图(强连通图)。

子图:

连通分量:无向图G的极大连通子图·称为G的连通分量。
极大连通子图的意思是:该子图是G的连通子图,将G的任何不在该子图中的顶点加入,子图不再连通。

强连通分量:有向图G的极大强连通子图称为G的强连通分量。
极大强连通子图的意思是:该子图是G的强连通子图,将G的任何不在该子图中的顶点加入,子图就不再是强连通的。

任何连通图的连通分量只有一个,那就是它本身!
极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边,该子图不再连通。
生成树:包含无向图G所有顶点的极小连通子图

生成树林:对非连通图,由各个连通分量的生成树组成的集合
图的类型定义
图的抽象数据类型定义

图的基本操作


图的存储结构
由于图的逻辑结构是多对多,所以图没有顺序存储结构,但可以借助二维数组来表示元素间的关系,这就是数组表示法,也称为邻接矩阵。
图最常用的是链式存储结构,包括邻接表、邻接多重表和十字链表;
重点学习邻接矩阵(数组表示法)和邻接表(链式表示法)。
邻接矩阵
数组(邻接矩阵表示法)
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间的关系)
设图A=(V,E)有n个顶点,则有:

图的邻接矩阵是一个二维数组A.arcs[n][n],定义为:

无向图的邻接矩阵表示法

无向图的邻接矩阵关于主对角线对称,且主对角线上的元素都为0;
顶点i的度=第i行(列)中1的个数
有向图的邻接矩阵表示法

有向图的邻接矩阵可能是不对称的
顶点i的出度=第i行元素之和
顶点i的入度=第i列元素之和
顶点的度=第i行元素之和+第i列元素之和
网(有权图)的邻接矩阵表示法

邻接矩阵的存储表示
用两个数组分别存储顶点表和邻接矩阵
#define MaxInt 32767//表示极大值
#define MVNum 100//最大顶点数
typedef char VerTexType;//设顶点的数据类型为字符型
typedef int ArcType;//设边的权值类型为整型
typedef struct{
VerTexType vexs[MVNum];//顶点表
ArcType arcs[MVNum][MVNum];//邻接矩阵
int vexnum,arcnum;//图的当前点数和边数
}AMGraph;//Adjacency Matrix Graph
用邻接矩阵创建无向网
算法思路:
输入总顶点数和总边数
依次输入点的信息存入顶点表中
初始化邻接矩阵,使每个边的权值初始化为极大值
构造邻接矩阵
Status CreateUDN(AMGraph &G){
cin>>G.vexnum>>G.arcnum;//输入总顶点数和总边数
for(i=0;i<G.vexnum;i++)
cin>>G.vexs[i];//依次输入点的信息
for(i=0;i<G.vexnum;i++)//初始化邻接矩阵
for(j=0;j<G.vexnum;j++)
G.arcs[i][j]=MaxInt;//边的权值均置为极大值
for(k=0;k<G.vexnum;k++){//构造邻接矩阵
cin>>v1>>v2>>w;//输入一条边所依附的顶点及边的权值
i=LocateVex(G,v1);
j=LocateVex(G,v2);//确定v1和v2在G中的位置
G.arcs[i][j]=w;//边<v1,v2>的权值置为w
G.arcs[j][i]=G.arcs[i][j];//置<v1,v2>的对称边<v2,v1>的权值为w
}
return OK;
}
int LocateVex(AMGraph G,VertexType u){//在图G中查找顶点u,存在则返回顶点表中的下标,否则返回-1
int i;
for(i=0;i<G.vexnum;i++)
if(u==G.vexs[i]) return i;
return -1;
}
延申

邻接矩阵的优缺点
优点
直观、简单、好理解
方便检查任意一对顶点间是否存在边
方便找任一顶点的所有邻接点
方便计算任一顶点的度
缺点
不便于增加和删除顶点
浪费空间(对稀疏图来说)
浪费时间(统计稀疏图的边数时)
邻接表
邻接表表示法(链式)

顶点:按编号顺序将顶点数据存储在一维数组中
关联同一顶点的边(以顶点为尾的弧):用单链表存储

头结点
data:存储顶点数据
firstarc:与该顶点邻接的任一顶点在一维数组中的下标
表结点(边表)
adjvex(邻接点域):存放与头结点邻接的其他顶点在表头数组中的位置
nextarc(链域):指示下一条边或弧
info:存储和边相关的数据
无向图

邻接表不唯一
若无向图中有n个顶点、e条边,则其邻接表需n个头结点和2e个表结点,适合存储稀疏图
无向图中顶点vi的度为第i个单链表中的结点数
有向图

有向图的邻接表的特点
顶点vi的出度为第i个单链表中的结点个数
顶点vi的入度为第i个单链表中邻接点域值是i-1的结点个数
有向图的逆邻接表的特点
顶点vi的入度为第i个单链表中的结点个数
顶点vi的出度为整个单链表中邻接点域值是i-1的结点个数
当邻接表的存储结构形成后,图便we唯一确定!
邻接表的存储表示
#define MVNum 100//最大顶点数
typedef struct ArcNode{//边结点的结构定义
int adjvex;//该边所指向的顶点的位置
struct ArcNode *nextarc;//指向下一条边的指针
OtherInfo info;//和边相关的权值或其他信息
typedef struct VNode{//顶点的结构定义
VertexType data;//顶点信息
ArcNode *firstarc;//指向第一条依附该顶点的边的指针
}VNode AdjList[MVNum];//AdjList表示邻接表类型
typedef struct{//图的结构定义
AdjList vertices;
int vexnum,arcnum;//图当前的顶点数和弧数
}ALGraph;
ALGraph G;//定义了邻接表表示的图G
G.vexnum=5;G.arcnum=5;//图G包含了5个顶点,5条边
G.vertices[1].data='b';//图G中第二个顶点是b
p=G.vertices[1].firstarc;//指针p指向顶点b的第一条边结点
p->adjvex=4;//p指针所指边结点是到下标为4的结点的边
用邻接表表示法创建无向网
算法思路
输入总顶点数和总边数
建立顶点表:依次输入点的信息存入顶点表中,使每个表头结点的指针域初始化为NULL
创建邻接表:依次输入每条边依附的两个顶点;确定两个顶底的序号i和j,建立边结点;将此边结点分别插入到vi和vj对应的两个边链表的头部
Status CreateUDG(ALGraph &G){//采用邻接表表示法,创建无向图G
cin>>G.vexnum>>G.arcnum;//输入总顶点数和总边数
for(i=0;i<G.vexnum;i++){//构造表头结点表
cin>>G.vertices[i].data;//输入顶点值
G.vertices[i].firstarcNULL;//将所有表头结点的指针域置空
}
for(k=0;k<G.arcnum;k++){//构造邻接表
cin>>v1>>v2;//输入一条边依附的两个顶点
i=LocateVex(G,v1);
j=LocateVex(G,v2);
p1=new ArcNode;//生成一个新的边结点p1
p1->adjvex=j;//邻接点1序号为j
p1->nextarc=G.vertices[i].firstarc;
G.vertices[i].firstarc=p1;//将结点p1插入顶点vi的边表头部
p2=new ArcNode;
p2->adjvex=i;
p2->nextarc=G.vertices[j].firstrc;
G.vertices[j]=firstarc=p2;//将新结点p2插入顶点vj的边表头部
}
return OK;
}
邻接矩阵与邻接表表示法的关系
联系:邻接表中每个链表对应邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。
区别:
对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关);
邻接矩阵的空间复杂度为O(n2),而邻接表的空间复杂度为O(n+e)。
用途:邻接矩阵多用于稠密图,而邻接表多用于稀疏图。

十字链表
作为有向图的另一种链式存储结构,十字链表其实是将有向图的邻接表和逆邻接表结合形成的链表。
有向图中的每一条弧对应十字链表中的一个弧结点,有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点。

邻接多重表



图的遍历
遍历定义:从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。
遍历实质:找每个顶点的邻接点的过程。
避免重复访问的方法:设置辅助数组,用来标记每个被访问过的顶点,防止被多次访问。
图常用的遍历:
深度优先搜索(Depth_First Search——DFS)(用栈辅助)
广度优先搜索(Bread_First Search——BFS)(用队列辅助)
深度优先搜索遍历 DFS
方法:


邻接矩阵表示的无向图深度遍历实现:

对邻接矩阵表示的无向图,确定遍历起点后,遍历次序只有一种。
算法实现:
void DFS(AMGraph G,int v){//图G为邻接矩阵类型
cout<<v;
visited[v]=true;//访问第v个结点
for(w=0;w<G.vexnum;w++)//依次检查邻接矩阵中v所在行
if((G.arcs[v][w]!=0)&&(!visited[w]))
DFS(G,w);//w是v的邻接点,若w未访问,则递归调用DFS,一次深度搜索到尽头后,一层递归结束,就返回上一层递归
}
算法效率分析:

广度优先搜索遍历 BFS
方法:从图的某一结点出发,首先依次访问该结点的所有邻接顶点,再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点;重复这个过程,直到所有顶点均被访问为止。


用队列暂存访问的顶点,当该顶点的所有邻接顶点都被访问完了,该顶点就出队。
算法实现
void BFS(Grapg G,int v){//按广度优先非递归遍历连通图G
cout<<v;
visited[v]=true;访问第v个顶点
InitQueue(Q);//初始化辅助队列
EnQueue(Q,v);//v进队
while(!QueueEmpty(Q)){//队列非空
DeQueue(Q,u);//队头元素出队并置为u
for(w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w))
if(!visited[w]){//w为u的尚未访问的邻接结点
cout<<w;
visited[w]=true;
EnQueue(Q.w);//w进队
}
}
}
算法效率分析

图的应用
最小生成树
生成树概念回顾
生成树:所有顶点均由边连接在一起,但不存在回路的图。
无向图(左一)和它的生成树:

生成树的共同特点:
生成树的顶点个数与图的顶点个数相同;
生成树是图的极小连通子图,去掉一条边就不连通了;
一个有n个顶点的连通图的生成树有n-1条边,含n个顶点n-1条边的图不一定是生成树;
在生成树中再加一条边必然形成回路;
生成树中任意两个顶点间的路径是唯一的.
无向图的生成树:和无向图遍历的原理一样,分为深度优先生成树和广度优先生成树。
最小生成树
概念
给定一个无向网,在该网的所有生成树中,使得各边权值之和最小的那颗生成树称为该网的最小生成树,也叫最小代价生成树。

构造最小生成树Minimum Spanning Tree
MST性质
设N=(V,E)是一个连通网,U是顶点集V的一个非空子集,若边(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一颗包含(u,v)的最小生成树。

解释:

构造方法1:普里姆算法(Prim)

构造方法2:克鲁斯卡尔算法(Kruskal)

两种算法的比较

最短路径
典型用途:交通网络问题
交通网络用有向网来表示:
顶点:表示地点
弧:表示两个地点有路连通
弧上的权值:表示两地点之间的距离、交通费或旅途中所花费的时间等
求A地到B地的运输时间最短或运费最省就是两个地点的最短路径问题。
所以要在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值最小的路径,即为最短路径。
常见问题
第一类问题:两点间最短路径

用Dijkstra算法(迪杰斯特拉)。
第二类问题:某源点到其他各点最短路径

用Floyd算法(弗洛伊德)。
Dijstra算法

Floyd算法
确定好源点和终点,两点的直接路径为当前最短路径
在两个顶点间逐个加入其他顶点,若路径变短,则修改当前最短路径,否则当前最短路径不变
当所有顶点都试探过后,算法结束。
有向无环图及其应用
有向无环图:没有回路的有向图,简称DAG图(Directed Acycline Graph)

应用场景:用来描述一个工程或系统的进行过程。
分类:
AOV网:用顶点表示活动,用弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network).
AOE网:以弧表示活动,以顶点表示活动的开始或结束事件,称这种有向图为边表示活动的网,简称为AOE网(Activity On Edge network).
拓扑排序


检查AOV网中是否存在环的方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。
关键路径
在关键路径中,把工程计划表示为边表示活动的网络,即AOE网,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。
事件表示在它之前的活动已经完成,在它之后的活动可以开始。


对于AOE网,我们要探究两个问题:
完成整项工程至少需要花多少时间?
影响工程进度的关键因素是什么?
a.关键路径:路径长度最长的路径。
b.路径长度:路径上各活动持续时间之和。
习题
课本习题
(5) G 是一个非连通无向图,共有28 条边,则该图至少有( )个顶点。
A. 7 B . 8 C. 9 D . 10
答案:C
解析:对于一个n个顶点的有向图,至多有n*(n-1)条边,而对于无向图,则至多有n*(n-1)/2条,因为G是一个非连通的图,所以至少要有一个顶点不和其他顶点相连,所以有(8+1)=9个顶点。
期末题库
一个图中包含k个连通分量,若按深度优先(DFS)搜索方法访问所有结点,则必须调用( )次深度优先遍历算法。 | A. k | B. 1 | C. k-1 | D. k+1 |
答案:A
解析:因为各个连通分量之间互不连通,所以要分别对每一个连通分量调用FDS算法,因此对于k个连通分量,要调用k次。
下面关于图的存储的叙述中,哪一个是正确的。( ) | A. 用相邻矩阵法存储图,占用的存储空间数只与图中结点个数有关,而与边数无关 | B. 用相邻矩阵法存储图,占用的存储空间数只与图中边数有关,而与结点个数无关 | C. 用邻接表法存储图,占用的存储空间数只与图中结点个数有关,而与边数无关 | D. 用邻接表法存储图,占用的存储空间数只与图中边数有关,而与结点个数无关 |
答案:A
解析:
A、B:用相邻矩阵法存储图,如果有k个结点,就需要开辟出大小为k的一维数组和k*k的二维数组,因此占用的存储空间和图中结点个数有关;无论图中有多少条边,二维数组里必然要填满1或0,所以存储空间和边数无关,A正确,B错误。
C、D:用邻接表法存储图,如果有x个结点和y条边,就要开辟出大小为k的一维数组和大小至少为y、至多为2y的链表结点空间,所以存储空间既和结点个数有关,又和边数有关,C和D都是错的。
带权有向图G用邻接矩阵A存储,则顶点i的入度等于A中( )。 | A. 第i行非∞的元素之和 | B. 第i列非∞的元素之和 | C. 第i行非∞且非0的元素个数 | D. 第i列非∞且非0的元素个数 |
答案:D
解析:带权有向图的邻接矩阵中,非0和∞的数字表示两点间边的权值。入度计算列,出度计算行,因此v i的入度等于邻接矩阵中第 i 列非 ∞ 且非0的元素个数。
假设一个有n个顶点和e条弧的有向图用邻接表表示,则删除与某个顶点vi相关的所有弧的时间复杂度是( B)。 | A. O(n) | B. O(e) | C. O(n+e) | D. O(n*e) |
答案:C
解析:要找到所有和顶点vi相关的弧并删除,需要先遍历邻接表的所有顶点,再遍历每个顶点的边,检查是否与顶点vi相连,所以这个操作的时间复杂度为O(n+e)。
下列说法不正确的是( )。 | A. 图的遍历是从给定的源点出发每一个顶点仅被访问一次 | B. 图的深度遍历不适用于有向图 | C. 遍历的基本算法有两种:深度遍历和广度遍历 | D. 图的深度遍历是一个递归过程 |
答案:C
解析:A、B、D都是对的,至于C,图的基本遍历算法有五种,分别是:先序遍历、中序遍历、后序遍历、深度遍历和广度遍历。
用有向无环图描述表达式(A+B)*((A+B)/A),至少需要顶点的数目为( )。 | A. 5 | B. 6 | C. 8 | D. 9 |
答案:A
解析:如下图所示,红色表示弃用。

具有n个顶点的无向图至少有( )条边。 | A. n | B. n(n-1) | C. 0 | D. n-1 |
答案:C
解析:n可以是1,或者这个无向图里一个包含多个顶点的连通子图也没有,就是一个个孤立的点。