数据结构-图
图: 表示“多对多”的关系 包含: 一组顶点:通常用V (Vertex) 表示顶点集合 一组边:通常用E (Edge) 表示边的集合 边是定点对(v,w) 有向边<v,w> 表示从v指向w的边 不考虑重边和自回路 抽象数据类型: 类型名称:图(Graph) 数据对象集:G(V,E)由一个非空的有限顶点集合V和一个有限边集合E组成。 Graph Create():建立并返回空图; Graph InsertVertex(GraphG, Vertex v):将v插入G; Graph InsertEdge(GraphG, Edge e):将e插入G; void DFS(GraphG, Vertex v):从顶点v出发深度优先遍历图G; void BFS(GraphG, Vertex v):从顶点v出发宽度优先遍历图G; void ShortestPath(GraphG, Vertex v, intDist[]):计算图G中顶点v到任意其他顶点的最短距离; void MST(GraphG):计算图G的最小生成树; 怎么在程序中表示一个图: 1.邻接矩阵:G[N][N]——N个顶点从0到N-1编号: 说明:矩阵中每个点取值1或0:1表示有边,0表示没有边相连 特点: [1]直观、简单、好理解 [2]方便检查任意一对顶点间是否存在边 [3]方便找任一顶点的所有“邻接点”(有边直接相连的顶点) [4]方便计算任一顶点的“度”(从该点发出的边数为“出 度”,指向该点的边数为“入度”) 无向图:对应行(或列)非0元素的个数 有向图:对应行非0元素的个数是“出度”;对应列非0元素的 个数是“入度” [5]浪费空间——存稀疏图(点很多而边很少)有大量无效元素 对稠密图(特别是完全图)还是很合算的 [6]浪费时间——统计稀疏图中一共有多少条边 2.邻接表:G[N]为指针数组,对应矩阵每行一个链表, 只存非0元素 说明:一个数组,数组每个元素为一个链表。数组大小为途中点的个数,每个链表表示一个点与其他点的连接 特点: [1]方便找任一顶点的所有“邻接点” [2]节约稀疏图的空间 需要N个头指针+ 2E个结点(每个结点至少2个域) [3]方便计算任一顶点的“度”? 对无向图:是的 [4]对有向图:只能计算“出度”;需要构造“逆邻接表”(存指向自己 的边)来方便计算“入度” [5]方便检查任意一对顶点间是否存在边?NO 图的遍历: 1.深度优先搜索(DFS) //类似于树的先序遍历 void DFS (Vertex v){ visit[v] = true; for (v的每个邻接点w){ if (!visit[w]) DFS(W); } } 若有N个顶点、E条边,时间复杂度是: 用邻接表存储图,O(N+E) 用邻接矩阵存储图,O(N^2) 2.广度优先搜索(BFS) //类似于层序遍历 void BFS ( Vertex V ) { visited[V] = true; Enqueue(V, Q); //队列 while(!IsEmpty(Q)){ V = Dequeue(Q); for( V 的每个邻接点W ) if( !visited[W] ) { visited[W] = true; Enqueue(W, Q); } } } 时间复杂度同1 图的联通性问题: 1.联通:如果从V到W存在一条(无向)路径,则称 V和W是连通的 2.路径::V到W的路径是一系列顶点{V, v1, v2, …, vn, W}的集合,其中任一对相邻的顶点间都有图 中的边。路径的长度是路径中的边数 (如果带 权,则是所有边的权重和)。如果V到W之间的所 有顶点都不同,则称简单路径 3.回路:起点等于终点的路径 4.连通图:图中任意两顶点均连通 对于不连通的图: 1.联通分量:无向图的极大联通子图,性质: 极大顶点数:再加1个顶点就不连通了 极大边数:包含子图中所有顶点相连的所有边 2.强连通:有向图中顶点V和W之间存在双向路 径,则称V和W是强连通的 3.强连通图:有向图中任意两顶点均强连通 4.强连通分量:有向图的极大强连通子图 不连通图的遍历: void ListComponents( Graph G ) { for( each V in G ) if( !visited[V] ) { DFS( V ); /*or BFS( V )*/ //每调用一次DFS(V),就 把V所在的连通分量遍历 了一遍。BFS也是一样。 } } 最短路径问题: 在网络中,求两个不同顶点之间的所有路径 中,边的权值之和最小的那一条路径 单源最短路径问题:从某固定源点出发,求其 到所有其他顶点的最短路径,可分为有权图的情况和无权图情况 1.无权图单源最短路径算法: 实际上就是通过广度优先搜索(BFS)遍历实现 dist[W] 保存每个点到源点s的最小距离 path[w]保存w的上一个点,用于保存最短路径 void Unweighted( Vertex S ) { Enqueue(S, Q); while(!IsEmpty(Q)){ V = Dequeue(Q); for( V 的每个邻接点W ) if( dist[W]==-1 ) { dist[W] = dist[V]+1; path[W] = V; Enqueue(W, Q); } } } 简单代码实现: public static void main(String[] args) throws InterruptedException { System.out.println("begin"); Node nodea = new Node("a"); Node nodeb = new Node("b"); Node nodec = new Node("c"); Node noded = new Node("d"); Node nodee = new Node("e"); Node nodef = new Node("f"); Node nodeg = new Node("g"); Map<Node, List<Node>> map = new HashMap<>(); List<Node> nodes1 = new ArrayList<>(); nodes1.add(nodeb); nodes1.add(nodee); map.put(nodea, nodes1); List<Node> nodes2 = new ArrayList<>(); nodes2.add(nodea); nodes2.add(nodef); nodes2.add(nodec); map.put(nodeb, nodes2); List<Node> nodes3 = new ArrayList<>(); nodes3.add(nodeb); nodes3.add(nodef); nodes3.add(nodeg); nodes3.add(noded); map.put(nodec, nodes3); List<Node> nodes4 = new ArrayList<>(); nodes4.add(nodec); nodes4.add(nodeg); map.put(noded, nodes4); List<Node> nodes5 = new ArrayList<>(); nodes5.add(nodea); nodes5.add(nodef); nodes5.add(nodeg); map.put(nodee, nodes5); List<Node> nodes6 = new ArrayList<>(); nodes6.add(nodeb); nodes6.add(nodec); nodes6.add(nodee); map.put(nodef, nodes6); List<Node> nodes7 = new ArrayList<>(); nodes7.add(nodee); nodes7.add(nodec); nodes7.add(noded); map.put(nodeg, nodes7); Map<Node, Integer> dist = new HashMap<>(); Map<Node, Node> path = new HashMap<>(); dist.put(nodea, 0); dist.put(nodeb, -1); dist.put(nodec, -1); dist.put(noded, -1); dist.put(nodee, -1); dist.put(nodef, -1); dist.put(nodeg, -1); Queue<Node> queue = new LinkedList<>(); queue.add(nodea); nodea.isVisit = true; while (!queue.isEmpty()) { Node poll = queue.poll(); List<Node> list = map.get(poll); if(list != null && list.size()>0){ for (Node node : list) { if(!node.isVisit){ dist.put(node, dist.get(poll) +1); path.put(node, poll); node.isVisit = true; queue.add(node); } } } } System.out.println(dist); //显示a到d的最短路径 Stack<Node> nodeSta= new Stack<>(); nodeSta.add(noded); Node tempNode = noded; while(!tempNode.equals(nodea)){ Node node = path.get(tempNode); nodeSta.add(node); tempNode = node; } System.out.println("a到d最短路径:"); while (!nodeSta.isEmpty()) { System.out.println(nodeSta.pop()); } System.out.println("end"); } public static class Node { String name; Boolean isVisit = false; public Node(String name) { super(); this.name = name; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Node other = (Node) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public String toString() { return "Node [name=" + name + "]"; } } 2.有权图单源最短路径算法: Dijkstra算法: a.令S={源点s + 已经确定了最短路径的顶点vi} b.对任一未收录的顶点v,定义dist[v]为s到v的最短路径长度,但该路径仅经过S中的顶点。即路径 {s(viS)v}的最小长度 c.若路径是按照递增(非递减)的顺序生成的,则 真正的最短路必须只经过S中的顶点(为什么?) 每次从未收录的顶点中选一个dist最小的收录(贪心) 增加一个v进入S,可能影响另外一个w的dist值! dist[w] = min{dist[w], dist[v] + <v,w>的权重} dist[W] 保存每个点到源点s的最小距离 //dist[W]的初始化:dist[s]设置为零,与s直接相连的节点设置为对应边的权值,其他初始化为无穷大 path[w]保存w的上一个点,用于保存最短路径 思路:每次从dist[W]中得到距离值最小的节点,将这个节点设置为已收录,遍历这个节点的邻接且未被收录节点,更新dist值 ,循环这个过程,直到所有点被收录 实现: void Dijkstra( Vertex s ) { while(1) { V = 未收录顶点中dist最小者; if( 这样的V不存在) break; collected[V] = true; for ( V 的每个邻接点W ) if( collected[W] == false ) if( dist[V]+E<V,W>< dist[W] ) { dist[W] = dist[V] + E<V,W>; path[W] = V; } } }/* 不能解决有负边的情况*/ public static void main(String[] args) throws InterruptedException { System.out.println("begin"); Node nodea = new Node("a"); Node nodeb = new Node("b"); Node nodec = new Node("c"); Node noded = new Node("d"); Node nodee = new Node("e"); Node nodef = new Node("f"); Node nodeg = new Node("g"); //类似邻接表,保存连接关系 Map<Node, List<Node>> map = new HashMap<>(); List<Node> nodes1 = new ArrayList<>(); nodes1.add(nodeb); nodes1.add(nodee); map.put(nodea, nodes1); List<Node> nodes2 = new ArrayList<>(); nodes2.add(nodea); nodes2.add(nodef); nodes2.add(nodec); map.put(nodeb, nodes2); List<Node> nodes3 = new ArrayList<>(); nodes3.add(nodeb); nodes3.add(nodef); nodes3.add(nodeg); nodes3.add(noded); map.put(nodec, nodes3); List<Node> nodes4 = new ArrayList<>(); nodes4.add(nodec); nodes4.add(nodeg); map.put(noded, nodes4); List<Node> nodes5 = new ArrayList<>(); nodes5.add(nodea); nodes5.add(nodef); nodes5.add(nodeg); map.put(nodee, nodes5); List<Node> nodes6 = new ArrayList<>(); nodes6.add(nodeb); nodes6.add(nodec); nodes6.add(nodee); map.put(nodef, nodes6); List<Node> nodes7 = new ArrayList<>(); nodes7.add(nodee); nodes7.add(nodec); nodes7.add(noded); map.put(nodeg, nodes7); //记录权值,即每条边的长度 Map<String, Integer> distMap = new HashMap<>(); distMap.put("a-b", 3); distMap.put("a-e", 2); distMap.put("b-f", 2); distMap.put("b-c", 3); distMap.put("c-f", 4); distMap.put("c-d", 2); distMap.put("e-f", 1); distMap.put("e-g", 1); distMap.put("c-g", 2); distMap.put("d-g", 6); //记录最短路径 Map<Node, Node> path = new HashMap<>(); //保存收录的点 Set<Node> s = new HashSet<>(); //最小堆保存节点,//记录最短路径 PriorityQueue<Node> queue = new PriorityQueue<>(1000, new Comparator<Node>() { public int compare(Node o1, Node o2) { return o1.distance - o2.distance; }; }); Node atoa = new Node("a", 0); Node btoa = new Node("b", Integer.MAX_VALUE); Node ctoa = new Node("c", Integer.MAX_VALUE); Node dtoa = new Node("d", Integer.MAX_VALUE); Node etoa = new Node("e", Integer.MAX_VALUE); Node ftoa = new Node("f", Integer.MAX_VALUE); Node gtoa = new Node("g", Integer.MAX_VALUE); queue.add(atoa); queue.add(btoa); queue.add(ctoa); queue.add(dtoa); queue.add(etoa); queue.add(ftoa); queue.add(gtoa); //核心算法 while(!queue.isEmpty()) { Node poll = queue.poll(); //弹出dis最小的点 s.add(poll); //收录这个点 List<Node> list = map.get(poll); //获取邻接点 for (Node node : list) { if(!s.contains(node)){ //如果临界点没有被收录,计算并更新新距离 Integer dis = distMap.get(poll.name + "-" + node.name) != null ? distMap.get(poll.name + "-" + node.name) :distMap.get(node.name + "-" + poll.name); int tempDis = poll.distance + dis; if(dis != null && tempDis < getDis(node, queue).distance) { node.distance = tempDis; queue.remove(node); queue.offer(node); //这两个实现最小堆的节点重排保证dis最小的点在根节点 path.put(node, poll); //path用于记录最小路径 } } } } System.out.println(s); //显示a到d的最短路径 Stack<Node> nodeSta= new Stack<>(); nodeSta.add(nodec); Node tempNode = nodec; while(!tempNode.equals(nodea)){ Node node = path.get(tempNode); nodeSta.add(node); tempNode = node; } System.out.println("a到c最短路径:"); while (!nodeSta.isEmpty()) { System.out.print(nodeSta.pop().name + "->"); if(nodeSta.size() == 1){ System.out.println(nodeSta.pop().name); } } System.out.println("end"); } 多源最短路径问题:求任意两顶点间的最短路径 方法1:直接将单源最短路算法调用|V|遍 T= O( |V|^3 + |E||V|) 方法2: Floyd算法 T= O( |V|^3 ) Floyd算法 使用邻接矩阵保存图,矩阵中D[i,j]保存的是i到j的距离,如果i,j间没有边则初始化为无穷大 依次将每个点K取出,比较之前记录的其他任意两个点如A B间的距离AB如果这个距离大于AK + KB则更新AB的值为AK + KB void Floyd() { for ( i = 0; i < N; i++ ) for( j = 0; j < N; j++ ) { D[i][j] = G[i][j]; //初始化 path[i][j] = -1; } for( k = 0; k < N; k++ ) for( i = 0; i < N; i++ ) for( j = 0; j < N; j++ ) if( D[i][k] + D[k][j] < D[i][j] ) { D[i][j] = D[i][k] + D[k][j]; //更新最短路径值 path[i][j] = k; //用于后续找出任意两点的最短路径 } } 最小生成树问题: //有等价关系:最小生成树存在↔图连通 是一颗树 无回路 |v|个顶点一定有|v|-1条边 是生成树: 包含全部顶点 V-1条边全部都在图里 边的权重和最小 得到最小生成树的算法: 贪心算法: 什么是“贪”:每一步都要最好的 什么是“好”:权重最小的边 需要约束: 只能用图里有的边 只能正好用掉|V|-1条边 不能有回路 Prim算法:让一棵小树长大 //属于贪心算法 void Prim() { MST = {s}; while(1) { V = 未收录顶点中dist最小者; if( 这样的V不存在) break; 将V收录进MST: dist[V] = 0; for ( V 的每个邻接点W ) if( W未被收录) if( E(V,W)< dist[W] ){ dist[W] = E(V,W); parent[W] = V; } } if( MST中收的顶点不到|V|个) Error ( “生成树不存在”); } 首先选择图中一个点为根节点,找到与根节点相连的最短的边,将这条边和这条边上另一个端点收录到树中,接着找到与树中顶点相连的所有边中 最短的边及顶点,在不形成回路的情况下,收录为树的一部分。如此循环知道所有的点都被收录进树中。 Kruskal算法—将森林合并成树 void Kruskal( Graph G ) { MST = { } ; while( MST 中不到|V|-1 条边&& E 中还有边) { 从E 中取一条权重最小的边E(v,w) ; 将E(v,w)从E 中删除; if( E(V,W)不在MST 中构成回路) 将E(V,W)加入MST; else 彻底无视E(V,W); } if( MST 中不到|V|-1 条边) Error ( “生成树不存在”); } 从图中找到权重最小的边,判断这条边加进mst中时是否构成回路,不构成回路则将其加入到mst中,否则不加入并从图中去掉这条边 接着按同样方式在图中继续找下一条边,知道边数达到v-1,则找到了最小生成树。如果找不到v-1条边,则生成树不存在 如何获取最小的边:使用最小堆 如何判断加入边后是否构成回路:并查集 拓扑排序: AOV网络:是一个有向图,所有真实的活动表现为图中的顶点,顶点和顶点之间的有向边标识两个事情的先后顺序,这样的图称为AOV网络 拓扑序:如果图中从V到W有一条有向路径, 则V一定排在W之前。满足此条件的顶点序列 称为一个拓扑序 获得一个拓扑序的过程就是拓扑排序 AOV如果有合理的拓扑序,则必定是有向无环 图(Directed Acyclic Graph, DAG) 获得拓扑序的算法: 1.时间复杂度:O(|V|^2) void TopSort() { for( cnt= 0; cnt< |V|; cnt++ ) { V = 未输出的入度为0的顶点; if( 这样的V不存在) { Error ( “图中有回路”); break; } 输出V,或者记录V的输出序号; for( V 的每个邻接点W) Indegree[W]––; } } 从图中每次获取入度为0的顶点,输出,遍历这个顶点的临界点,将邻接点的入度减一 2.T= O( |V| + |E| ) voidTopSort() { for( 图中每个顶点V ) if( Indegree[V]==0 ) Enqueue( V, Q ); while( !IsEmpty(Q) ) { V = Dequeue( Q ); 输出V,或者记录V的输出序号; cnt++; for( V 的每个邻接点W ) if( ––Indegree[W]==0 ) Enqueue( W, Q ); } if( cnt!= |V| ) Error( “图中有回路”); } 将度为0的顶点放在一个队列中,这样每次都能快速得到度为0的顶点
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构