数据结构中图的应用.docx
《数据结构中图的应用.docx》由会员分享,可在线阅读,更多相关《数据结构中图的应用.docx(33页珍藏版)》请在冰豆网上搜索。
数据结构中图的应用
图在数据结构中应用十分广泛,对于图来说最重要的当然是算法,而且相当的一部分都是很专业的,一般的人几乎不会接触到;相对而言,结构就显得分量很轻。
你可以看到关于图中元素的操作很少,远没有单链表那里列出的一大堆“接口”。
——一个结构如果复杂,那么能确切定义的操作就很有限。
基本储存方法
不管怎么说,还是先得把图存起来。
不要看书上列出了好多方法,根本只有一个——邻接矩阵。
如果矩阵是稀疏的,那就可以用十字链表来储存矩阵(见前面的《稀疏矩阵(十字链表)》)。
如果我们只关系行的关系,那么就是邻接表(出边表);反之,只关心列的关系,就是逆邻接表(入边表)。
下面给出两种储存方法的实现。
#ifndefGraphmem_H
#defineGraphmem_H
#include
#include
usingnamespacestd;
templateclassNetwork;
constintmaxV=20;//最大节点数
template
classAdjMatrix
{
friendclassNetwork>;
public:
AdjMatrix():
vNum(0),eNum(0)
{
vertex=newname[maxV];edge=newdist*[maxV];
for(inti=0;i}
~AdjMatrix()
{
for(inti=0;idelete[]edge;delete[]vertex;
}
boolinsertV(namev)
{
if(find(v))returnfalse;
vertex[vNum]=v;
for(inti=0;ivNum++;returntrue;
}
boolinsertE(namev1,namev2,distcost)
{
inti,j;
if(v1==v2||!
find(v1,i)||!
find(v2,j))returnfalse;
if(edge[i][j]!
=NoEdge)returnfalse;
edge[i][j]=cost;eNum++;returntrue;
}
name&getV(intn){returnvertex[n];}//没有越界检查
intnextV(intm,intn)//返回m号顶点的第n号顶点后第一个邻接顶点号,无返回-1
{
for(inti=n+1;i=NoEdge)returni;
return-1;
}
private:
intvNum,eNum;
distNoEdge,**edge;name*vertex;
boolfind(constname&v)
{
for(inti=0;ireturnfalse;
}
boolfind(constname&v,int&i)
{
for(i=0;ireturnfalse;
}
};
template
classLinkedList
{
friendclassNetwork>;
public:
LinkedList():
vNum(0),eNum(0){}
~LinkedList()
{
for(inti=0;i}
boolinsertV(namev)
{
if(find(v))returnfalse;
vertices.push_back(vertex(v,newlist));
vNum++;returntrue;
}
boolinsertE(constname&v1,constname&v2,constdist&cost)
{
inti,j;
if(v1==v2||!
find(v1,i)||!
find(v2,j))returnfalse;
for(list:
:
iteratoriter=vertices[i].e->begin();
iter!
=vertices[i].e->end()&&iter->vIDif(iter==vertices[i].e->end())
{
vertices[i].e->push_back(edge(j,cost));eNum++;returntrue;
}
if(iter->vID==j)returnfalse;
vertices[i].e->insert(iter,edge(j,cost));eNum++;returntrue;
}
name&getV(intn){returnvertices[n].v;}//没有越界检查
intnextV(intm,intn)//返回m号顶点的第n号顶点后第一个邻接顶点号,无返回-1
{
for(list:
:
iteratoriter=vertices[m].e->begin();
iter!
=vertices[m].e->end();iter++)if(iter->vID>n)returniter->vID;
return-1;
}
private:
boolfind(constname&v)
{
for(inti=0;ireturnfalse;
}
boolfind(constname&v,int&i)
{
for(i=0;ireturnfalse;
}
structedge
{
edge(){}
edge(intvID,distcost):
vID(vID),cost(cost){}
intvID;
distcost;
};
structvertex
{
vertex(){}
vertex(namev,list*e):
v(v),e(e){}
namev;
list*e;
};
intvNum,eNum;
vectorvertices;
};
#endif
这个实现是很简陋的,但应该能满足后面的讲解了。
现在这个还什么都不能做,不要急,在下篇将讲述图的DFS和BFS
DFS和BFS
对于非线性的结构,遍历都会首先成为一个问题。
和二叉树的遍历一样,图也有深度优先搜索(DFS)和广度优先搜索(BFS)两种。
不同的是,图中每个顶点没有了祖先和子孙的关系,因此,前序、中序、后序不再有意义了。
仿照二叉树的遍历,很容易就能完成DFS和BFS,只是要注意图中可能有回路,因此,必须对访问过的顶点做标记。
最基本的有向带权网
#ifndefGraph_H
#defineGraph_H
#include
#include
usingnamespacestd;
#include"Graphmem.h"
template
classNetwork
{
public:
Network(){}
Network(distmaxdist){data.NoEdge=maxdist;}
~Network(){}
boolinsertV(namev){returndata.insertV(v);}
boolinsertE(namev1,namev2,distcost){returndata.insertE(v1,v2,cost);}
name&getV(intn){returndata.getV(n);}
intnextV(intm,intn=-1){returndata.nextV(m,n);}
intvNum(){returndata.vNum;}
inteNum(){returndata.eNum;}
protected:
bool*visited;
staticvoidprint(namev){cout<private:
memdata;
};
#endif
你可以看到,这是在以mem方式储存的data上面加了一层外壳。
在图这里,逻辑上分有向、无向,带权、不带权;储存结构上有邻接矩阵和邻接表。
也就是说分开来有8个类。
为了最大限度的复用代码,继承关系就非常复杂了。
但是,多重继承是件很讨厌的事,什么覆盖啊,还有什么虚拟继承,我可不想花大量篇幅讲语言特性。
于是,我将储存方式作为第三个模板参数,这样一来就省得涉及虚拟继承了,只是这样一来这个Network的实例化就很麻烦了,不过这可以通过typedef或者外壳类来解决,我就不写了。
反正只是为了让大家明白,真正要用的时候,最好是写专门的类,比如无向无权邻接矩阵图,不要搞的继承关系乱七八糟。
DFS和BFS的实现
public:
voidDFS(void(*visit)(namev)=print)
{
visited=newbool[vNum()];
for(inti=0;iDFS(0,visit);
delete[]visited;
}
protected:
voidDFS(inti,void(*visit)(namev)=print)
{
visit(getV(i));visited[i]=true;
for(intn=nextV(i);n!
=-1;n=nextV(i,n))
if(!
visited[n])DFS(n,visit);
}
public:
voidBFS(inti=0,void(*visit)(namev)=print)//n没有越界检查
{
visited=newbool[vNum()];queuea;intn;
for(n=0;nvisited[i]=true;
while(i!
=-1)//这个判断可能是无用的
{
visit(getV(i));
for(n=nextV(i);n!
=-1;n=nextV(i,n))
if(!
visited[n]){a.push(n);visited[n]=true;}
if(a.empty())break;
i=a.front();a.pop();
}
delete[]visited;
}
DFS和BFS函数很难写得像树的遍历方法那么通用,这在后面就会看到,虽然我们使用了DFS和BFS的思想,但是上面的函数却不能直接使用。
因为树的信息主要在节点上,而图的边上还有信息。
测试程序
#include
usingnamespacestd;
#include"Graph.h"
intmain()
{
Network>a;
a.insertV('A');a.insertV('B');
a.insertV('C');a.insertV('D');
a.insertE('A','B',1);a.insertE('A','C',2);
a.insertE('B','D',3);
cout<<"DFS:
";a.DFS();cout<cout<<"BFS:
";a.BFS();cout<return0;
}
老实说,这个类用起来真的不是很方便。
不过能说明问题就好。
无向图
要是在纸上随便画画,或者只是对图做点示范性的说明,大多数人都会选择无向图。
然而在计算机中,无向图却是按照有向图的方法来储存的——存两条有向边。
实际上,当我们说到无向的时候,只是忽略方向——在纸上画一条线,难不成那线“嗖”的就出现了,不是从一头到另一头画出来的?
无向图有几个特有的概念,连通分量、关节点、最小生成树。
下面将分别介绍,在此之前,先完成无向图类的基本操作。
无向图类
template
classGraph:
publicNetwork
{
public:
Graph(){}
Graph(distmaxdist):
Network(maxdist){}
boolinsertE(namev1,namev2,distcost)
{
if(Network:
:
insertE(v1,v2,cost))
returnNetwork:
:
insertE(v2,v1,cost);
returnfalse;
}
};
仅仅是添加边的时候,再添加一条反向边,很简单。
连通分量
这是无向图特有的,有向图可要复杂多了(强、单、弱连通),原因就是无向图的边怎么走都行,有向图的边好像掉下无底深渊就再也爬不上来了。
有了DFS,求连通分量的算法就变得非常简单了——对每个没有访问的顶点调用DFS就可以了。
voidcomponents()
{
visited=newbool[vNum()];inti,j=0;
for(i=0;icout<<"Components:
"<for(i=0;i{
if(!
visited[i]){cout<<'('<<++j<<')';DFS(i);cout<}
delete[]visited;
}
关节点
下定义是人们认识事物的一个方法,因为概念使得人们能够区分事物——关于这个还有个绝对的运动和相对的静止的哲学观点(河水总在流,但是长江还叫长江,记得那个著名的“不可能踏进同一条河里”吗?
)因此,能否有个准确的概念往往是一门学科发展程度的标志,而能否下一个准确的定义反映了一个人的思维能力。
说这么多废话,原因只有一个,我没搞清楚什么叫“关节点”——参考书有限,不能仔细的考究了,如有误解,还望指正。
严版是这么说的:
如果删除某个顶点,将图的一个连通分量分割成两个或两个以上的连通分量,称该顶点为关节点。
——虽然没有提到图必须是无向的,但是提到了连通分量已经默认是无向图了。
殷版是这么说的:
在一个无向连通图中,……(余下同严版)。
问题出来了,非连通图是否可以讨论含有关节点?
我们是否可以说某个连通分量中含有关节点?
遗憾的是,严版留下这个问题之后,在后面给出的算法是按照连通图给的,这样当图非连通时结果就是错的。
殷版更是滑头,只输出重连通分量,不输出关节点,自己虽然假定图是连通的,同样没有连通判断。
翻翻离散数学吧,结果没找到什么“关节点”,只有“割点”,是这样的:
一个无向连通图,如果删除某个顶点后,变为非连通图,该顶点称为割点。
权当“割点”就是“关节点”,那么算法至少也要先判断是否连通吧?
可是书上都直接当连通的了……
关于算法不再细说,书上都有。
下面的示例,能输出每个连通分量的“关节点”(是不是可以这样叫,我也不清楚)。
dfn储存的是每个顶点的访问序号,low是深度优先生成树上每个非叶子顶点的子女通过回边所能到达的顶点最小的访问序号。
把指向双亲的边也当成回边并不影响判断,因此不必特意区分,殷版显式区分了,属于画蛇添足。
这样一来,if(low[n]>=dfn[i])cout<=就显得很尴尬了,因为只能等于不可能大于。
还要注意的是,生成树的根(DFS的起始点)是单独判断的。
voidarticul()
{
dfn=newint[vNum()];low=newint[vNum()];inti,j=0,n;
for(i=0;ifor(i=0;i{
if(!
dfn[i])
{
cout<<'('<<++j<<')';dfn[i]=low[i]=count=1;
if((n=nextV(i))!
=-1)articul(n);boolout=false;//访问树根
while((n=nextV(i,n))!
=-1)
{
if(dfn[n])continue;
if(!
out){cout<articul(n);//访问其他子女
}
cout<}
}
delete[]dfn;delete[]low;
}
private:
voidarticul(inti)
{
dfn[i]=low[i]=++count;
for(intn=nextV(i);n!
=-1;n=nextV(i,n))
{
if(!
dfn[n])
{
articul(n);
if(low[n]if(low[n]>=dfn[i])cout<}
elseif(dfn[n]}
}
int*dfn,*low,count;
最小生成树
说人是最难伺候的,真是一点不假。
上面刚刚为了“提高可靠性”添加了几条多余的边,这会儿又来想办法怎么能以最小的代价把所有的顶点都连起来。
可能正是人的这种精神才使得人类能够进步吧——看着现在3GHz的CPU真是眼红啊,我还在受500MHz的煎熬,然后再想想8086……
正如图的基本元素是顶点和边,从这两个方向出发,就能得到两个算法——Kruskal算法(从边出发)、Prim算法(从顶点出发)。
据说还有别的方法,恕我参考资料有限,不能详查。
最小生成树的储存
显然用常用的树的储存方法来储存没有必要,虽然名曰“树”,实际上,这里谁是谁的“祖先”、“子孙”并不重要。
因此,用如下的MSTedge结构数组来储存就可以了。
template
classMSTedge
{
public:
MSTedge(){}
MSTedge(intv1,intv2,distcost):
v1(v1),v2(v2),cost(cost){}
intv1,v2;
distcost;
booloperator>(constMSTedge&v2){return(cost>v2.cost);}
booloperator<(constMSTedge&v2){return(costbooloperator==(constMSTedge&v2){return(cost==v2.cost);}
};
Kruskal算法
最小生成树直白的讲就是,挑选N-1条不产生回路最短的边。
Kruskal算法算是最直接的表达了这个思想——在剩余边中挑选一条最短的边,看是否产生回路,是放弃,不是选定然后重复这个步骤。
说起来倒是很简单,做起来就不那么容易了——判断是否产生回路需要并查集,在剩余边中找一条最短的边需要最小堆(并不需要对所有边排序,所以堆是最佳选择)。
Kruskal算法的复杂度是O(eloge),当e接近N^2时,可以看到这个算法不如O(N^2)的Prim算法,因此,他适合于稀疏图。
而作为稀疏图,通常用邻接表来储存比较好。
另外,对于邻接矩阵储存的图,Kruskal算法比Prim算法占不到什么便宜(初始还要扫描N^2条“边”)。
因此,最好把Kruskal算法放在Link类里面。
templateintLink:
:
MinSpanTree(MSTedge*a)
{
MinHeap>E;inti,j,k,l=0;
MFSetsV(vNum);list:
:
iteratoriter;
for(i=0;ifor(iter=vertices[i].e->begin();iter!
=vertices[i].e->end();iter++)
E.insert(MSTedge(i,iter->vID,iter->cost));//建立边的堆
for(i=0;i{
j=V.find(E.top()