浅谈数据结构-拓扑排序
在生活、工作中进行一项任务,必须有先后顺序,这就是流程图,在流程图中有if分支,也就是下一个活动的展开,可能上一次活动中所有分支执行完毕,这样就形成了拓扑结构。接入项目中比较简单,我们可以通过链表表示项目之间各个活动的关系,当项目流程比较复杂时,我们需要有向图来描述这种关系,而且是不能循环的有向图,我们称之为有向无环图。
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的有限关系,这样的有向图为顶点表是活动的图,成为AOV网(Activity On Vertex Network)。在AOV网中弧表示活动之间的某种约束关系。
一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行。如图3-6是一个具有三个顶点的回路,由<A,B>边可得B活动必须在A活动之后,由<B,C>边可得C活动必须在B活动之后,所以推出C活动必然在A活动之后,但由<C,A>边可得C活动必须在A活动之前,从而出现矛盾,使每一项活动都无法进行。这种情况若在程序中出现,则称为死锁或死循环,是应该必须避免的。
在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
一、算法思想
从AOV网中选取一个入度为0的顶点输出,然后删除此顶点,并删除以此顶点为尾的弧,继续重复此操作,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。 在左图中入度为0的点位c1和c2,如按照拓扑排序算法进行运算结果为:
(1) C1,C8,C9,C2,C3,C5,C4,C7,C6
(2) C2,C1,C3,C5,C4,C6,C8,C9,C7
(3) C1,C2,C3,C8,C9,C5,C4,C6,C7
由AOV网构造出拓扑序列的实际意义是:如果按照拓扑序列中的顶点次序,在开始每一项活动时,能够保证它的所有前驱活动都已完成,从而使整个工程顺序进行,不会出现冲突的情况。
二、算法分析
- 从AOV网络中删除入度为0的顶点;有入度这个概念,则用链表的存储结构储存数据。
//边表结点 typedef struct EdgeNode { int nNodevex; //邻接点的点表中结点的坐标 EdgeType nNodeWeight; //用于网图中边的权值 EdgeNode* next; //链域,指向下一个邻接点 }EdgeNode,*pEdgeNode; typedef struct VertexNodeAdv { int in; //顶点的度 VertexType nNodeData; //顶点表中存储的数据 pEdgeNode pFirstNode; //顶点表和边表中关联指针,指向边表头指针 }VertexNodeAdv,*pVertexNodeAdv,VertexListAdv[MAXVEX];; //图结构 typedef struct { VertexListAdv vertexList; int numVertess,numEdges; }GraphListAdv;
- 删除此顶点还有顶点尾的弧;此时删除顶点(输出数据),删除顶点为尾的弧,意味这弧消失,图中一个顶点的入度减1.必须在顶点表中有表示度的变量。
- 继续循环,直到不存在度为0的顶点;就是循环停止条件是顶点全部输出。在程序中用堆栈图中顶点。
- 入度为0的顶点全部入栈,进入循环后,一顶点出栈,获取这顶点中连接的下一个顶点。
三、例图解释
(1) 在(a)图中v0和v1的入度都为0,不妨选择v0并输出之,接着删去顶点v0及出边<0,2>,得到的结果如(b)图所示。
(2) 在(b)图中只有一个入度为0的顶点v1,输出v1,接着删去v1和它的三条出边<1,2>,<1,3>和<1,4>,得到的结果如(c)图所示。
(3) 在(c)图中v2和v4的入度都为0,不妨选择v2并输出之,接着删去v2及两条出边<2,3>和<2,5>,得到的结果如(d)图所示。
(4) 在(d)图上依次输出顶点v3,v4和v5,并在每个顶点输出后删除该顶点及出边,操作都很简单,不再赘述。
为了利用C++语言在计算机上实现拓扑排序算法,AOV网采用邻接表表示较方便。
四、代码
int GraphData::GetGraphAdvListLocation(GraphListAdv* pList,char chpoint) { GraphListAdv *pGraphList = pList; int i = 0; for (i = 0;i< pGraphList->numVertess;i++) { if (pGraphList->vertexList[i].nNodeData == chpoint) { break;; } } if (i >= pGraphList->numVertess) { return -1; } return i; } void GraphData::CreateGraphListAdv(GraphListAdv* pList,int numVer,int numEdegs) { int weight = 0; GraphListAdv *pGraphList = pList; pGraphList->numVertess = numVer; pGraphList->numEdges = numEdegs; EdgeNode* firstNode,*secondNode; //创建顶点表 for (int i= 0; i < numVer;i++) { pGraphList->vertexList[i].nNodeData = getchar(); pGraphList->vertexList[i].pFirstNode = NULL; while(pGraphList->vertexList[i].nNodeData == '\n') { pGraphList->vertexList[i].nNodeData = getchar(); } } //创建边表 for(int k = 0; k < pGraphList->numEdges; k++) { char p, q; printf("输入边(vi,vj)上的下标i,下标j和权值:\n"); p = getchar(); while(p == '\n') { p = getchar(); } q = getchar(); while(q == '\n') { q = getchar(); } scanf("%d", &weight); int m = -1; int n = -1; m = GetGraphAdvListLocation(pGraphList, p); n = GetGraphAdvListLocation(pGraphList, q); if(n == -1 || m == -1) { fprintf(stderr, "there is no this vertex.\n"); return; } //getchar(); //字符p在顶点表的坐标为m,与坐标n的结点建立联系权重为weight firstNode = new EdgeNode(); firstNode->nNodevex = n; firstNode->next = pGraphList->vertexList[m].pFirstNode; pGraphList->vertexList[n].in++; firstNode->nNodeWeight = weight; pGraphList->vertexList[m].pFirstNode = firstNode; ////第二个字符second //secondNode = new EdgeNode(); //secondNode->nNodevex = m; //secondNode->next = pGraphList->vertexList[n].pFirstNode; //pGraphList->vertexList[n].in++; //secondNode->nNodeWeight = weight; //pGraphList->vertexList[n].pFirstNode = secondNode; } } void GraphData::TopoLogicalSort(GraphListAdv *pList) { EdgeNode *e; int ncount = 0; //统计输出顶点个数,用于判断是否有环 int nIndex ; // y用于保存度为0的坐标 stack<int> staNode; for (int i = 0;i < pList->numVertess; i++) { if (pList->vertexList[i].in == 0) { staNode.push(i); //将入度为0的顶点入栈 } } while(!staNode.empty()) { nIndex = staNode.top(); // 获取入度为0的顶点 staNode.pop(); //出栈 printf("%c ->",pList->vertexList[nIndex].nNodeData); //打印顶点 ncount ++; //对此顶点的处理:下一个顶点入度减1,如果为0还要进栈 for (e = pList->vertexList[nIndex].pFirstNode;e;e = e->next) { //对此顶点弧表遍历 int temp = e->nNodevex; //将temp号顶点邻接点的入度减1,若为0,则入栈,以便于下次循环输出 if (!(--pList->vertexList[temp].in)) { staNode.push(temp); } } } if (ncount < pList->numVertess) { printf(" 图有内环"); } }
将例图解释中的图输入到程序中,其中0对应a,1对应b,等输入其中,进行拓扑排序输出。拓扑排序输出类似广度遍历。
首先将入度为0的顶点压入堆栈;堆栈为0,1.(a,b)
判断堆栈是否为空,不为空,压出;此时为1.(b),输出。
获取顶点1的弧尾顶点,有4,3,2,入度分别减1,入度为0的入栈,4入栈(e)。
4(e)出栈,输出,继续循环。