Kruskal算法 采取的贪心算法的思想
基本思路
- 将图的每一条边以顺序排列 之后从小到大进行选边
- 选边的条件是将每选择一条边就要将其两端的顶点合并成一个 ,表示已经将这条边添加进来了 防止出现圈
所需要的结构
图存储
// 为了方便 使用数组来存储图的边
typedef struct node node;
typedef struct node* pnode;
struct node {
int weight; // 用来存储图的权值
int start; // 用来记录起始点 用于数字
int stop; // 用来记录终止点 数字
char st; // 用来记录起始点字母
char sp; // 用来记录终止点字母
};
上面实际上是通过一个函数将对应的坐标的字母转换成了数字
转换:A- - -0 B- - -1 如此下去
static int alpha_to_digit(char ch)
{
int re = 0;
if ('A' <= ch && ch <= 'Z')
re = ch - 'A';
else
re = ch - 'a';
return re;
}
图的存储方法
static pnode create_graph(int MAX)
{
pnode G = (pnode)malloc(sizeof(node) * MAX); // 这里给出边的具体容量 也就是MAX
if (G == NULL)
{
printf("Out of space\n");
exit(-1);
}
return G;
}
图的初始化方法
将字母转换成数字 是为了后续节点计算的方便性 但是不直接输入数字 是为了后面看结果方便一点
static void Initial_graph(pnode G, int edges)
{
for (int i = 0; i < edges; i++)
{ // 这里根据边来存储
getchar();
int weight;
char start, stop;
scanf("%c %c %d", &start, &stop, &weight); // 起始点 终止点 权重
G[i].st = start; // 用来存储起始字母
G[i].sp = stop; // 用来存储结束字母
G[i].start = alpha_to_digit(start); // 通过字母转数字方法进行转化
G[i].stop = alpha_to_digit(stop);
G[i].weight = weight;
}
}
优先队列实现(左式堆实现)
左式堆的性质
- NULL节点的零路径长为-1,0个或者1个节点的零路径长为0
- 左式堆的左子节的零路径长最少等于右子节点的零路径长
- 父节点的零路径长为子节点中零路径长最小的+1
队列结构定义
typedef struct queue queue;
typedef struct queue* position;
typedef struct queue* priorityqueue;
struct queue {
node item; // 存放图的节点
struct queue* left;
struct queue* right;
int Npl; // 零路径长
};
队列的创建
static priorityqueue create_priorityqueue()
{
priorityqueue H;
H = NULL;
return H;
}
队列的插入操作
这里采用宏替换是为了保证和二叉堆的insert的兼容 因为左式堆的需要返回 而二叉堆的insert什么也不返回 或者 采用**指针 这会使得代码中充满额外的*(数据结构原话)
#define Insert(X, H) (H = Insert1(X, H)); // 采用宏替换
static priorityqueue Insert1(node N, priorityqueue H)
{
// 为节点N生成一个指针用来存放相应的节点
priorityqueue SingleNode;
SingleNode = (priorityqueue)malloc(sizeof(queue));
if (SingleNode == NULL)
{
printf("out of space\n");
exit(-1);
}
else {
SingleNode->item = N;
SingleNode->Npl = 0; // 零节点的元素Npl为0
SingleNode->left = SingleNode->right = NULL;
H = Merge(SingleNode, H); // 然后将新节点与斜堆合并
}
return H;
}
队列的判断是否为空函数
static int IsEmpty(priorityqueue H)
{
return H == NULL;
}
队列的合并操作
Merge函数是用来确定是哪一个点和哪一个点合并
合并之后 左式堆会判断一下合并之后的点是否满足左式堆的Npl 如果右子节点大于左子节点 则进行交换
static void SwapChildren(priorityqueue H)
{
queue tmp = *H->left; // 将left的值取出
*H->left = *H->right;
*H->right = tmp;
}
static priorityqueue Merge1(priorityqueue H1, priorityqueue H2)
{
// 如果H1.left为NULL 则说明H1是没有子节点的
// 因为左式堆的左子节点的NPL >= 右子节点 可以直接将H2 直接赋值给H1
if (H1->left == NULL)
H1->left = H2;
else {
H1->right = Merge(H1->right, H2);
if (H1->left->Npl < H1->right->Npl)
// 维持左式堆的性质
SwapChildren(H1);
// 左式堆的左子节点的NPL >= 右子节点的NPL
// 左式堆父节点的NPL为节点中NPL最小的+1
H1->Npl = H1->right->Npl + 1;
}
return H1;
}
static priorityqueue Merge(priorityqueue H1, priorityqueue H2)
{
// 与空节点合并 直接返回非空节点
if (H1 == NULL)
return H2;
if (H2 == NULL)
return H1;
// 这里是根据item的weight权重进行比较
if (H1->item.weight < H2->item.weight)
return Merge1(H1, H2); // 保证堆序性质 使得小的在前面
else
return Merge1(H2, H1);
}
队列的删除操作
同样也是使用宏替换 这里的X是用来带回被删除的节点中存放的图节点
#define DeleteMin(H, X) H = DeleteMin1(H, X);
static priorityqueue DeleteMin1(priorityqueue H, pnode X)
{
priorityqueue LeftHeap, RightHeap;
if (IsEmpty(H))
{
printf("Priorityqueue is empty\n");
return H;
}
LeftHeap = H->left; // 用于指向被删除的左子节点
RightHeap = H->right; // 用于指向被删除的右子节点
*X = H->item; // 用X将要删除的node带出来
free(H);
return Merge(LeftHeap, RightHeap);//然后两个合并就成了另一个左式堆
}
最后采用的不相交集合 Disjoin_set
结构
#define MAX 20
static int visited[MAX]; // 用来确定访问节点
static int Rank[MAX]; // 用来控制树高 不会太高
初始化
static void Initial_DisjoinSet()
{
for (int i = 0; i < MAX; i++)
{
visited[i] = i; // 将visited[i]的值指向自己 用来结束标记
Rank[i] = 0; // 为后续节点进行访问的时候 不会使森林变得很高
}
}
查找元素
static int find(x)
{
if (visited[x] == x) // 指向元素自己 说明是根节点
return x;
else {
return visited[x] = find(visited[x]); // 一直查询到根节点元素
}
}
合并两个元素
// 合并两个元素 并且 该元素都是指向自己的root型节点
static void unite(int x, int y)
{
if (Rank[x] < Rank[y]) // 说明x的树高是小于y的树高 直接将x指向y 避免树高增加
visited[x] = y;
else {
visited[y] = x;
if (Rank[x] == Rank[y])
Rank[x]++;
}
}
static void link(int x, int y)
{
// 首先通过find查询到当前元素所属的子集
unite(find(x), find(y));
}
比较两个元素是否处于同一个集合中
static int same(int x, int y)
{
// 比较两个元素的根节点是否相同
return find(x) == find(y);
}
kruskal算法
static void kruskal(pnode G,int edges)
{
priorityqueue H = NULL;
for (int i = 0; i < edges; i++)
{
// 将所有的边都插入进优先队列中
Insert(G[i], H);
}
Initial_DisjoinSet();// 将互质集合全部初始化
// 从优先队列中取出权值最小的边
int totalCost = 0;
while (!IsEmpty(H))
{
node i;
DeleteMin(H, &i); // 要删除的节点由i带回
if (same(i.start, i.stop))
// 说明这条边的两个点 已经被选取了
continue;
link(i.start, i.stop);
printf("边起始%c 结束%c 权值为%d被选择\n", i.st, i.sp, i.weight);
totalCost += i.weight;
}
printf("总花费%d", totalCost);
}
展示一下图的边
static void show_graph(pnode G, int edges)
{
for (int i = 0; i < edges; i++)
{
printf("起始点为%c, 终止为%c, 权值为%d\n", G[i].st, G[i].sp, G[i].weight);
}
}
主程序
int main()
{
int n;
scanf("%d", &n); // 确定边的数量
pnode G = create_graph(n);
Initial_graph(G, n);
show_graph(G, n);
kruskal(G, n);
}
测试例程 采用的数据结构习题9-15
19
A D 4
A E 4
A B 3
D E 5
D H 6
H I 4
I J 7
J G 8
B C 10
B E 2
B F 3
E H 2
E F 11
E I 1
C F 6
C G 1
F G 2
F J 11
F I 3
测试结果
至此 kruskal算法结束了
(如果有什么不对的地方请提出来 大佬轻喷)