拓扑排序(Topological Sorting)是图论中的一个概念,用于对有向无环图(DAG, Directed Acyclic Graph)进行排序。在拓扑排序中,节点按照线性顺序排列,使得对于图中的每一条有向边 (u, v),节点 u 都在节点 v 之前。这样的排序满足所有有向边的方向性要求。
拓扑排序的算法
-
深度优先搜索(DFS):
- 利用DFS进行遍历,每次DFS时,如果节点已经访问过,则表示图中存在环,无法进行拓扑排序。
- 在DFS过程中,记录节点的访问状态,通常有三种状态:未访问、访问中、已访问。
- 当节点从访问中变为已访问时,将其加入到结果序列的末尾。
-
广度优先搜索(BFS):
- 利用BFS进行遍历,同时记录每个节点的入度。
- 将所有入度为0的节点加入到一个队列中。
- 依次取出队列中的节点,并将其加入到结果序列中,同时更新其相邻节点的入度。
- 重复上述过程,直到队列为空。
拓扑排序的应用
- 任务调度:在任务之间存在依赖关系时,可以使用拓扑排序来确定任务的执行顺序。
- 课程规划:用于确定学生选修课程的顺序,确保先修课程在后续课程之前完成。
- 项目管理:确定项目中各个阶段或任务的执行顺序。
拓扑排序的Java实现(基于DFS):
import java.util.*;
public class TopologicalSort {
private Map<Integer, List<Integer>> adjList;
private boolean[] visited;
private List<Integer> topologicalOrder;
public TopologicalSort(Map<Integer, List<Integer>> adjList) {
this.adjList = adjList;
this.visited = new boolean[adjList.size()];
this.topologicalOrder = new ArrayList<>();
}
public List<Integer> topologicalSort() {
for (int node : adjList.keySet()) {
if (!visited[node]) {
dfs(node);
}
}
return topologicalOrder;
}
private void dfs(int node) {
visited[node] = true;
for (int neighbor : adjList.get(node)) {
if (!visited[neighbor]) {
dfs(neighbor);
}
}
topologicalOrder.add(node);
}
public static void main(String[] args) {
Map<Integer, List<Integer>> adjList = new HashMap<>();
adjList.put(1, Arrays.asList(2, 3));
adjList.put(2, Arrays.asList(4));
adjList.put(3, Arrays.asList(4));
adjList.put(4, Collections.emptyList());
adjList.put(5, Collections.emptyList());
TopologicalSort topologicalSort = new TopologicalSort(adjList);
List<Integer> order = topologicalSort.topologicalSort();
System.out.println("Topological order: " + order);
}
}
在大厂面试中,与拓扑排序相关的问题通常涉及到对图的处理和算法设计。以下是三道可能出现在大厂面试中的编程题目,以及相应的Java源码实现。
题目 1:课程依赖关系
描述:
给定一个课程列表和课程之间的依赖关系,为所有课程安排一个上课时间表。每个课程都有一个先修课程列表,如果课程 A 依赖于课程 B,则 B 必须在 A 之前上课。返回一个按时间顺序排列的课程列表,如果无法安排,则返回空列表。
示例:
输入: [["a", "b"], ["b", "c"], ["a", "c"]]
输出: ["a", "b", "c"]
Java 源码:
import java.util.*;
public class CourseSchedule {
private Map<String, List<String>> graph;
private Set<String> visited;
private List<String> order;
public List<String> findOrder(String[] courses) {
graph = new HashMap<>();
visited = new HashSet<>();
order = new ArrayList<>();
for (String course : courses) {
String[] coursePair = course.split(", ");
if (!graph.containsKey(coursePair[1])) {
graph.put(coursePair[1], new ArrayList<>());
}
graph.get(coursePair[1]).add(coursePair[0]);
}
for (String course : graph.keySet()) {
if (!visited.contains(course)) {
dfs(course);
}
}
return order.isEmpty() ? new ArrayList<>() : order;
}
private void dfs(String course) {
visited.add(course);
for (String preCourse : graph.get(course)) {
if (!visited.contains(preCourse)) {
dfs(preCourse);
}
}
order.add(0, course); // 逆序添加到列表中以保持拓扑顺序
}
public static void main(String[] args) {
CourseSchedule solution = new CourseSchedule();
String[] courses = {"a, b", "b, c", "a, c"};
List<String> result = solution.findOrder(courses);
System.out.println("Course order: " + result);
}
}
题目 2:项目任务调度
描述:
给定一个项目任务列表和任务之间的依赖关系,每个任务都有一个唯一的标识符和一个任务完成时间。如果任务 A 依赖于任务 B,则 B 必须在 A 之前完成。返回一个按完成时间顺序排列的任务列表,如果无法调度,则返回空列表。
示例:
输入: tasks = ["a(2)", "b(1)", "c(5)", "d(3)", "e(2)"], dependencies = ["a(b)", "e(c)"]
输出: ["b", "e", "a", "d", "c"]
Java 源码:
import java.util.*;
public class TaskScheduler {
private Map<String, Integer> taskTime;
private Map<String, List<String>> dependencies;
private Queue<String> queue;
public List<String> scheduleTasks(String[] tasks, String[] dependencies) {
taskTime = new HashMap<>();
for (String task : tasks) {
String[] parts = task.split("\\(");
taskTime.put(parts[0], Integer.parseInt(parts[1]));
}
this.dependencies = new HashMap<>();
for (String dep : dependencies) {
String[] parts = dep.split("(?=\\()");
this.dependencies.put(parts[0], parts[1]);
}
queue = new LinkedList<>();
for (String task : taskTime.keySet()) {
if (!dependencies.containsKey(task)) {
queue.add(task);
}
}
while (!queue.isEmpty()) {
String task = queue.poll();
if (taskTime.containsKey(task)) {
for (String nextTask : taskTime.keySet()) {
if (dependencies.get(nextTask).equals(task)) {
taskTime.remove(nextTask);
if (taskTime.size() == 1) {
return Arrays.asList(task);
}
for (String t : taskTime.keySet()) {
if (!queue.contains(t)) {
queue.add(t);
}
}
}
}
}
}
return Collections.emptyList();
}
public static void main(String[] args) {
TaskScheduler solution = new TaskScheduler();
String[] tasks = {"a(2)", "b(1)", "c(5)", "d(3)", "e(2)"};
String[] dependencies = {"a(b)", "e(c)"};
List<String> result = solution.scheduleTasks(tasks, dependencies);
System.out.println("Task order: " + result);
}
}
题目 3:活动选择问题
描述:
给定一系列活动和它们的开始时间及结束时间,选择最大的活动集合,其中活动之间不会相互重叠。每次选择一个活动时,都必须在开始时间早于结束时间的前提下,选择结束时间最早的活动。
示例:
输入: [[1, 2], [3, 4], [2, 6]]
输出: [2, 3] 或 [3, 4]
Java 源码:
import java.util.*;
public class ActivitySelection {
public int[][] maxActivities(int[][]活动时间) {
Arrays.sort(活动时间, (a, b) -> a[1] - b[1]); // 按结束时间排序
int n =活动时间.length;
int[] dp = new int[n];
int[] prev = new int[n];
int count = 1;
dp[0] = 1;
prev[0] = -1;
for (int i = 1; i < n; i++) {
if (活动时间[i][0] >= dp[i - 1]) {
// 如果当前活动的开始时间大于等于上一个活动的结束时间,则选择
dp[i] = dp[i - 1] + 1;
prev[i] = i - 1;
} else {
dp[i] = dp[i - 1];
prev[i] = prev[i - 1];
}
if (dp[i] > count) {
count = dp[i];
}
}
int[] result = new int[count];
for (int i = 0; i < count; i++) {
result[i] =活动时间[prev[n - 1 - i]][0];
}
return new int[][]{{result[0], dp[n - 1]}, {活动时间[prev[n - 1]][0], dp[n - 1]}};
}
public static void main(String[] args) {
ActivitySelection solution = new ActivitySelection();
int[][] 活动时间 = {{1, 2}, {3, 4}, {2, 6}};
int[][] result = solution.maxActivities(活动时间);
System.out.println("Max activities: " + Arrays.deepToString(result));
}
}
这些题目和源码展示了拓扑排序在解决实际问题中的应用。在面试中,能够根据问题的特点选择合适的算法并实现其解决方案是非常重要的。希望这些示例能够帮助你更好地准备面试!