最小生成树之Kruskal算法(超详细原理+完整可运行C语言代码)

Kruskal 算法

文章目录

  • [Kruskal 算法](#Kruskal 算法)
    • [1. 生成树](#1. 生成树)
      • [1.1 生成树的定义](#1.1 生成树的定义)
      • [1.2 生成树的属性](#1.2 生成树的属性)
    • [2. 最小生成树](#2. 最小生成树)
    • [3. Kruskal 算法](#3. Kruskal 算法)
      • [3.1 简介](#3.1 简介)
      • [3.2 举例](#3.2 举例)
    • [4. 边集数组](#4. 边集数组)
    • [5. 代码实现](#5. 代码实现)
      • [5.0 前置 邻接矩阵](#5.0 前置 邻接矩阵)
      • [5.1 头文件部分](#5.1 头文件部分)
      • [5.2 函数实现](#5.2 函数实现)
        • [5.2.1 从邻接矩阵中初始化边集数组](#5.2.1 从邻接矩阵中初始化边集数组)
        • [5.2.2 排序边集数组 (选择排序)](#5.2.2 排序边集数组 (选择排序))
        • [5.2.3 Kruskal算法](#5.2.3 Kruskal算法)
      • [5.3 main 函数](#5.3 main 函数)
      • [5.4 输出结果](#5.4 输出结果)

1. 生成树

1.1 生成树的定义

  • 对于有n个顶点的连通图,至少有n-1条边,那么连接所有顶点的极小连通子图就是生成树。如果在图的生成树种任意加一条边,则必然生成回路

1.2 生成树的属性

  • ⼀个连通图可以有多个⽣成树;

  • ⼀个连通图的所有⽣成树都包含相同的顶点个数和边数;

  • ⽣成树当中不存在环;

  • 移除⽣成树中的任意⼀条边都会导致图的不连通, ⽣成树的边最少特性;

  • 在⽣成树中添加⼀条边会构成环。

  • 对于包含n个顶点的连通图,⽣成树包含n个顶点和n-1条边;

  • 对于包含n个顶点的⽆向完全图最多包含 n^n-2颗⽣成树。

2. 最小生成树

  • 对于连通网来说,边是带权值的,生成树的各边也带权值,因此把生成树各边的权值总和称为生成树的权,把权值最小的生成树称为最小生成树

  • 举个例子

  • 上图中,原来的带权图可以⽣成左侧的两个最⼩⽣成树,这两颗最⼩⽣成树的权值之和最⼩,且

    包含原图中的所有顶点

  • 我们⼜如何从原图得到最⼩⽣成树呢?

  • 最⼩⽣成树算法有很多,其中最经典的就是 克鲁斯卡尔(Kruskal)算法普⾥姆(Prim)算法,也是我们考试、⾯试当中经常遇到的两个算法。

  • 下面来写 克鲁斯卡尔(Kruskal)算法 , **普⾥姆(Prim)**在下节来写

3. Kruskal 算法

3.1 简介

  • 克鲁斯卡尔算法(Kruskal)是⼀种使⽤贪婪⽅法(贪心算法)的最⼩⽣成树算法。该算法初始将图视为森林,图中的每⼀个顶点视为⼀棵单独的树。⼀棵树只与它的邻接顶点中权值最⼩且不违反最⼩⽣成树属性(不构成环)的树之间建⽴连边。

  • 该算法的基本思想是从小到大加入边,是个贪心算法。在加入边时,需要通过 并查集 来判断该边的两个端点是否属于同一个集合 (树),避免形成 "环"

3.2 举例

  • 第⼀步:将图中所有的边按照权值进⾏⾮降序排列
  • 第⼆步:从图中所有的边中选择可以构成最⼩⽣成树的边

  • 1、选择权值最⼩的边 V4-V7 ,没有环形成,则添加:

  • 2、选择边 V2-V8,没有形成环,则添加
  • 3、选择边V0 - V1没有形成环,则添加
  • 4、选择边V0 - V5,没有形成环,添加
  • 5、选择边V1 - V8,没有形成环,添加
  • 6、选择V3 - V7,没有形成环,添加
  • 7、选择V1 - V6,没有形成环,添加
  • 8、选择边V5 - V6添加这条边将导致形成环,舍弃,不添加
  • 9、选择边V1 - V2添加这条边将导致形成环,舍弃,不添加
  • 10、选择边V6 - V7,没有成环,添加
  • 此时已经包含了图中顶点个数9减1条边,也就是8条边,算法停⽌

4. 边集数组

  • 边集数组是由两个⼀维数组构成,⼀个是存储顶点的信息,另⼀个是存储边的信息,这个边数组每个数据元素由⼀条边的起点下标(begin)、终点下标(end)和权(weight)组成。
  • 具体看下面代码实现吧

5. 代码实现

5.0 前置 邻接矩阵

  • 由于要用到邻接矩阵的内容 这里贴一下我们之前写过的部分
c 复制代码
//邻接矩阵的顶点结构 表示一个顶点的信息
typedef struct 
{
	int no;          //顶点编号
	char* show;      //显示行为
} MatrixVertex;

typedef int MatrixEdge;    //边里面存的内容

//图邻接矩阵表示法
typedef struct 
{
	MatrixVertex vex[MaxNodeNum];              //存储顶点信息,使用一维数组存储顶点
	int nodeNum;                               //约束实际顶点数量
	MatrixEdge edges[MaxNodeNum][MaxNodeNum];  //边的集合    //存储边的结构,矩阵
	int edgeNum;   //边的数量
	int directed;  //是否有向      
}MGraph;
           
/* 1.初始化邻接矩阵  
*    graph     :哪个图
*    names[]   : 顶点名称
*	 num       :顶点数量
*	 directed  :是否有向
*	 edgeValue :用来初始化边 0 ? ∞
*/
void initMGraph(MGraph* graph,  char* names[], int num, int directed, int edgeValue);
/*
* 2.向邻接表添加边
* graph:  向哪个图添加边
* x    :  起始顶点的编号
* y    : 终点顶点的编号
* w    : 边的权值
*/
void addMGraphEdge(MGraph* graph, int x, int y, int w);
c 复制代码
void initMGraph(MGraph* graph,  char* names[], int num, int directed, int edgeValue) 
{
    //初始化 是否有向 顶点数量 边的数量
	graph->directed = directed;
	graph->nodeNum = num;
	graph->edgeNum = 0;
	//初始化 顶点集合 边的集合
	memset(graph->vex, 0, sizeof(graph->vex));
	memset(graph->edges, 0, sizeof(MatrixEdge) * MaxNodeNum * MaxNodeNum);
	//初始化顶点编号 显示行为 边
	for (int i = 0;i < num;i++) 
    {
		graph->vex[i].no = i;
		graph->vex[i].show = names[i];
		for (int j = 0;j < num;j++) 
        {
			graph->edges[i][j] = edgeValue;
		}
	}
}
void addMGraphEdge(MGraph* graph, int x, int y, int w)
{
	if (x<0 || x>graph->nodeNum) 
    {
		return;
	}
	if (y<0 || y>graph->nodeNum) 
    {
		return;
	}
	if (!isEdge(graph->edges[x][y])) 
    {  //没边加边
		graph->edges[x][y] = w;             
		if (graph->directed == 0) 
        {   //无向图  
			graph->edges[y][x] = w;
		}
		graph->edgeNum++;
	}

}

5.1 头文件部分

c 复制代码
/*
* 利用邻接矩阵 生成一个边集数组 利用边集数组实现Kruskal算法
* 算法思路:
* a.将所有的边按权值的大小进行升序排序 从小到大依此提取边的情况 满足条件
* b.加入这条边,和之前已经选中的边不能组成环路 如果不能 这条边就归入生成树的结果部分
* c.如果能,舍去,再去找下一条,直到筛选出n-1条边
*/

//边集数组的结构
typedef struct 
{
	int begin;    //边的起点(顶点1)
	int end;      //边的终点(顶点2)
	int weight;   //边的权值
}EdgeSet;

//1. 从邻接矩阵中初始化边集数组 
void initEdgeSet(const MGraph* graph, EdgeSet* edges);

//2. 排序边集数组 (选择排序) 
void sortEdgeSet(EdgeSet* edges, int num);

//3. Kruskal算法                           
int KruskalMGraph(const EdgeSet* edges, int num, int node_num, EdgeSet* result);

5.2 函数实现

5.2.1 从邻接矩阵中初始化边集数组
c 复制代码
void initEdgeSet(const MGraph* graph, EdgeSet* edges) 
{
	int k = 0;
	for (int i = 0;i < graph->nodeNum;++i)  //遍历每个顶点
    {
		for (int j = i+1;j < graph->nodeNum;++j) //遍历上三角 (无向图)
        {  
			if (graph->edges[i][j] > 0) 
            {//边权>0为有效边     
				edges[k].begin = i;
				edges[k].end = j;
				edges[k].weight = graph->edges[i][j];
				k++;
		    }
		}
	}
}
5.2.2 排序边集数组 (选择排序)
c 复制代码
/*因为是c语言 所以我们自己手写个排序*/
void sortEdgeSet(EdgeSet* edges, int num) 
{
	EdgeSet tmp;
	for (int i = 0;i < num;++i) 
    {
		for (int j = i + 1;j < num;++j)
        {
			if (edges[j].weight < edges[i].weight) 
            {
				memcpy(&tmp, &edges[i], sizeof(EdgeSet));
				memcpy(&edges[i], &edges[j], sizeof(EdgeSet));
				memcpy(&edges[j], &tmp, sizeof(EdgeSet));
			}
		}
	}
}
5.2.3 Kruskal算法
c 复制代码
/*并查集 用来判断是不是同一个根*/
static int getRoot(const int *Uset,int a) 
{
	while (Uset[a] != a)  
    {
		a = Uset[a];
	}
	// 当Uset[a]==a时,a就是根节点,返回
	return a;
}

//Kruskal算法
int KruskalMGraph(const EdgeSet* edges, int num, int node_num, EdgeSet* result) 
{	
	int* uSet;
	int count = 0;
	int sum = 0;

	//1 初始化并查集
	uSet = malloc(sizeof(int) * node_num);
	if (uSet == NULL) 
    {
		printf("malloc failed\n");
		return -1;
	}
	for (int i = 0;i < node_num;++i) 
    {
		uSet[i] = i; 
	}

	//2 从已经排序好的边集中 找到最小的边 不构成环
	for (int i = 0;i < num;++i) 
    {
		int a = getRoot(uSet, edges[i].begin);
		int b = getRoot(uSet, edges[i].end);
		if (a != b) 
        {
			//不构成环
			uSet[a] = b;   //a的父节点加到b  b的父节点加到a也可以 
			result[count].begin = edges[i].begin;
			result[count].end = edges[i].end;
			result[count].weight = edges[i].weight;
			sum += edges[i].weight;
			count++;
            
			if (count == node_num - 1) 
            {//此时已满足最小生成树了
				break;
			}
		}
	}
	free(uSet);
	return sum;
}

5.3 main 函数

c 复制代码
void setupMGraph(MGraph *graph,int edageValue) 
{	 /*                0   1   2   3   4   5   6         */
	char* names[] = { "A","B","C","D","E","F","G" };
	initMGraph(graph, names, sizeof(names) / sizeof(names[0]), 0, edageValue);
    //加边
	addMGraphEdge(graph, 0, 1, 12);
	addMGraphEdge(graph, 0, 5, 16);
	addMGraphEdge(graph, 0, 6, 14);
	addMGraphEdge(graph, 1, 2, 10);
	addMGraphEdge(graph, 1, 5, 7);
	addMGraphEdge(graph, 2, 3, 3);
	addMGraphEdge(graph, 2, 4, 5);
	addMGraphEdge(graph, 2, 5, 6);
	addMGraphEdge(graph, 3, 4, 4);
	addMGraphEdge(graph, 4, 5, 2);
	addMGraphEdge(graph, 4, 6, 8);
	addMGraphEdge(graph, 5, 6, 9);
}

//用来打印排序后的边
void showEdges(EdgeSet* edges, int num) 
{
	for (int i = 0;i < num;i++) 
    {
		printf("<%d>---<%d>---<%d>\n", edges[i].begin, edges[i].end, edges[i].weight);
	}
}

void test04() 
{
	MGraph graph;      //邻接矩阵
	EdgeSet* edges;    //边
	EdgeSet* result;   //接受Krusal的结果
	
    //初始化邻接矩阵
	setupMGraph(&graph,0);   
    //申请边
	edges = malloc(sizeof(EdgeSet) * graph.edgeNum);
	if (edges == NULL) 
    {
		return;
	}
    
    //初始化边集数组
	initEdgeSet(&graph, edges);
    //按权值 排序边
	sortEdgeSet(edges, graph.edgeNum);
	//排序后的边
	/*showEdges(edges, graph.edgeNum)
    printf("\n");*/
    
	//最小生成树的边 是节点的个数减1
	result = malloc(sizeof(EdgeSet) * (graph.nodeNum) - 1);
	if (result == NULL) 
    {
		free(edges);
		return;
	}
	
    //最小生成树的权值
	int sum = KruskalMGraph(edges, graph.edgeNum, graph.nodeNum, result);
    //打印该最小生成树
	for (int i = 0;i < graph.nodeNum - 1;i++) 
    {
		printf("edge %d: <%s>---<%d>---<%s>\n", i + 1,
			graph.vex[result[i].begin].show, result[i].weight, graph.vex[result[i].end].show);
	}

	printf("sum = %d", sum);

	free(result);
	free(edges);
}

int main()
{
    test04();
    return 0;
}

5.4 输出结果

c 复制代码
edge 1: <E>---<2>---<F>
edge 2: <C>---<3>---<D>
edge 3: <D>---<4>---<E>
edge 4: <B>---<7>---<F>
edge 5: <E>---<8>---<G>
edge 6: <A>---<12>---<B>
sum = 36
  • 测试案例可以参考这张图

嘻嘻嘻嘻 Kruskal 部分到此结束😆😆

Prim 静候更新 👻👻👻😸😸😸

(有错误欢迎指出) (疑问也是)❤️❤️😍😍💖💖

持续更新中...

相关推荐
caimouse1 小时前
ReactOS 窗口系统分析(5):可见区域与窗口 DC — vis.c + windc.c
c语言·开发语言
OPEN-F1 小时前
Python进阶教程:装饰器与生成器深入
开发语言·python
happyprince2 小时前
01-整体观-Codex全貌解读(源码)
算法·ai编程
俊基科技2 小时前
别自己写算法了,让这颗37.5毫米的模组替你“闭嘴干活”
神经网络·算法·硬件开发·ai降噪·语音模块·回音消除
千里码aicood3 小时前
基于Python的电商数据采集系统(大数据+爬虫)
开发语言·python
_Narcissus_3 小时前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
陈皮波比茶3 小时前
Python项目实战-网络机器人(爬虫)
开发语言·网络·爬虫·python·机器人
Cerman3 小时前
QEMU启动OVMF shell超时分析
c语言·windows·经验分享
世人万千丶3 小时前
鸿蒙项目实战 - 社区活动编排板:标签云布局算法与自动换行
学习·算法·华为·harmonyos·鸿蒙