克鲁斯卡尔算法
最小生成树算法
什么是最小生成树算法,最小生成树有什么作用呢?
首先看下面的一个拓扑结构。最小生成树就是要寻找遍历整个图中所有节点所走过的路径最短的那些边。
最小生成树中利用克鲁斯卡尔算法的过程为如下所示:
算法思想:在Kruskal算法中,首先顶点之间的边进行排序。构造树的时候从排序的数组中选出节点。分析节点是否会构成一个环。
本例子中排序由快速排序算法执行。其执行时间为elog(e)。那么在for循环里面其时间复杂度为O(V^2),于是其算法的复杂度由排序算法决定。
//利用快速排序将边从小到大进行排序
void sortEdges(int start,int end)
{
if(start<end)
{
int position=Parition(start,end);
cout<<"start: "<<start<<" end: "<<end<<endl;
for(int index=0;index<10;index++)
{
cout<<edges[index].weight<<" ";
}
cout<<endl<<endl;
sortEdges(start,position-1);
sortEdges(position+1,end);
}
}
int Parition(int start,int end)
{
int randIndex=rand()%(end-start+1);
EdgeExtend temp=edges[start+randIndex];
edges[start+randIndex]=edges[start];
edges[start]=temp;
int location=start;
EdgeExtend compareData=edges[location];
cout<<"比较数据是: "<<compareData.weight<<endl;
while(start<end)
{
while(edges[start+1].weight<compareData.weight&&start<end)//从比较数据的后面一个
//开始进行比较
start++;
while(edges[end].weight>compareData.weight&&start<end)
end--;
if (start<end)
{
EdgeExtend tempEdge=edges[start+1];
edges[start+1]=edges[end];
edges[end]=tempEdge;
}
}
if (start<=end)//当确定比较元素应该加入的位置时,交换值。
{
*(edges+location)=*(edges+start);
*(edges+start)=compareData;
}
return (start);
}
执行结果为
在分析是否会构成环时利用联合查找来判断。
什么是联合查找。还记得上一篇关于图论的文章中提及如何判断某个节点是否会构成回路吗,利用到的是标记。对于无向图而言,每访问一次就对其标记进行赋值。当发现节点已经被访问过但是两个节点所在的边并没有被记录过,则判断其将构成回路。对于有向图,我们这样的考虑:在某棵子树下,从当前节点开始,将其访问标志visited(u)=i++,其中的i为整个网络的遍历顺序,遍历子节点之后将访问visited(u)设为无穷大。通过递归,则其子节点的标记必然会大于父节点,但是如果子节点访问的下一个节点已经被访问过了,而且标志并非为无穷大,那么我们认为它的某个父节点的遍历并没有结束,且该节点又指向了父节点,此时就产生了回路。
这种算法问题在哪里?对于我们的生成树算法,判断节点是否会产生回路都要对整个子树进行一次判断。由此Union-Find算法出现。其数据结构如下:
struct NodeExtend
{
int length;
int key;//值
NodeExtend* nextNode;//下一个节点
NodeExtend* root;//根节点
};
它的核心思想是:利用root节点相等与否来判断两个节点是否属于同一个集合。如果相等则不进行处理,也就是这条边不放入生成树中。对应下面的edge(bc),此时b,c均属于以a为表头的一个链表中,那么bc边就无需加入。不得不说,一个Node*root就可以避免产生回路是个相当聪明的方法。那么如何的处理更新呢?在联合查找中利用Node* next来记录在链表中的下一个节点,通过更新下一跳节点的根节点实现整个网络边的联合。为了降低算法的复杂性,利用length比较链表的长度,将较短的链表合并进来。
注意在联合查找中利用的是单向循环链表。
void unionList(NodeExtend* firstRoot,NodeExtend* secondRoot)
{
NodeExtend *tempNode=secondRoot->nextNode;//记录要整合的另个链表的下一个节点
while(tempNode!=secondRoot)//
{
tempNode->root=firstRoot;//更新第二个集合中所有元素的根节点
tempNode=tempNode->nextNode;
}
secondRoot->root=firstRoot;//将第二个链表的根节点指向第一个根节点
tempNode=firstRoot->nextNode;//将两个双向链表的下一个值进行互换
firstRoot->nextNode=secondRoot->nextNode;
secondRoot->nextNode=tempNode;
}
因此对于unionList的算法复杂度取决于什么呢?对,答案是顶点数目,O(v).下面图示为Kruskal算法构建边过程。
(1)插入edge(fg),将两个节点构成一个单向循环链表 |
(2)插入edge(ac),此时会有两个有两个元素的单向循环链表 |
(3) 插入edge(ab)将b节点加入到ac构成的单向循环链表中 |
(4)插入edge(df),其中edge(bc)将会构成回路 |
(5)插入edge(eg)
|
(6)插入edge(cf),同时将两个链表联合。后面的元素将不再被插入,因为会构成回路 |
//克鲁斯卡尔最小生成树算法算法为:
void KruskalAlgorithm()
{
for (int i=0;i<edgeNum;i++)
{
updateGraph(edges[i].startNode,edges[i].endNode,edges[i].weight);//关键是判断是否会存在环的情况
}
}
void updateGraph(NodeExtend *startNode,NodeExtend *endNode,int weight)//利用联合查找算法unionFind
{
if (startNode->root==endNode->root)//如果两者都属于同一个集合
{
return;
}
else//反之进行不同的更新
{
NodeExtend *secondRoot;
NodeExtend *firstRoot;
if (startNode->root->length>=endNode->root->length)
{
firstRoot=startNode->root;
secondRoot=endNode->root;
}
else
{
firstRoot=endNode->root;//
secondRoot=startNode->root;//后面链表为待整合的链表
}
unionList(firstRoot,secondRoot);//联合链表,用于判断是否会构成环
cout<<endl<<"插入的起始节点为"<<startNode->key
<<", 终止节点为:"<<endNode->key
<<", 值为:"<<weight;
}
}
算法的结果:
从运算结果中我们可以看到边的权重为9,13,15,16的均没有被加入到树中,因为它们将会构成回路。
最后本程序的部分数据结构为
struct EdgeExtend//边的定义
{
NodeExtend *startNode;//起始节点
NodeExtend *endNode;//结束节点
int weight;//权重
};
class GraphTree//图
{
public:
int nodeNum;//节点的总数
int edgeNum;//边的总数
EdgeExtend* edges;//边
NodeExtend* nodes;//全部的节点
int** matrix;
int** newMatrix;
public:
GraphTree()
{
nodeNum=7;
edgeNum=10;
initData();
}
void initData()//初始化
{
edges=new EdgeExtend[edgeNum];
nodes=new NodeExtend[nodeNum];
matrix=new int*[nodeNum];
newMatrix=new int*[nodeNum];
for (int i=0;i<nodeNum;i++)
{
matrix[i]=new int[nodeNum];
newMatrix[i]=new int[nodeNum];
for (int j=0;j<nodeNum;j++)
{
newMatrix[i][j]=0;
matrix[i][j]=0;
}
nodes[i].key=i;
nodes[i].length=1;
nodes[i].root=&nodes[i];//根节点以及下一个节点均指向自己
nodes[i].nextNode=&nodes[i];
}
matrix[0][1]=6;matrix[0][2]=5;
matrix[1][4]=13;matrix[1][2]=9;
matrix[2][3]=16;matrix[2][5]=12;
matrix[3][4]=15;matrix[3][5]=7;
matrix[4][6]=8;matrix[5][6]=3;
int nEdgesPos=0;
for (int i=0;i<nodeNum;i++)
{
for(int j=0;j<nodeNum;j++)
{
if (matrix[i][j]>0)
{
edges[nEdgesPos].startNode=&nodes[i];
edges[nEdgesPos].endNode=&nodes[j];
edges[nEdgesPos].weight=matrix[i][j];
nEdgesPos++;
}
}
}
cout<<"对边的权重进行排序,过程如下"<<endl;
sortEdges(0,edgeNum-1);
cout<<"排序结果"<<endl;
for(int index=0;index<10;index++)
{
cout<<edges[index].weight<<" ";
}
KruskalAlgorithm();
}
小结:
1)在值拷贝与引用拷贝中并不熟练,程序初期出现在快速排序之后单个节点的根节点与下一个节点的地址竟然不是本身。
2)快排中利用随机选择比较数,但是索引的位置出错,13位于最后一位
3)在联合查找算法中,待整合的链表先于其它的节点进行更新父节点,这就完全是逻辑错误