Kruskal 算法
最后更新
Kruskal 算法用来构建最小生成树(MST),也就是连接所有节点、没有环、总权重最小的一组边。它先把所有边按权重排序,然后贪心地依次加入当前最便宜的边,前提是这条边连接的两个节点还没有连通。点击上方的“播放”,观看这棵树如何每次加入一条最便宜的安全边,逐渐长大。
“是否已经连通”的判断由并查集(不相交集合)数据结构完成:如果一条边的两个端点已经在同一个连通分量中,就跳过它,因为加入它会形成环。排序是主要开销,所以总复杂度是 O(E log E)。Kruskal 算法特别适合稀疏图。
时间复杂度与空间复杂度
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(E log E) | 主要花在对边排序上 |
| 并查集操作 | ≈ O(E α(V)) | 使用路径压缩后,每次判断接近常数时间 |
| 空间 | O(V + E) | 边列表 + 不相交集合结构 |
| 最适合 | 稀疏图 | 基于全局边列表工作 |
分步说明
| 步骤 | 发生了什么 |
|---|---|
| 1 | 把所有边按权重升序排序。 |
| 2 | 让每个节点各自成为一个连通分量(并查集)。 |
| 3 | 取出下一条最便宜的边。 |
| 4 | 如果它的两个端点在不同的连通分量中,就把它加入树中,并合并这两个分量。 |
| 5 | 否则跳过它(加入会形成环)。 |
| 6 | 当树有 V − 1 条边时停止。 |
实例演示
图中有节点 A, B, C, D 和边 A-B(1)、B-C(2)、A-C(3)、C-D(4)、B-D(5)。排序后的边:A-B(1)、B-C(2)、A-C(3)、C-D(4)、B-D(5):
| 边(权重) | 处理前的连通分量 | 操作 |
|---|---|---|
A-B(1) | {A} {B} {C} {D} | 属于不同分量:加入 MST,合并为 {A,B}。 |
B-C(2) | {A,B} {C} {D} | 属于不同分量:加入 MST,合并为 {A,B,C}。 |
A-C(3) | {A,B,C} {D} | A 和 C 已经连通:跳过(会形成环)。 |
C-D(4) | {A,B,C} {D} | 属于不同分量:加入 MST,合并为 {A,B,C,D}。 |
| 停止 | {A,B,C,D} | 树已有 V - 1 = 3 条边。MST = A-B, B-C, C-D,总权重 7。 |
什么时候用 Kruskal 算法
| 适合使用 | 不适合使用 |
|---|---|
| 图是稀疏的(边数相对节点数较少)。 | 图是稠密的:这时用堆实现的 Prim 算法通常更快。 |
| 边已经以全局列表的形式存在,可以直接排序。 | 边只能通过邻接表获得,需要逐个节点扫描。 |
| 你想要一个基于并查集的简单实现。 | 你需要让树从某个特定的起始节点开始生长。 |
| 你想为一个不连通的图构建生成森林。 | 边以流的形式不断到来,无法先做完整排序。 |
Kruskal's Algorithm 代码
简洁、可运行的 Kruskal's Algorithm 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。
Kruskal's Algorithm 的 Python 代码
1def find(parent, x):2 while parent[x] != x:3 parent[x] = parent[parent[x]] # path compression4 x = parent[x]5 return x6
7
8def kruskal(vertices, edges):9 # Take the cheapest edge that does not close a cycle10 parent = {v: v for v in vertices}11 mst, total = [], 012 for w, u, v in sorted(edges):13 root_u, root_v = find(parent, u), find(parent, v)14 if root_u != root_v:15 parent[root_u] = root_v16 mst.append((u, v, w))17 total += w18 return mst, total19
20
21vertices = ["A", "B", "C", "D", "E"]22edges = [23 (4, "A", "B"), (1, "A", "C"), (3, "B", "C"), (2, "B", "D"),24 (5, "C", "D"), (6, "C", "E"), (7, "D", "E"),25]26
27mst, total = kruskal(vertices, edges)28for u, v, w in mst:29 print(f"{u} - {v} (weight {w})")30print("Total MST weight:", total)Kruskal's Algorithm 的 JavaScript 代码
1const nodes = ["A", "B", "C", "D", "E"];2const edges = [3 ["A", "B", 4], ["A", "C", 2], ["B", "C", 1], ["B", "D", 5],4 ["C", "D", 8], ["C", "E", 10], ["D", "E", 2],5];6
7function kruskal() {8 // Union-find with path compression detects cycles cheaply9 const parent = Object.fromEntries(nodes.map((n) => [n, n]));10 const find = (x) => (parent[x] === x ? x : (parent[x] = find(parent[x])));11 const mst = [];12 let total = 0;13 const sorted = [...edges].sort((a, b) => a[2] - b[2]);14 for (const [u, v, w] of sorted) {15 const rootU = find(u);16 const rootV = find(v);17 if (rootU !== rootV) {18 parent[rootU] = rootV;19 mst.push(`${u}-${v} (${w})`);20 total += w;21 }22 }23 return { mst, total };24}25
26const { mst, total } = kruskal();27console.log("MST edges:", mst.join(", "));28console.log("Total weight:", total);Kruskal's Algorithm 的 Java 代码
1import java.util.Arrays;2
3public class Main {4 static int[] parent;5
6 static int find(int x) {7 if (parent[x] != x) parent[x] = find(parent[x]); // path compression8 return parent[x];9 }10
11 static boolean union(int a, int b) {12 int rootA = find(a), rootB = find(b);13 if (rootA == rootB) return false; // would create a cycle14 parent[rootA] = rootB;15 return true;16 }17
18 public static void main(String[] args) {19 int n = 6;20 int[][] edges = {21 {0, 1, 4}, {0, 2, 3}, {1, 2, 1}, {1, 3, 2},22 {2, 3, 4}, {3, 4, 2}, {4, 5, 6}, {2, 5, 7}23 };24 Arrays.sort(edges, (a, b) -> a[2] - b[2]);25 parent = new int[n];26 for (int i = 0; i < n; i++) parent[i] = i;27
28 int total = 0, used = 0;29 for (int[] e : edges) {30 if (union(e[0], e[1])) {31 System.out.println("Take edge " + e[0] + "-" + e[1] + " (weight " + e[2] + ")");32 total += e[2];33 if (++used == n - 1) break;34 }35 }36 System.out.println("MST total weight: " + total);37 }38}Kruskal's Algorithm 的 C++ 代码
1#include <algorithm>2#include <iostream>3#include <numeric>4#include <vector>5
6struct Edge {7 int u, v, weight;8};9
10struct DSU {11 std::vector<int> parent;12 explicit DSU(int n) : parent(n) {13 std::iota(parent.begin(), parent.end(), 0);14 }15 int find(int x) {16 if (parent[x] != x) parent[x] = find(parent[x]); // path compression17 return parent[x];18 }19 bool unite(int a, int b) {20 int ra = find(a), rb = find(b);21 if (ra == rb) return false; // would create a cycle22 parent[ra] = rb;23 return true;24 }25};26
27int main() {28 const int n = 5;29 std::vector<Edge> edges = {30 {0, 1, 4}, {0, 2, 2}, {1, 2, 1}, {1, 3, 5},31 {2, 3, 8}, {2, 4, 10}, {3, 4, 3},32 };33 // Greedily take the cheapest edge that connects two components34 std::sort(edges.begin(), edges.end(),35 [](const Edge& a, const Edge& b) { return a.weight < b.weight; });36 DSU dsu(n);37 int total = 0;38 std::cout << "MST edges:\n";39 for (const Edge& e : edges) {40 if (dsu.unite(e.u, e.v)) {41 std::cout << " " << e.u << " - " << e.v42 << " (weight " << e.weight << ")\n";43 total += e.weight;44 }45 }46 std::cout << "Total weight: " << total << "\n";47 return 0;48}Kruskal's Algorithm 的 C 代码
1#include <stdbool.h>2#include <stdio.h>3#include <stdlib.h>4
5#define N 56
7typedef struct {8 int u, v, weight;9} Edge;10
11int parent[N];12
13int find(int x) {14 if (parent[x] != x) parent[x] = find(parent[x]); // path compression15 return parent[x];16}17
18bool unite(int a, int b) {19 int ra = find(a), rb = find(b);20 if (ra == rb) return false; // would create a cycle21 parent[ra] = rb;22 return true;23}24
25int cmpEdge(const void* a, const void* b) {26 return ((const Edge*)a)->weight - ((const Edge*)b)->weight;27}28
29int main(void) {30 Edge edges[] = {31 {0, 1, 4}, {0, 2, 2}, {1, 2, 1}, {1, 3, 5},32 {2, 3, 8}, {2, 4, 10}, {3, 4, 3},33 };34 int m = sizeof(edges) / sizeof(edges[0]);35 for (int i = 0; i < N; i++) parent[i] = i;36 // Greedily take the cheapest edge that connects two components37 qsort(edges, m, sizeof(Edge), cmpEdge);38 int total = 0;39 printf("MST edges:\n");40 for (int i = 0; i < m; i++) {41 if (unite(edges[i].u, edges[i].v)) {42 printf(" %d - %d (weight %d)\n",43 edges[i].u, edges[i].v, edges[i].weight);44 total += edges[i].weight;45 }46 }47 printf("Total weight: %d\n", total);48 return 0;49}Kruskal 算法常见问题
最小生成树是什么?
V 个节点,它恰好有 V - 1 条边。Kruskal 算法和 Prim 算法有什么区别?
两者都用贪心策略构建最小生成树。Kruskal 对所有边做全局排序,借助并查集加入不会形成环的最便宜的边。Prim 从一个起始节点出发向外生长出一棵树,每次加入离开这棵树的最便宜的边。Kruskal 适合稀疏图;Prim(配合堆)适合稠密图。
Kruskal 算法为什么要用并查集?
加入一条边之前,Kruskal 必须检查它的两个端点是否已经连通,因为加入这样的边会形成环。并查集(不相交集合)能回答“它们在同一个分量里吗?”,并以接近常数的均摊时间合并分量,让算法保持高效。
什么时候该用 Kruskal 算法而不是 Prim 算法?
E 较小时,O(E log E) 的排序开销很低。在 E 接近 V² 的稠密图上,使用二叉堆或斐波那契堆的 Prim 算法往往更快,因为它不需要事先把所有边排序。Kruskal 算法能用于不连通的图吗?
V - 1 条边,而是得到一个最小生成森林:每个连通分量各有一棵 MST。这是自然而然的结果,因为并查集永远不会合并彼此之间没有路径的节点。实现 Kruskal 算法时最常见的错误是什么?
省略并查集的路径压缩和按秩合并,这会让每次连通性判断从接近常数时间退化到接近线性时间,甚至成为运行时间的主要部分。另一个常见 bug 是加入边之后忘了真正合并两个分量,导致后面的边形成环。