【数据结构与算法】第39篇:图论(三):最小生成树——Prim算法与Kruskal算法

目录

一、最小生成树基础概念

[1.1 什么是最小生成树](#1.1 什么是最小生成树)

[1.2 应用场景](#1.2 应用场景)

二、Prim算法

[2.1 算法思想](#2.1 算法思想)

[2.2 图解示例](#2.2 图解示例)

[2.3 代码实现](#2.3 代码实现)

三、Kruskal算法

[3.1 算法思想](#3.1 算法思想)

[3.2 并查集实现](#3.2 并查集实现)

[3.3 边结构体](#3.3 边结构体)

[3.4 Kruskal算法实现](#3.4 Kruskal算法实现)

[四、Prim vs Kruskal](#四、Prim vs Kruskal)

五、完整性能对比

六、算法选择建议

七、小结

八、思考题


一、最小生成树基础概念

1.1 什么是最小生成树

对于一个带权无向连通图,生成树是包含所有顶点的无环连通子图。最小生成树是边权之和最小的生成树。

示例

text

复制代码
原图:          最小生成树:
   1 —— 2          1 —— 2
   |  \  |           \   |
  4    5  3           5   3
   |    \|             \  |
   4 —— 5              4
边权和=1+3+4+5=13

1.2 应用场景

场景 说明
网络布线 铺设成本最低的线路
道路建设 连接所有城市的最短公路
电路设计 连接所有引脚的最短连线
聚类分析 最小生成树切割用于分类

二、Prim算法

2.1 算法思想

Prim算法是贪心算法 ,从一个顶点开始,每次选择连接已选集合和未选集合的最短边,将新顶点加入集合,直到所有顶点都被覆盖。

步骤

  1. 任选一个起点,加入集合U

  2. 在连接U和V-U的边中,选权值最小的边,将对应顶点加入U

  3. 重复步骤2,直到U包含所有顶点

2.2 图解示例

text

复制代码
初始图:
    1
  0 — 1
  |   / \
  4  2   3
  | /     \
  2 — 3 — 4
    5     6

起点0:
U={0},选边0-1(1)
U={0,1},选边1-2(2)
U={0,1,2},选边2-3(5)
U={0,1,2,3},选边3-4(6)
U={0,1,2,3,4}

2.3 代码实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 100
#define INF INT_MAX

// Prim算法(邻接矩阵)
void prim(int graph[MAX_VERTICES][MAX_VERTICES], int n) {
    int selected[MAX_VERTICES] = {0};  // 是否已在生成树中
    int minEdge[MAX_VERTICES];         // 到当前树的最小边权
    int parent[MAX_VERTICES];          // 记录父节点
    
    // 初始化
    for (int i = 0; i < n; i++) {
        minEdge[i] = INF;
        parent[i] = -1;
    }
    
    // 从顶点0开始
    minEdge[0] = 0;
    
    int totalWeight = 0;
    
    for (int count = 0; count < n; count++) {
        // 找到未选顶点中minEdge最小的顶点
        int u = -1;
        for (int i = 0; i < n; i++) {
            if (!selected[i] && (u == -1 || minEdge[i] < minEdge[u])) {
                u = i;
            }
        }
        
        selected[u] = 1;
        totalWeight += minEdge[u];
        
        // 输出选中的边
        if (parent[u] != -1) {
            printf("边 %d - %d 权值: %d\n", parent[u], u, minEdge[u]);
        }
        
        // 更新相邻顶点的minEdge
        for (int v = 0; v < n; v++) {
            if (graph[u][v] != INF && !selected[v] && graph[u][v] < minEdge[v]) {
                minEdge[v] = graph[u][v];
                parent[v] = u;
            }
        }
    }
    
    printf("最小生成树总权值: %d\n", totalWeight);
}

int main() {
    int n = 5;
    int graph[MAX_VERTICES][MAX_VERTICES];
    
    // 初始化无穷大
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            graph[i][j] = (i == j) ? 0 : INF;
        }
    }
    
    // 添加边
    graph[0][1] = graph[1][0] = 1;
    graph[0][2] = graph[2][0] = 4;
    graph[1][2] = graph[2][1] = 2;
    graph[1][3] = graph[3][1] = 3;
    graph[2][3] = graph[3][2] = 5;
    graph[2][4] = graph[4][2] = 4;
    graph[3][4] = graph[4][3] = 6;
    
    printf("Prim算法最小生成树:\n");
    prim(graph, n);
    
    return 0;
}

运行结果:

text

复制代码
Prim算法最小生成树:
边 0 - 1 权值: 1
边 1 - 2 权值: 2
边 1 - 3 权值: 3
边 2 - 4 权值: 4
最小生成树总权值: 10

三、Kruskal算法

3.1 算法思想

Kruskal算法也是贪心算法,按边权从小到大考虑,如果加入该边不会形成环,就加入生成树。

需要并查集来检测是否形成环。

步骤

  1. 将所有边按权值从小到大排序

  2. 初始化并查集,每个顶点独立

  3. 遍历每条边,如果边的两个顶点不在同一集合,加入生成树,合并集合

  4. 重复直到生成树有 n-1 条边

3.2 并查集实现

c

复制代码
// 并查集结构
typedef struct {
    int parent[MAX_VERTICES];
    int rank[MAX_VERTICES];
} UnionFind;

// 初始化
void ufInit(UnionFind *uf, int n) {
    for (int i = 0; i < n; i++) {
        uf->parent[i] = i;
        uf->rank[i] = 0;
    }
}

// 查找(路径压缩)
int ufFind(UnionFind *uf, int x) {
    if (uf->parent[x] != x) {
        uf->parent[x] = ufFind(uf, uf->parent[x]);
    }
    return uf->parent[x];
}

// 合并(按秩合并)
void ufUnion(UnionFind *uf, int x, int y) {
    int rootX = ufFind(uf, x);
    int rootY = ufFind(uf, y);
    
    if (rootX == rootY) return;
    
    if (uf->rank[rootX] < uf->rank[rootY]) {
        uf->parent[rootX] = rootY;
    } else if (uf->rank[rootX] > uf->rank[rootY]) {
        uf->parent[rootY] = rootX;
    } else {
        uf->parent[rootY] = rootX;
        uf->rank[rootX]++;
    }
}

3.3 边结构体

c

复制代码
typedef struct {
    int u, v, weight;
} Edge;

// 比较函数(用于排序)
int cmpEdge(const void *a, const void *b) {
    return ((Edge*)a)->weight - ((Edge*)b)->weight;
}

3.4 Kruskal算法实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 100
#define MAX_EDGES 1000

typedef struct {
    int u, v, weight;
} Edge;

typedef struct {
    int parent[MAX_VERTICES];
    int rank[MAX_VERTICES];
} UnionFind;

void ufInit(UnionFind *uf, int n) {
    for (int i = 0; i < n; i++) {
        uf->parent[i] = i;
        uf->rank[i] = 0;
    }
}

int ufFind(UnionFind *uf, int x) {
    if (uf->parent[x] != x) {
        uf->parent[x] = ufFind(uf, uf->parent[x]);
    }
    return uf->parent[x];
}

void ufUnion(UnionFind *uf, int x, int y) {
    int rootX = ufFind(uf, x);
    int rootY = ufFind(uf, y);
    
    if (rootX == rootY) return;
    
    if (uf->rank[rootX] < uf->rank[rootY]) {
        uf->parent[rootX] = rootY;
    } else if (uf->rank[rootX] > uf->rank[rootY]) {
        uf->parent[rootY] = rootX;
    } else {
        uf->parent[rootY] = rootX;
        uf->rank[rootX]++;
    }
}

int cmpEdge(const void *a, const void *b) {
    return ((Edge*)a)->weight - ((Edge*)b)->weight;
}

void kruskal(Edge edges[], int n, int edgeCount) {
    // 1. 按权值排序
    qsort(edges, edgeCount, sizeof(Edge), cmpEdge);
    
    // 2. 初始化并查集
    UnionFind uf;
    ufInit(&uf, n);
    
    // 3. 选择边
    int selectedEdges = 0;
    int totalWeight = 0;
    
    printf("Kruskal算法最小生成树:\n");
    for (int i = 0; i < edgeCount && selectedEdges < n - 1; i++) {
        int u = edges[i].u;
        int v = edges[i].v;
        int w = edges[i].weight;
        
        if (ufFind(&uf, u) != ufFind(&uf, v)) {
            ufUnion(&uf, u, v);
            printf("边 %d - %d 权值: %d\n", u, v, w);
            totalWeight += w;
            selectedEdges++;
        }
    }
    
    printf("最小生成树总权值: %d\n", totalWeight);
}

int main() {
    Edge edges[] = {
        {0, 1, 1},
        {0, 2, 4},
        {1, 2, 2},
        {1, 3, 3},
        {2, 3, 5},
        {2, 4, 4},
        {3, 4, 6}
    };
    int edgeCount = sizeof(edges) / sizeof(edges[0]);
    
    kruskal(edges, 5, edgeCount);
    
    return 0;
}

运行结果:

text

复制代码
Kruskal算法最小生成树:
边 0 - 1 权值: 1
边 1 - 2 权值: 2
边 1 - 3 权值: 3
边 2 - 4 权值: 4
最小生成树总权值: 10

四、Prim vs Kruskal

对比项 Prim算法 Kruskal算法
核心思想 从顶点扩展 从边选择
数据结构 数组/堆 并查集
时间复杂度 O(V²)(数组)/ O(E log V)(堆) O(E log E)
适用图 稠密图 稀疏图
实现复杂度 中等 中等(需并查集)
是否依赖起点

时间复杂度分析

  • Prim(邻接矩阵):O(V²),适合 V ≤ 2000

  • Prim(二叉堆+邻接表):O(E log V),适合稀疏图

  • Kruskal:O(E log E),主要开销在排序


五、完整性能对比

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 生成随机图
void generateRandomGraph(int n, int edgeCount, Edge edges[]) {
    srand(time(NULL));
    for (int i = 0; i < edgeCount; i++) {
        edges[i].u = rand() % n;
        edges[i].v = rand() % n;
        while (edges[i].u == edges[i].v) {
            edges[i].v = rand() % n;
        }
        edges[i].weight = rand() % 100 + 1;
    }
}

int main() {
    int n = 100;           // 顶点数
    int sparseEdges = 500;    // 稀疏图:约 5n 条边
    int denseEdges = 5000;    // 稠密图:约 50n 条边
    
    Edge *edges = (Edge*)malloc(denseEdges * sizeof(Edge));
    
    // 稀疏图测试
    generateRandomGraph(n, sparseEdges, edges);
    clock_t start = clock();
    kruskal(edges, n, sparseEdges);
    clock_t end = clock();
    printf("Kruskal(稀疏图): %.2f ms\n\n", 
           (double)(end - start) / CLOCKS_PER_SEC * 1000);
    
    // 稠密图测试
    generateRandomGraph(n, denseEdges, edges);
    start = clock();
    kruskal(edges, n, denseEdges);
    end = clock();
    printf("Kruskal(稠密图): %.2f ms\n\n", 
           (double)(end - start) / CLOCKS_PER_SEC * 1000);
    
    free(edges);
    return 0;
}

六、算法选择建议

场景 推荐 理由
稠密图(边数接近 V²) Prim(邻接矩阵) O(V²) 优于 O(E log E)
稀疏图(边数接近 V) Kruskal O(E log E) 很快
需要处理大量顶点 Kruskal 内存占用小
实现简单 Prim(数组版) 代码量少
动态添加顶点 Prim 可以增量扩展

七、小结

这一篇我们学习了最小生成树的两种经典算法:

算法 核心 数据结构 时间复杂度 适用场景
Prim 顶点扩展 数组/堆 O(V²) / O(E log V) 稠密图
Kruskal 边选择 并查集 O(E log E) 稀疏图

关键点

  • Prim:维护到当前树的最短距离

  • Kruskal:边排序 + 并查集判环

  • 并查集:路径压缩 + 按秩合并

下一篇我们讲最短路径(Dijkstra与Floyd)。


八、思考题

  1. Prim算法从一个顶点开始,如果从不同顶点开始,得到的最小生成树相同吗?

  2. Kruskal算法中,为什么排序后按顺序选边就能得到最小生成树?

  3. 如果图中有权值相同的边,最小生成树是否唯一?

  4. 并查集的路径压缩和按秩合并分别优化了什么?

欢迎在评论区讨论你的答案。

相关推荐
liliangcsdn2 小时前
sentence-transformer如何离线加载和使用模型
开发语言·前端·php
Crazy________2 小时前
4.10dockerfile构建镜像
java·开发语言
weixin_513449962 小时前
walk_these_ways项目学习记录第九篇(通过行为多样性 (MoB) 实现地形泛化)--学习算法
学习·算法·机器学习
fish_xk2 小时前
c++内存管理
开发语言·c++·算法
Tisfy2 小时前
LeetCode 3740.三个相等元素之间的最小距离 I:今日先暴力,“明日“再哈希
算法·leetcode·哈希算法·题解·模拟·遍历·暴力
独特的螺狮粉2 小时前
城市空气质量简易指数查询卡片:鸿蒙Flutter框架 实现的空气质量查询应用
开发语言·flutter·华为·架构·harmonyos
网域小星球2 小时前
C语言从0入门(八)|函数基础:封装、调用与参数传递精讲
c语言·开发语言
东宇科技2 小时前
如何使用js进行抠图。识别商品主体
开发语言·javascript·ecmascript
Dxy12393102162 小时前
Python使用PyEnchant详解:打造高效拼写检查工具
开发语言·python