图的基本概念
点 边 无向图 有向图
简单图:没有多重边和环 注意数据结构中只讨论简单图 多重图
简单完全图:边数为(n-1)+(n-2)+…+1 = (n-1)*n/2 子图
-
无向图中:
连通(存在路径相连的两顶点)
连通图(任意两顶点连通)
极大连通子图(连通子图包含其所有的边)
极小连通子图 (保持图连通又要使得边数最少的子图)
连通分量(无向图的极大连通子图称为连通分量) -
有向图中:
强连通(存在双向路径的两顶点)
强连通图(任意两点强连通)
极大强连通子图(连通子图包含其所有的边)
极小强连通子图(保持图强连通又要使边数最少的子图)
强连通分量:(有向图的极大连通子图)
生成树、生成森林
连通图的生成树是包含图中图中所有顶点的极小连通子图
非连通图的生成森林是所有连通分量的生成树构成的
顶点的度
无向图中顶点连接的边的总数为顶点的度
所有点的度的和为2m,m为图的边数(每条边提供2度)
有向图中以点为起点的边数为点的出度
以点为终点的边数为点的入度
入度之和(或出度之和)为边数(因为每条边提供1入度、出度)
边的权和网
在图中每条边都可以被赋予一个值指示确定的意义,我们称边被赋值的图为带权图(也称网),该数值称为边的权值
稠密图、稀疏图
边很少的图称为稀疏图,反之边很多的图称为稠密图,衡量的标准是一个边与点的关系
|E | <= |V|log|V|时称为稀疏图,否则称为稠密图(一个粗略的估计)
路径、路径长度和回路
一个顶点到一个顶点的路径为一个顶点序列{v_p,v1,v2,v3,v4,…,v_q}
其中相邻的两顶点之间要存在边
途经的边的个数称为路径的长度
第一个点和最后一个点相连的路径称为回路(不建议称为环,虽然在简单图中不至于引起歧义,但是在多重图中会与环的定义矛盾)
简单回路、简单路径
在路径序列中没有重复出现的点的路径称为简单路径。除第一个点和最后一个点外,其他点都不重复出现的回路称为简单回路
距离
最短路径的长度
有向树
一个顶点的入度为0(根节点)
其他节点的入度为1的图称之为有向树
图的存储和基本操作
邻接矩阵法
用一个一维数组存储顶点信息
用一个二维数组存储顶点之间的邻接关系
对于非带权图,二维数组用0和1来表示是够存在边
对于带权图,二位数组用来存储数据信息
#define MaxVertexNum 100
typedef char VertexType;
typedef int EdgeType;
typedef struct {
VertexType vertex[MaxVertexNum];
EdgeType Edge[VertexType][VertexType];
int vernum,arcnum;
}MGraph;
在无向非带权简单图中,我们设邻接矩阵为矩阵A 根据矩阵乘法的运算法则,A*A的i,j元表示从第i个点到第j个点经其他点周转一次的路径数目。考虑在5个顶点的图中A(3,2) = a(3,1) × a(1,2) + a(3,4) × a(4,2) + a(3,5) × a(5,2) ;考察每一个乘积项,如果第一项为1,则点3和点2有途径点1的路径,其他项也有相似的论述;A(3,2)即表示点3和点2途径1,4,5点的路径总数;
通过存在性地定理定义我们不妨直接给出定义:A^n表示两点经过n-1个中转点(路径长度为n)的路径数量。
邻接表法
#define VertexMaxNum 100
typedef struct ArcNode{
int adjvex;
struct ArcNode *next;
}ArcNode;
typedef struct VNode{
VertexType data;
ArcNode *first;
}VNode,AdjList[VertexMaxNum];
typedef struct {
AdjList vertices;
int vernum,arcnum;
}ALGraph; //邻接表存储的图类型
特点:假设图有M个点,N条边
- 在无向图中所需要的存储空间为O(M + 2N),N条边在两端的顶点的邻接表中各出现一次,因此共出现2N次。在有向图中需要的存储空间是O(M + N),N条边在作为其起点的顶点的邻接表中出现一次。
- 对于稀疏图,采用邻接表可以极大节省存储空间
- 在邻接表中,给定一顶点很容易找到他的邻边(读取该顶点的邻接表),但很难确定两个顶点之间是否有边
- 在有向图的邻接表表示中,求一个给定顶点的出度只需要计算其邻接表的顶点的个数,但是求一个顶点的入度需要遍历所有顶点的邻接表。因此,也有人用逆邻接表的方式来存储有向图(用邻接表来存储以顶点为终点的边)。(如果将每个顶点的所有关联的边都存储到邻接表中,会使得每条边存储两次,实际上,无向图的邻接表存储正是将图中的每条边存储了两次,所以邻接表存储稠密图来节省节省存储空间可能得不偿失)
- 图的邻接表存储并不唯一,因为和某个顶点关联的边是无序的,但是在建立邻接表时,会默认存在一种序,这种映射显然不是唯一的
十字链表
十字链表存储时,对于每个弧都有一个节点,每个顶点也有一个顶点,要实现这种存储方式,关键在于怎么把弧和顶点联系在一起,用顶点中的信息联系还是用弧节点的信息联系,抑或是用顶点和图两方信息将顶点和弧的节点建立联系
从函数的角度我们还可以做如下思考,可否在第三种结构中将顶点和弧的信息联系在一起?
下面是一种实现方式
弧节点
#define MaxVertexNum 100
typedef struct Arc{
int tailvex; //作为终点的顶点在顺序表中的位置
int headvex; //作为起点的顶点在顺序表中的位置
struct Arc *hlink; //弧头相同的下一条弧
struct Arc *tlink; //弧尾相同的下一条弧
//infotype info; //权值等边的附加信息
}Arc;
typedef struct Vertex{
datatype data;
struct arc *firstin;
struct arc *firstout;
}Vertex,OxList[MaxVertexNum];
typedef struct {
OxList vertices;
int vernum,arcnum;
}OxGraph;
完事发现这玩意就是邻接表和逆邻接表的结合,同时会导致每条边存储两次,假设有n个顶点m条边,最终空间复杂度为O(n + 2m);不过在获得一个顶点的所有关联的边的时候这存储方式有很好的使用效果,只要遍历该顶点的十字链表就ok啦。如此一来我们很容易得到顶的的度(入度和出度)
邻接表的缺点
在邻接表中我们很容易的到一个顶点的信息,但是不容易得到两个点的邻接信息。要想知道两个点是够邻接,需要遍历两个边表(实际上十字链表解决了这个问题,因为十字链表的边界点存储了与其关联的两个顶点的位置),因此我们只要仿照十字链表,在邻接表的边节点中加入关联的点的位置就好啦。
#define VertexMaxNum 100
typedef struct ArcNode{
int ivex;
int jvex;
struct ArcNode *next;
infoType info;
}ArcNode;
typedef struct VNode{
VertexType data;
ArcNode *first;
}VNode,AdjList[VertexMaxNum];
typedef struct {
AdjList vertices;
int vernum,arcnum;
}ALGraph; //邻接表存储的图类型
图的遍历
图的遍历即从图中的某一顶点出发,按照某种搜索方式沿着图中的边对图中所有的顶点访问且仅访问一次。
但是图的顶点之间关系为邻接关系,这种邻接关系是等价的,无法给出一个序,所以我们要防止一个顶点被多次访问,为了达到这种目的,我们引入辅助数组visited[ ]来记录顶点是够已经访问
无论是深度优先搜索还是广度优先搜索,每次遍历只能遍历一颗最大连通子图,因此要使得图中每个节点被遍历,我们要再一次搜索过程后,遍历辅助数组,找到第一个零元,完事对这个零元对应的顶点所在的最大连通子树进行一次搜索。直至不存在零元。
另一种解决方案是对辅助数组进行n次遍历,n为顶点数,若辅助数组对应元素为1,则不再从对应节点进行遍历,若辅助数组对应元素为0,则从该节点进行一次搜索,这种方式显然比第三段叙述的要好,因为只需要遍历一次辅助数组,而第三段所提出的方式要遍历m次(最大连通子图的个数)数组。
#define MaxVertexNum 100
void FS(Graph G,int i){
}
void FSTravers(Graph G){
for(int i = 0;i < MaxVertexNum;i ++){
visited[i] = 0;
}
for(int j = 0;j < MaxVertexNum; j ++){
if(visited[j] == 0){
FS(G,j);
}
}
}
其中BFS为深度优先搜索或广度优先搜索(或其他可以遍历一个最大联通子图的访问方式)
深度优先搜索
对一个最大连通子树的一次深度优先搜索:
访问v_n时,如果v_n存在没有访问的邻接顶点,访问该邻接顶点,否则访问v_n并修改辅助矩阵;
void DFS(Graph G,int i){
visit(i);
visited[i] = 1;
for(int w = FirstNeibor(G,v);w >=0;w = NextNeibor(G,v,w)){
if(!visited[w]){
DFS(G,w);
}
}
}
广度优先搜索
整个广度优先搜索分为m个过程(m个最大连通子树),因此在做完一次广度优先搜索后,寻找辅助数组中的第一个零元,再从该顶点出发进行下一次的广度优先搜索。
对于一个最大连通子树的广度优先搜索,怎么做呢?
在我们遍历到一个节点后,先访问这个节点,将其辅助数组改为已访问,随后访问该节点所有的邻接顶点,因为要对每个顶点进行广度优先搜索,我们要借助队列,在本层进行广度优先搜索后,从队列中吐出本层的第一个顶点节点,对该节点进行广度优先搜索并入队,完事队列中下一个顶点出队,这个元素是上一层中第二个顶点节点,依次这样做,直到本层的全部顶点都被访问,此时上一层的全部节点以出队,本层的第一个顶点在队首,我们重复上面的操作,就实现了逐层访问的效果。
void BFS(Graph G,int i){
InitQueue(Q);
visit(i);
visited[i] = 1;
Enqueue(Q,i);
while(!IsEmpty(Q)){
DeQueue(Q,i);
for(int w = FirstNeibor(G,i);w >= 0;w = NextNeibor(G,w)){
if(visited[w] == 0){
visit(w);
visited[w] = 1;
Enqueue(Q);
}
}
}
}
注意到代码中有一个操作,先将第一个顶点入队,然后在循环中出队,这是为了将“队首顶点出队,访问该顶点的所有相邻未访问节点并将其入队”的操作应用到所有顶点,而注意到所有顶点访问完之前,都有队列非空的条件,所以将队列空作为所有顶点已访问的条件。
图的基本应用
最小生成树和拓扑排序本质上都是对一堆数据的序的构造,最终的到的结果不同,最小生成树算法从一个无序集得到一个偏序集,拓扑排序从偏序集得到一个全序集。我们可以在算法中感受一下序关系是怎么产生的,连通和序又有怎样的关系。
最小生成树
目的:将图中无序的顶点排序,构成一个偏序集合;
序关系的依据:边的权值
生成的偏序集的特征:
- 连通
- 边的数目最小
- 边的权值之和最小
我们在这里讨论带权连通无向图的最小生成树,在最小生成树的算法中一般用到下面的性质:
假设U是顶点集V的一个非空子集,若(u,v)(其中u∈U,v∈V-U)是具有最小权值的边,则必存在一个包含(u,v)的最小生成树
显然上面性质很适用于贪心算法,保持每次选择最优,并且判断每步完成之后是否为最优解。
关于最小生成树算法的通用写法:
GENERIC_MST(G){
T = NULL;
while(T不是最小生成树){
T = T ∪ (u,v);
}
}
问题:
- 怎么判断T不是最小生成树?
- (u,v)怎么选?
Prim算法
假设图是带权无向连通图;
算法策略:
- 使一点v构成V1;
- 寻找N(V1)\V1中与V1权值最小的边,令其加入V1;
- 重复上述过程直到N(V1)\V1为空;
问题:
- 怎么存储N(V1)
- 怎么寻找N(V1)\V1中与V1权值最小的边
- 怎么判断N(V1)\V1为空
对问题的一些思考:
- 我们最终构造的是一个偏序结构,所以要用一个方便存储偏序结构的存储方式,这种存储方式自然可以是树形结构,可以构造一个树节点结构体例如
typedef struct TreeNode{
Vertex v;
TreeNode *parent;
TreeNode[] child;
int childNum;
}TreeNode;
-
这个有些麻烦,提出一个想法,先对所有点的边按权值排序,形成一个边的权,在选取V1的时候
算法实现:
拓扑排序
Object:有向无环图
目的:将偏序集合排序成为一个全序集合
很明显拓扑排序不是唯一的,因为偏序集合中的一些元素不存在序关系,在排成全序集合时穿插起来的元素排序是相对自由的
策略:从一个入度为0的点出发(将该点入栈);
将栈顶元素出栈并加入序列,遍历所有与之关联的边,并将边的终点入栈;重复上述过程直至栈为空;
最短路径
可以借助广度优先遍历(BFS)来求解最短路径问题,因为广度优先搜索总是按照距离由近到远来遍历图中的每一个点
在编程的过程中发现,要直接得到两个点之间的最短路径不是很容易,其时间空间复杂度都与获得一个点到所有点的最短路径相同,如此一来,不如直接求出一个最短路径数组,记录从一个点到其他所有点的最短路径。
Why?因为要求得最短路径,就要记录上一层的最短路径长度,逐层记录路径长度,在求得一点的最短路径时,已经记录了所有比该路径的最短路径小的顶点的最短路径长度。
我们不妨引入一个数组path[ ]来记录路径长度。
int BFS_Min_Path(Graph G,int i){
int path[MaxVertexNum]; //初始化路径长度
for(int j = 0;j < MaxVertexNum;j ++){
path[j] = 0;
}
InitQueue(Q);
visit(i);
visited[i] = 1;
EnQueue[i];
while(IsEmppty(Q)){
DeQueue(Q,i);
for(int w = i.firstNeibor; w >= 0; w = w.nextNeibor){
if(visited[w] = 0){
visited[w] = 1;
path[w] = path[i]++;
EnQueue(Q,w);
}
}
}
}
关键路径
相信你只是怕伤害我
不是骗我
相爱过谁会舍得