常用图算法的 Java 实现
·
笔试和面试中,常用的图算法除了深度优先搜索、广度优先搜索,还有如下五种:
- 单源最短路径的(堆优化)Dijkstra 算法
- 多源最短路径的 Floyd-Warshall 算法
- 最小生成树的 Kruskal 算法
- 最小生成树的 Prim 算法
- 拓扑排序的 Kahn 算法
下面分别给出算法原理简析和代码实现。
堆优化的 Dijkstra 算法
import java.util.*;
public class Main {
public static void main(String[] args) {
// A -> B 2
// A -> C 6
// B -> C 3
Map<Character, Map<Character, Integer>> graph = new HashMap<>();
graph.put('A', new HashMap<>());
graph.get('A').put('B', 2);
graph.get('A').put('C', 6);
graph.put('B', new HashMap<>());
graph.get('B').put('C', 3);
graph.put('C', new HashMap<>());
Map<Character, Integer> res = heapDijkstra(graph, 'A');
for (char c : res.keySet()) {
System.out.println("from A to " + c + ": " + res.get(c));
}
}
private static Map<Character, Integer> heapDijkstra(Map<Character, Map<Character, Integer>> graph, char src) {
Map<Character, Integer> distances = new HashMap<>();
PriorityQueue<Map.Entry<Character, Integer>> pq = new PriorityQueue<>(Comparator.comparingInt(Map.Entry::getValue));
distances.put(src, 0);
pq.add(new AbstractMap.SimpleEntry<>(src, 0));
while (!pq.isEmpty()) {
Map.Entry<Character, Integer> entry = pq.poll();
char vertex = entry.getKey();
int distance = entry.getValue();
if (distances.get(vertex) < distance) continue;
Map<Character, Integer> neighbors = graph.get(vertex);
for (char neighbor : neighbors.keySet()) {
int newDistance = distance + neighbors.get(neighbor);
if (!distances.containsKey(neighbor) || newDistance < distances.get(neighbor)) {
distances.put(neighbor, newDistance);
pq.add(new AbstractMap.SimpleEntry<>(neighbor, newDistance));
}
}
}
return distances;
}
}

为快速求出当前最小的距离,使用堆。这里避免了堆的 remove 操作,堆中可能有已过时的数据,每次弹出堆的根,要进行额外判断。从堆中弹出的顶点 + 距离,只要不过时,可以证明,就是起点到该顶点的最短路径长度。
Floyd-Warshall 算法
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Character, Map<Character, Integer>> graph = new HashMap<>();
graph.put('A', new HashMap<>());
graph.get('A').put('B', 1);
graph.get('A').put('D', 3);
graph.put('B', new HashMap<>());
graph.get('B').put('C', 1);
graph.get('B').put('D', 2);
graph.put('C', new HashMap<>());
graph.get('C').put('A', 2);
graph.get('C').put('D', 4);
graph.put('D', new HashMap<>());
floydWarshall(graph);
}
private static void floydWarshall(Map<Character, Map<Character, Integer>> graph) {
Map<Character, Integer> map = new HashMap<>();
int n = 0;
for (char c : graph.keySet()) {
map.put(c, n++);
}
int[][] distances = new int[n][n];
for (int i = 0; i < n; i++) {
Arrays.fill(distances[i], Integer.MAX_VALUE);
distances[i][i] = 0;
}
for (char c : graph.keySet()) {
for (char c2 : graph.get(c).keySet()) {
distances[map.get(c)][map.get(c2)] = graph.get(c).get(c2);
}
}
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
if (distances[i][k] == Integer.MAX_VALUE) continue;
for (int j = 0; j < n; j++) {
if (distances[k][j] == Integer.MAX_VALUE) continue;
distances[i][j] = Math.min(distances[i][j], distances[i][k] + distances[k][j]);
}
}
}
System.out.print(" ");
for (char c : graph.keySet()) {
System.out.print(" " + c);
}
System.out.println();
for (char c : graph.keySet()) {
System.out.print(c);
for (char c2 : graph.keySet()) {
int d = distances[map.get(c)][map.get(c2)];
System.out.print(" " + (d == Integer.MAX_VALUE ? "X" : d));
}
System.out.println();
}
}
}

输入的图最好是邻接矩阵的形式。否则需要设置从顶点到数组下标的映射。
Kruskal 算法
最小生成树问题仅针对无向图。
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Character, Map<Character, Integer>> graph = new HashMap<>();
graph.put('A', new HashMap<>());
graph.get('A').put('B', 1);
graph.get('A').put('C', 2);
graph.get('A').put('D', 3);
graph.put('B', new HashMap<>());
graph.get('B').put('A', 1);
graph.get('B').put('C', 4);
graph.get('B').put('D', 5);
graph.put('C', new HashMap<>());
graph.get('C').put('A', 2);
graph.get('C').put('B', 4);
graph.get('C').put('D', 6);
graph.put('D', new HashMap<>());
graph.get('D').put('A', 3);
graph.get('D').put('B', 5);
graph.get('D').put('C', 6);
Set<Edge> res = kruskal(graph);
for (Edge e : res) {
System.out.println(e);
}
}
private static class Edge {
char u;
char v;
int weight;
public Edge(char u, char v, int weight) {
this.u = u;
this.v = v;
this.weight = weight;
}
@Override
public String toString() {
return "(" + u + "-" + v + ", " + weight + ")";
}
}
private static class UnionFind {
Map<Character, Character> parents = new HashMap<>();
public UnionFind(Set<Character> vertices) {
for (char c : vertices) {
parents.put(c, c);
}
}
public char find(char u) {
if (parents.get(u) != u) {
parents.put(u, find(parents.get(u)));
}
return parents.get(u);
}
public void union(char u, char v) {
char rootU = find(u);
char rootV = find(v);
if (rootU != rootV) {
parents.put(rootU, rootV);
}
}
}
private static Set<Edge> kruskal(Map<Character, Map<Character, Integer>> graph) {
PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight));
for (char c : graph.keySet()) {
Map<Character, Integer> graph2 = graph.get(c);
for (char c2 : graph2.keySet()) {
if (c < c2) pq.offer(new Edge(c, c2, graph2.get(c2)));
}
}
Set<Edge> mst = new HashSet<>();
UnionFind uf = new UnionFind(graph.keySet());
while (!pq.isEmpty()) {
Edge e = pq.poll();
if (uf.find(e.u) != uf.find(e.v)) {
uf.union(e.u, e.v);
mst.add(e);
if (mst.size() == graph.keySet().size() - 1) break;
}
}
if (mst.size() < graph.keySet().size() - 1) System.out.println("The graph is not connected!");
return mst;
}
}
-
提取所有边并去重(通过比较顶点大小避免重复)
-
按权重排序边后,使用并查集动态检测环,选择不形成环的最小边
Prim 算法
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Character, Map<Character, Integer>> graph = new HashMap<>();
graph.put('A', new HashMap<>());
graph.get('A').put('B', 1);
graph.get('A').put('C', 2);
graph.get('A').put('D', 3);
graph.put('B', new HashMap<>());
graph.get('B').put('A', 1);
graph.get('B').put('C', 4);
graph.get('B').put('D', 5);
graph.put('C', new HashMap<>());
graph.get('C').put('A', 2);
graph.get('C').put('B', 4);
graph.get('C').put('D', 6);
graph.put('D', new HashMap<>());
graph.get('D').put('A', 3);
graph.get('D').put('B', 5);
graph.get('D').put('C', 6);
Set<Edge> res = prim(graph);
for (Edge e : res) {
System.out.println(e);
}
}
private static class Edge {
char u;
char v;
int weight;
public Edge(char u, char v, int weight) {
this.u = u;
this.v = v;
this.weight = weight;
}
@Override
public String toString() {
return "(" + u + "-" + v + ", " + weight + ")";
}
}
private static Set<Edge> prim(Map<Character, Map<Character, Integer>> graph) {
Set<Character> visited = new HashSet<>();
char start = graph.keySet().iterator().next();
visited.add(start);
PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight));
for (char v : graph.get(start).keySet()) {
pq.add(new Edge(start, v, graph.get(start).get(v)));
}
Set<Edge> mst = new HashSet<>();
while (!pq.isEmpty() && visited.size() < graph.keySet().size()) {
Edge e = pq.poll();
if (visited.contains(e.v)) break;
mst.add(e);
visited.add(e.v);
for (char v : graph.get(e.v).keySet()) {
if (!visited.contains(v)) {
pq.add(new Edge(e.v, v, graph.get(e.v).get(v)));
}
}
}
if (mst.size() < graph.keySet().size() - 1) System.out.println("The graph is not connected!");
return mst;
}
}

-
从任意顶点出发,维护优先队列存储连接已选/未选顶点的边
-
每次选择最小边并扩展已选顶点集合,直到覆盖所有顶点
Kahn 算法
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Character, Set<Character>> graph = new HashMap<>();
graph.put('A', Set.of('C', 'D'));
graph.put('B', Set.of('D', 'E'));
graph.put('C', Set.of('F'));
graph.put('D', Set.of('G', 'H'));
graph.put('E', Set.of('I'));
graph.put('F', new HashSet<>());
graph.put('G', new HashSet<>());
graph.put('H', new HashSet<>());
graph.put('I', new HashSet<>());
List<Character> res = kahn(graph);
for (char c : res) {
System.out.print(c + " ");
}
}
private static List<Character> kahn(Map<Character, Set<Character>> graph) {
Map<Character, Integer> inDegree = new HashMap<>();
for (char c : graph.keySet()) {
inDegree.put(c, 0);
}
for (char c : graph.keySet()) {
for (char cc : graph.get(c)) {
inDegree.put(cc, inDegree.get(cc) + 1);
}
}
List<Character> res = new ArrayList<>();
Queue<Character> queue = new LinkedList<>();
for (char c : graph.keySet()) {
if (inDegree.get(c) == 0) {
queue.add(c);
}
}
while (!queue.isEmpty()) {
char c = queue.poll();
for (char cc : graph.get(c)) {
inDegree.put(cc, inDegree.get(cc) - 1);
if (inDegree.get(cc) == 0) {
queue.add(cc);
}
}
res.add(c);
}
if (res.size() < graph.keySet().size()) System.out.println("The graph is cyclic!");
return res;
}
}
![]()
用一个哈希表记录顶点的入度,每次选取入度为 0 的顶点放入队列中。
更多推荐

所有评论(0)