图G由顶点集v和边集E组成,记为G=(V,E)。
V(G)表示图G中顶点的有限非空集。–vertex
V={v1,vs,...,vn}∣V∣表示图G中顶点的个数,也称图G的阶V=\{ v_1, v_s,..., v_n\}\\ |V|表示图G中顶点的个数,也称图G的阶 V={v1,vs,...,vn}∣V∣表示图G中顶点的个数,也称图G的阶
E(G)表示图G中顶点之间的关系(边)集合。–edge
E={(u,v),l∈V,v∈V}∣E∣表示图G中边数E= \{(u, v), l\in V, v\in V\}\\ |E|表示图G中边数 E={(u,v),l∈V,v∈V}∣E∣表示图G中边数
注意:线性表可以是空表,树可以是空树,但图不能是空图。也就是V一定是非空集。(E可以是空集)
![]()
无向图
若边集E是无向边(简称边)有限集合时,则图G为无向图。
边是顶点的无序对,对应使用圆括号。记为 (v, w) 或 (w, v) ,因为 (v, w) = (w, v) 。
用公共边的顶点互为邻接点,如对于边 (v,w) ,那么v和w互为邻接点。边 (v, w) 依附于顶点w和v,或者说边 (v, w) 和顶点v、w相关联。
G2=(V2,E2)V2={A,B,C,D,E}E2={(A,B),(B,D),(B,E),(C,D),(C,E),(D,E)}G_2=(V_2,E_2)\\ V_2= \{A,B,C,D,E\}\\ E_2=\{ (A,B),(B,D),(B,E),(C,D),(C,E),(D,E)\} G2=(V2,E2)V2={A,B,C,D,E}E2={(A,B),(B,D),(B,E),(C,D),(C,E),(D,E)}
有向图
若边集E是有向边(也称弧)的有限集合时,则图G为有向图。
弧是顶点的有序对,对应使用尖括号。记为
。
其中v、w是顶点,v称为弧尾,w称为弧头。
G1=(V1,E1)V1={A,B,C,D,E}E1={,,,,,,,
简单图
①不存在重复边(有向图会考虑方向);②不存在顶点到自身的边
数据结构课程讨论的是简单图(简单无向图和简单有向图)
多重图
①存在重复边(有向图也会考虑方向);②不存在顶点到自身的边
简单图和多重图也分为无向图和有向图
无向图:
顶点v的度是指依附于该顶点的边的条数,记为TD(v)。
在n个顶点,e条边的无向图中,每一条边都共享两个度,所以节点度之和为:
∑i=1nTD(vi)=2e\sum_{i=1}^{n}TD(v_i)=2e i=1∑nTD(vi)=2e
有向图:
入度是以顶点v为终点的有向边的数目,记为ID(v);
出度是以顶点v为起点的有向边的数目,记为OD(v)。
顶点v的度等于其入度和出度之和,即TD(v)= ID(v)+ OD(v)。
在n个顶点,e条边的有向图中,每一条边都贡献一个出度和一个入度,所以有向图中出度和入度相等,都等于弧条数。
∑i=1nID(vi)=∑i=1nOD(vi)=e\sum_{i=1}^{n}ID(v_i)=\sum_{i=1}^{n}OD(v_i)=e i=1∑nID(vi)=i=1∑nOD(vi)=e
路径――顶点v到顶点w,之间的一条路径是指顶点序列v,a,l,k,d,j,f,o,i,u,w
无向图中指的是从v沿着边到达w经过的顶点序列,有向图中指的是从v沿着弧到达w经过的顶点序列
回路——第一个顶点和最后一个顶点相同的路径称为回路或环
简单路径――在路径序列中,顶点不重复出现的路径称为简单路径。
简单回路――除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
路径长度―—路径上边的数目
点到点的距离――点到点之间最短路径的长度称为点到点的距离。
若所求两点之间根本不存在路径,则记该距离为无穷∞
此外注意点到点距离,在无向图中交换点的顺序,距离不变。有向图中交换点顺序,距离可能发生变化!
无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的
有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的
对于无向图,若任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。
对于有向图,若任何一对顶点都是强连通的,则称此图为强连通图。
无向图考虑连通性,有向图考虑强连通性。所以说到连通图就是无向图,强连通图就是有向图
n个顶点的无向图
若G为连通图,则最少有n-1条边(设其中一个点为连通图,其余n-1个点每次加入之前的连通图都需要加上一条边,所以最少n-1条边)
若G为非连通图,则最多可能有Cn-12条边。(选一个点孤立,其余n-1个点两两连通)
n个顶点的有向图
若G为强连通图,则最少有n条边(形成回路)。
无向图
设有两个图 G= (V,E)
和 G'= (V',E')
,若 V'
是 V
的子集,且 E'
是 E
的子集,则称 G
'是 G
的子图。(注意图,中边一定是依附于顶点的)
若
V'(G)
=V(G)
,子图顶点集和原图顶点集相同,这样的子图叫左生成子图。 (也可以看成原图去掉某些边既可得到生成子图)
有向图
设有两个图 G= (V,E)
和 G'= (V',E')
,若 V'
是 V
的子集,且 E'
是 E
的子集,则称 G
'是 G
的子图。(注意图,中边一定是依附于顶点的)
若
V'(G)
=V(G)
,子图顶点集和原图顶点集相同,这样的子图叫左生成子图。 (也可以看成原图去掉某些边既可得到生成子图)
无向图的极大连通子图称为连通分量(所谓极大连通子图就是:子图必须连通,且包含尽可能多的顶点和边)
有向图中的极大强连通子图称为有向图的强连通分量(所谓极大强连通子图就是:子图必须强连通,且包含尽可能多的顶点和边)
(无向)连通图的生成树是包含图中全部顶点的一个极小连通子图(边尽可能的少,但保持连通)
若图中顶点数为n,则它的生成树含有n-1条边。
(n个节点(无向)连通图最少含有n-1条边)
对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路)
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
无向图的极大连通子图称为连通分量
无向完全图——无无向图中任意两个顶点之间都存在边
若无向图的顶点数∣v∣=n,则∣E∣∈[0,Cn2]=[0,n(n−1)2]若无向图的顶点数|v|=n,则|E| ∈ [0,C_n^2]=[0,\frac{n(n-1)}{2}] 若无向图的顶点数∣v∣=n,则∣E∣∈[0,Cn2]=[0,2n(n−1)]
有向完全图——有向图中任意两个顶点之间都存在方向相反的两条弧
若有向图的顶点数∣v∣=n,则∣E∣∈[0,Cn2]=[0,n(n−1)]若有向图的顶点数|v|=n,则|E| ∈ [0,C_n^2]=[0,n(n-1)] 若有向图的顶点数∣v∣=n,则∣E∣∈[0,Cn2]=[0,n(n−1)]
稀疏图——边数很少的图称为稀疏图
稠密图——边多
具体边多变少,没有绝对界限,可以参考
一般来说∣E∣<∣V∣log∣V∣时,可以将G视为稀疏图一般来说|E|< |V|log|V|时,可以将G视为稀疏图 一般来说∣E∣<∣V∣log∣V∣时,可以将G视为稀疏图
树——不存在回路,且连通的无向图
n个顶点的图,若边数大于n-1,则一定有回路
有向树——一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。
邻接矩阵是表示顶点之间相邻关系的矩阵(方阵)。
邻接矩阵(Adjacency Matrix)*/*əˈdʒeɪsənsi/ /ˈmeɪtrɪks/
无向图中每一条边对应到邻接矩阵的两个1,并且无向图的邻接矩阵是关于主对角线对称的。求无向图的一个节点的度,就是求该节点在对应邻接矩阵对应列或对应行中1的个数,时间复杂度为O( |V| )
有向图中每一条边对应到邻接矩阵的一个1,并且有向图的邻接矩阵的行坐标表示弧尾,列坐标表示弧头。
- 求有向图节点出度,就是求对应行(弧尾)中1的个数,时间复杂度为O( |V| )。
- 求有向图入度,就是求对应列(弧头)中1的个数,时间复杂度为O( |V| )
- 求有向图的度,就是求入度加上出度,时间复杂度为O( |V| )
对应图的定义为:
#define MAX_VERTEX_NUM 100 //顶点集最大值
typedef char VertexType;
typedef bool EdgeType;//非带权图的邻接矩阵
struct AMGraph {VertexType Vertex[MAX_VERTEX_NUM];//顶点集EdgeType Edge[MAX_VERTEX_NUM][MAX_VERTEX_NUM];//边集int vexnum, edgenum;//图当前的定点数和弧数
};
边集是对应矩阵中的零一序列,可以用int类型存储,但bool占用字节更小。二维数组模拟矩阵,含义如下:
A[i][j]={1,若(vi,vj)或
邻接矩阵表示法创建无向图【算法步骤】
这里给出Java实现的邻接矩阵的创建
package Graph.adjacency_Matrix;import java.util.Scanner;/*** 图的邻接矩阵存储** @author Answer* @create 2022-11-30 18:56*/
public class AMGraph {static private Scanner scan = new Scanner(System.in);private int vertexNum, edgeNum;//顶点数目、边数//开辟更多的空间以防后面插入新的节点,预设值20个顶点private char[] vertex = new char[20];//顶点集private int[][] edge = new int[20][20];/*** 创建邻接矩阵图*/void createGraph() {//输入顶点相关信息,顶点数目和边数while (vertexNum == 0 || vertexNum > 20) {System.out.print("输入顶点数: ");vertexNum = scan.nextInt();if (vertexNum > 20) System.out.println("顶点数目不得超过20,重新输入");}System.out.print("输入边数: ");edgeNum = scan.nextInt();//依次输入顶点集System.out.println("请依次输入顶点集----");for (int i = 0; i < vertexNum; i++) {vertex[i] = scan.next().charAt(0);}int x = 0, y = 0;//表示邻接点int w = 0;//边权for (int i = 0; i < edgeNum; i++) {System.out.println("输入第" + (i + 1) + "条边(顶点形式)--");x = scan.nextInt();y = scan.nextInt();System.out.println("输入边权:");w = scan.nextInt();edge[x][y] = edge[y][x] = w;//邻接边设置为权值}}void printMatrix() {System.out.print(" ");//输出列标for (int i = 0; i < vertexNum; i++) {System.out.print(vertex[i] + " ");}System.out.println();for (int i = 0; i < vertexNum; i++) {System.out.print(vertex[i] + " ");//输出行标for (int j = 0; j < vertexNum; j++) {System.out.print(edge[i][j] + " ");}System.out.println();}}}
调用createGraph和printMatrix两个方法,相应的测试图例:
在带权图中,邻接矩阵中存储的是边的权值,若点之间没有邻接关系,则设置为无穷。
但在有些带权图的邻接矩阵中会将自己指向自己的边权值设置为0,所以这类图,矩阵数值为0或无穷说明对应两个顶点不邻接
参考带权图邻接矩阵定义方式(和不带权图是一样的,只是举证存储的是边的权值)
#define MAX_VERTEX_NUM 100 //顶点集最大值
#define MAX INT_MAX //用int类型的最大值表示无穷,也就是没有邻接关系
typedef char VertexType;
typedef int EdgeType;//带权图邻接矩阵定义
struct AMGraph {VertexType Vertex[MAX_VERTEX_NUM];//顶点集EdgeType Edge[MAX_VERTEX_NUM][MAX_VERTEX_NUM];//边集,矩阵存储的是边权值int vexnum, edgenum;//图当前的定点数和弧数
};
对于n个节点的图,顶点集空间复杂度为 O(N),边集空间复杂度为 O(N2)。所以邻接矩阵的空间复杂度为 ==O(N2) ==。对于稀疏图,边集元素少,邻接矩阵中存储的有效元素少。所以邻接矩阵适合存储稠密图。
对于无向图,由于无向图的邻接矩阵关于主对角线对称,所以可以使用矩阵的压缩存储(用一维数组只存储上三角区/下三角区)
性质一
设图G的邻接矩阵为A(矩阵元素为0/1),则 An 的元素A[i][j]
等于由顶点 i 到顶点 j 的长度为n的路径的数目。
邻接表法就是顺序存储+链式存储。(有些类似数的存储)
期间使用到三个结构体,顶点结构体,边/弧结构体,邻接表结构体
邻接表定义的C++描述
#define MAX_VERTEX_NUM 100 //顶点最大值
typedef bool EdgeType;
typedef char VertexType;//顶点,数组存储
struct VNode {VertexType data;//存储节点信息ArcNode* first;//指向邻接点构成的链表
};//边(弧)节点,用来表示VNode的邻接点构成的链表
struct ArcNode {int adjvex;//表示邻接点,对应数组中的下标ArcNode* next;//int weight;//如果是带权图,可以加上权值
};struct ALGraph {VNode vertices[MAX_VERTEX_NUM];//顶点集合int vexnum, edgenum;//当前顶点和边个数
};
邻接表法描述:先将图中每一个顶点抽象成顶点结构体,每一个顶点结构体包含顶点信息和邻接点链表。最后用一维数组存储这些顶点结构体。
不同于邻接矩阵,邻接矩阵只要确定顶点编号,其对应的邻接矩阵是唯一的。而邻接表法中,由于邻接点链表中邻接点顺序无要求。所以同一个图,其邻接表表示是不唯一的。如下:
![]()
空间复杂度
对于无向图,每一条边对应邻接表中两个边节点(邻接点链表中每一个节点,也能看成一条边),所以邻接表法存储的无向图对应空间复杂度为
O(|V|+2|E|)
。对于有向图,每一条边对应邻接表中一个边节点(该边节点对应弧头),所以邻接表法存储的有向图对应的空间复杂度为
O(|V|+|E|)
邻接表求度,出度,入读
求无向图节点的度–只需要求数组对应节点,其邻接点链表中节点个数即可
求有向图节点的出度–只需要求数组对应节点,其邻接点链表中节点个数即可
求有向图节点的入读–遍历整个数组的左右邻接点链表,有该节点则入读加一
邻接矩阵适合存储稠密图,对于稀疏图存在浪费空间问题。其空间复杂度为 |V|2 。邻接表法解决空间复杂度的问题,但其存储有向图时寻找入度不太方便
这里给出Java实现无向图的邻接表的样例(只有基本的功能)
【算法步骤】
package Graph.adjacency_list;import java.util.Scanner;/*** @ClassName ALGraph* @Description TODO* @Author 林卓为* @Date 2022-2022/12/4 21:52**/class VNode {char data;//存储顶点数据ArcNode firstArc;//第一条邻接边,实际存储的是邻接点,两个邻接点表示一条边
}class ArcNode {int index;//存储顶点编号(顶点存储在顶点集合数组对应下标即为编号)ArcNode nextArc;//下一个邻接点,实际存储的是邻接点,两个邻接点表示一条边
}public class ALGraph {static private Scanner scanner = new Scanner(System.in);private int vertexNum;//当前有效顶点个数private int arcNum;//当前边的有效顶点个数private VNode[] vertices = new VNode[20];//顶点集,预设值20个顶点public void createALGraph() {while (vertexNum == 0 || vertexNum > 20) {System.out.print("输入顶点个数: ");vertexNum = scanner.nextInt();if (vertexNum > 20) System.out.println("图顶点数容量为20,重新输入");}System.out.print("输入边数: ");arcNum = scanner.nextInt();//输入顶点System.out.println("依次输入顶点--");for (int i = 0; i < vertexNum; i++) {vertices[i] = new VNode();vertices[i].data = scanner.next().charAt(0);}//输入边集int x = 0, y = 0;//用于表示邻接点for (int i = 0; i < arcNum; i++) {System.out.println("输入第" + (i + 1) + "对邻接点");x = scanner.nextInt();y = scanner.nextInt();//注意无向图的临界表边节点是冗余的!!!ArcNode p1 = new ArcNode();p1.index = x;ArcNode p2 = new ArcNode();p2.index = y;//无向图冗余边两个,头插p2.nextArc = vertices[x].firstArc;vertices[x].firstArc = p2;p1.nextArc = vertices[y].firstArc;vertices[y].firstArc = p1;}}void printGraph() {for (int i = 0; i < vertexNum; i++) {System.out.print(vertices[i].data + "-->");ArcNode cur = vertices[i].firstArc;while (cur != null) {System.out.print(cur.index + " ");cur = cur.nextArc;}System.out.println();}}
}
注意十字链表发只能用于存储有向图
十字链表法使用到两种类型的节点,顶点节点和弧节点。
顶点节点:数据域+以该节点为弧头的一条弧+以该节点为弧尾的弧节一条弧(每个顶点对应唯一顶点节点)
弧节点:弧尾节点标号+弧头节点标号(标号指的顶点再数组中的标号)+(权值)+弧头相同的下一条弧+弧尾相同的下一条弧(每条弧对应唯一弧节点,其弧为弧节点中弧尾指向弧头)
弧在存储上只能用邻接点来表示,也就是两个邻接点决定一条弧
弧节点实际上就是记录弧头相同的弧链表(图上绿色)和弧尾相同的弧链表(图上橙色)
十字链表发存储简述:顶点节点表示每一个顶点,弧节点表示每一个每一条弧,弧中存储着弧的相邻节点表示该弧。然后每一个顶点链接到弧上,弧节点用指针也串联这图顶点之间的关系。
十字链表发找出边和入边都很方便,先说结论:
- 如何找到指定顶点的所有出边?――顺着绿色线路找
- 如何找到指定顶点的所有入边?——顺着橙色线路找
以一个顶点起始,沿着弧尾相同的下一个弧,这个链表就能找到以当前节点为弧尾的所有弧(可以这么理解,该链表都是公用一个弧尾,而这个弧尾就是顶点。在图中就是沿着绿色向下看)
相同的,以一个顶点起始,沿着弧头相同的下一个弧,这个链表就能找到以当前节点为弧头的所有弧(可以这么理解,该链表都是公用一个弧头,而这个弧头就是顶点。在图中就是沿着橙色向下看)
十字链表发空间复杂度是 O(|V|+|E|)
。(和邻接表发相同)
邻接表法存储无向图,每条边对应两份冗余信息,删除顶点,删除边等操作时间复杂度高。而邻接矩阵存储的空间复杂度高为 O(N)
。对此我们可以使用邻接多重表
邻接多重表和十字链表法类似的。
邻接多重表法存储无向图用到两种节点
邻接多重表删除操作思路清晰的(照着图的物理模型思考)
这里可以分为删除边节点和顶点来分开思考(删边和删点往往是一起进行的,删了边往往会影响指向这条边的顶点)
邻接多重表的空间复杂度为 O(|V|+2|E|)
,空间复杂度比邻接表(同一边冗余)和邻接矩阵(N2)都要好
只探讨邻接矩阵和邻接表法的基本操作
下面探讨几个重要的算法,其余的a简单自己想ba
判断图G中是否存在边
或
(x,y)
,实际上就是判断两个点是否邻接(Adjacent)。
对于邻接矩阵法,找到邻接矩阵边或弧对应位置判断即可。(随机存取,时间复杂度为O(1)
,邻接矩阵算法更优)
对于邻接表法,遍历对应边节点(弧节点,时间复杂度为O(1)~O(|V|)
)。
这里给出无向图和有向图的邻接矩阵和邻接表:
列出图G中节点x邻接的边(也就是列出x的邻接点)
对于邻接矩阵法存储的图,无向图遍历其矩阵对应的行或列。有向图遍历行即为出弧,遍历列即为入弧。(时间复杂度O(|V|)
)
对于邻接表法存储的图,无向图直接遍历顶点对应的边节点链表即可。有向图顶点对应弧节点链表即为以该节点为弧尾的弧,遍历其余弧节点链表就是以该节点为弧头对应的弧(无向图邻接表时间复杂度O(1)~O(|V|)
,有向图邻接表对应时间复杂度为O(|E|)
)
这里给出无向图和有向图的邻接矩阵和邻接表:
图G插入节点x
邻接矩阵就是矩阵加入新的行和列,时间复杂度是O(1)
邻接表法就是再数组尾部插入对应节点即可,时间复杂度是O(1)
有向图和无向图操作一致
删除图G中顶点x
邻接矩阵存储的图,可以直接删除对应行对应列,但需要移动矩阵中大量的元素,不科学。我们可以将对应行对应列矩阵值设置为0,数组顶点加上bool型标志,表示其是否为空节点(有效节点),这种方法时间复杂度为O(|V|)
邻接表存储的图,对于无向图,删除顶点和对应的边表,并且遍历其余边表,找到对应顶点一并删除(因为邻接表存储的图,边是冗余的,也就是存了两份)。对于有向图,删除顶点和对应的弧表(出边),遍历其余弧表,删除该顶点对应入弧。总得来说,都得遍历边一次(除非该点是孤立点)。时间复杂度为 O(1)~O(|E|)
。
若无向边(x,y)或有向边不存在,则向有向图中添加该边。
邻接矩阵,直接改变举证对应位置值即可(无向图改两个值,有向图改一个值)。(时间复杂度O(1)
)
邻接表法,无向图需要修改边两头两个邻接点对应的边表,可以头插也可以尾插。有向图需要修改弧尾对应的弧表即可。(时间复杂度为O(1)
头插为例)
求图G中顶点x的第一个邻接点,若有返回顶点号,若x没有邻接点或图中不存在x,则返回-1。
邻接矩阵存储的图,无向图,按行(或列)从前往后遍历。有向图,按行找出边按列找入边。对应时间复杂度O(1)~O(|V|)
邻接表存储的图,无向图,按对应顶点的边表从前向后遍历即可。有向图按应顶点的边表从前向后遍历找出边,遍历其余边表找入边。无向图找第一个邻接点和有向图找出边对应的时间复杂度为O(1)
。有向图找入边时间复杂度O(|V|)~O(|E|)
(下图我觉得是错了的)
假设图G中顶点y是顶点x的一个邻接点**,返回除y之外顶点x的下一个邻接点的顶点号**,若y是x的最后一个邻接点,则返回-1。
邻接矩阵存储的表中,在x对应行进行遍历,第y列的下一个为1的边对应即为所求。
邻接表中,在数组中找到x顶点的边链表,其中y节点下一个接待你即为所求
广度优先搜索遍历的过程如下。
- 从图中某个顶点v出发,访问v。
- 依次访问v的各个未曾访问过的邻接点。
- 分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。重复步骤2,直至图中所有已被访问的顶点的邻接点都被访问到。
【算法步骤】
从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后使v入队。
只要队列不空,则重复下述操作:
队头元素u出队
依次检查u的所有邻接点w,如果visited[w]的值为false(说明该点没有被访问),则访问w,并置visited[w]的值为true,然后使w入队。
bool visited [MAX_VERTEX_NUM] ;//访问标记数组//从顶点v出发, 广度优先遍历图G(实际上就是广度优先遍历连通分支)
void BFS(Graph G,int v){ visit(v); //访问初始顶点Vvisited [v]=true; //对v做已访问标记Enqueue(Q,v); //顶点v入队列Qwhile(!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);//访问顶点wvisited [w]=true;//对w做已访问标记EnQueue(Q,W) ;//顶点w入队列}}
}
一个图的BFS是不唯一的,考虑因素有广度优先遍历BFS起点就是不唯一的。其次采用图的不同存储形式或图存储形式表示方式不唯一(同一个图邻接表有多种表现形式)。对于图的邻接矩阵顶点编号确定,其图的邻接矩阵是唯一的,所以往往认为图的邻接矩阵唯一
- 同一个图的邻接矩阵表示方式唯一 ,因此广度优先遍历序列唯一
- 同一个图邻接表表示方式不唯一,因此广度优先遍历序列不唯一
上述算法是连通图的广度优先遍历,或者说是连通分支的广度优先遍历,无法实现对非连通图进行广度优先遍历。非连通图含有多个连通分支,我们分别对这些连通分支采用广度优先遍历,就能实现对非连通图的广度优先遍历。visited数组记录顶点是否访问,所以我么可以用一个循环反复遍历visited数组来获得未被访问的连通分支,调用BFS函数遍历连通分支,直到所有顶点均被访问过。
具体实现代码如下
bool visited [MAX_VERTEX_NUM] ;//访问标记数组//对图G进行广 度优先遍历
void BFSTraverse(Graph G){ for(i=0;i visit(v); //访问初始顶点Vvisited [v]=TRUE; //对v做已访问标记Enqueue(Q,v); //顶点v入队列Qwhile(!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);//访问顶点Wvisited [w]=TRUE;//对w做已访问标记EnQueue(Q,W) ;//顶点w入队列}}
}
上面是无向图的广度优先遍历,对于有向图的BFS需要使用的是非连通无向图的BFS,修改firstNeighbor和nextNeighbor函数即可
广度优先遍历算法时间复杂度来自顶点访问和邻接点查找
邻接矩阵存储的图:
访问|V|个顶点需要O( |V| )的时间
查找每个顶点的邻接点都需要O( |V| )的时间,而总共有|M|个顶点
时间复杂度= O( |V|2 )–(|V|2+|V|)
邻接表存储的图:
访问IM个顶点需要0( |V| )的时间
查找各个顶点的邻接点共需要0( |E| )的时间,实际上是O( 2*|E| )–冗余
时间复杂度= O( |V|+|E| )
连通图对应广度优生成树,非连通图对应广度优先生成森林
对连通图图按照广度优先遍历,每一个顶点的所有邻接点作为其孩子,就形成了广度优先生成树。
类比,同一个图,广度优先遍历可以不同,那么其广度优先生成树也是可以不同的!
如下,同一个图,不同邻接表存储,其广度优先生成树也是不一样的
![]()
相应的,对非连通图图按照广度优先遍历,就形成了广度优先生成森林。
深度优先搜索(Depth First Search,DFS)历类似于树的先序遍历,是树的先序遍历的推广对于一个连通图,深度优先搜索遍历的过程如下
- 从图中某个顶点v出发,访问v。
- 找出刚访问过的顶点的第一个未被访问的邻接点,访问该顶点。以该顶点为新顶点重复此步骤,直至刚访问过的顶点没有未被访问的邻接点为止。
- 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,访问该顶点。
- 重复步骤(2)和步骤 (3),直至图中所有顶点都被访问过,搜索结束
算法实现
bool visited [MAX_ _VERTEX_ _NUM]; //访问标记数组//从顶点v出发,深度优先遍历图G
void DFS(Graph G,int v){visit(v); //访问顶点vvisited[v]=true; //设已访问标记for (w=FirstNeighbor(G,v);W>=0;w=NextNeighor(G,v,w))if(!visited[w]) //w为u的尚未访问的邻接顶点DFS(G,w);
}
可以看到上述算法是连通图的深度优先算法,类似的我们只需要增加循环获取不同的连通分支,对不同连通分支实现DFS,就能实现非连通图的深度优先遍历。如下:
bool visited [MAX_ _VERTEX_ _NUM]; //访问标记数组//对图G进行深度优先遍历
void DFSTraverse(Graph G){for(v=0; vvisit(v); //访问顶点vvisited[v]=true; //设已访问标记for (w=FirstNeighbor(G,v);W>=0;w=NextNeighor(G,v,w))if(!visited[w]) //w为u的尚未访问的邻接顶点DFS(G,w);
}
和广度优先遍历一样,一个图的DFS是不唯一的,考虑因素有深度优先遍历DFS起点就是不唯一的。其次采用图的不同存储形式或图存储形式表示方式不唯一(同一个图邻接表有多种表现形式)。对于图的邻接矩阵顶点编号确定,其图的邻接矩阵是唯一的,所以往往认为图的邻接矩阵唯一
- 同一个图的邻接矩阵表示方式唯一 ,因此深度优先遍历序列唯一
- 同一个图邻接表表示方式不唯一,因此深度优先遍历序列不唯一
空间复杂度:来自函数递归调用栈
时间复杂度分析:时间复杂度=访问各结点所需时间+探索各条边所需时间
邻接矩阵存储的图:
访问 |V| 个顶点需要O(V)的时间,查找每个顶点的邻接点都需要O(V)的时间,而总共有 |V| 个顶点
时间复杂度= O( |V|2 )
邻接表存储的图
访问|M|个顶点需要O( |V| )的时间,查找各个顶点的邻接点共需要O( |E| )的时间,
时间复杂度= O( |V|+|E| )
深度优先遍历中递归栈中每一层对应深度优先生成树中的每一层。
- 同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯-,深度优先生成树也唯一
- 同一个图邻接表表示方式不唯- -,因此深度优先遍历序列不唯一, 深度优先生成树也不唯一
相应的非连通图的深度优先遍历对应的就是深度优先生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图,同一个图有不同的生成树,其边权之和最小对应的生成树就是最小生成树,最小生成树可以有多个,但边权之和最小肯定是只有一个的(最字懂吧)
带权连通图,边权之和最小的生成树称为最小生成树
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的
- 最小生成树的边数=顶点数- 1。砍掉一条则不连通,增加一条边则会出现回路
- 如果一个连通图本身就是一棵树,则其最小生成树就是它本身
- 只有连通图才有生成树,非连通图只有生成森林
Prim算法(普里姆)描述:
从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。(选点,实际上还是选边)下面以v0为起点开始构造最小生成树
Prim使用到两个数组,一个数组记录各个节点是否加入树,另一个数组用于记录各个节点加入当前最小生成树的最低代价。如上图以V0为起点构造最小生成树,和V0非相邻的点数组对应值为无穷,其余的记录相应权值即可。
实现过程如下
注意:每一次选完点都需要更新lowCost数组的值,计算各个未加入生成树的顶点和刚刚加入生成树的顶点的边权,比较边权较lowCost数组是否有减小
时间复杂度为:O( |V|2 )适合稠密图。
Kruskal算法(克鲁斯卡尔) 描述:
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通(选边)
算法中判断顶点是否同属于一个集合,使用到并查集的内容
时间复杂度为:O( |E|log2|E| )适合稀疏图
无权图可以视为每条边带权为1的特殊的带权图
利用广度优先遍历求最短路径就是–就是对BFS的小修改,在visit个顶点时,修改其最短路径长度数组d[]并在数组path[]记录该节点的最短路径的直接前驱结点
//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(GraphMatrix G, int u, int path[], int d[], bool visited[]) {//数组d记录各个顶点到原始顶点的最短路径的长度//数组path记录该最短路径的直接前驱//d[i]表示从u到i结点的最短路径for (int i = 0; i < G.verNum; ++i) {d[i] = INT_MAX;//初始化路径长度,无穷path[i] = -1;//最短路径从哪个顶点过来,记录直接前驱 }d[u] = 0;visited[u] = true;queueQ;Q.push(u); //入队while (!Q.empty()) {//BFS算法主过程,依次访问队列中每个顶点的邻接点u = Q.front();Q.pop();//队头元素u出队for (int w = firstNeighbor(G, u); w >= 0; w = nextNeighbor(G, u, w))if (!visited[w]) {//w为u的尚未访问的邻接顶点d[w] = d[u] + 1;//路径长度加1path[w] = u;//最短路径应从u到wvisited[w] = true; //设已访问标记Q.push(w);}}
}
下面给出BFS求无权图最短路径的例子,这里求的是2号顶点到各个顶点的最短距离。友友们可以试着写出两个数组的变化过程
我么可以根据d数组求最短路径长度,根据path数组求该最短路径(就是求路径顶点数)。如:
2到8的最短路径长度: d[8] = 3,也就是最短路径长度为3
通过path数组可知,2到8的最短路径为:8 - 7 - 6 - 2
其实广度优先遍历对应的广度优先生成树,每个顶点对应在广度优先生成树的层数就是其最短路径!!可以根据下图比对d数组数值和层数的关系
广度优先遍历算法求无权图的最短路径,迪杰斯特拉算法求带权图或者无权图的最短路径。实际上无向图可以看成有向图(每条边对应两条方向相反的弧)所以解决了
带权路径长度–当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度
当前顶点是V0,对应dist数组记录每一个顶点到V0的最短路径
迪杰斯特拉算法使用到三个数组
得到两个数组,可以根据dist数组求最短路径长度,根据path数组求最短路径。
上面描述的是迪杰斯特拉算法的手算过程,对于代码实现算法:
初始条件:若从V0开始,迪杰斯特拉算法使用到三个数组–
- 令
final[0]=false;
dist[0]=0;
path[0]=-1;
- 其余顶点
final[k]=false;
(false表示该顶点最短路径待定)disk[k]=arcs[0][k];
(arcs[0][k]
表示顶点0到k弧的长度)path[k]=(arcs[0][k]==无穷)?-1:0
(path记录最短路径的直接前驱)
n-1轮处理:每一轮确定一个最短路径
循环遍历final数组,找到还没有确定最短路径,且dist最小的顶点 Vi,令
final[i]=true
,并检查所有Vi的邻接点,对于 Vi 的邻接点 Vj,若final[j]=false;
且dist[i]+arcs[i][j]
(也就是找到了更短的路径)则进行修改: dist[j]=dist[i]+arcs[i][j]
,path[j]=i
(arcs[i][j]
表示顶点Vi和Vj的弧的距离)
- 可以看出实现的关键点在于判断顶点是否临界,且返回顶点距离。
迪杰斯特拉算法需要进行n-1轮处理,每一轮处理确定一个最短路径。
第一步:先找到没确定最短路径且final值为false的顶点,
第二部:接着比对未确定最短路径的邻接点,当前最短路径是否有减少
对于邻接矩阵和邻接表第一步时间复杂度都是O(|V|)
对于邻接矩阵,第二部需要遍历整一行,时间复杂度为O(|V|)
,但对于邻接表则不需要这么多
但每一轮处理的时间复杂度都是O(|V|)
,总共需要进行n-1轮处理,时间复杂度为O( |V|2 )
上一小节求最小生成树的的Prim算法中,用到两个数组,isJoin数组记录顶点是否加入生成树,lowCost数组记录每一个节点当前加入最小生成树所需要的最小代价。lowCost数组和次数的迪杰斯特拉算法中dist数组很类似。
结论: Dijkstra 算法不适用于有负权值的带权图
Floyd算法–求出每一对顶点之间的最短路径
使用动态规划思想,将问题的求解分为多个阶段
对于n个顶点的图G,求任意一对顶点 Vi - Vj 之间的最短路径可分为如下几个阶段:
初始:不允许在其他顶点中转,最短路径是?
#0:若允许在V0 中转,最短路径是?
#1:若允许在V0、V1 中转,最短路径是?
#2:若允许在V0、V1、V2 中转,最短路径是?
…
#n-1:若允许在Vo、V1、V2 … Vn-1中转,最短路径是?
这里先以最简单的三个顶点的图来演示Floyd算法
通过弗洛伊德算法得到两个最终的数组,记录各个顶点之间最短路径的二维数组,和记录最短路径的中转点。具体如何使用呢,最短路径二维数组直接用没什么好说的。如何通过path数组求得最短路径?由于最短路径可能存在多个中转点,所以需要反复根据path数组求最短路径的中转点,反复递归,直到中转点为-1(代表没有中转点)。上面的例子都是只有一个中转点的。后面Floyd算法例子分析会举一个复杂的例子。
- 根据A(2)可知,V1到V2最短路径长度为4。根据path(2)可知,完整路径信息为V1_V2
- 根据A(2)可知,V0到V2最短路径长度为10。根据path(2)可知,完整路径信息为V0_V1_V2
使用到两个矩阵(二维数组)
Floyd算法核心(三层循环)
//......准备工作,根据图的信息初始化矩阵A和path (如上图)
for (int k=0; k //考虑以Vk作为中转点for(int i=0; i //遍历整个矩阵, i为行号,j为列号for (int j=0; jif (A[i][j]>A[i][k]+A[k][j]){ //以 Vk为中转点的路径更短A[i][j]=A[i][k]+A[k][j]; //更新最短路径长度path[i][j]=k;//中转点}}}
}
相应的时间复杂度为O( |V|3),空间复杂度为O( |V|2)
上面用的是简单的例子,不能很好的体现path数组的使用,这里用一个复杂的例子
最终得到:两个数组,试着求0号顶点到4号顶点的最短路径
上一小节的迪杰斯特拉算法不能解决负权值的图,弗洛伊德算法能解决部分负权值图。弗洛伊德解决不了带有负权回路的图(有负权值的边组成回路)这中图可能没有最短路径,如下图,每转一圈路径都会减少
没有环的有向图,则称为有向无环图。简称DAG图(Directed Acyclic Graph)
前面计算表达式可用树结构转化为后缀表达式,如果处理表达式树中重复项,那么就能够得到有向无环图。
那么如何写出表达式的有向无环图?可以根据树来求解,但不系统。解法如下:
步骤一:根据唯一操作数初步写下有向无环图。首先表达式对应的有向无环图,操作数一定只有一份。不可能存在多个节点存储同一个操作数。
![]()
step1:略
step2:就按照自己计算顺序给每一个运算符标号
step3:运算符放在操作数(子树)的上一层
从底向上逐层检查同层的运算符是否可以合体,合并同类项(操作数独一份不需要合体)
AOV网,Activity On Vertex Network–用顶点表示活动的网
- AVO网一定是一个有向无环图
用DAG图(有向无环图)表示一个工程,顶点表示活动。有向边
i,Vj>表示活动 Vi 必须先于活动 Vj
拓扑排序就是AOV网的顶点的一种排序,排成一个线性的序列,该序列满足若存在有向边
拓扑排序实现算法思路
- 从AOV网中选择一个没有前驱(入度为0)的顶点并输出。
- 从网中删除该顶点和所有以它为起点的有向边。
- 重复1. 和2. 直到当前的AOV网为空或当前网中不存在无前驱的顶点为止(说明该图有回路)。
拓扑排序分析:
- 维数组
indegree[i]
:存放各顶点人度,没有前驱的顶点就是入度为0的顶点。删除顶点及以它为尾的弧的操作,可不必真正对图的存储结构进行改变,可用弧头顶点的入度减1的办法来实现。- 栈S:暂存所有入度为0的顶点,这样可以避免重复查找数
indegree[i]
检测人度为0的顶点,提高算法的效率。(当然你也可以每一遍历数组查找入读为0的顶点,设置类似广度优先遍历和深度优先的visited数组标识该顶点是否已经排好序,如果没有排好序且入读为0,则入栈)- 一维数组
print[i]
:记录拓扑序列的顶点序号。
这里给出AOV网(有向无环图)的邻接表对应拓扑排序:
邻接表定义:
#define MAX_VERTEX_NUM 100 //顶点最大值
typedef bool EdgeType;
typedef char VertexType;//顶点,数组存储
struct VNode {VertexType data;//存储节点信息ArcNode* first;//指向邻接点构成的链表
};//边(弧)节点,用来表示VNode的邻接点构成的链表
struct ArcNode {int adjvex;//表示邻接点,对应数组中的下标ArcNode* next;//int weight;//如果是带权图,可以加上权值
};struct Graph {VNode vertices[MAX_VERTEX_NUM];//顶点集合int vexnum, edgenum;//当前顶点和边个数
};
拓扑排序函数体:
bool TopologicalSort(Graph G){Initstack(S); //初始化栈,存储入度为0的顶点//栈初始化,循环先将入读为零的顶点入栈for(int i=0; i//栈不空,则存在入度为0的顶点Pop(S,i); //栈顶元素出栈,栈顶元素存储在i变量中print[count++]=i;//拓扑排序顶点加1int v;//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈sfor(p=G.vertices[i].first;p!=NULL;p=p->nextarc){v=p->adjvex; //i顶点指向顶点在数组中的下标//入度为0,则入栈if(!(-- indegree[v]))Push(S,v) ;}if(count
O(|V|+|E|)
//for循环遍历所有顶点+沿着边进行拓扑排序对一个AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序:
拓扑排序就是选择入度为零,逆拓扑排序就是选则出度为零。具体实现区别不大(邻接表实现逆拓扑排序,在修改出度时需要遍历整个邻接表!这里引入逆邻接表,邻接表记录的时出边,而逆邻接表记录的时入边,具体如下:)
就是在原来DFS基础上改的(下面的这个代码时没有判断成环问题的)
bool visited [MAX_ _VERTEX_ _NUM]; //访问标记数组//对图G进行深度优先遍历
void DFSTraverse(Graph G){for(v=0; vvisited[v]=true; //设已访问标记for (w=FirstNeighbor(G,v);W>=0;w=NextNeighor(G,v,w))if(!visited[w]) //w为u的尚未访问的邻接顶点DFS(G,w);print(v); //访问顶点v
}
这里注意一点:拓扑排序和逆拓扑排序时不唯一的。拓扑排序可以解决事件排序问题,也可以解决图是否有huan
知识体系:
与AOV网相对应的AOE网,AOE网表示用边表示活动的网络。AOE网就是带权有向无环图,其中顶点表示事件,有向边表示活动,以边上的权值表示完成该活动的开销
- AOV网–用顶点表示活动的网络(Activity On Edge Network)
AOE网具有以下两个性质:
- 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
- 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
另外,有些活动是可以并行进行的
AOE网中最长的带权路径称为关键路径,相应d额关键路径上的活动称为关键活动。
完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长
下面定义四个描述量:
对于有些活动,其活动最早发生时间和最迟发生时间必须一致,否则整个工程会延后。但有些又不要求一致,如下:
对此引入新的概念:
活动 ai 的时间余量 d(i)=l(i)-e(i)
,表示在不增加整个工程所需总时间的情况下,活动 ai 可以延后的时间。
时间余量为零的活动就是对应的关键活动,关键活动组成的路径就是关键路径。(也就是整个工程的最长带权路径)
求所有事件的最早发生时间 ve( )–求前往后求
![]()
求解事件最早发生时间依据公式:ve(k) = Max { ve(j) + Weight(Vj, vk) }
以当前事件的任意前驱加上弧权值,求出最大值就是当前事件的最早发生时间
按照拓扑排序的顺序来求解事件的最早发生时间,确保了求解当前事件最早发生时间时,与其相关的事件的最早发生事件已经被求完了。
求所有事件的最迟发生时间 vl( )–从后往前求
![]()
求解事件最迟发生事件依据公 vl(k) = Min { vl(j) - Weight(vk,vj ) }
以当前事件的任意后继减去弧权值,求出最小值就是当前事件的最迟发生时间
求所有活动的最早发生时间–就是弧事件最早发生时间!!
求所有活动的最迟发生时间–就是弧尾的最迟发生时间-弧权值
求所有活动的时间余量 d()—就是活动的最晚发生时间-活动最早发生时间
活动时间余量为零的就是关键活动,相应的就能求出关键路径
关键活动:a2、a5、a7
关键路径:V1,V3,V4,V6
若关键活动耗时增加,则整个工程的工期将增长
缩短关键活动的时间,可以缩短整个工程的工期
当宿短到一定程度时,关键活动可能会变成非关键活动
可能有多条关键路径,只提高-条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短I期的目的。
上一篇:【MySQL进阶】SQL优化