图的存储、遍历以及Dijkstra/Floyd/Kruskal/Prim/拓扑排序/关键路径(实验8--作业)

【数据结构与算法】图(Graph)【详解】 图(Graph)是由顶点的有穷非空集合V ( G ) 和顶点之间边的集合E ( G )组成,通常表示为: G = ( V , E ) ,其中,G表示个图,V 是图G中顶点的集合,E是图G中边的集合。若V = { v 1 , v 2 , . . . , v n } ,则用∣ V ∣表示图G中顶点的个数,也称图G的阶,E = { ( u , v ) ∣ u ∈ V , v ∈ V } ,用∣ E ∣表示图G中边的条数。注意:线性表可以是空表,树可以是空树,但图不可以是空图。 阅读详情

图–数据结构操作与算法全解析

一、引言

图作为一种重要的数据结构,在计算机科学与众多领域中都有着广泛的应用。它能够有效地描述和解决各种复杂的关系问题,如网络拓扑、路径规划、资源分配等。本文将详细介绍图的相关操作和知识点,包括图的创建、遍历(深度优先遍历和广度优先遍历)、最小生成树(Kruskal 和 Prim 算法)、最短路径(Djikstra 和 Floyd 算法)以及拓扑排序,并结合具体代码进行深入剖析。

二、图的基本概念

图由顶点(Vertex)和边(Edge)组成。顶点表示对象,边表示对象之间的关系。根据边是否有方向,图可分为有向图(Directed Graph)和无向图(Undirected Graph);根据边是否有权重,又可分为有权图(Weighted Graph)和无权图(Unweighted Graph)。在实际应用中,这些不同类型的图各有其适用场景,例如社交网络可以用无向图表示用户之间的好友关系,而交通网络则可以用有权有向图表示道路的方向和距离。

三、图的存储结构

(一)邻接矩阵

  1. 定义与原理
    • 邻接矩阵是用一个二维数组来表示图中顶点之间的关系。对于一个具有 n 个顶点的图,其邻接矩阵 arcs 的大小为 n×n。如果 arcs[i][j] = 1(或边的权重值,对于有权图),表示顶点 i 和顶点 j 之间有边相连;如果 arcs[i][j] = 0(或 MaxInt,表示无穷大,对于无权图),则表示顶点 i 和顶点 j 之间没有边相连。在无向图中,邻接矩阵是对称的,因为边没有方向,即 arcs[i][j] = arcs[j][i]。
  2. 代码实现与分析
    • 在给定的代码中,AMGraph 结构体中的 arcs 二维数组就是用于存储邻接矩阵。在 CreateUDN 函数中,首先初始化邻接矩阵,将所有元素设置为 MaxInt,表示初始时顶点之间没有边相连。然后,当输入边的信息时,通过 Locate 函数找到顶点在数组中的下标 m 和 n,并将 arcs[m][n] 和 arcs[n][m](对于无向图)设置为边的权重值 a,从而构建起邻接矩阵。这种存储结构的优点是简单直观,容易判断两个顶点之间是否有边,并且在获取某个顶点的邻接顶点时,可以通过遍历一行(或一列)数组快速得到。然而,其缺点是对于稀疏图(边较少的图),会浪费大量的存储空间,因为大部分元素可能都是 MaxInt。
//邻接矩阵的结构体创建

typedef char VerTexType;  //假设顶点的数据类型为字符型
typedef int ArcType;  // 假设边的权值类型为整型
typedef struct { 
	VerTexType   vexs[MVNum]; // 顶点表 
	ArcType     arcs[MVNum][MVNum]; // 邻接矩阵 
	int     vexnum, arcnum; // 图的当前顶点数和弧(边)数 
	GraphKind kind;//图的种类
} AMGraph;


邻接矩阵创建无向网以及其打印

//邻接矩阵创建无向网
Status CreateUDN(AMGraph &G){
	cout<<"所需点的个数:";cin>>G.vexnum;
	cout<<"所需边的个数:";cin>>G.arcnum;
	G.kind = UDN;//无向网
	//输入顶点的信息
	cout<<"请依次输入顶点信息"<<endl;
	for(int i = 0;i<G.vexnum;i++){
		cout<<"第"<<i+1<<"个:";
		cin>>G.vexs[i];
	}
	//初始化邻接矩阵
	for(int i=0;i<G.vexnum ;i++) 	 //初始化邻接矩阵
		for(int j=0;j<G.vexnum ;j++) 	
			G.arcs[i][j]=MaxInt;
	//输入边的信息
	cout<<"请依次输入边的信息"<<endl;
	cout<<"输入格式 点v1 点v2 边长度a"<<endl;
	for(int i = 0;i<G.arcnum;i++){
		VerTexType v1,v2;
		ArcType a;
		cout<<"第"<<i+1<<"个:";
		cin>>v1>>v2>>a;
		Edges[i].Head = v1;
		Edges[i].Tail = v2;
		Edges[i].lowcost = a;
		int m = Locate(G,v1),n = Locate(G,v2);
		G.arcs[m][n] = G.arcs[n][m] = a;//构建边成功
	}
	return OK;
}
//打印邻接矩阵内容
void PrintUDN(AMGraph &G){
	cout<<"vexnum:"<<G.vexnum<<"\tarcnum:"<<G.arcnum<<endl<<endl;
	cout<<"\t";
	for(int i = 0 ;i<G.arcnum;i++)cout<<G.vexs[i]<<"\t";
	cout<<endl;
	for(int i = 0;i<G.vexnum;i++){
		cout<<G.vexs[i]<<"\t";
		for(int j = 0;j<G.vexnum;j++){
			if(G.arcs[i][j] == MaxInt) cout<<"∞\t";
			else cout<<G.arcs[i][j]<<"\t";
		}
		cout<<endl;
	}	
}

(二)邻接表

  1. 定义与原理
    • 邻接表是一种链式存储结构,它为图中的每个顶点建立一个单链表,链表中的节点表示与该顶点相邻接的顶点。每个链表节点包含两个部分:邻接顶点的下标 adjvex 和指向下一个邻接顶点节点的指针 nextarc。对于有权图,还可以在节点中添加一个字段来存储边的权重信息 info。
  2. 代码实现与分析
    • 在代码中的 ALGraph 结构体定义了邻接表。vertices 是一个数组,每个元素是一个 VNode 结构体,代表一个顶点。VNode 结构体中的 firstarc 指针指向该顶点的邻接顶点链表的头节点。在 CreateDG 函数中,通过头插法创建邻接表。当输入一条边 v1 到 v2 时,先找到 v1 和 v2 对应的顶点下标 i 和 j,然后创建一个新的边节点 p,将其 adjvex 设置为 j,并将 p 插入到顶点 i 的邻接顶点链表的头部,即 p->nextarc = G.vertices[i].firstarc; G.vertices[i].firstarc = p;。邻接表的优点是对于稀疏图能够节省存储空间,只存储实际存在的边。其缺点是在判断两个顶点之间是否有边时,需要遍历相应顶点的邻接链表,效率相对较低,不如邻接矩阵直接通过数组下标访问那么快速。
//邻接表的相关结构体创建

typedef enum {DG, DN, UDG, UDN} GraphKind;  
//{有向图,有向网,无向图,无向网} 
typedef char VerTexType;  //假设顶点的数据类型为字符型
typedef int ArcType;  // 假设边的权值类型为整型

typedef struct ArcNode {//边结点
	int   adjvex; 	// 该弧(边)所指向的顶点的位置 
	struct ArcNode *nextarc; //指向下一条边
	InfoType   *info; 	 //和边相关的信息	
} ArcNode; 

typedef struct VNode {//表头结点
	VerTexType   data; 	// 顶点信息 
	ArcNode       *firstarc; // 指向第一条依附该顶点的弧(边) 
} VNode, AdjList[MVNum]; 

typedef struct { 
	AdjList   vertices; 	//顶点数组 
	int   vexnum, arcnum; // 图的当前顶点数和弧(边)数
	GraphKind kind;//图的种类 
} ALGraph; 

邻接表创建有向网及其打印

//邻接表创建有向网
Status CreateDG(ALGraph &G){
	cout<<"所需点的个数:";cin>>G.vexnum;
	cout<<"所需边的个数:";cin>>G.arcnum;
	G.kind = DG;//有向网
	//输入顶点的信息
	//构造头节点
	cout<<"请依次输入顶点信息"<<endl;
	for(int i = 0;i<G.vexnum;i++){
		cout<<"第"<<i+1<<"个:";
		cin>>G.vertices[i].data;
		G.vertices[i].firstarc = nullptr;
	}
	//边的信息 采用头插法
	cout<<"请依次输入边的信息"<<endl;
	cout<<"输入格式 点v1 点v2"<<endl;
	for(int k = 0;k<G.arcnum;k++){
		VerTexType v1,v2;
//		ArcType a;
		cin>>v1>>v2;
		int i = Locate(G,v1);
		int j = Locate(G,v2);
		ArcNode* p = new ArcNode;//新的边节点
		p->adjvex = j;//边指向的顶点的序号
		
		//头插法
		//p的下一个边改成第i个点后依附的第一条弧
		p->nextarc = G.vertices[i].firstarc;
		G.vertices[i].firstarc = p;
	}
	return OK;
}
//打印邻接表
void PrintDG(ALGraph &G) {
	for (int i = 0; i < G.vexnum; i++) {
		cout << "[" << G.vertices[i].data << "]";
		
		ArcNode* p = G.vertices[i].firstarc;
		while (p!= nullptr) {
			int t = p->adjvex;
			cout << "->" << p->adjvex << "("<< G.vertices[t].data<<")";
			p = p->nextarc;
		}
		
		cout << endl;
	}
}

四、图的遍历

(一)深度优先遍历(DFS)

  1. 原理与算法流程
    • 深度优先遍历的基本思想是从图中的某个顶点 v 出发,访问该顶点,然后递归地遍历与 v 相邻接且未被访问过的顶点,直到所有与 v 有路径相通的顶点都被访问到。如果图中还有未被访问的顶点,则任选一个未被访问的顶点作为起始点,重复上述过程,直到图中所有顶点都被访问过。
  2. 代码实现与分析(以邻接矩阵为例)
    • 在 dfs 函数中,首先输出当前顶点 G.vexs[v],并将其标记为已访问 visited[v] = true。然后通过一个循环遍历所有顶点,如果 G.arcs[v][w]!= MaxInt 表示顶点 v 和顶点 w 之间有边相连,且顶点 w 未被访问过,则递归调用 dfs(G, w) 继续深度优先遍历。这种递归的方式能够沿着一条路径尽可能深地访问顶点,直到无法继续,然后回溯到上一个顶点,继续探索其他未被访问的路径。时间复杂度为 O(V2)O(V^2)O(V2),其中 V 是图的顶点数,因为对于每个顶点都可能需要遍历所有其他顶点来判断是否有边相连。空间复杂度为 O(V)O(V)O(V),主要是用于存储递归调用栈和标记数组 visited。
//邻接矩阵构造无向网的DFS
bool visited[MVNum];
void dfs(AMGraph &G,int v){
	cout<<G.vexs[v];visited[v] = true;
	for(int w = 0;w<G.vexnum;w++){
		if(G.arcs[v][w]!=MaxInt&&!visited[w])
			dfs(G,w);
	}
}
//邻接表构造有向网的DFS
bool visited[MVNum];
void dfs(ALGraph &G,int v){
	cout<<G.vertices[v].data;
	visited[v] = true;
	ArcNode *p = G.vertices[v].firstarc;
	while(p!=nullptr){
		int w = p->adjvex;//p的周围的点的下标
		if(!visited[w]) dfs(G,w);//第w个点未被访问 =>访问下
		p = p->nextarc;
	}
}

(二)广度优先遍历(BFS)

  1. 原理与算法流程
    • 广度优先遍历的基本思想是从图中的某个顶点 v 出发,先访问该顶点,然后依次访问与 v 相邻接的所有未被访问过的顶点,再按照这些顶点被访问的先后顺序,依次访问它们相邻接的未被访问过的顶点,直到图中所有顶点都被访问过。它类似于层次遍历,先访问距离起始顶点最近的一层顶点,然后再依次访问距离更远的层次的顶点。
  2. 代码实现与分析(以邻接矩阵为例)
    • 在 bfs 函数中,首先将所有顶点标记为未访问。然后将起始顶点 v 标记为已访问并输出,将其下标放入队列 Q 中。在循环中,当队列不为空时,取出队首元素 u,并遍历所有顶点。如果顶点 u 和顶点 v 之间有边相连且顶点 v 未被访问过,则将顶点 v 标记为已访问并输出,然后将其下标放入队列 Q 中。这样可以保证按照距离起始顶点的层次顺序依次访问顶点。时间复杂度为 O(V2)O(V^2)O(V2),与深度优先遍历类似,因为都需要遍历邻接矩阵。空间复杂度为 O(V)O(V)O(V),主要用于存储队列和标记数组 visited。
//邻接矩阵构造无向网的BFS
//广度优先搜索
void bfs(AMGraph &G) {
	for (int i = 0; i < G.vexnum; i++) {
		visited[i] = false;
	}
	
	queue<int> Q;
	
	for (int i = 0; i < G.vexnum; i++) {
		if (!visited[i]) {
			visited[i] = true;
			cout << G.vexs[i];
			Q.push(i);
			
			while (!Q.empty()) {
				int u = Q.front();
				Q.pop();
				
				for (int v = 0; v < G.vexnum; v++) {
					if (G.arcs[u][v]!= MaxInt &&!visited[v]) {
						visited[v] = true;
						cout << G.vexs[v];
						Q.push(v);
					}
				}
			}
		}
	}
}
//邻接表构造有向网的BFS
void bfs(ALGraph &G){
	for (int i = 0; i < G.vexnum; i++) {
		visited[i] = false;
	}
	queue<int> Q;
	
	for(int i =0;i<G.vexnum;i++){
		if(!visited[i]){
			visited[i] = true;
			cout<<G.vertices[i].data;
			Q.push(i);
			while(!Q.empty()){
				int u = Q.front();//Q存放的是下标
				Q.pop();
				ArcNode *p = G.vertices[u].firstarc;
				while(p!=nullptr){
					if(!visited[p->adjvex]){
						visited[p->adjvex] = true;
						cout<<G.vertices[p->adjvex].data;
						Q.push(p->adjvex);
					}
					p = p->nextarc;
				}
			}
		}
	}
}

五、最小生成树

(一)Prim 算法

  1. 原理与算法流程

    • Prim 算法的基本思想是从图中的任意一个顶点开始,逐步构建最小生成树。首先将起始顶点加入到最小生成树的顶点集合 (U) 中,然后在集合 (U) 和集合 (V - U)((V) 是图的所有顶点集合)之间的边中,选择一条权值最小的边,将这条边的另一个顶点加入到集合 (U) 中。重复这个过程,直到集合 (U) 包含了图中的所有顶点。在这个过程中,需要维护一个数组 (lowcost) 来记录集合 (V - U) 中每个顶点到集合 (U) 中顶点的最小权值边的权值,以及一个数组 (vset) 来标记顶点是否已经加入到集合 (U) 中。
      -Prim算法 又叫加边法 实现流程如图所示在这里插入图片描述
  2. 代码实现与分析(以邻接矩阵为例)

    • 在 Prim 函数中,首先进行初始化。将起始顶点 v0 加入到集合 U 中,即 vset[v] = 1,并将与起始顶点相邻接的顶点的 lowcost 值设置为相应边的权值 lowcost[i] = G.arcs[v][i]。然后在循环中,找到 lowcost 数组中的最小值 min,其对应的顶点 k 就是要加入到集合 U 中的顶点。输出加入的边 G.vexs[prevV] - G.vexs[v] : lowcost[v],并更新 prevV = v 和 v = k。接着,对于集合 (V - U) 中的顶点,如果通过新加入的顶点 v 到该顶点的边权值更小,就更新 lowcost 数组的值。时间复杂度为 (O(V^2)),其中 (V) 是图的顶点数,因为在每次选择最小权值边时,都需要遍历 lowcost 数组来找到最小值,这个过程的时间复杂度为 (O(V)),而总共需要进行 (V - 1) 次这样的选择操作。空间复杂度为 (O(V)),主要用于存储 lowcost、vset 和其他辅助变量。
//求最小生成树
void Prim(AMGraph &G,int v0){
	cout<<"利用Prim算法构建最小生成树所加入的边是"<<endl;
	//初始化
	int v = v0;
	int k ;//存放最小的权值的下标
	int vset[MVNum];//存放在U中的点的下标
	ArcType lowcost[MVNum];//存放权值
	for(int i = 0;i<G.vexnum;i++){
		lowcost[i] = G.arcs[v][i];//存放每个点到v的权值
		vset[i] = 0;//点都还未进U 所以都是0
	}
	vset[v] = 1;//首先把第一个点存进去

	for(int i = 0;i<G.vexnum - 1;i++){//剩余的其他几个节点遍历,依次存入U 共需要遍历n-1次
		//找lowcost的最小点
		int min = MaxInt;
		for(int j = 0;j<G.vexnum;j++){
			if(min>lowcost[j] && vset[j] == 0){//找未在U中的最小值
				min = lowcost[j];//找最小值
				k = j;//更新最小值对应点的下标(k点要存进U中去)
			}
		}
		vset[k] = 1;//第k个点存进U
		int prevV = v;//存入边的起点
		v = k;//更新v为k  此时v为存入的边的终点
		cout << G.vexs[prevV] << " - " << G.vexs[v] << " : " << lowcost[v] << endl;
		//更新lowcost  只是更新值 不会显露lowcost的来源信息 就是v->w,v,w不知
		for(int j = 0;j<G.vexnum;j++){
			if(vset[j] == 0 && G.arcs[v][j]<lowcost[j]){
				lowcost[j] = G.arcs[v][j];
			}
		}
	}
}

(二)Kruskal 算法

  1. 原理与算法流程

    • Kruskal 算法的基本思想是将图中的所有边按照权值从小到大进行排序,然后依次选择边。如果选择的边不会形成环,就将其加入到最小生成树中,直到选择了 (V - 1) 条边为止,其中 (V) 是图的顶点数。为了判断选择的边是否会形成环,使用了并查集数据结构,通过维护一个数组 Vexset 来记录每个顶点所属的连通分量。
      Kruskal算法又叫加点法 实现流程如图所示
    • 在这里插入图片描述
  2. 代码实现与分析(以邻接矩阵为例,结合辅助数组 Edges)

    • 在 Kruskal 函数中,首先将辅助数组 Edges 按照边的权值进行排序,这可以通过调用 sort 函数并传入自定义的比较函数 compareEdges 来实现。然后初始化并查集数组 Vexset,使得每个顶点的初始连通分量为其自身下标。接着遍历排序后的边数组,对于每条边,找到其两个顶点在 Vexset 中的连通分量 vs1 和 vs2。如果 vs1!= vs2,说明这条边不会形成环,将其加入到最小生成树中,并输出该边信息 Edges[i].Head - Edges[i].Tail : Edges[i].lowcost,然后通过更新 Vexset 数组来合并两个连通分量。时间复杂度主要取决于边的排序操作,一般为 (O(E log E)),其中 (E) 是图的边数,以及并查集操作的时间复杂度,在优化较好的情况下可以接近线性时间。空间复杂度为 (O(E)),主要用于存储辅助数组 Edges 和并查集数组 Vexset。
// 比较函数,用于指定按照lowcost进行排序
bool compareEdges(const Edge& a, const Edge& b) {
	return a.lowcost < b.lowcost;
}
void Kruskal(AMGraph &G){
	cout<<"利用Kruskal算法构建最小生成树加入的边是"<<endl;
	//辅助数组 用祖先的所在位置表示所在连通分量
	//如果某条选取边的起点终点的Vexset值相等 则不能加进去
	//加进去会使得成环
	int Vexset[MVNum];
	sort(Edges,Edges+G.arcnum,compareEdges);//边按照权值排序
	for(int i = 0;i<G.arcnum;i++){
		Vexset[i] = i;
	}
	//遍历所有边
	for(int i = 0;i<G.arcnum;i++){
		//找当前边的下标
		int v1 = Locate(G,Edges[i].Head);
		int v2 = Locate(G,Edges[i].Tail);
		//当前边的首尾节点所在的连通分量vs1,vs2
		int vs1 = Vexset[v1];
		int vs2 = Vexset[v2];
		if(vs1!=vs2){//说明成不了环 输出
			cout<<Edges[i].Head<<" - "<<Edges[i].Tail<<": "<<Edges[i].lowcost<<endl;
			//合并vs1,vs2(并查集操作)
			for(int j = 0;j<G.arcnum;j++){
				if(Vexset[j] == vs2){
					Vexset[j] = vs1;//vs2合并为vs1
				}
			}
		}
	}
	
}

六、最短路径

(一)Djikstra 算法

  1. 原理与算法流程

    • Djikstra 算法用于计算带权有向图中一个顶点到其他所有顶点的最短路径。其基本思想是从起始顶点开始,逐步确定到其他顶点的最短路径。首先将起始顶点到自身的距离设置为 (0),到其他顶点的距离设置为无穷大(在代码中用 MaxInt 表示),然后将起始顶点标记为已确定最短路径的顶点。在每次迭代中,选择一个未确定最短路径且距离起始顶点最近的顶点 (v),将其标记为已确定,并更新与 (v) 相邻接的顶点到起始顶点的距离(如果通过 (v) 到达这些顶点的距离更短)。重复这个过程,直到所有顶点都被确定了最短路径。
      在这里插入图片描述
  2. 代码实现与分析(以邻接矩阵为例)

    • 在 Dijkstra 函数中,初始化部分包括设置标记数组 S、路径数组 Path 和距离数组 Dist。将起始顶点 v0 的相关信息初始化,即 S[v0] = true,Dist[v0] = 0,并根据邻接矩阵设置其他顶点到起始顶点的初始距离和路径信息。然后在循环中,找到未确定最短路径且距离起始顶点最近的顶点 v,将其标记为已确定 S[v] = true。接着更新与 v 相邻接的顶点的距离和路径信息,如果 Dist[i] > Dist[v] + G.arcs[i][v],则更新 Dist[i] = Dist[v] + G.arcs[i][v] 和 Path[i] = v。最后,通过 PrintPath_Dijkstra 函数输出从起始顶点到每个顶点的最短路径及其长度。时间复杂度为 (O(V^2)),因为在每次迭代中都需要遍历所有顶点来找到未确定最短路径且距离最近的顶点,以及更新相邻接顶点的距离。空间复杂度为 (O(V)),用于存储标记数据 S、路径数组 Path 和距离数组 Dist。
      Dijkstra算法及其最短路径的打印
void PrintPath_Dijkstra(AMGraph &G,int start,int end,int Path[]){
	cout<<"从"<<G.vexs[start]<<"到"<<G.vexs[end]<<"的最短路径:";
	int current = end;
	//倒着存放路径
	stack<int>pathStack;
	while(current != start){
		pathStack.push(current);
		current = Path[current];
	}
	pathStack.push(start);
	
	VerTexType temp =  G.vexs[pathStack.top()];
	pathStack.pop();
	if(!pathStack.empty()) cout<<temp;
	
	while(!pathStack.empty()){
		cout<<"->"<<G.vexs[pathStack.top()];
		pathStack.pop();
	}
}
void Dijkstra(AMGraph &G,int v0){
	cout<<"利用Dijkstra算法计算得 "<<G.vexs[v0]<<" 到各点的最短路径的信息如下"<<endl;
	//----初始化----
	bool S[G.vexnum];//存放已经存的顶点
	int Path[G.vexnum];//存放自起点来的在最短路径里面的点的下标
	int Dist[G.vexnum];//存放自起点来的最短路径的长度
	int n = G.vexnum;
	for(int i = 0;i<n;i++){
		S[i] = false;//都还未存入
		Dist[i] = G.arcs[v0][i];
		if(Dist[i]!=MaxInt){
			Path[i] = v0;
		}else{
			Path[i] = -1;
		}
	}
	S[v0] = true;//起点存入
	Dist[v0] = 0;//起点到自身的距离
	//----初始化完毕----
	for(int i = 1;i<n;i++){//剩余n-1个点
		int min = MaxInt;
		int v;
		//未加入S的点中找最小值
		for(int i = 0;i<n;i++){
			if(!S[i] && Dist[i]<min){
				v = i;//所找到的最短路径中一段的终点
				min = Dist[i];//最小值
			}
		}
		S[v] = true;//v0->v的最短路径已找到 加入进去
		for(int i = 0;i<n;i++){
			if(Dist[i]>Dist[v]+G.arcs[i][v]){
				//找到更短的边时 加入
				Dist[i] = Dist[v] + G.arcs[i][v];
				Path[i] = v;
			}
		}
	}
	//输出到每个点的最短路径
	for(int i = 0;i<n;i++){
		if(i != v0){ //消除自身到自身的路径
		PrintPath_Dijkstra(G,v0,i,Path);
		cout<<"\t路径长度为"<<Dist[i]<<endl;
		}
	}
}

(二)Floyd 算法

  1. 原理与算法流程

    • Floyd 算法用于计算带权有向图中所有顶点对之间的最短路径。其基本思想是通过动态规划的方法,逐步考虑加入中间顶点来更新两点之间的最短路径。首先初始化一个二维数组 Dist 来存储顶点之间的初始距离(直接距离或无穷大),以及一个二维数组 Path 来存储最短路径中每个顶点的前驱顶点。然后,对于每一个可能的中间顶点 k,遍历所有顶点对 i 和 j,如果通过中间顶点 k 可以使 i 到 j 的路径更短,即 Dist[i][j] > Dist[i][k] + Dist[k][j],则更新 Dist[i][j] 和 Path[i][j]。经过 (n) 次迭代((n) 为图的顶点数)后,Dist 数组就存储了所有顶点对之间的最短路径长度,Path 数组可以用于回溯最短路径。
      在这里插入图片描述
  2. 代码实现与分析(以邻接矩阵为例)

    • 在 Floyd 函数中,初始化 Dist 和 Path 数组,根据邻接矩阵设置初始距离和路径信息。然后通过三重循环进行迭代,外层循环控制中间顶点 k,中层循环遍历起始顶点 i,内层循环遍历终点顶点 j。在每次迭代中,如果发现通过中间顶点 k 可以优化 i 到 j 的最短路径,就更新 Dist[i][j] 和 Path[i][j]。最后,输出经过 (n) 次迭代后的 Dist 和 Path 数组的结果。时间复杂度为 (O(V^3)),因为有三重循环,分别遍历所有顶点作为中间顶点、起始顶点和终点顶点。空间复杂度为 (O(V^2)),用于存储 Dist 和 Path 数组。
      Floyd算法及其辅助数组的打印
void Floyd(AMGraph &G){
	int n = G.vexnum;
	int Path[MVNum][MVNum];
	int Dist[n][n];
	for(int i = 0;i<n;i++){
		for(int j = 0;j<n;j++){
			Dist[i][j] = G.arcs[i][j];
			if(Dist[i][j]!=MaxInt && i!=j){//i 和 j 之间有弧 将j的前驱置为 i
				Path[i][j] = i;//说明从i到j的最短路径的终点的前一个点是i
			}else Path[i][j] = -1;
		}
	}
	
	for(int k = 0;k<n;k++){
		for(int i = 0;i<n;i++){
			for(int j = 0;j<n;j++){
				if(Dist[i][j]>Dist[i][k]+Dist[k][j]){//每个点都当一次中间点 最后得到最短路径
					Dist[i][j] = Dist[i][k]+Dist[k][j];
					Path[i][j] = Path[k][j];
					//因为Path[i][j]代表的是i 到 j的最短路径的终点的前一个点的下标
					//所以更改为中间点
				}
			}
		}
	}
	//打印出来D(n-1)和Path(n-1)的内容
	cout<<"通过Floyd算法"<<endl;
	cout<<"经过 "<<n<<" 次迭代后"<<endl;
	cout<<"D"<<(n-1)<<"和Path"<<(n-1)<<"的结果如下所示"<<endl;
	cout<<"\nD("<<n-1<<"):\n"<<endl;
	
	for(int i = 0;i<n;i++){
		if(i==0)cout<<"\t";
		cout<<"("<<G.vexs[i]<<")"<<"\t";
	}
	cout<<endl;
	
	for(int i = 0;i<n;i++){
		cout<<"("<<G.vexs[i]<<")"<<"\t";
		for(int j = 0;j<n;j++){
			cout<<Dist[i][j]<<"\t";
		}
		cout<<endl;
	}
	cout<<"\nPath("<<n-1<<"):\n"<<endl;
	
	for(int i = 0;i<n;i++){
		if(i==0)cout<<"\t";
		cout<<"("<<G.vexs[i]<<")"<<"\t";
	}
	cout<<endl;
	
	for(int i = 0;i<n;i++){
		cout<<"("<<G.vexs[i]<<")"<<"\t";
		for(int j = 0;j<n;j++){
			cout<<Path[i][j]<<"\t";
		}
		cout<<endl;
	}

}

七、拓扑排序

(一)原理与算法流程

  • 拓扑排序用于有向无环图(DAG),其目的是将图中的顶点排成一个线性序列,使得对于图中的任意一条有向边((u, v)),顶点(u)在序列中都排在顶点(v)之前。其基本思想是通过计算每个顶点的入度,然后将入度为(0)的顶点依次加入到排序结果中,并删除这些顶点及其出边,同时更新剩余顶点的入度。重复这个过程,直到所有顶点都被加入到排序结果中或者发现图中存在环(如果还有顶点未被处理且其入度不为(0))。

(二)代码实现与分析(以邻接表为例)

  1. 计算入度

    • 在FindIndegree函数中,首先初始化一个数组indegree,将所有顶点的入度设置为(0)。然后遍历邻接表,对于每个顶点的邻接边,将其指向顶点的入度加(1)。例如,当遍历到顶点(i)的邻接边节点(p)时,执行indegree[p->adjvex]++,这样就正确计算出了每个顶点的入度。时间复杂度为(O(V + E)),其中(V)是图的顶点数,(E)是图的边数,因为需要遍历所有顶点和边来计算入度。
  2. 拓扑排序过程

    • 在TopologicalSort函数中,先调用FindIndegree函数计算入度并将结果存储在indegree数组中。接着创建一个栈(S),将入度为(0)的顶点入栈。然后进入循环,当栈不为空时,弹出栈顶顶点(i),将其加入拓扑排序结果数组topo中,并更新其邻接顶点的入度。如果某个邻接顶点的入度变为(0),则将其入栈。如果最终topo数组中的顶点数量小于图中的顶点数量(G.vexnum),说明图中存在环,返回ERROR;否则返回OK。时间复杂度为(O(V + E)),因为在最坏情况下,每个顶点和边都可能被处理一次。空间复杂度为(O(V)),主要用于存储入度数组indegree、栈(S)和拓扑排序结果数组topo。
      流程示意图如图所示
      在这里插入图片描述
      代码实现如下
//拓扑排序
void FindIndegree(ALGraph &G,int *indegree){//求每个点的入度
	for(int i = 0;i<G.vexnum;i++){
		indegree[i] = 0;//初始化
	}
	for(int i = 0;i<G.vexnum;i++){
		ArcNode * p = G.vertices[i].firstarc;
		while(p != NULL){//为什么写while(!p)就运行不了
			indegree[p->adjvex] ++;
			p = p->nextarc;
		}
	}
}
Status TopologicalSort(ALGraph &G,int topo[]){
	int indegree[G.vexnum];
	FindIndegree(G,indegree);//入度个数存放完毕
	stack<int> S;
	for(int i = 0;i<G.vexnum;i++){
		if(indegree[i] == 0){//入度为0的点
			S.push(i);
		}
	}
	int m = 0;
	while(!S.empty()){//度为0的点
		int i = S.top();S.pop();
		topo[m] = i; m++;//拓扑排序依次存入 m自增
		ArcNode * p = G.vertices[i].firstarc;
		while(p!=NULL){
			//删除度为0的点的出度对其他点的影响
			indegree[p->adjvex] -- ;
			if(indegree[p->adjvex]==0) S.push(p->adjvex);//如果该点入度为0 则入栈
			p = p->nextarc;
		}
	}
	if(m<G.vexnum) return ERROR;//m不到G.vexnum 说明有环
	else return OK;
}

八、关键路径

(一) 原理与算法流程

关键路径算法用于在有向无环图(DAG)中确定项目的最短完成时间以及关键活动序列。其核心原理基于事件的最早发生时间和最迟发生时间的计算。

首先,通过拓扑排序确定图中顶点的一个线性序列。因为只有在拓扑排序可行(即图无环)的前提下才能进行关键路径分析。然后,按照拓扑序列依次计算每个事件的最早发生时间(ve)。从起始事件开始,其最早发生时间初始化为(0),对于其他事件,其最早发生时间是其所有前驱事件的最早发生时间加上相应活动持续时间的最大值。这意味着一个事件必须在其所有前驱活动都完成后才能开始,取最大值是因为要等待最长的前驱路径完成。

接着,计算每个事件的最迟发生时间(vl)。从最后一个事件开始,其最迟发生时间等于其最早发生时间,然后对于其他事件,其最迟发生时间是其所有后继事件的最迟发生时间减去相应活动持续时间的最小值。这保证了在不影响整个项目最短完成时间的前提下,每个事件可以最晚开始的时间。

最后,通过比较每个活动的最早开始时间(等于其起始事件的最早发生时间)和最迟开始时间(等于其终止事件的最迟发生时间减去活动持续时间),若两者相等,则该活动为关键活动,由所有关键活动构成的路径即为关键路径。关键路径决定了整个项目的最短完成时间,因为关键路径上的任何活动延迟都会导致项目整体延迟。

(二) 代码的实现与分析

1.拓扑排序部分
在TopologicalOrder函数中:

  • 先调用FindInDegree函数计算各顶点入度并存入indegree数组,然后初始化空栈S。
  • 遍历顶点,将入度为(0)的顶点入栈。
  • 进入循环,当栈非空时,弹出栈顶顶点i存入topo数组,对其邻接顶点处理:邻接顶点k入度减(1),若入度变为(0)则入栈。
  • 循环结束后,根据topo数组中顶点数量与图顶点数比较判断是否有回路并返回相应结果。
    这部分代码为关键路径计算提供了正确的顶点处理顺序基础,时间复杂度为(O(V + E)),空间复杂度主要取决于栈和入度数组,为(O(V))。

2.关键路径计算部分
在CriticalPath函数中:

  • 先调用TopologicalOrder函数获取拓扑序列,失败则返回ERROR。
  • 初始化ve数组为(0),按拓扑次序遍历顶点,通过邻接边更新后继顶点的ve值,取最大值确保正确计算最早发生时间。
  • 初始化vl数组为最后顶点的ve值,按逆拓扑次序遍历顶点,通过邻接边更新当前顶点的vl值,取最小值确保正确计算最迟发生时间。
  • 最后遍历顶点及其邻接顶点,计算活动的最早开始时间e和最迟开始时间l,若相等则输出关键活动对应的顶点关系。
    整体时间复杂度为(O(V + E)),主要源于对顶点和边的多次遍历,空间复杂度为(O(V)),用于存储ve、vl和topo等数组。代码通过合理的数据结构和循环逻辑,准确地实现了关键路径算法的功能,在项目规划与管理等领域有重要应用价值。
    实现流程如下
    在这里插入图片描述
//关键路径算法

#include <iostream>
using namespace std;

#define MVNum 100                       	//最大顶点数
#define BDNum MVNum * (MVNum - 1)			//最大边数
#define OK 1	
#define ERROR 0 

typedef char VerTexType;

//- - - - -图的邻接表存储表示- - - - - 
typedef struct ArcNode{                		//边结点 
    int adjvex;                          	//该边所指向的顶点的位置
	int weight;								//权值
    struct ArcNode *nextarc;          		//指向下一条边的指针 
}ArcNode; 

typedef struct VNode{ 
    VerTexType data;                    	//顶点信息
    ArcNode *firstarc;                		//指向第一条依附该顶点的边的指针 
}VNode, AdjList[MVNum];               		//AdjList表示邻接表类型 

typedef struct{ 
    AdjList vertices;                 		//邻接表 
	AdjList converse_vertices;				//逆邻接表
    int vexnum, arcnum;              		//图的当前顶点数和边数 
}ALGraph;
//- - - - - - - - - - - - - - - -

//- - - - -顺序栈的定义- - - - -
typedef struct{
	int *base;
	int *top;
	int stacksize;
}spStack;
//- - - - - - - - - - - - - - - -

int indegree[MVNum];						//数组indegree存放个顶点的入度
int ve[BDNum];								//事件vi的最早发生时间
int vl[BDNum];								//事件vi的最迟发生时间
int topo[MVNum];							//记录拓扑序列的顶点序号
spStack S;

//----------------栈的操作--------------------
void InitStack(spStack &S){
	//栈的初始化
	S.base = new int[MVNum];
	if(!S.base)
		exit(1);
	S.top = S.base;
	S.stacksize = MVNum;
}//InitStack

void Push(spStack &S , int i){
	//入栈
	if(S.top - S.base == S.stacksize)
		return;
	*S.top++ = i;
}//Push

void Pop(spStack &S , int &i){
	//出栈
	if(S.top == S.base)
		return;
	i = *--S.top;
}//Pop

bool StackEmpty(spStack S){
	//判断栈是否为空
	if(S.top == S.base)
		return true;
	return false;
}//StackEmpty
//---------------------------------------

int LocateVex(ALGraph G , VerTexType v){
	//确定点v在G中的位置
	for(int i = 0; i < G.vexnum; ++i)
		if(G.vertices[i].data == v)
			return i;
		return -1;
}//LocateVex

int CreateUDG(ALGraph &G){ 
	//创建有向图G的邻接表、逆邻接表
	int i , k;
	
	cout <<"请输入总顶点数,总边数,以空格隔开:";
	cin >> G.vexnum >> G.arcnum;				//输入总顶点数,总边数 
    cout << endl;

	cout << "输入点的名称,如a" << endl;
	
	for(i = 0; i < G.vexnum; ++i){          		//输入各点,构造表头结点表
		cout << "请输入第" << (i+1) << "个点的名称:";
		cin >> G.vertices[i].data;           		//输入顶点值
		G.converse_vertices[i].data = G.vertices[i].data;
		//初始化表头结点的指针域为NULL 
		G.vertices[i].firstarc=NULL;			
		G.converse_vertices[i].firstarc=NULL;
    }//for
	cout << endl;

	cout << "输入边依附的顶点及其权值,如a b 3" << endl;

	for(k = 0; k < G.arcnum;++k){        			//输入各边,构造邻接表
		VerTexType v1 , v2;
		int i , j , w;
		cout << "请输入第" << (k + 1) << "条边依附的顶点及其权值:";
		cin >> v1 >> v2 >> w;                		//输入一条边依附的两个顶点
		i = LocateVex(G, v1);  j = LocateVex(G, v2);
		//确定v1和v2在G中位置,即顶点在G.vertices中的序号 

		ArcNode *p1=new ArcNode;               		//生成一个新的边结点*p1 
		p1->adjvex=j;                   			//邻接点序号为j
		p1->nextarc = G.vertices[i].firstarc;  G.vertices[i].firstarc=p1;
		p1->weight = w;
		//将新结点*p1插入顶点vi的边表头部

		ArcNode *p2=new ArcNode;               		//生成一个新的边结点*p1 
		p2->adjvex=i;                   			//逆邻接点序号为i
		p2->nextarc = G.converse_vertices[j].firstarc;  G.converse_vertices[j].firstarc=p2;
		p2->weight = w;
		//将新结点*p1插入顶点vi的边表头部
    }//for 
    return OK; 
}//CreateUDG

void FindInDegree(ALGraph G){
	//求出各顶点的入度存入数组indegree中 
	int i , count;

	for(i = 0 ; i < G.vexnum ; i++){
		count = 0;
		ArcNode *p = G.converse_vertices[i].firstarc;
		if(p){
			while(p){
				p = p->nextarc;
				count++;
			}
		}//if
		indegree[i] = count;
	}//for
}//FindInDegree

int TopologicalOrder(ALGraph G , int topo[]){ 
    //有向图G采用邻接表存储结构 
    //若G无回路,则生成G的一个拓扑序列topo[]并返回OK,否则ERROR 
	int i , m;
    FindInDegree(G);              				//求出各顶点的入度存入数组indegree中 
    InitStack(S);                          		//栈S初始化为空 
    for(i = 0; i < G.vexnum; ++i)
		if(!indegree[i]) Push(S, i);     		//入度为0者进栈 
	m = 0;                               		//对输出顶点计数,初始为0 
	while(!StackEmpty(S)){                		//栈S非空 
		Pop(S, i);                          	//将栈顶顶点vi出栈
		topo[m]=i;                         		//将vi保存在拓扑序列数组topo中 
		++m;                             		//对输出顶点计数 
		ArcNode *p = G.vertices[i].firstarc;    //p指向vi的第一个邻接点 
		while(p){
			int k = p->adjvex;					//vk为vi的邻接点   
			--indegree[k];                   	//vi的每个邻接点的入度减1 
			if(indegree[k] ==0)  Push(S, k);	//若入度减为0,则入栈 
			p = p->nextarc;                		//p指向顶点vi下一个邻接结点 
		}//while 
	}//while
	
	if(m < G.vexnum)  return ERROR;    			//该有向图有回路 
	else return OK;
}//TopologicalOrder

int CriticalPath(ALGraph G){ 
    //G为邻接表存储的有向网,输出G的各项关键活动
	int n , i , k , j , e , l;
    if (!TopologicalOrder(G, topo))  return ERROR; 
    //调用拓扑排序算法,使拓扑序列保存在topo中,若调用失败,则存在有向环,返回ERROR 
    n = G.vexnum;                  				//n为顶点个数 
    for(i = 0; i < n; i++)               		//给每个事件的最早发生时间置初值0 
		ve[i] = 0; 


    /*――――――――――按拓扑次序求每个事件的最早发生时间-――――-―――――*/ 
    for(i = 0;i < n; i++){                 
		k = topo[i];                   			//取得拓扑序列中的顶点序号k             
		ArcNode *p = G.vertices[k].firstarc;    //p指向k的第一个邻接顶点  
		while(p != NULL){            			//依次更新k的所有邻接顶点的最早发生时间   
			j = p->adjvex;               		//j为邻接顶点的序号                   
			if(ve[j] < ve[k] + p->weight)    	//更新顶点j的最早发生时间ve[j] 
				ve[j] = ve[k] + p->weight;     
			p = p->nextarc;              		//p指向k的下一个邻接顶点  
		} //while 
    } //for 

    for(i=0;i<n;i++)                 			//给每个事件的最迟发生时间置初值ve[n-1] 
		vl[i]=ve[n-1];
	
    /*――――――――――按逆拓扑次序求每个事件的最迟发生时间-――――-―――――*/ 
    for(i = n - 1;i >= 0; i--){               
		k = topo[i];                   			//取得拓扑序列中的顶点序号k             
		ArcNode *p = G.vertices[k].firstarc;    //p指向k的第一个邻接顶点  
		while(p != NULL){            			//根据k的邻接点,更新k的最迟发生时间   
			j = p->adjvex;              		//j为邻接顶点的序号                   
			if(vl[k] > vl[j] - p->weight)    	//更新顶点k的最迟发生时间vl[k] 
				vl[k] = vl[j] - p->weight;       
			p = p->nextarc;              		//p指向k的下一个邻接顶点  
		}//while 
    }//for 

    /*――――――――――――判断每一活动是否为关键活动-――――――-―――――*/
	cout << endl;
	cout << "关键活动路径为:";
    for(i = 0;i < n; i++){                		//每次循环针对vi为活动开始点的所有活动 
        ArcNode *p = G.vertices[i].firstarc;    //p指向i的第一个邻接顶点  
        while(p != NULL) {    
			j = p->adjvex;             			//j为i的邻接顶点的序号    
			e = ve[i];                 			//计算活动<vi, vj>的最早开始时间 
			l = vl[j] - p->weight;      		//计算活动<vi, vj>的最迟开始时间 
			if(e == l)               			//若为关键活动,则输出<vi, vj> 
				cout << G.vertices[i].data << "-->" << G.vertices[j].data << " ";
			p = p->nextarc;              		//p指向i的下一个邻接顶点  
		} //while 
	} //for  
	return OK;
}//CriticalPath

int main(){
	cout << "************算法6.13 关键路径算法**************" << endl << endl;
	ALGraph G;
	CreateUDG(G);
	int *topo = new int [G.vexnum];
	
	cout << endl;
	cout << "有向图创建完成!" << endl << endl;
	
	if(!CriticalPath(G))
		cout << "网中存在环,无法进行拓扑排序!" <<endl << endl;
	cout << endl;
	return OK;
}//main

九、 代码汇总

邻接矩阵

/*
图的创建
深度优先遍历
广度优先遍历
最小生成树(Kruskal和Prim)
最短路径(Djikstra和Floyd)
*/

#include<bits/stdc++.h>
using namespace std;

#define MaxInt  32767       // 最大值∞ 
#define MVNum	 100		// 最大顶点数 
#define MAXSIZE 100 		//最大长度
#define OVERFLOw -2
#define OK 1
#define TeleType char
#define ERROR 0

typedef int Status;
typedef enum {DG, DN, UDG, UDN} GraphKind;  
//{有向图,有向网,无向图,无向网} 
typedef char VerTexType;  //假设顶点的数据类型为字符型
typedef int ArcType;  // 假设边的权值类型为整型
typedef struct { 
	VerTexType   vexs[MVNum]; // 顶点表 
	ArcType     arcs[MVNum][MVNum]; // 邻接矩阵 
	int     vexnum, arcnum; // 图的当前顶点数和弧(边)数 
	GraphKind kind;//图的种类
} AMGraph;
//辅助数组 Edges定义
typedef struct Edge {
	VerTexType Head;//边的起点
	VerTexType Tail;//边的终点
	ArcType lowcost;//边的权值
}Edge;
Edge Edges[MVNum];//两个顶点的信息和权值
//确定顶点的位置
int Locate(AMGraph &G, VerTexType v){
	int t = -1;
	for(int i = G.vexnum-1;i>=0;i--){
		if(G.vexs[i] == v){
			t = i;
		}
	}
	return t;//return -1 相当于没找到
}

//邻接矩阵创建无向网
Status CreateUDN(AMGraph &G){
	cout<<"所需点的个数:";cin>>G.vexnum;
	cout<<"所需边的个数:";cin>>G.arcnum;
	G.kind = UDN;//无向网
	//输入顶点的信息
	cout<<"请依次输入顶点信息"<<endl;
	for(int i = 0;i<G.vexnum;i++){
		cout<<"第"<<i+1<<"个:";
		cin>>G.vexs[i];
	}
	//初始化邻接矩阵
	for(int i=0;i<G.vexnum ;i++) 	 //初始化邻接矩阵
		for(int j=0;j<G.vexnum ;j++) 	
			G.arcs[i][j]=MaxInt;
	//输入边的信息
	cout<<"请依次输入边的信息"<<endl;
	cout<<"输入格式 点v1 点v2 边长度a"<<endl;
	for(int i = 0;i<G.arcnum;i++){
		VerTexType v1,v2;
		ArcType a;
		cout<<"第"<<i+1<<"个:";
		cin>>v1>>v2>>a;
		Edges[i].Head = v1;
		Edges[i].Tail = v2;
		Edges[i].lowcost = a;
		int m = Locate(G,v1),n = Locate(G,v2);
		G.arcs[m][n] = G.arcs[n][m] = a;//构建边成功
	}
	return OK;
}
//打印邻接矩阵内容
void PrintUDN(AMGraph &G){
	cout<<"vexnum:"<<G.vexnum<<"\tarcnum:"<<G.arcnum<<endl<<endl;
	cout<<"\t";
	for(int i = 0 ;i<G.arcnum;i++)cout<<G.vexs[i]<<"\t";
	cout<<endl;
	for(int i = 0;i<G.vexnum;i++){
		cout<<G.vexs[i]<<"\t";
		for(int j = 0;j<G.vexnum;j++){
			if(G.arcs[i][j] == MaxInt) cout<<"∞\t";
			else cout<<G.arcs[i][j]<<"\t";
		}
		cout<<endl;
	}	
}
bool visited[MVNum];
void dfs(AMGraph &G,int v){
	cout<<G.vexs[v];visited[v] = true;
	for(int w = 0;w<G.vexnum;w++){
		if(G.arcs[v][w]!=MaxInt&&!visited[w])
			dfs(G,w);
	}
}
//广度优先搜索
void bfs(AMGraph &G) {
	for (int i = 0; i < G.vexnum; i++) {
		visited[i] = false;
	}
	
	queue<int> Q;
	
	for (int i = 0; i < G.vexnum; i++) {
		if (!visited[i]) {
			visited[i] = true;
			cout << G.vexs[i];
			Q.push(i);
			
			while (!Q.empty()) {
				int u = Q.front();
				Q.pop();
				
				for (int v = 0; v < G.vexnum; v++) {
					if (G.arcs[u][v]!= MaxInt &&!visited[v]) {
						visited[v] = true;
						cout << G.vexs[v];
						Q.push(v);
					}
				}
			}
		}
	}
}
//求最小生成树
void Prim(AMGraph &G,int v0){
	cout<<"利用Prim算法构建最小生成树所加入的边是"<<endl;
	//初始化
	int v = v0;
	int k ;//存放最小的权值的下标
	int vset[MVNum];//存放在U中的点的下标
	ArcType lowcost[MVNum];//存放权值
	for(int i = 0;i<G.vexnum;i++){
		lowcost[i] = G.arcs[v][i];//存放每个点到v的权值
		vset[i] = 0;//点都还未进U 所以都是0
	}
	vset[v] = 1;//首先把第一个点存进去

	for(int i = 0;i<G.vexnum - 1;i++){//剩余的其他几个节点遍历,依次存入U 共需要遍历n-1次
		//找lowcost的最小点
		int min = MaxInt;
		for(int j = 0;j<G.vexnum;j++){
			if(min>lowcost[j] && vset[j] == 0){//找未在U中的最小值
				min = lowcost[j];//找最小值
				k = j;//更新最小值对应点的下标(k点要存进U中去)
			}
		}
		vset[k] = 1;//第k个点存进U
		int prevV = v;//存入边的起点
		v = k;//更新v为k  此时v为存入的边的终点
		cout << G.vexs[prevV] << " - " << G.vexs[v] << " : " << lowcost[v] << endl;
		//更新lowcost  只是更新值 不会显露lowcost的来源信息 就是v->w,v,w不知
		for(int j = 0;j<G.vexnum;j++){
			if(vset[j] == 0 && G.arcs[v][j]<lowcost[j]){
				lowcost[j] = G.arcs[v][j];
			}
		}
	}
}
// 比较函数,用于指定按照lowcost进行排序
bool compareEdges(const Edge& a, const Edge& b) {
	return a.lowcost < b.lowcost;
}
void Kruskal(AMGraph &G){
	cout<<"利用Kruskal算法构建最小生成树加入的边是"<<endl;
	//辅助数组 用祖先的所在位置表示所在连通分量
	//如果某条选取边的起点终点的Vexset值相等 则不能加进去
	//加进去会使得成环
	int Vexset[MVNum];
	sort(Edges,Edges+G.arcnum,compareEdges);//边按照权值排序
	for(int i = 0;i<G.arcnum;i++){
		Vexset[i] = i;
	}
	//遍历所有边
	for(int i = 0;i<G.arcnum;i++){
		//找当前边的下标
		int v1 = Locate(G,Edges[i].Head);
		int v2 = Locate(G,Edges[i].Tail);
		//当前边的首尾节点所在的连通分量vs1,vs2
		int vs1 = Vexset[v1];
		int vs2 = Vexset[v2];
		if(vs1!=vs2){//说明成不了环 输出
			cout<<Edges[i].Head<<" - "<<Edges[i].Tail<<": "<<Edges[i].lowcost<<endl;
			//合并vs1,vs2(并查集操作)
			for(int j = 0;j<G.arcnum;j++){
				if(Vexset[j] == vs2){
					Vexset[j] = vs1;//vs2合并为vs1
				}
			}
		}
	}
	
}
void PrintPath_Dijkstra(AMGraph &G,int start,int end,int Path[]){
	cout<<"从"<<G.vexs[start]<<"到"<<G.vexs[end]<<"的最短路径:";
	int current = end;
	//倒着存放路径
	stack<int>pathStack;
	while(current != start){
		pathStack.push(current);
		current = Path[current];
	}
	pathStack.push(start);
	
	VerTexType temp =  G.vexs[pathStack.top()];
	pathStack.pop();
	if(!pathStack.empty()) cout<<temp;
	
	while(!pathStack.empty()){
		cout<<"->"<<G.vexs[pathStack.top()];
		pathStack.pop();
	}
}
void Dijkstra(AMGraph &G,int v0){
	cout<<"利用Dijkstra算法计算得 "<<G.vexs[v0]<<" 到各点的最短路径的信息如下"<<endl;
	//----初始化----
	bool S[G.vexnum];//存放已经存的顶点
	int Path[G.vexnum];//存放自起点来的在最短路径里面的点的下标
	int Dist[G.vexnum];//存放自起点来的最短路径的长度
	int n = G.vexnum;
	for(int i = 0;i<n;i++){
		S[i] = false;//都还未存入
		Dist[i] = G.arcs[v0][i];
		if(Dist[i]!=MaxInt){
			Path[i] = v0;
		}else{
			Path[i] = -1;
		}
	}
	S[v0] = true;//起点存入
	Dist[v0] = 0;//起点到自身的距离
	//----初始化完毕----
	for(int i = 1;i<n;i++){//剩余n-1个点
		int min = MaxInt;
		int v;
		//未加入S的点中找最小值
		for(int i = 0;i<n;i++){
			if(!S[i] && Dist[i]<min){
				v = i;//所找到的最短路径中一段的终点
				min = Dist[i];//最小值
			}
		}
		S[v] = true;//v0->v的最短路径已找到 加入进去
		for(int i = 0;i<n;i++){
			if(Dist[i]>Dist[v]+G.arcs[i][v]){
				//找到更短的边时 加入
				Dist[i] = Dist[v] + G.arcs[i][v];
				Path[i] = v;
			}
		}
	}
	//输出到每个点的最短路径
	for(int i = 0;i<n;i++){
		if(i != v0){ //消除自身到自身的路径
		PrintPath_Dijkstra(G,v0,i,Path);
		cout<<"\t路径长度为"<<Dist[i]<<endl;
		}
	}
}

void Floyd(AMGraph &G){
	int n = G.vexnum;
	int Path[MVNum][MVNum];
	int Dist[n][n];
	for(int i = 0;i<n;i++){
		for(int j = 0;j<n;j++){
			Dist[i][j] = G.arcs[i][j];
			if(Dist[i][j]!=MaxInt && i!=j){//i 和 j 之间有弧 将j的前驱置为 i
				Path[i][j] = i;//说明从i到j的最短路径的终点的前一个点是i
			}else Path[i][j] = -1;
		}
	}
	
	for(int k = 0;k<n;k++){
		for(int i = 0;i<n;i++){
			for(int j = 0;j<n;j++){
				if(Dist[i][j]>Dist[i][k]+Dist[k][j]){//每个点都当一次中间点 最后得到最短路径
					Dist[i][j] = Dist[i][k]+Dist[k][j];
					Path[i][j] = Path[k][j];
					//因为Path[i][j]代表的是i 到 j的最短路径的终点的前一个点的下标
					//所以更改为中间点
				}
			}
		}
	}
	//打印出来D(n-1)和Path(n-1)的内容
	cout<<"通过Floyd算法"<<endl;
	cout<<"经过 "<<n<<" 次迭代后"<<endl;
	cout<<"D"<<(n-1)<<"和Path"<<(n-1)<<"的结果如下所示"<<endl;
	cout<<"\nD("<<n-1<<"):\n"<<endl;
	
	for(int i = 0;i<n;i++){
		if(i==0)cout<<"\t";
		cout<<"("<<G.vexs[i]<<")"<<"\t";
	}
	cout<<endl;
	
	for(int i = 0;i<n;i++){
		cout<<"("<<G.vexs[i]<<")"<<"\t";
		for(int j = 0;j<n;j++){
			cout<<Dist[i][j]<<"\t";
		}
		cout<<endl;
	}
	cout<<"\nPath("<<n-1<<"):\n"<<endl;
	
	for(int i = 0;i<n;i++){
		if(i==0)cout<<"\t";
		cout<<"("<<G.vexs[i]<<")"<<"\t";
	}
	cout<<endl;
	
	for(int i = 0;i<n;i++){
		cout<<"("<<G.vexs[i]<<")"<<"\t";
		for(int j = 0;j<n;j++){
			cout<<Path[i][j]<<"\t";
		}
		cout<<endl;
	}

}
int main(){
	AMGraph G;
	CreateUDN(G);
	PrintUDN(G);
	for(int i =0;i<G.vexnum;i++){
		visited[i] = false;
	}
	cout<<"DFS遍历后的结果 "<<endl;
	dfs(G,0);
	cout<<"\nBFS遍历后的结果 "<<endl;
	bfs(G);
	cout<<endl;
	Prim(G,0);//0开始构造最小生成树
	Kruskal(G);
	Dijkstra(G,0);
	Floyd(G);
	return 0;
}
/*
6 7
a
b
c
d
e
f
a b 3
a c 6
a e 5
b c 9
c d 7
d e 4
d f 2
*/

运行结果如下
在这里插入图片描述

邻接表

/*
图的创建
深度优先遍历
广度优先遍历
拓扑排序
关键路径
*/

#include<bits/stdc++.h>
using namespace std;

#define MaxInt  32767       // 最大值∞ 
#define MVNum	 100		// 最大顶点数 
#define MAXSIZE 100 		//最大长度
#define OVERFLOw -2
#define OK 1
#define TeleType char
#define InfoType int//代表边的权值
#define ERROR 0

typedef int Status;
typedef enum {DG, DN, UDG, UDN} GraphKind;  
//{有向图,有向网,无向图,无向网} 
typedef char VerTexType;  //假设顶点的数据类型为字符型
typedef int ArcType;  // 假设边的权值类型为整型

typedef struct ArcNode {//边结点
	int   adjvex; 	// 该弧(边)所指向的顶点的位置 
	struct ArcNode *nextarc; //指向下一条边
	int weight;		//权值	
} ArcNode; 

typedef struct VNode {//表头结点
	VerTexType   data; 	// 顶点信息 
	ArcNode       *firstarc; // 指向第一条依附该顶点的弧(边) 
} VNode, AdjList[MVNum]; 

typedef struct { 
	AdjList   vertices; 	//邻接表 
	AdjList   converse_vertices;//逆邻接表
	int   vexnum, arcnum; // 图的当前顶点数和弧(边)数
	GraphKind kind;//图的种类 
} ALGraph; 

int topo[MVNum];//拓扑序列
int ve[MVNum];//最早发生时间
int vl[MVNum];//最晚发生时间
//找点的位置
int Locate(ALGraph &G,VerTexType v){
	int t = -1;
	for(int i = 0;i<G.vexnum;i++){
		if(G.vertices[i].data == v)
		{
			t = i;
			break;
		}
	}
	return t;
}
//邻接表创建有向网
Status CreateDG(ALGraph &G){
	cout<<"所需点的个数:";cin>>G.vexnum;
	cout<<"所需边的个数:";cin>>G.arcnum;
	G.kind = DG;//有向网
	//输入顶点的信息
	//构造头节点
	cout<<"请依次输入顶点信息"<<endl;
	for(int i = 0;i<G.vexnum;i++){
		cout<<"第"<<i+1<<"个:";
		cin>>G.vertices[i].data;
		G.converse_vertices[i].data = G.vertices[i].data;
		G.vertices[i].firstarc = nullptr;
		G.converse_vertices[i].firstarc = nullptr;
	}
	//边的信息 采用头插法
	cout<<"请依次输入边的信息"<<endl;
	cout<<"输入格式 点v1 点v2 权值w"<<endl;
	for(int k = 0;k<G.arcnum;k++){
		VerTexType v1,v2;
//		ArcType a;
		int w;
		cin>>v1>>v2>>w;
		int i = Locate(G,v1);
		int j = Locate(G,v2);
		ArcNode* p = new ArcNode;//新的边节点
		p->adjvex = j;//边指向的顶点的序号
		//头插法
		//p的下一个边改成第i个点后依附的第一条弧
		p->nextarc = G.vertices[i].firstarc;
		G.vertices[i].firstarc = p;
		p->weight = w;
		
		//逆邻接表的存储
		ArcNode* q = new ArcNode;//新的节点
		q->adjvex = i;//反着插 便于找出度
		q->nextarc = G.converse_vertices[j].firstarc;
		G.converse_vertices[j].firstarc = q;
		q->weight = w;
	}
	return OK;
}
//打印邻接表
void PrintDG(ALGraph &G) {
	for (int i = 0; i < G.vexnum; i++) {
		cout << "[" << G.vertices[i].data << "]";
		
		ArcNode* p = G.vertices[i].firstarc;
		while (p!= nullptr) {
			int t = p->adjvex;
			cout << "->" << p->adjvex << "("<< G.vertices[t].data<<")";
			p = p->nextarc;
		}
		
		cout << endl;
	}
}
bool visited[MVNum];
void dfs(ALGraph &G,int v){
	cout<<G.vertices[v].data;
	visited[v] = true;
	ArcNode *p = G.vertices[v].firstarc;
	while(p!=nullptr){
		int w = p->adjvex;//p的周围的点的下标
		if(!visited[w]) dfs(G,w);//第w个点未被访问 =>访问下
		p = p->nextarc;
	}
}
void bfs(ALGraph &G){
	for (int i = 0; i < G.vexnum; i++) {
		visited[i] = false;
	}
	queue<int> Q;
	
	for(int i =0;i<G.vexnum;i++){
		if(!visited[i]){
			visited[i] = true;
			cout<<G.vertices[i].data;
			Q.push(i);
			while(!Q.empty()){
				int u = Q.front();//Q存放的是下标
				Q.pop();
				ArcNode *p = G.vertices[u].firstarc;
				while(p!=nullptr){
					if(!visited[p->adjvex]){
						visited[p->adjvex] = true;
						cout<<G.vertices[p->adjvex].data;
						Q.push(p->adjvex);
					}
					p = p->nextarc;
				}
			}
		}
	}
	cout<<endl;
}
//拓扑排序
void FindIndegree(ALGraph &G,int *indegree){//求每个点的入度
	for(int i = 0;i<G.vexnum;i++){
		indegree[i] = 0;//初始化
	}
	for(int i = 0;i<G.vexnum;i++){
		ArcNode * p = G.vertices[i].firstarc;
		while(p){
			indegree[p->adjvex] ++;
			p = p->nextarc;
		}
	}
}
Status TopologicalSort(ALGraph &G,int topo[]){
	int indegree[G.vexnum];
	FindIndegree(G,indegree);//入度个数存放完毕
	stack<int> S;
	for(int i = 0;i<G.vexnum;i++){
		if(indegree[i] == 0){//入度为0的点
			S.push(i);
		}
	}
	int m = 0;
	while(!S.empty()){//度为0的点
		int i = S.top();S.pop();
		topo[m] = i; m++;//拓扑排序依次存入 m自增
		ArcNode * p = G.vertices[i].firstarc;
		while(p!=NULL){
			//删除度为0的点的出度对其他点的影响
			indegree[p->adjvex] -- ;
			if(indegree[p->adjvex]==0) S.push(p->adjvex);//如果该点入度为0 则入栈
			p = p->nextarc;
		}
	}
	if(m<G.vexnum) return ERROR;//m不到G.vexnum 说明有环
	else return OK;
}
//关键路径
int CriticalPath(ALGraph &G){
	if (!TopologicalSort(G, topo))  return ERROR;
	int n = G.vexnum;
	for(int i = 0;i<n;i++){
		ve[i] = 0;//初始化一下
	}
	//正向拓扑序处理ve数组
	for(int i = 0;i<n;i++){
		int k = topo[i];//按照拓扑排序的顺序
		//除了源点每个点都有入度
		ArcNode *p = G.vertices[k].firstarc;
		while(p != nullptr){
			int  j = p->adjvex;//p的箭头指向对应的序号
			if(ve[j] < ve[k]+p->weight){
				ve[j] = ve[k]+p->weight;
			}
			p = p->nextarc;
		}
	}
	//逆向拓扑序处理vl数组
	for(int i = 0;i<n;i++){
		vl[i] = ve[n-1];
	}
	for(int i = n-1;i>=0;i--){
		int k = topo[i];
		ArcNode *p = G.vertices[k].firstarc;
		while(p != nullptr){
			int j  = p->adjvex;
			if(vl[k] > vl[j] - p->weight){
				vl[k] = vl[j] - p->weight;
			}
			p = p->nextarc;
		}
	}
	//判断是否为关键路径
	cout<<"\n关键活动路径为:"<<endl;
	for(int i = 0;i<n;i++){
		ArcNode *p = G.vertices[i].firstarc;
		while(p != nullptr){
			int j = p->adjvex;//p是由vi发出的指向vj所以为vi --p--> vj
			int e = ve[i];//活动的最早开始时间为箭头起始事件的最早开始时间
			int l = vl[j] - p->weight;//活动的最晚开始时间为箭头指向事件的最晚开始时间减去箭头权值
			if(e == l){//说明是关键路径
				cout<<G.vertices[i].data<<" -> "<<G.vertices[j].data<<" 、";
			}
			p = p->nextarc;
		}
	}
// 判断是否为关键路径并输出完整关键路径
	cout << "\n完整路径为: ";
// 用于标记是否已经开始输出路径
	bool pathStarted = false; 
	for (int i = 0; i < n; i++) {
		int j;
		ArcNode *p = G.vertices[i].firstarc;
		while (p!= nullptr) {
			j = p->adjvex;
			int e = ve[i];
			int l = vl[j] - p->weight;
			// 如果是关键路径则输出连接关系
			if (e == l) { 
				if (!pathStarted) {
					// 如果是路径起始点,直接输出起始点并标记路径已开始
					cout << G.vertices[i].data; 
					pathStarted = true;
				}
				cout << " -> " << G.vertices[j].data;
				// 如果已经到达拓扑排序最后一个顶点对应的节点,结束循环
				if (G.vertices[j].data == G.vertices[topo[n - 1]].data) {
					break;
				}
			}
			p = p->nextarc;
		}
		// 如果路径已经开始且已经到达拓扑排序最后一个顶点对应的节点,结束外层循环
		if (pathStarted && G.vertices[topo[n - 1]].data == G.vertices[j].data) {
			break;
		}
	}
	return OK;
}
int main(){
	ALGraph G;
	CreateDG(G);
	PrintDG(G);
	for(int i =0;i<G.vexnum;i++){
		visited[i] = false;
	}
	cout<<"DFS遍历后的结果:"<<endl;
	dfs(G,0);
	cout<<"\nBFS遍历后的结果:"<<endl;
	bfs(G);

	Status sortStatus = TopologicalSort(G, topo);
	if (sortStatus == OK) {
		cout << "\n拓扑排序结果: ";
		for (int i = 0; i < G.vexnum; i++) {
			cout << G.vertices[topo[i]].data;
			if (i!= G.vexnum - 1) cout << " -> ";
		}
		cout << endl;
	} else {
		cout << "图中存在环,无法进行拓扑排序。" << endl;
	}
	CriticalPath(G);
	return 0;
}
/*
9 11
a
b
c
d
e
f
g
h
i
a b 6
a c 4
a d 5
b e 1
c e 1
d f 2
e g 9
e h 7
f h 4
g i 2
h i 4
*/

运行结果如下
在这里插入图片描述

十、总结

通过以上对图的各种操作和算法的详细介绍,我们可以看到图数据结构在处理复杂关系问题时的强大能力。从图的创建开始,无论是邻接矩阵还是邻接表的存储方式,都为后续的操作提供了基础。深度优先遍历和广度优先遍历是图的基本遍历方式,能够帮助我们探索图的结构。最小生成树算法(Prim 和 Kruskal)在解决网络设计等问题中具有重要应用,如构建通信网络时找到成本最小的连接方式。最短路径算法(Djikstra 和 Floyd)则在导航、路由等领域发挥关键作用,帮助我们找到两点之间的最优路径。拓扑排序在任务调度、课程安排等方面有着实际意义,确保各项任务或课程能够按照合理的顺序进行。在实际应用中,我们需要根据具体问题的特点选择合适的图存储结构和算法,以达到高效解决问题的目的。同时,理解这些算法的原理和实现细节,有助于我们在面对更复杂的图相关问题时能够灵活运用和创新。希望本文对大家理解图的操作和算法有所帮助,在今后的学习和工作中能够更好地应用图数据结构解决实际问题。

数据结构核心:树与图的存储、遍历及经典算法解析 数据结构是计算机科学的基础,它研究数据的组织、存储和操作方式,直接影响程序的效率和可维护性。从线性结构到非线性结构,思维模式需要从一维跃迁至多维。树和图作为两种核心的非线性数据结构,分别模拟了层次关系和网络关系,其设计逻辑围绕高效表示与访问复杂关系展开。深度优先遍历和广度优先遍历是探索这两种结构的通用范式,体现了搜索思想,是回溯、动态规划等高级算法的基础。在工程实践中,对树和图的深刻理解,能直接提升系统设计能力与代码质量,例如在数据库索引、社交网络分析、路径规划等场景中,二叉搜索树、图的邻接表存储、Dijk 阅读详情

相关推荐

28、存储区域网络(SAN)拓扑结构与设计全解析

本文全面解析了存储区域网络(SAN)的拓扑结构与设计方法,涵盖光纤通道仲裁环(FC-AL)、交换式结构、SAN分区技术及五种交换式拓扑类型(星型、级联、环形、网状、树形)。详细介绍了SAN的设计阶段、分区设置的重要性与操作步骤,并提供了多项配置最佳实践。文章还分析了不同拓扑在实际场景中的应用,总结了关键要点,并展望了SAN未来的发展趋势,帮助读者构建高效、可靠的企业级存储网络。

tgb34567890的博客 149

拓扑结构模型——数据的存储和使用

拓扑结构

changxm11的专栏 1941

绑定压力的计算方法

压强的公式为P=F/S,其中P为ACF要求的本压压强范围,S为受压面积,F为本压压头施加的力。绑定参数需要按线体按压头进行DOE确认,并使用DOE确认好的参数按压头重新绑定标识好送ACF厂商实测固化率、粒子变形的情况,避免绑定参数导致的异常,在绑定参数的设定过程中温度和时间可以使用仪器实测但绑定的压力则需要换算。工程上绑定压力换算的原则是:1、FPC属于柔性物料,柔性物料的受压面积按FPC绑定的受压面积=FPC绑定的长度。一、工程上FOG FPC本压压力换算(柔性材料以挤压的接触面积=受力面积)F=P。

weixin_53757859的博客 1533

【Java数据结构】04-图(Prim,Kruskal,Dijkstra,topo)

讲解最小支撑树(Prim算法、Kruskal算法)、最短路径(Dijkstra算法)、拓扑排序的实现,图的定义、邻接矩阵和邻接表、DFS和BFS(广度优先遍历和深度优先遍历)

m0_73569204的博客 1338

MQTT阿里云OTA 调试记录

2.验证,这个验证是什么意思呢,就是阿里云会把你包的信息,比如包大小,streamid(相当于包的识别码),版本号发送给你的设备,然后如果你没发送升级成功的包给阿里云,这里会是一直验证中的状态,那你的设备每次重新连接阿里云,发送connect包给阿里云的时候,阿里云就会发送一次这个验证(upgrade)包。提取到了之后就可以发送请求包了,这个请求包比较坑,也可能是我没付费,他的包一次最多只有1024个字节的数据。之前说了,验证状态下,设备发送connect包给阿里云,阿里云就会发一个upgrade包给你。

qq_42585211的博客 1231

深入解析MTK平台hang_detect机制:从原理到实战调试

本文深入解析了MTK平台hang_detect机制的工作原理与实战调试技巧。作为系统健康监测的核心机制,hang_detect通过多层级监控体系检测应用层和内核层异常,结合Kernel_exception等日志信息快速定位问题。文章详细介绍了典型问题案例、调试方法及预防措施,帮助开发者提升MTK平台系统稳定性调试能力。

purple的博客 1004

网络的拓扑结构

网络的划分 局域网(覆盖范围1km以内) 城域网(覆盖范围20km以内) 广域网(覆盖访问20km以上) IP地址 ipv4: uint_32_t ipv6: uint_8_t[16] 用于在网络中唯一标识一台主机 端口号 unit16_t 在一台主机上唯一标识一个进程。 网络中的每条数据都带有两条信息:源端端口号/对端端口号 IP地址和端口号搭配使用,表示一台主机上的哪一条数据,要发送到哪一台主机的哪一个进程上去。 通信协议 通信协议:数据格式约定 通过网络中服务不同,使用的协议不同,划分了不同的协

Diligent_wu的博客 2591

MounRiver避坑实录:手把手迁移CH32V307 EVT工程到自定义目录

本文详细记录了将沁恒CH32V307的EVT工程迁移到自定义目录的完整流程与避坑指南。针对MounRiver Studio的虚拟链接机制,重点讲解了如何清理旧配置、重新设置头文件路径、链接脚本等关键步骤,帮助开发者创建独立、可维护的MCU项目工程。

uber9的博客 243

Prim与Kruskal算法在两种存储结构下的实现

Prim与Kruskal算法总结与C语言实现

weixin_46127065的博客 581

妙算(manifold-2G)通过网线连接笔记本实现静态ip上网

本质就是共享笔记本wifi,配置以太网静态ip,然后在工控机上连接

slender_1031的博客 903

【Unity】Unity性能分析解决方案——UPR

由Unity中国团队提供的一套性能优化解决方案,其是一系列性能工具的集合,并可将收集的数据进行可视化。

Davidorzs的博客 1008

【数据结构】图的应用---最小生成树(Prim,Kruskal)、最短路径(BFS,Dijkstra,Floyd)、拓扑排序、关键路径、有向无环图表达式

最小生成树(Prim,Kruskal)、最短路径(BFS,Dijkstra,Floyd)、拓扑排序、关键路径、有向无环图表达式

m0_61628700的博客 1320

Hadoop在大数据领域的电商数据分析案例

随着电商行业的迅猛发展,每天都会产生海量的数据,如用户浏览记录、购买行为、商品信息等。如何从这些数据中提取有价值的信息,为电商企业的决策提供支持,成为了电商企业面临的重要问题。Hadoop作为一个开源的分布式计算平台,具有高可靠性、高扩展性和高效性等特点,能够很好地处理大数据。本文的目的就是通过实际案例,详细介绍Hadoop在电商数据分析中的应用,范围涵盖了从数据采集、存储到分析的整个流程。本文首先介绍背景信息,让读者了解Hadoop在电商数据分析中的重要性和应用场景。

Java大师兄的博客 1178

数据结构:图

p.s.自用

2301_79277083的博客 2448

LAMMPS-使用MATLAB计算径向分布函数

首先需要使用LAMMPS进行分子动力学模拟,并获取到所需的数据文件,LAMMPS是一款开源的分子动力学模拟软件,可以模拟原子、分子和离子的运动行为,对于本文的目标假设你已经完成了LAMMPS的安装和模拟过程,获得了数据文件。)是一种重要的统计工具,用于描述原子或分子之间的空间分布情况,通过计算RDF可以获得物质的结构信息、相互作用距离以及相位转变等重要参数,本文将介绍如何使用MATLAB计算LAMMPS生成的数据文件的径向分布函数。变量替换为你的实际数据文件的名称。

weixin_50547796的博客 514

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

数据结构(十一)----图的应用

数据结构(十一)----图和有向无环图的应用 一.最小生成树 1.Prim算法(普里姆) 2.Kruskal算法(克鲁斯卡尔): 二.最短路径(BFS算法) 1.单源最短路径 (1)BFS算法(无权图) (2)Dijkstra算法(带权图,无权图) 2.各顶点间的最短路径 (1)Floyd(带权图,无权图) 三.有向无环图(DAG) 1.算术表达式 2.拓扑排序 •逆拓扑排序 3.关键路径

weixin_69884785的博客 2574

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

数据结构与算法要点总结(8):图的存储、图的遍历、最小生成树、最短路径、活动网络

DFS由于有回溯这个步骤,因此需要记录"来时路",因此需要使用栈或递归。BFS是一层一层 or 一圈一圈进行的,因此需要借助队列。

wow_10086的博客 1149

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

数据结构之图的基本概念及应用(超详细)

图是一种非常常用的一类数据结构,但同时它也是最为复杂的,几乎涉及到之前所有学过的数据结构,学习该数据结构不光需要对之前的知识都有所了解,还需要付出大量的时间和耐心。

m0_74140776的博客 2517

机试: 图 (图的存储、图的遍历、最短路径、最小生成树、拓扑排序、关键路径)

参考《算法笔记》 目录图的存储图的遍历 图的存储 邻接矩阵 用一个二维数组表示,在顶点数小于 1000 时使用 (否则空间开销太大) 邻接表 每个顶点的出边可以偷懒用 vector 表示 (不用链表),因此如果邻接表只存放每条边的终点编号,而不存放边权, 则 vector 中的元素类型可以直接定义为 int 型: vector<int> Adj[N]; 如果需要同时存放边的终点编号和边权, 那么可以建立结构体 Node, 用来存放每条边的终点编号和边权 struct Nod.

连理o的博客 414

数据结构——图(详解)

本文系统讲解图数据结构,涵盖基本概念(无向/有向图、连通性)、存储方式(邻接矩阵、邻接表等)、遍历算法(DFS/BFS)、最小生成树(Kruskal/Prim)、最短路径(Dijkstra)、拓扑排序及关键路径。采用C语言从底层实现并提供代码接口,帮助掌握图的理论与应用。

2501_93095927的博客 3231

数据结构--图--知识点总结

数据结构–图–总结 1.图的基本概念 2.图的存储结构 3.图的遍历 BFS、DFS 4.图的应用 4.1 最小生成树 Prim、Kruskal 4.2 最短路径 Dijstra、Floyd 4.3 AOV网 拓扑排序 4.4 AOE网 关键路径 1.图的基本概念 图、无向图、有向图、完全图 度、入度、出度 路径:由顶点和相邻顶点序偶构成的边所形成的序列 连通...

不行不至 1万+

『基础』DS-6图

25版王道数据结构第六章图全部学习笔记

weixin_43287494的博客 1016

【数据结构复习之路】图(严蔚敏版)两万余字&超详细讲解

图(Graph)是一种复杂的非线性数据结构,由顶点集合及顶点间的关系(也称弧或边)集合组成。可以表示为:GVVR其中V是顶点的有穷非空集合;VR是顶点之间关系的有穷集合,也叫做弧或边集合。弧是顶点的有序对,边是顶点的无序对。⚠️注意:线性表可以是空表,树可以是空树,但图不可以是空。上图中画红叉的图是不符合图的定义的!1.1 有向图若vw>∈VR,则vw表示从v到w的一条弧,且称v为弧尾,称w为弧头。

m0_74215326的博客 2566

数据结构笔记5:图

图的基本概念 题目 图的存储结构: 题目 图的遍历 题目 图的应用 最小生成树 Prim算法 Kruskal算法 最短路径 Dijkstra Floyd 拓补排序 关键路径 题目 Ⅲ:回路所有点度>=2 以下关于图的叙述中,正确的是(CA.强连通有向图的任何顶点到其他

越开源越幸运 4278

图(概念、存储、遍历、应用)

图基本概念存储方式邻接矩阵法邻接表法十字链表邻接多重表遍历深度优先搜索广度优先搜索基本应用最小生成树prim算法(普里姆算法)kruskal算法(克鲁斯卡尔算法)最短路径Dijkstra算法(迪杰斯特拉算法)Floyd算法(弗洛伊德算法)拓扑排序关键路径 基本概念 有向图:顶点之间的边是有向的 无向图:顶点之间的边是无向的 简单图:没有重复的边,没有到自身的边 完全图:有n(n-1)/2条边的无向图(每一个顶点都与其他顶点两两相连,没有方向) 有向完全图:有n(n-1)条边的有向图(每一个顶点都与其他顶点

qq_35963993的博客 822

软考图论核心:存储结构与算法实战解析

图论作为数据结构的重要分支,通过顶点和边的抽象模型解决复杂关系问题。其核心在于邻接矩阵与邻接表两种存储结构的选择——前者以O(1)时间复杂度实现快速查询但空间消耗大,后者则通过链式结构优化稀疏图存储。在算法层面,DFS/BFS遍历构成图处理的基础,Prim和Kruskal算法分别采用贪心与并查集策略解决最小生成树问题,而Dijkstra和Floyd算法则针对不同场景处理最短路径。这些技术在软考中占比高达30%,尤其适用于社交网络分析、物流路径规划等工程场景,2024年新增的A*算法考点更凸显其应用价值。

weixin_34216107的博客 284
上一篇: 哈夫曼树及应用(实验7--作业)
下一篇: 层次分析法(AHD)
ByteMaster_
博客等级 码龄3年 328粉丝 29原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值