当前位置: 首页 > news >正文

数据结构与算法 - 图

一、概念

图是有顶点(vertex)和边(edge)组成的数据结构,例如

该图有4个顶点:A、B、C、D以及四条有向边,有向图中,边是单向的。

1. 有向图 VS 无向图

如果是无向图,那么边是双向的,下面是一个无向图的例子

2. 度

度是指与该顶点相邻的边的数量

例如,上图中

  • A、B、C、E、F这几个顶点的度数为2
  • D顶点度数为4

有向图中,细分为入度和出度,参见下图

  • A(2out / 0 in)
  • B、C、E(1 out / 1 in)
  • D(2 out / 2 in)
  • F(0 out / 2 in)

3. 权

边可以有权重,代表从源顶点到目标顶点的距离、费用、时间或其他度量。

4. 路径

路径被定义为从一个顶点到另一个顶点的另一个顶点的一系列连续边,例如上图中【北京】到【上海有多条路径

  • 北京 - 上海
  • 北京 - 武汉 - 上海

路径长度

  • 不考虑权重,长度就是边的数量
  • 考虑权重,一般就是权重累加

5. 环

在有向图中,从一个顶点开始,可以通过若干条有向边返回到该顶点,那么就形成了一个环

6. 图的连通性

如果两个顶点之间存在路径,则这两个顶点是连通的,所有顶点都连通,则该图被称为连通图。若子图连通,则称为连通分量。

二、图的表示

比如说,下面的无向图

用邻接矩阵可以表示为:

  A B C D
A 0 1 1 0
B 1 0 0 1 
C 1 0 0 1
D 0 1 1 0

用邻接表可以表示为:

A -> B -> C
B -> A -> D
C -> A -> D
D -> B -> C

有向图的例子

  A B C D
A 0 1 1 0
B 0 0 0 1
C 0 0 0 1
D 0 0 0 0
A - B - C
B - D
C - D
D - empty

三、Java表示

顶点

package com.itheima.datastructure.graph;import java.util.List;
import java.util.Objects;/*** 顶点*/
public class Vertex {String name;public List<Edge> edges;boolean visited;  // 是否被访问过,用在BFS和DFSint inDegree;  // 入度,用在拓扑排序int status;  // 状态 0-未访问 1-访问中 2-访问过,用在拓扑排序static final Integer INF = Integer.MAX_VALUE;int dist = INF;  // 距离Vertex prev = null;  // 记录从何而来public Vertex(String name) {this.name = name;}public String getName() {return name;}@Overridepublic String toString() {String n = name;if (visited) {n = "\u001B[31m" + name + "\u001B[0m";}return n + '(' + (dist == Integer.MAX_VALUE ? "∞" : String.valueOf(dist)) + ") <- " + (prev == null ? "null" : prev.name);}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Vertex vertex = (Vertex) o;return Objects.equals(name, vertex.name);}@Overridepublic int hashCode() {return name != null ? name.hashCode() : 0;}
}

package com.itheima.datastructure.graph;public class Edge {public Vertex linked;public int weight;public Edge(Vertex linked) {this(linked, 1);}public Edge(Vertex linked, int weight) {this.linked = linked;this.weight = weight;}
}

四、DFS(深度优先搜索)

package com.itheima.datastructure.graph;import java.util.LinkedList;
import java.util.List;public class Dfs {public static void main(String[] args) {Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");Vertex v5 = new Vertex("v5");Vertex v6 = new Vertex("v6");v1.edges = List.of(new Edge(v3), new Edge(v2), new Edge(v6));v2.edges = List.of(new Edge(v4));v3.edges = List.of(new Edge(v4), new Edge(v6));v4.edges = List.of(new Edge(v5));v5.edges = List.of();v6.edges = List.of(new Edge(v5));dfs2(v1);}/*** 递归方式实现深度优先遍历* @param v*/private static void dfs(Vertex v) {v.visited = true;System.out.print(v.name + " ");for (Edge edge : v.edges) {if(!edge.linked.visited) {dfs(edge.linked);}}}/*** 非递归方式深度优先遍历* @param v*/public static void dfs2(Vertex v) {LinkedList<Vertex> stack = new LinkedList<>();stack.push(v);while (!stack.isEmpty()) {Vertex pop = stack.pop();pop.visited = true;System.out.println(pop.name);for (Edge edge : pop.edges) {if(!edge.linked.visited) {stack.push(edge.linked);}}}}}

五、BFS(广度优先搜索)

package com.itheima.datastructure.graph;import java.util.LinkedList;
import java.util.List;public class Bfs {public static void main(String[] args) {Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");Vertex v5 = new Vertex("v5");Vertex v6 = new Vertex("v6");v1.edges = List.of(new Edge(v3), new Edge(v2), new Edge(v6));v2.edges = List.of(new Edge(v4));v3.edges = List.of(new Edge(v4), new Edge(v6));v4.edges = List.of(new Edge(v5));v5.edges = List.of();v6.edges = List.of(new Edge(v5));bfs(v1);}/*** 广度优先遍历 - 队列* @param v*/private static void bfs(Vertex v) {LinkedList<Vertex> queue = new LinkedList<>();v.visited = true;queue.offer(v);while(!queue.isEmpty()) {Vertex poll = queue.poll();System.out.println(poll.name);for (Edge edge : poll.edges) {if(!edge.linked.visited) {edge.linked.visited = true;queue.offer(edge.linked);}}}}
}

六、拓扑排序

1. Kahn

package com.itheima.datastructure.graph;import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;public class TopologicalSort {public static void main(String[] args) {Vertex v1 = new Vertex("网页基础");Vertex v2 = new Vertex("Java基础");Vertex v3 = new Vertex("JavaWeb");Vertex v4 = new Vertex("Spring框架");Vertex v5 = new Vertex("微服务框架");Vertex v6 = new Vertex("数据库");Vertex v7 = new Vertex("实战项目");v1.edges = List.of(new Edge(v3)); // +1v2.edges = List.of(new Edge(v3)); // +1v3.edges = List.of(new Edge(v4));v6.edges = List.of(new Edge(v4));v4.edges = List.of(new Edge(v5));v5.edges = List.of(new Edge(v7));v7.edges = List.of();List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);topologicalSort(graph);}private static void topologicalSort(List<Vertex> graph) {// 1. 统计每个顶点的入度for (Vertex v : graph) {  // 顶点集for (Edge edge : v.edges) {  // 边集edge.linked.inDegree++;}}// 2. 将入度为0的顶点加入队列LinkedList<Vertex> queue = new LinkedList<>();for (Vertex v : graph) {if(v.inDegree == 0) {queue.offer(v);}}// 3. 队列中不断移除顶点,每移除一个顶点,把他相邻顶点入度减1,若减到0则入队List<String> result = new ArrayList<>();while(!queue.isEmpty()) {Vertex poll = queue.poll();result.add(poll.name);for (Edge edge : poll.edges) {edge.linked.inDegree--;if(edge.linked.inDegree == 0) {queue.offer(edge.linked);}}}if(result.size() != graph.size()) {System.out.println("图中出现环");} else {for (String key : result) {System.out.println(key);}}}
}

2. DFS

package com.itheima.datastructure.graph;import java.util.LinkedList;
import java.util.List;public class TopologicalSortDFS {public static void main(String[] args) {Vertex v1 = new Vertex("网页基础");Vertex v2 = new Vertex("Java基础");Vertex v3 = new Vertex("JavaWeb");Vertex v4 = new Vertex("Spring框架");Vertex v5 = new Vertex("微服务框架");Vertex v6 = new Vertex("数据库");Vertex v7 = new Vertex("实战项目");v1.edges = List.of(new Edge(v3));v2.edges = List.of(new Edge(v3));v3.edges = List.of(new Edge(v4));v6.edges = List.of(new Edge(v4));v4.edges = List.of(new Edge(v5));v5.edges = List.of(new Edge(v7));v7.edges = List.of();List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);LinkedList<String> result = new LinkedList<>();for (Vertex v : graph) {if(v.status==0) {dfs(v, result);}}System.out.println(result);}private static void dfs(Vertex v, LinkedList<String> result) {if(v.status == 2) {// 已经访问过了return;}if(v.status == 1) {// 访问中throw new RuntimeException("发现环");}v.status = 1;// 遍历相邻节点for (Edge edge : v.edges) {dfs(edge.linked, result);}v.status = 2;// 向回走再压栈result.push(v.name);}
}

七、最短路径

1. Dijkstra

算法描述:

1. 将所有顶点标记为未访问,创建一个未访问顶点的集合

2. 为每个顶点分配一个临时距离值

  • 对于我们的初始顶点,将其设置为零
  • 对于所有其他顶点,将其设置为无穷大

3. 每次选择最小临时距离的未访问顶点,作为新的当前顶点

4. 对于当前顶点,遍历其所有未访问的邻居,并更新他们的临时距离为更小

  • 例如,1->6的距离为14,而1->3->6的距离是11.这时将距离更新为11
  • 否则,将保留上次距离值

5. 当前顶点的邻居处理完成后,把它从未访问集合中删除

代码:

package com.itheima.datastructure.graph;import java.util.ArrayList;
import java.util.List;public class Dijkstra {public static void main(String[] args) {Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");Vertex v5 = new Vertex("v5");Vertex v6 = new Vertex("v6");v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));v2.edges = List.of(new Edge(v4, 15));v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));v4.edges = List.of(new Edge(v5, 6));v5.edges = List.of();v6.edges = List.of(new Edge(v5, 9));List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6);dijkstra(graph, v1);}private static void dijkstra(List<Vertex> graph, Vertex source) {// 1. 将所有顶点标记为未访问,创建一个未访问顶点的集合ArrayList<Vertex> list = new ArrayList<>(graph);// 2. 初始顶点距离设置为0source.dist = 0;while(!list.isEmpty()) {// 3. 选择最小临时距离的未访问节点,作为新的当前顶点Vertex curr = chooseMinDistVertex(list);// 4. 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小updateNeighbourDist(curr, list);// 5. 当前顶点的邻居处理完成后,把它从未访问集合中移除list.remove(curr);}for (Vertex v : graph) {System.out.println(v.name + " " + v.dist);}}/*** 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小* @param curr* @param list*/private static void updateNeighbourDist(Vertex curr, ArrayList<Vertex> list) {for (Edge edge : curr.edges) {Vertex n = edge.linked;if(list.contains(n)) {int dist = curr.dist + edge.weight;if(dist < n.dist) {n.dist = dist;}}}}/*** 选取当前顶点 - 找到距离值最小的顶点* @param list* @return*/private static Vertex chooseMinDistVertex(ArrayList<Vertex> list) {Vertex min = list.get(0);for (int i = 1; i < list.size(); i++) {if(list.get(i).dist < min.dist) {min = list.get(i);}}return min;}
}

改进:优先级队列

1. 创建一个优先级队列,放入所有顶点(队列大小会达到边的数量)

2. 为每个顶点分配一个临时距离值

  • 对于初始顶点,将其设置为零
  • 对于其他所有顶点,将其设置为无穷大

3. 每次选择最小临时距离的未访问顶点,作为新的当前顶点

4. 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小,若距离更新需要加入队列

  • 例如,1->6的距离是14,而1->3->6的距离是11。这时将距离更新为11
  • 否则,将保留上次距离值

5. 当前顶点的邻居处理完成后,把它从队列中删除

package com.itheima.datastructure.graph;import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;public class DijkstraPriorityQueue {public static void main(String[] args) {Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");Vertex v5 = new Vertex("v5");Vertex v6 = new Vertex("v6");v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));v2.edges = List.of(new Edge(v4, 15));v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));v4.edges = List.of(new Edge(v5, 6));v5.edges = List.of();v6.edges = List.of(new Edge(v5, 9));List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6);dijkstra(graph, v1);}private static void dijkstra(List<Vertex> graph, Vertex source) {// 1. 创建一个优先级队列,放入所有顶点(小根堆)PriorityQueue<Vertex> queue = new PriorityQueue<>(Comparator.comparing(v -> v.dist));// 2. 对于初始顶点,将其临时距离值设置为0source.dist = 0;for (Vertex v : graph) {queue.offer(v);}while(!queue.isEmpty()) {System.out.println(queue);// 3. 选取当前顶点(距离值最小的顶点)Vertex curr = queue.peek();// 4. 更新当前顶点邻居距离if(!curr.visited) {updateNeighbourDist(curr, queue);curr.visited = true;}// 5. 移除当前节点queue.poll();}for (Vertex v : graph) {System.out.println(v.name + " " + v.dist + " " + (v.prev != null ? v.prev.name : "null"));}}private static void updateNeighbourDist(Vertex curr, PriorityQueue<Vertex> queue) {for (Edge edge : curr.edges) {Vertex n = edge.linked;if(!n.visited) {int dist = curr.dist + edge.weight;if(dist < n.dist) {n.dist = dist;n.prev = curr;queue.offer(n);}}}}
}

2. Bellman-Ford

问题

按照Dijkstra算法,得出

  • v1 -> v2最短距离2
  • v1 -> v3最短距离1
  • v1 -> v4最短距离2

事实应当是

  • v1 -> v2最短距离2
  • v1 -> v3最短距离0
  • v1 -> v4最短距离1
package com.itheima.datastructure.graph;import java.util.List;/*** Bellman-Ford算法,可以处理负边*/
public class BellmanFord {public static void main(String[] args) {// 正常情况/*Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");Vertex v5 = new Vertex("v5");Vertex v6 = new Vertex("v6");v1.edges = List.of(new Edge(v3, 9), new Edge(v2, 7), new Edge(v6, 14));v2.edges = List.of(new Edge(v4, 15));v3.edges = List.of(new Edge(v4, 11), new Edge(v6, 2));v4.edges = List.of(new Edge(v5, 6));v5.edges = List.of();v6.edges = List.of(new Edge(v5, 9));List<Vertex> graph = List.of(v4, v5, v6, v1, v2, v3);*/// 负边情况/*Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 1));v2.edges = List.of(new Edge(v3, -2));v3.edges = List.of(new Edge(v4, 1));v4.edges = List.of();List<Vertex> graph = List.of(v1, v2, v3, v4);*/// 负环情况Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");v1.edges = List.of(new Edge(v2, 2));v2.edges = List.of(new Edge(v3, -4));v3.edges = List.of(new Edge(v4, 1), new Edge(v1, 1));v4.edges = List.of();List<Vertex> graph = List.of(v1, v2, v3, v4);bellmanFord(graph, v1);}private static void bellmanFord(List<Vertex> graph, Vertex source) {source.dist = 0;int size = graph.size();for (int i = 0; i < size - 1; i++) {for (Vertex s : graph) {for (Edge edge : s.edges) {Vertex e = edge.linked;if(s.dist != Integer.MAX_VALUE && s.dist + edge.weight < e.dist) {e.dist = s.dist + edge.weight;e.prev = s;}}}}for (Vertex v : graph) {System.out.println(v);}}
}

负环

如果在【顶点-1】轮处理完成后,还能继续找到更短距离,表示发现了负环。比如,在上面这张图中经过了3轮处理后,还能继续找到更短距离,直到无穷小。

3. Floy-Warshall

package com.itheima.datastructure.graph;import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;/*** Floyd - Warshall多源最短路径算法*/
public class FloydWarshall {public static void main(String[] args) {Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");v1.edges = List.of(new Edge(v3, -2));v2.edges = List.of(new Edge(v1, 4), new Edge(v3, 3));v3.edges = List.of(new Edge(v4, 2));v4.edges = List.of(new Edge(v2, -1));List<Vertex> graph = List.of(v1, v2, v3, v4);// 负环情况/*Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");v1.edges = List.of(new Edge(v2, 2));v2.edges = List.of(new Edge(v3, -4));v3.edges = List.of(new Edge(v4, 1), new Edge(v1, 1));v4.edges = List.of();List<Vertex> graph = List.of(v1, v2, v3, v4);*/floydWarshall(graph);}private static void floydWarshall(List<Vertex> graph) {int size = graph.size();int[][] dist = new int[size][size];Vertex[][] prev = new Vertex[size][size];// 1. 初始化for (int i = 0; i < size; i++) {Vertex v = graph.get(i);Map<Vertex, Integer> map = v.edges.stream().collect(Collectors.toMap(e -> e.linked, e -> e.weight));for (int j = 0; j < size; j++) {Vertex u = graph.get(j);if (v == u) {dist[i][j] = 0;} else {dist[i][j] = map.getOrDefault(u, Integer.MAX_VALUE);prev[i][j] = map.get(u) != null ? v : null;}}}// print(dist);print(prev);// 2. 看能否借路到达其他顶点/*** v2 -> v1* dist[1][0]*/for (int k = 0; k < size; k++) {  // 借助第k个顶点到达其他顶点for (int i = 0; i < size; i++) {  // 第i行for (int j = 0; j < size; j++) {  // 第j列// 第i行的顶点,借助第k个顶点,到达第j列顶点// 可达且距离更小if (dist[i][k] != Integer.MAX_VALUE &&dist[k][j] != Integer.MAX_VALUE &&dist[i][k] + dist[k][j] < dist[i][j]) {dist[i][j] = dist[i][k] + dist[k][j];prev[i][j] = prev[k][j];}}}// 如果对角线上有一个值小于0,则出现了负环// print(dist);}print(prev);for (int i = 0; i < size; i++) {for (int j = 0; j < size; j++) {path(prev, graph, i, j);}}}private static void print(int[][] dist) {System.out.println("---------------------");for (int[] row : dist) {System.out.println(Arrays.stream(row).boxed().map(x -> x == Integer.MAX_VALUE ? "∞" : String.valueOf(x)).map(s -> String.format("%2s", s)).collect(Collectors.joining(",", "[", "]")));}}private static void print(Vertex[][] prev) {System.out.println("---------------------");for (Vertex[] row : prev) {System.out.println(Arrays.stream(row).map(v -> v == null ? "null" : v.name).map(s -> String.format("%5s", s)).collect(Collectors.joining(",", "[", "]")));}}private static void path(Vertex[][] prev, List<Vertex> graph, int i, int j) {LinkedList<String> stack = new LinkedList<>();System.out.println("[" + graph.get(i).name + "," + graph.get(j).name + "]");stack.push(graph.get(j).name);while(i != j) {Vertex p = prev[i][j];stack.push(p.name);j = graph.indexOf(p);}System.out.println(stack);}
}

八、最小生成树

1. Prim

Prim算法是一个用于寻找带权图的最小生成树的贪心算法,其基本思路如下:

1. 初始化:选择一个初始顶点,将其加入到生成树中,设置一个优先队列(或最小堆)来存储边,边的权重作为优先级。

2. 扩展树:

  • 从已加入生成树的顶点出发,找到与生成树中顶点相连的所有边,并选择其中权重最小的边
  • 将这条边的另一个顶点加入生成树,同时更新优先队列中与新加入顶点相连的边

3. 重复步骤:重复以上过程,直到所有顶点都被加入到生成树中。

时间复杂度:O(E log V),其中E是边的数量,V是顶点的数量。

package com.itheima.datastructure.graph;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** 最小生成树*/
public class Prim {public static void main(String[] args) {Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");Vertex v5 = new Vertex("v5");Vertex v6 = new Vertex("v6");Vertex v7 = new Vertex("v7");v1.edges = List.of(new Edge(v2, 2), new Edge(v3, 4), new Edge(v4, 1));v2.edges = List.of(new Edge(v1, 2), new Edge(v4, 3), new Edge(v5, 10));v3.edges = List.of(new Edge(v1, 4), new Edge(v4, 2), new Edge(v6, 5));v4.edges = List.of(new Edge(v1, 1), new Edge(v2, 3), new Edge(v3, 2),new Edge(v5, 7), new Edge(v6, 8), new Edge(v7, 4));v5.edges = List.of(new Edge(v2, 10), new Edge(v4, 7), new Edge(v7, 6));v6.edges = List.of(new Edge(v3, 5), new Edge(v4, 8), new Edge(v7, 1));v7.edges = List.of(new Edge(v4, 4), new Edge(v5, 6), new Edge(v6, 1));List<Vertex> graph = List.of(v1, v2, v3, v4, v5, v6, v7);prim(graph, v1);}private static void prim(List<Vertex> graph, Vertex source) {// 1. 创建一个未访问顶点的集合ArrayList<Vertex> list = new ArrayList<>(graph);// 2. 为初始顶点设置临时距离值为0source.dist = 0;while(!list.isEmpty()) {// 3. 选择最小临时距离的未访问顶点,作为新的当前顶点Vertex min = chooseMinDistVertex(list);// 4. 对于当前顶点,遍历其所有未访问的邻居,并更新它们的临时距离为更小updateNeighboursDist(min);// 5. 当前顶点的邻居处理完成后,把它从未访问集合中删除list.remove(min);min.visited = true;System.out.println("------------------");for (Vertex v : graph) {System.out.println(v);}}}private static void updateNeighboursDist(Vertex curr) {for (Edge edge : curr.edges) {Vertex n = edge.linked;if(!n.visited) {int dist = edge.weight;if(dist < n.dist) {n.dist = dist;n.prev = curr;}}}}private static Vertex chooseMinDistVertex(ArrayList<Vertex> list) {Vertex min = list.get(0);for (int i = 1; i < list.size(); i++) {if(list.get(i).dist < min.dist) {min = list.get(i);}}return min;}
}

2. Kruskal

package com.itheima.datastructure.graph;import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;public class Kruskal {static class Edge implements Comparable<Edge> {List<Vertex> vertices;int start;int end;int weight;public Edge(List<Vertex> vertices, int start, int end, int weight) {this.vertices = vertices;this.start = start;this.end = end;this.weight = weight;}public Edge(int start, int end, int weight) {this.start = start;this.end = end;this.weight = weight;}@Overridepublic int compareTo(Edge o) {return Integer.compare(this.weight, o.weight);}@Overridepublic String toString() {return vertices.get(start).name + "<->" + vertices.get(end).name + "(" + weight + ")";}}public static void main(String[] args) {Vertex v1 = new Vertex("v1");Vertex v2 = new Vertex("v2");Vertex v3 = new Vertex("v3");Vertex v4 = new Vertex("v4");Vertex v5 = new Vertex("v5");Vertex v6 = new Vertex("v6");Vertex v7 = new Vertex("v7");List<Vertex> vertices = List.of(v1, v2, v3, v4, v5, v6, v7);// 优先队列(小根堆) -> 根据边的权重比较PriorityQueue<Edge> queue = new PriorityQueue<>(List.of(new Edge(vertices,0, 1, 2),new Edge(vertices,0, 2, 4),new Edge(vertices,0, 3, 1),new Edge(vertices,1, 3, 3),new Edge(vertices,1, 4, 10),new Edge(vertices,2, 3, 2),new Edge(vertices,2, 5, 5),new Edge(vertices,3, 4, 7),new Edge(vertices,3, 5, 8),new Edge(vertices,3, 6, 4),new Edge(vertices,4, 6, 6),new Edge(vertices,5, 6, 1)));kruskal(vertices.size(), queue);}private static void kruskal(int size, PriorityQueue<Edge> queue) {List<Edge> result = new ArrayList<>();DisjointSet set = new DisjointSet(size);// 小于顶点的个数减1 -> 还没找完while(result.size() < size - 1) {// 获取权重最小的边进行处理Edge poll = queue.poll();int start = set.find(poll.start);int end = set.find(poll.end);// 如果结果不相等,说明这两个顶点不相交 -> 不连通if(start != end) {result.add(poll);// 相交 -> 连通set.union(start, end);}}for (Edge edge : result) {System.out.println(edge);}}
}

九、不相交集合(并查集合)

使用并查集(Union-Find)来管理连通分量,从而有效地检查边的连通性。

package com.itheima.datastructure.graph;import java.util.Arrays;/*** 不相交集合(并查集合)*/
public class DisjointSet {int[] s;  // 老大int[] size;  // 顶点个数public DisjointSet(int size) {s = new int[size];this.size = new int[size];/*** 索引对应顶点* 元素是用来表示与之有关系的顶点* 索引  0  1  2  3  4  5  6* 元素 [0, 1, 2, 3, 4, 5, 6] 表示一开始顶点直接没有联系(只与自己有联系)*/for (int i = 0; i < size; i++) {s[i] = i;this.size[i] = 1;}}// find是找到集合中的老大 - 优化1:路径压缩public int find(int x) {// 索引和值相等的就是老大if(x == s[x]) {return x;}// 路径压缩return s[x] = find(s[x]);}// union是让两个集合“相交”,即选出新老大,x、y是原老大索引// 把顶点个数少的集合连接到顶点个数多的集合public void union(int x, int y) {if(size[x] < size[y]) {// y是新老大,x新小弟s[x] = y;// 更新老大的元素个数size[y] = size[y] + size[x];} else {// x是新老大,y新小弟s[y] = x;// 更新老大的元素个数size[x] = size[x] + size[y];}// 可以优化代码为:/*if(size[x] < size[y]) {int t = x;x = y;y = t;}s[y] = x;size[x] = size[x] + size[y];*/}@Overridepublic String toString() {return "内容:" +  Arrays.toString(s) + "\n大小:" + Arrays.toString(size);}public static void main(String[] args) {DisjointSet set = new DisjointSet(7);System.out.println(set);int x = set.find(0);int y = set.find(3);System.out.println("老大分别是:" + x + " " + y);if(x != y) {set.union(x, y);System.out.println(set);}}
}

十、习题

1. 省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

示例 1:

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

示例 2:

输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

提示:

  • 1 <= n <= 200
  • n == isConnected.length
  • n == isConnected[i].length
  • isConnected[i][j] 为 1 或 0
  • isConnected[i][i] == 1
  • isConnected[i][j] == isConnected[j][i]

解法一:深度优先搜索DFS。时间复杂度O(n^2),与输入矩阵大小成正比,适合n的最大值为200

class Solution {public int findCircleNum(int[][] isConnected) {int n = isConnected.length;boolean[] visited = new boolean[n];int provinceCount = 0;for (int i = 0; i < n; i++) {if(!visited[i]) {// 在未访问的城市上进行DFSdfs(isConnected, visited, i);// 每遍历完一个省份,增加计数provinceCount++;}}return provinceCount;}private void dfs(int[][] isConnected, boolean[] visited, int city) {// 标记城市为已访问visited[city] = true;for (int j = 0; j < isConnected.length; j++) {// 如果有连接且问访问的城市,则继续DFS。结束时会访问完一个省份的所有城市if(isConnected[city][j] == 1 && !visited[j]) {dfs(isConnected, visited, j);}}}
}

解法二:广度优先搜索BFS。时间复杂度O(n^2),与输入矩阵大小成正比,适合n的最大值为200

class Solution {public int findCircleNum(int[][] isConnected) {int n = isConnected.length;boolean[] visited = new boolean[n];int provinceCount = 0;for (int i = 0; i < n; i++) {if (!visited[i]) {// 在未访问的城市上进行BFSbfs(isConnected, visited, i);// 每遍历完一个省份,增加计数provinceCount++;}}return provinceCount;}private void bfs(int[][] isConnected, boolean[] visited, int city) {LinkedList<Integer> queue = new LinkedList<>();queue.offer(city);// 标记城市为已访问visited[city] = true;while (!queue.isEmpty()) {// 取出队列中的城市int currentCity = queue.poll();for (int j = 0; j < isConnected.length; j++) {// 如果有连接且未访问,则加入队列if (isConnected[currentCity][j] == 1 && !visited[j]) {visited[j] = true;queue.offer(j);}}}}
}

解法三:并查集

class Solution {static class DisjionSet {private int[] parent;private int count;public DisjionSet(int n) {parent = new int[n];for (int i = 0; i < n; i++) {// 初始化每个城市的父节点为自身parent[i] = i;}count = n; // 初始时省份数量为城市数}public int find(int city) {if (parent[city] != city) {// 路径压缩parent[city] = find(parent[city]);}return parent[city];}public void union(int city1, int city2) {int root1 = find(city1);int root2 = find(city2);if (root1 != root2) {// 合并两个省份parent[root1] = root2;// 合并后省份数量减少count--;}}public int getCount() {return count;}}public int findCircleNum(int[][] isConnected) {int n = isConnected.length;DisjionSet set = new DisjionSet(n);for (int i = 0; i < n; i++) {for (int j = i + 1; j < n; j++) {// 如果两个城市连接,则合并if (isConnected[i][j] == 1) {set.union(i, j);}}}return set.getCount();}
}

2. 所有可能路径

给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序

 graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。

示例 1:

输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3

示例 2:

输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]

提示:

  • n == graph.length
  • 2 <= n <= 15
  • 0 <= graph[i][j] < n
  • graph[i][j] != i(即不存在自环)
  • graph[i] 中的所有元素 互不相同
  • 保证输入为 有向无环图(DAG)

解法一:深度优先搜索DFS

class Solution {public List<List<Integer>> allPathsSourceTarget(int[][] graph) {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();path.add(0);// 从节点0开始深度优先搜索dfs(graph, 0, path, result);return result;}private void dfs(int[][] graph, int node, List<Integer> path, List<List<Integer>> result) {// 到了第n - 1个节点if (node == graph.length - 1) {result.add(new ArrayList<>(path));return;}for (int neighbor : graph[node]) {path.add(neighbor);// 从相邻节点继续递归dfs(graph, neighbor, path, result);path.remove(path.size() - 1); // 回溯}}
}

解法二:广度优先搜索BFS

class Solution {public List<List<Integer>> allPathsSourceTarget(int[][] graph) {List<List<Integer>> result = new ArrayList<>();Queue<List<Integer>> queue = new LinkedList<>();List<Integer> startPath = new ArrayList<>();startPath.add(0);queue.offer(startPath);while (!queue.isEmpty()) {List<Integer> currentPath = queue.poll();// 获取某条路径的最后一个节点int lastNode = currentPath.get(currentPath.size() - 1);if (lastNode == graph.length - 1) {result.add(new ArrayList<>(currentPath));}for (int neighbor : graph[lastNode]) {// 在当前路径基础上添加新的节点List<Integer> newPath = new ArrayList<>(currentPath);newPath.add(neighbor);queue.offer(newPath);}}return result;}
}

3. 连接所有点的最小费用

给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。

连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。

请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。

示例 1:

输入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
输出:20
解释:

我们可以按照上图所示连接所有点得到最小总费用,总费用为 20 。
注意到任意两个点之间只有唯一一条路径互相到达。

示例 2:

输入:points = [[3,12],[-2,5],[-4,1]]
输出:18

示例 3:

输入:points = [[0,0],[1,1],[1,0],[-1,1]]
输出:4

示例 4:

输入:points = [[-1000000,-1000000],[1000000,1000000]]
输出:4000000

示例 5:

输入:points = [[0,0]]
输出:0

提示:

  • 1 <= points.length <= 1000
  • -10^6 <= xi, yi <= 10^6
  • 所有点 (xi, yi) 两两不同。

解法一:最小生成树Prim

class Solution {public int minCostConnectPoints(int[][] points) {int n = points.length;if (n == 1) {return 0;}boolean[] inMST = new boolean[n]; // 是否在最小生成树中int[] minWeight = new int[n];Arrays.fill(minWeight, Integer.MAX_VALUE);// 从第一个节点开始minWeight[0] = 0; // 初始化第一个点的权重为0int totalCost = 0;for (int i = 0; i < n; i++) {// 找到下一个未包含在MST中 且 权重最小的点int u = -1;for (int j = 0; j < n; j++) {if (!inMST[j] && (u == -1 || minWeight[j] < minWeight[u])) {u = j;}}// 将该点加入到MST中inMST[u] = true;totalCost += minWeight[u];// 更新其他点的最小权重for (int v = 0; v < n; v++) {if (!inMST[v]) {// u到v的权重int weight = manhattanDistance(points[u], points[v]);// 更新点v的权重minWeight[v] = Math.min(minWeight[v], weight);}}}return totalCost;}/*** 计算曼哈顿距离* * @param p1 连接点1* @param p2 连接点2* @return*/private int manhattanDistance(int[] p1, int[] p2) {return Math.abs(p1[0] - p2[0]) + Math.abs(p1[1] - p2[1]);}
}

解法二:Kruskal算法

class Solution {// 边的类static class Edge implements Comparable<Edge> {private int src, dest, weight;public Edge(int src, int dest, int weight) {this.src = src;this.dest = dest;this.weight = weight;}@Overridepublic int compareTo(Edge o) {return Integer.compare(this.weight, o.weight);}}static class DisjointSet {int[] parent, rank;public DisjointSet(int n) {parent = new int[n];rank = new int[n];for (int i = 0; i < n; i++) {parent[i] = i;rank[i] = 0;}}public int find(int u) {if (parent[u] != u) {parent[u] = find(parent[u]);}return parent[u];}public void union(int u, int v) {int rootU = find(u);int rootV = find(v);if (rootU != rootV) {if (rank[rootU] > rank[rootV]) {parent[rootV] = rootU;} else if (rank[rootU] < rank[rootV]) {parent[rootU] = rootV;} else {parent[rootV] = rootU;rank[rootU]++;}}}}public int minCostConnectPoints(int[][] points) {int n = points.length;List<Edge> edges = new ArrayList<>();// 创建所有边for (int i = 0; i < n; i++) {for (int j = i + 1; j < n; j++) {int weight = manhattanDistance(points[i], points[j]);edges.add(new Edge(i, j, weight));}}// 按边权重排序Collections.sort(edges);DisjointSet set = new DisjointSet(n);int totalCost = 0;for (Edge edge : edges) {if (set.find(edge.src) != set.find(edge.dest)) {set.union(edge.src, edge.dest);totalCost += edge.weight;}}return totalCost;}/*** 计算曼哈顿距离* * @param p1 连接点1* @param p2 连接点2* @return*/private int manhattanDistance(int[] p1, int[] p2) {return Math.abs(p1[0] - p2[0]) + Math.abs(p1[1] - p2[1]);}
}

4. 网络延迟时间

有 n 个网络节点,标记为 1 到 n

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 

示例 1:

输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2

示例 2:

输入:times = [[1,2,1]], n = 2, k = 1
输出:1

示例 3:

输入:times = [[1,2,1]], n = 2, k = 2
输出:-1

提示:

  • 1 <= k <= n <= 100
  • 1 <= times.length <= 6000
  • times[i].length == 3
  • 1 <= ui, vi <= n
  • ui != vi
  • 0 <= wi <= 100
  • 所有 (ui, vi) 对都 互不相同(即,不含重复边)

解法一:最短路径 - Dijkstra

class Solution {public int networkDelayTime(int[][] times, int n, int k) {// 创建一个邻接表来表示图// 每个节点映射到它所有邻接节点及其相应的传递时间Map<Integer, List<int[]>> graph = new HashMap<>();for (int[] time : times) {// 先加入key -> uigraph.putIfAbsent(time[0], new ArrayList<>());// 后加入value -> vi wigraph.get(time[0]).add(new int[] { time[1], time[2] });}// 小根堆优先队列 跟踪当前最短路径的节点及其到达时间PriorityQueue<int[]> minHead = new PriorityQueue<>(Comparator.comparing(a -> a[1]));minHead.offer(new int[] { k, 0 }); // {节点, 时间}// 跟踪到达每个节点的最短时间Map<Integer, Integer> minTime = new HashMap<>();minTime.put(k, 0); // 节点,时间// Dijkstra算法// 从节点k开始,逐步扩展到邻接节点,更新每个节点的最短传递时间while (!minHead.isEmpty()) {int[] current = minHead.poll();int node = current[0];int time = current[1];// 遍历当前节点的所有邻接节点if (graph.containsKey(node)) {for (int[] neighbor : graph.get(node)) {int nextNode = neighbor[0];int travelTime = neighbor[1];// 计算到达邻接节点的时间int newTime = time + travelTime;// 如果通过当前节点到达某个邻接节点的时间比已知的时间更短,则更新时间并将该邻接节点加入优先级队列if (newTime < minTime.getOrDefault(nextNode, Integer.MAX_VALUE)) {minTime.put(nextNode, newTime);minHead.offer(new int[] { nextNode, newTime });}}}}// 找到所有节点的最大时间int maxTime = 0;for (int i = 1; i <= n; i++) {// 如果存在未到达的节点,则返回-1if (!minTime.containsKey(i)) {return -1;}maxTime = Math.max(maxTime, minTime.get(i));}return maxTime;}
}

5. k站中转内最便宜的航班

有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi

现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1

示例 1:

输入: 
n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1
输出: 700 
解释: 城市航班图如上
从城市 0 到城市 3 经过最多 1 站的最佳路径用红色标记,费用为 100 + 600 = 700。
请注意,通过城市 [0, 1, 2, 3] 的路径更便宜,但无效,因为它经过了 2 站。

示例 2:

输入: 
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1
输出: 200
解释: 
城市航班图如上
从城市 0 到城市 2 经过最多 1 站的最佳路径标记为红色,费用为 100 + 100 = 200。

示例 3:

输入:n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 0
输出:500
解释:
城市航班图如上
从城市 0 到城市 2 不经过站点的最佳路径标记为红色,费用为 500。

提示:

  • 1 <= n <= 100
  • 0 <= flights.length <= (n * (n - 1) / 2)
  • flights[i].length == 3
  • 0 <= fromi, toi < n
  • fromi != toi
  • 1 <= pricei <= 10^4
  • 航班没有重复,且不存在自环
  • 0 <= src, dst, k < n
  • src != dst

解法一:最短路径Dijkstra。执行耗时13ms

class Solution {public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {// 使用邻接表表示图List<List<int[]>> graph = new ArrayList<>();for (int i = 0; i < n; i++) {graph.add(new ArrayList<>());}// 填充图for (int[] flight : flights) {graph.get(flight[0]).add(new int[] { flight[1], flight[2] });}// 小根堆,元素为 {当前花费,当前城市,当前站数}PriorityQueue<int[]> minHeap = new PriorityQueue<>(Comparator.comparing(a -> a[0]));minHeap.offer(new int[] { 0, src, 0 });// 记录到达每个城市的最小费用,并且要注意站数的限制int[][] minCost = new int[n][k + 2]; // k + 2以考虑k站和出发城市// 初始化为最大值for (int i = 0; i < n; i++) {Arrays.fill(minCost[i], Integer.MAX_VALUE);}minCost[src][0] = 0; // 从src开始,费用为0// Dijkstra核心逻辑while (!minHeap.isEmpty()) {int[] current = minHeap.poll();int cost = current[0]; // 当前花费int city = current[1]; // 当前城市int stops = current[2]; // 当前站数// 如果当前城市是目的地且经过站数不超过kif (city == dst) {return cost;}// 如果经过的站数已经达到k+1,跳过if (stops > k) {continue;}// 遍历当前城市的所有邻接城市for (int[] neighbor : graph.get(city)) {int nextCity = neighbor[0]; // 目标城市int price = neighbor[1]; // 价格int newCost = cost + price; // 新的总费用// 如果新费用低于当前记录,更新并添加到优先队列if (newCost < minCost[nextCity][stops + 1]) {minCost[nextCity][stops + 1] = newCost;minHeap.offer(new int[] { newCost, nextCity, stops + 1 });}}}// 如果没有找到有效路径,返回-1return -1;}
}

解法二:最短路径 - Bellman-Ford。执行耗时5ms

    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {// 初始化源城市的最小费用为0,其他城市为无穷大int[] prices = new int[n];Arrays.fill(prices, Integer.MAX_VALUE);prices[src] = 0;// 最多迭代k+1次for (int i = 0; i <= k; i++) {int[] tempPrices = Arrays.copyOf(prices,n);// 遍历所有的边for (int[] flight : flights) {int u = flight[0], v = flight[1], cost = flight[2];if(prices[u] == Integer.MAX_VALUE) {// 无法到达城市ucontinue;}// 更新到达每个城市的最小费用tempPrices[v] = Math.min(tempPrices[v], prices[u] + cost);}prices = tempPrices;}return prices[dst] == Integer.MAX_VALUE ? -1 : prices[dst];}

6. 课程表

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • prerequisites[i] 中的所有课程对 互不相同

解法一:拓扑排序

使用入度和队列来实现拓扑排序。每次从队列中取出一个课程,并将其对应的后置课程的入度减一,如果某个课程的入度减至0,说明可以上这门课程。

class Solution {// 等同于在判断有向图是否存在环 -> 拓扑排序public boolean canFinish(int numCourses, int[][] prerequisites) {int[] inDegree = new int[numCourses];List<List<Integer>> graph = new ArrayList<>();for (int i = 0; i < numCourses; i++) {graph.add(new ArrayList<>());}// 处理节点的先后关系for (int[] pre : prerequisites) {// 要想修读pre[0],先修pre[1] pre[1] -> pre[0]graph.get(pre[1]).add(pre[0]);// 后置课程的入度加1inDegree[pre[0]]++;}Queue<Integer> queue = new LinkedList<>();for (int i = 0; i < numCourses; i++) {if (inDegree[i] == 0) {queue.offer(i);}}int courseCompleted = 0;// 节点出队while (!queue.isEmpty()) {int course = queue.poll();courseCompleted++;for (Integer neighbor : graph.get(course)) {// 节点出队后,后置课程的入度减1inDegree[neighbor]--;if (inDegree[neighbor] == 0) {queue.offer(neighbor);}}}return numCourses == courseCompleted;}
}

解法二:深度优先搜索DFS + 递归标记

    public boolean canFinish(int numCourses, int[][] prerequisites) {List<List<Integer>> graph = new ArrayList<>();for (int i = 0; i < numCourses; i++) {graph.add(new ArrayList<>());}// 处理图中节点的先后关系for (int[] pre : prerequisites) {graph.get(pre[1]).add(pre[0]);}// 维护一个状态数组跟踪每个课程的访问状态 0-未访问 1-正在访问 2-已访问int[] visited = new int[numCourses];for (int i = 0; i < numCourses; i++) {// 判断是否存在环,如果存在环,则说明无法完成所有可能(不存在拓扑排序)if(hasCycle(graph, visited, i)) {return false;}}return true;}// 如果在DFS遍历的过程中再次访问到正在访问的节点,则说明存在环,无法完成所有的课程private boolean hasCycle(List<List<Integer>> graph, int[] visited, int node) {if(visited[node] == 1) return true;  // 当前路径中存在环if(visited[node] == 2) return false; // 已经访问过了,无需再遍历visited[node] = 1;  // 正在访问for (Integer neighbor : graph.get(node)) {// 递归if(hasCycle(graph, visited, neighbor)) {return true;}}visited[node] = 2;  // 标记为已访问return false;}

7. 课程表Ⅱ

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

  • 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

示例 3:

输入:numCourses = 1, prerequisites = []
输出:[0]

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= numCourses * (numCourses - 1)
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • ai != bi
  • 所有[ai, bi] 互不相同

解法一:拓扑排序(入度+队列)。执行耗时7ms

class Solution {public int[] findOrder(int numCourses, int[][] prerequisites) {List<Integer> order = new ArrayList<>();int[] inDegree = new int[numCourses];List<List<Integer>> graph = new ArrayList<>();for (int i = 0; i < numCourses; i++) {graph.add(new ArrayList<>());}// 处理节点的先后关系for (int[] pre : prerequisites) {// 要想修读pre[0],先修pre[1] pre[1] -> pre[0]graph.get(pre[1]).add(pre[0]);// 后置课程的入度加1inDegree[pre[0]]++;}Queue<Integer> queue = new LinkedList<>();for (int i = 0; i < numCourses; i++) {if (inDegree[i] == 0) {queue.offer(i);}}// 节点出队while (!queue.isEmpty()) {int course = queue.poll();order.add(course);for (Integer neighbor : graph.get(course)) {// 节点出队后,后置课程的入度减1inDegree[neighbor]--;if (inDegree[neighbor] == 0) {queue.offer(neighbor);}}}return order.size() == numCourses ? order.stream().mapToInt(i -> i).toArray() : new int[0];}
}

解法二:深度优先搜索DFS。执行耗时5ms

class Solution {private List<List<Integer>> graph;private int[] visited;private List<Integer> order;public int[] findOrder(int numCourses, int[][] prerequisites) {// 初始化图和状态数组graph = new ArrayList<>();for (int i = 0; i < numCourses; i++) {graph.add(new ArrayList<>());}visited = new int[numCourses];order = new ArrayList<>();// 构建图for (int[] pre : prerequisites) {graph.get(pre[1]).add(pre[0]);}// DFS 遍历每门课程for (int i = 0; i < numCourses; i++) {if (visited[i] == 0) {if (!dfs(i)) {return new int[0]; // 返回空数组表示无法完成所有课程}}}// 结果反转Collections.reverse(order);return order.stream().mapToInt(i -> i).toArray();}private boolean dfs(int course) {if (visited[course] == 1) { // 发现环return false;}if (visited[course] == 2) { // 已访问return true;}// 标记为正在访问visited[course] = 1;for (int neighbor : graph.get(course)) {if (!dfs(neighbor)) {return false;}}// 标记为已访问,并加入结果visited[course] = 2;order.add(course);return true;}
}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • EFCore中结合Dapper执行SQL任意查询
  • 初识C++:开启C++之旅
  • Angular组件概念
  • 基于 Android studio 实现停车场管理系统--原创
  • Java String 去掉特殊字符之前的内容方法
  • 实训日记day29
  • 主成分分析(PCA)
  • 自然语言处理(NLP)--数据增强
  • 文本纠错实现定位与标记
  • JMeter进阶技巧:参数化与数据驱动测试
  • Polars简明基础教程八:Series 和 DataFrame 以及它们之间的转换_B
  • Qt窗口交互场景、子窗口数据获取
  • AcWing 723. PUM
  • 改善工作流
  • Spring Boot如何实现数据脱敏?
  • [NodeJS] 关于Buffer
  • ECMAScript 6 学习之路 ( 四 ) String 字符串扩展
  • PHP CLI应用的调试原理
  • Python代码面试必读 - Data Structures and Algorithms in Python
  • 表单中readonly的input等标签,禁止光标进入(focus)的几种方式
  • 初识MongoDB分片
  • 对象引论
  • 简单基于spring的redis配置(单机和集群模式)
  • 看图轻松理解数据结构与算法系列(基于数组的栈)
  • 爬虫进阶 -- 神级程序员:让你的爬虫就像人类的用户行为!
  • 想晋级高级工程师只知道表面是不够的!Git内部原理介绍
  • 学习Vue.js的五个小例子
  • 用mpvue开发微信小程序
  • linux 淘宝开源监控工具tsar
  • PostgreSQL 快速给指定表每个字段创建索引 - 1
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • ​linux启动进程的方式
  • ​软考-高级-系统架构设计师教程(清华第2版)【第1章-绪论-思维导图】​
  • #鸿蒙生态创新中心#揭幕仪式在深圳湾科技生态园举行
  • (echarts)echarts使用时重新加载数据之前的数据存留在图上的问题
  • (八)c52学习之旅-中断实验
  • (机器学习-深度学习快速入门)第一章第一节:Python环境和数据分析
  • (南京观海微电子)——COF介绍
  • (十八)用JAVA编写MP3解码器——迷你播放器
  • (实测可用)(3)Git的使用——RT Thread Stdio添加的软件包,github与gitee冲突造成无法上传文件到gitee
  • (转)JAVA中的堆栈
  • (转)setTimeout 和 setInterval 的区别
  • (状压dp)uva 10817 Headmaster's Headache
  • .“空心村”成因分析及解决对策122344
  • .Family_物联网
  • .FileZilla的使用和主动模式被动模式介绍
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .net core webapi 大文件上传到wwwroot文件夹
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .NET/C# 如何获取当前进程的 CPU 和内存占用?如何获取全局 CPU 和内存占用?
  • .NetCore发布到IIS
  • .net反编译工具
  • .NET企业级应用架构设计系列之结尾篇
  • .net中调用windows performance记录性能信息
  • ::什么意思