最近学到数据结构图的那一部分,最先接触的具有算法味道的就是连个最小生成树算法,prim算法和Kruskal算法,两个算法都是用来求最小生成树的,其二者各有所长,今天我们就来介绍一下者两种最小生成树算法。
首先我们先来看一下什么叫做最小生成树。
定义:在一个连通网的所有生成树中,选中的n-1条边权值(代价)之和最小的生成树被称为该连通网的最小代价生成树,也就时最小生成树(摘自数据结构与算法);
图画的有点丑不要介意啊!
1.Prim
我们先用prim算法来求这张图的最小生成树,在写代买之前,我先要给大家讲一讲如何用prim算法生成最小生成树
1>选择起点。
2>找寻与起点最近的点。(现在我们就有两个点可以作为起点了,一个是刚找到的点,一个是我们的起点)
3>找寻所用可作为起点的点可达到的点(不可往回走)中最近的点加入起点行列。
4>........继续找就行。
总结:我感觉prim算法就是一个贪心算法,始终是要找距离起始点(不唯一)最近的点作为下一个点。
下面为Java代码:
//这里就不浪费时间构造举证了,这个矩阵我直接给出来了。
//这里用0代表到自身的距离,用65535表示当前点无法到达的点的距离 即 无穷!
private int[][] map=
{
{0,5,6,65535,65535,65535,65535,65535,65535,65535}
,{5,0,65535,3,65535,65535,65535,65535,65535,65535}
,{6,65535,0,6,3,65535,65535,65535,65535,65535}
,{65535,3,6,0,3,4,4,65535,65535,65535}
,{65535,65535,3,3,0,1,65535,4,65535,65535}
,{65535,65535,65535,4,1,0,65535,65535,5,65535}
,{65535,65535,65535,4,65535,65535,0,65535,65535,4}
,{65535,65535,65535,65535,4,65535,65535,0,2,65535}
,{65535,65535,65535,65535,65535,5,65535,2,0,2}
,{65535,65535,65535,65535,65535,65535,4,65535,2,0}
};
下面为prim算法的核心代码
public void prim() {
//用来标记某个点是否已经到达过了
int[] flag=new int[10];
//用来记录每一个点的前驱节点
int[] startpoint=new int[10];
//我们下来以第一个点作为出发点找一边
for(int i=0;i<10;i++) {
startpoint[i]=0;
flag[i]=map[0][i];//通过改变行标可设置不同的出发点,我这里是设置零点为出发点
}
//我们还需要找九个点,才能完成最小生成树的查找,所以循环九次
for(int k=0;k<9;k++) {
//因为我们选取得是第一个点作为起始点,所以不需要关注第一个点了,只需要在剩余点中找
int Index=-1;
int min=65535;
//这个循环下来,我们就找到下一个目标点了
for(int i=1;i<10;i++) {
if(flag[i]!=0 && flag[i]<min) {
min=flag[i];
Index=i;
}
}
//Index对应得下标为我们下一个要访问得下标,在标记数组中将flag[Index]置零,表示已经访问过了。
flag[Index]=0;
//接下来我们需要得是刷新两个数组,一个是flag一个是startpoint
/**
* 那么如何刷新呢?我们可以探究以下,你先想当前得flag数组中记录着三种数据分别是:
* 0:表示自身点或者以访问过得点
* 65535:表示当前点还不可到达得点
* 0~65535:当前结点可到达的下一个节点距当前节点得距离
*
* 那么我们已经找到了下一个节点,我们当然需要刷新flag数组和startpoint数组了。
*
* 怎么刷新呢?如果当前节点和下一个节点都可以到达某一个节点则比较各自权值得大小,保留小的。
*
* 怎么保留前驱节点呢?如果我们当前节点和新找到得节点都可以同时到达某个节点,我们需要选取“权值较小的”,
* 如果权值较小的是“新找到的点”那么我们需要将该点的前驱置为Index否则不变,前驱的设置是为了的到遍历的路径。
*
*/
for(int j=1;j<10;j++) {
if(flag[j]!=0 && map[Index][j]<flag[j]) {//比较,保留近拥有较近路径的
flag[j]=map[Index][j];
startpoint[j]=Index;//设置前驱为Index
}
}
}
//显示各点的前驱节点,可以拼出遍历路径哦!
for(int i=0;i<10;i++) {
System.out.println(i+"的前驱节点为"+startpoint[i]);
}
}
运行结果为:
0的前驱节点为0
1的前驱节点为0
2的前驱节点为4
3的前驱节点为1
4的前驱节点为3
5的前驱节点为4
6的前驱节点为3
7的前驱节点为4
8的前驱节点为7
9的前驱节点为8
通过运行结果我们可以构造出遍历的路径,这里我就不构造了。
2.Kruskal
卡鲁斯卡尔(Kruskal)算法就是找到这个途中所有的边,然后将他们的起始点,终止点,边上的权值存入一个二维数组中,然后对这个二维数组依照每行的权值进行排序,从上到下一次选择,直到构成最小生成树!
总结:在我看来,这也是一个贪心算法,贪心的是最小边。每次都找最小边。
public void Kruskal() {
//这里我直接看着图吧从0~9点的所有路径都写出来了,每行分别保存起始点,终止点,权值
int[][] side= {{0,1,5},{0,2,6},{1,3,3},{2,3,6},{2,4,3},{3,4,3},{3,5,4},
{3,6,4},{4,5,1},{4,7,4},{5,8,5},{6,9,4},{7,8,2},{8,9,2}
};
//这是一个记录数组,记录0~9个点是否被访问;
int[] point= new int[10];
for(int i=0;i<10;i++) {
point[i]=0;//这九个点全部初始化为零
}
//按升序对side数组进行升序
for(int i=0;i<side.length-1;i++) {
for(int j=0;j<side.length-1-i;j++) {
if(side[j][2]>side[j+1][2]) {
int[] temp=side[j];
side[j]=side[j+1];
side[j+1]=temp;
}
}
}
//因为十个点所以最小生成树需要要有九个边所以循环九次
for(int i=0;i<9;i++) {
//因为我们已经对side数组进行排序了,所以我们就从数组的最小下标向后选取,就是权值最小的了
for(int j=0;j<side.length;j++) {
/**
* 读者可能会有疑问,为什么起始点为什么不能为被访问过的呢?
*
* 你想想,起始点和终止点可以同时为被访问过的,
* 那么我们构成的最小生成树就很可能出现回路,而树中是不允许出现回路的,而且回路的存在违背了“最小”
*/
if(side[j][1]!=-1 && side[j][0]!=-1) {//如果该边的起点和终点不同时为-1(-1代表被访问过了),我们就选取这条边
System.out.println("第"+i+"条边为:"+" sta:"+side[j][0]+" end:"+side[j][1]+" pow:"+side[j][2]);
side[j][0]=-1;//选取后则该边的起始点,终止点就被访问过了,我们就把它置为-1!
side[j][1]=-1;
break;//结束循环,寻找下一条边
}
}
}
}
运行结果为:
第0条边为: sta:4 end:5 pow:1
第1条边为: sta:7 end:8 pow:2
第2条边为: sta:8 end:9 pow:2
第3条边为: sta:1 end:3 pow:3
第4条边为: sta:2 end:4 pow:3
第5条边为: sta:3 end:4 pow:3
第6条边为: sta:3 end:5 pow:4
第7条边为: sta:3 end:6 pow:4
第8条边为: sta:4 end:7 pow:4
有运行结果我们可以得到最下生成树的形状。这里我就不给大家画了,大家也需要去思考。
今天的介绍就到这里了,谢谢!