图的遍历与应用

图的遍历

从图中的某个顶点出发,按某种方法对图中的所有顶点访问且仅访问一次。
① 为每个顶点设一个访问标志,用以标示是否被访问过;
② 访问标志用数组visited[n]来表示 。

深度优先搜索:

是指按照深度方向搜索 ,它类似于树的先根遍历

基本思想:
1)从图中某个顶点v0出发,访问v0。
2)找出刚访问顶点vi的第一个未被访问的邻接点,访问该顶点。重复此步骤,直到当前的顶点没有未被访问的邻接点为止。
3)返回前一个访问过的顶点,找出其下一个未被访问的邻接点,访问之;转2)。

过程示例:
A为起始顶点,实箭头代表访问方向,虚箭头代表回溯方向,箭头旁数字代表搜索顺序。
访问序列为:A、B、C、F、E、G、D、H、I。

深度优先搜索的算法:

#define True 1
#define False 0
#define Error 1   /*出错*/
#define Ok 1
int visited[MAX_VERTEX_NUM];   /*访问标志数组*/ 
void TraverseGraph (Graph g) 
{
	for(vi = 0; vi < g.vexnum; vi++)  visited[vi] = False;/*标志数组初始化*/
	for(vi = 0; vi < g.vexnum; vi++)	/*每顶点循环一次*/
	 if (!visited[vi])  DepthFirstSearch(g,vi)/*连通图仅循环一次*/
}/* TraverseGraph */ 

深度遍历v0所在的连通子图:

void  DepthFirstSearch(Graph g, int v0) 
{
	visit(v0);visited[v0] =True;/*访问v0,并置访问标志数组*/
	w = FirstAdjVertex(g, v0);
	while ( w != -1) 
	{/*邻接点存在*/
		if(!visited[w])  DepthFirstSearch(g, w);  /*递归调用*/
   		w = NextAdjVertex(g, v0, w); /*找下一个邻接点*/
  	}
} /*DepthFirstSearch*/ 

用邻接矩阵方式实现深度优先搜索:

void DepthFirstSearch(AdjMatrix g, int v0)   
{
	visit(v0);
	visited[v0] = True;
	for(vj = 0; vj < g.vexnum; vj++)
		if(!visited[vj] && g.arcs[v0][vj].adj == 1) 
			DepthFirstSearch(g, vj);
}

用邻接表方式实现深度优先搜索:

void DepthFirstSearch(AdjList g, int v0)   
{	visit(v0);
	visited[v0] = True;
	p = g.vertex[v0].firstarc;//v0边表头,指向第一邻接点
	while(p != NULL)
	{	if(!visited[p->adjvex])
			DepthFirstSearch(g, p->adjvex);
		p = p->nextarc;
	}
}

以邻接表作为存储结构,查找每个顶点的邻接点的时间复杂度为O(e), 其中e是无向图中的边数或有向图中弧数, 则深度优先搜索图的时间复杂度为O(n+e)。

用非递归过程实现深度优先搜索:

每次循环弹出一顶点进行访问,压入其所有邻接点;
下次弹出最后一个邻接点访问,再压入所有邻接点。

void DepthFirstSearch(Graph g, int v0) 
{  /*从v0出发深度优先搜索图g*/
InitStack(&S);  /*初始化空栈*/       
Push(&S, v0)while ( ! Is_Empty(S))
  { Pop(&S, &v);  
	if (!visited(v)) 
	  { /*栈中可能有重复结点*/
	     visit(v);  
	     visited[v]=True; 
	     w = FirstAdj(g, v);  /*求v的第一个邻接点*/
	     while (w != -1 )
	     { if (!visited(w))  Push(&S, w);
		    w = NextAdj(g, v, w);  /*求v相对于w的下一个邻接点*/ }
	  }
  }
} 

广度优先搜索:

按照广度方向搜索,它类似于树的按层次遍历。
基本思想:类似声波、水波
1)从图中某个顶点v0出发,首先访问v0;
2)依次访问v0的各个未被访问的邻接点;
3)分别从这些邻接点(端结点)出发,依次访问它们的各个未被访问的邻接点(新的端结点);
访问时应保证:
如果Vi和Vk为当前端结点,且Vi在Vk之前被访问,则Vi的所有未被访问的邻接点应在Vk的所有未被访问的邻接点之前访问。

4)重复(3),直到所有端结点均没有未被访问的邻接点为止;
5)若此时还有顶点未被访问,则选一个未被访问的顶点作为起始点,重复上述过程,直至所有顶点均被访问过为止。

搜索过程示例:
箭头代表搜索方向,箭头旁边的数字代表搜索顺序,A为起始顶点。
访问序列为:A、B、E、D、C、G、F、H、I。

广度优先搜索连通子图的算法:

void  BreadthFirstSearch(Graph g, int v0)
{
    visit(v0); visited[v0] = True;
    InitQueue(&Q);  
    EnterQueue(&Q, v0)/* v0进队*/
    while ( ! Empty(Q))
     {  DeleteQueue(&Q, &v);  /*队头元素出队*/
	    w = FirstAdj(g, v);  /*求v的第一个邻接点*/
	    while (w != -1 )
	    { if (!visited(w))  
	        { visit(w); visited[w] = True;
        	  EnterQueue(&Q, w); 
        	}
 		   w = NextAdj(g, v, w);  /*求下一个邻接点*/
 		}     
 	}  
} 

图的应用

图的连通性问题:

1、无向图的连通分量
对于连通图:
仅需要调用一次搜索过程,即从任一个顶点出发,便可以遍历图中的各个顶点。
对于非连通图:
需要多次调用搜索过程,而每次调用得到的顶点访问序列恰为各连通分量中的顶点集。

调用搜索过程的次数就是该图连通分量的个数。

2、两顶点间的简单路径
常需找一顶点u到另一顶点v的简单路径。要求:
① 顶点均不同(无环路);
② 简单路径可能多条,跟选择算法相关。

算法思想:(遍历实质就是探路过程)
① 从顶点u开始,进行深度(或广度)优先搜索,如果能搜索到顶点v,则表明顶点u到v有一条简单路径;
② 搜索同时记录线路,既得路径。
设置数组:pre[n]
记录顶点遍历前驱,vi->vj,pre[j]=i,搜索完后从pre追溯路径
pre代替visited数组,初始pre[n]={-1},pre[j]记录了前驱表示已被访问

int *pre;
void one_path(Graph *G, int u, int v)
//找一条从第u个顶点到第v个顶点的简单路径
{	int i;
	pre = (int *)malloc(G->vexnum*sizeof(int));
	for(i = 0; i < G->vexnum; i++)	pre[i] = -1;    	
	pre[u] = -2;      /*表示第u个顶点已被访问,且无前驱*/
    DFS_path(G, u, v);    /*用深度优先搜索从u到v的简单路径。*/    	
    free(pre);
}

int DFS_path(Graph *G, int u, int v)
{  /*深度优先u到v*/
    int j;
    for(j = firstadj(G,u); j >= 0; j = nextadj(G, u, j))
	   if(pre[j] == -1)//j未被访问
	   {	pre[j] = u;//标记j前驱为u
            if(j == v)
            {  print_path(pre, v); 
               return 1;
            }
            else if(DFS_path(G, j, v)) return1;//以j为新起点继续 
	   }
    return 0;
}

3、图的生成树
生成树:
一个连通图的生成树是指一个极小连通子图,它含有图的全部顶点,但只有足以构成树的n-1条边。任添加一条边必构成一个环。
最小生成树:
一个连通网的所有生成树中,各边的代价之和最小的那棵生成树。
MST性质:
设N=(V,{E}) 是一连通网,U 是顶点集V的一个非空子集。若(u , v)是一条具有最小权值的边,其中u∈U,v∈V-U,则存在一棵包含边(u , v)的最小生成树。

最小生成树算法:

(1) 普里姆算法——加点法

假设N=(V,{E})是连通网,TE为最小生成树中边的集合。
1)初始U={u0}(u0∈V), TE=φ;
2)在所有u∈U, v∈V-U的边中选一条代价最小的边(u,v)并入集合TE,同时将v并入U;
3)重复(2),直到U=V为止。
此时,TE中必含有n-1条边,则T=(V,{TE})为N的最小生成树。
在这里插入图片描述
注意:选择最小边时可能有多条同样权值的边,任选其一。
算法需设一辅助数组closedge[ ],以记录从U到V-U具有最小代价的边。

算法思想:
① 将初始顶点u加入U,其余顶点i,closedge[i]均初始化为i到u的边信息;
② 循环n-1次:
a:从各组最小边closedge[]中选最小的closedge[v],将v加入U中;
b:更新剩余的每组最小边信息closedge[i].

struct
{ 
	VertexData adjvex;  //当前顶点到U集中顶点的邻接点
    int lowcost;   //当前顶点到U集中顶点最小代价
} closedge[MAX_VERTEX_NUM];  //辅助数组

MiniSpanTree_Prim(AdjMatrix gn, int u) 
{/*从顶点u出发*/
    closedge[u].lowcost = 0;   /*初始化,U={u},U集中顶点lowcost为0 */
    for (i = 0; i < gn.vexnum; i++) /*对V-U中的顶点i,初始化closedge[i]*/
  	   if ( i != u) 
  	   {  closedge[i].adjvex = u; 
  	      closedge[i].lowcost = gn.arcs[u][i].adj;
  	   }
    for (e = 1; e <= gn.vexnum - 1; e++) 
    {  /*找n-1条边(n= gn.vexnum) */
	    v = Minium(closedge);     /* closedge[v]存当前最小边(u, v)的信息*/
	    u = closedge[v].adjvex;   	/* u∈U*/
       	printf(u, v);  closedge[v].lowcost = 0;     /*将顶点v0纳入U集合*/
       	for (i = 0; i < vexnum; i++)    /*v并入U后更新closedge[i]*/
           if (gn.arcs[v][i].adj < closedge[i].lowcost)
             {
            	closedge[i].lowcost = gn.arcs[v][i].adj;        
            	closedge[i].adjvex = v;
             }
    }
}

(2)克鲁斯卡尔算法——加边法:

假设N = (V, {E})是连通网,将其边按权值从小到大排序;
1)将n个顶点看成n个集合;
2)按权值由小到大的顺序选择边,所选边应满足两个顶点不在同一个顶点集合内,将该边放到生成树边的集合中。同时将该边的两个顶点所在的顶点集合合并;
3)重复2),直到所有的顶点都在同一个顶点集合内。

在这里插入图片描述

稠密图——普里姆算法;
稀疏图——克鲁斯卡尔算法。

有向无环图的应用:

可用来描述工程或系统的进行过程,施工图、课程间制约关系等。

1、拓扑排序

AOV-网:顶点表示活动的网;
① 用顶点表示活动;
② 用弧表示活动间的优先关系的有向无环图 。
拓扑序列:
有向图G = (V,{E})的顶点线性序列(vi1,vi1,vi3,…,vin) ,必须满足前驱后继关系。

AOV-网的特性:
① 先行关系具有可传递性;
② 拓扑序列不是唯一的。

求拓扑排序的基本思想:
1、从有向图中选一个无前驱的顶点输出;
2、将此顶点和以它为起点的弧删除;
3、重复1、2直到不存在无前驱的顶点;
4、若此时输出的顶点数小于有向图中的顶点数,则说明有向图中存在回路,否则输出的顶点的顺序即为一个拓扑序列。

1、基于邻接矩阵表示的存储结构:

算法步骤:
对邻接矩阵的列进行排序编号:
1)取1作为第一新序号;
2)找一个未新编号的、值全为0的列j,若找到,则转3);
否则,若所有的列全部都编过号,拓扑排序结束; 若有列未曾被编号,则该图中有回路;
3)输出列号对应的顶点j,把新序号赋给所找到的列;
4)将矩阵中j对应的行全部置为0;
5)新序号加1,转2);

在这里插入图片描述
拓扑序列为:v1,v6,v4,v3,v2,v5或v1,v3,v2,v6,v4,v5.

2、基于邻接表的存储结构

在这里插入图片描述

附设一个存放各顶点入度的数组indegree[ ],于是有 :
1)找G中无前驱的顶点——查找indegree[i]为零的顶点vi;
2)删除以i为起点的所有弧——对链在顶点i后面的所有邻接顶点k,将对应的indegree[k]减1。

为避免重复检测入度为零的顶点,可再设置一个辅助栈:
若某一顶点的入度减为0,则将它入栈。每当输出某一顶点时,便将它从栈中删除。

算法思想:
1、首先求各顶点入度,并将入度为0的顶点入栈;
2、只要栈不空,则重复下面处理:
将栈顶顶点i出栈并打印;
将顶点i的每一个邻接点k的入度减1,如果顶点k的入度变为0,则将顶点k入栈;

算法只修改indegree[]和堆栈,不修改邻接表。

拓扑排序算法:

int TopoSort (AdjList G)
{   Stack S; int indegree[MAX_VERTEX_NUM];    
    int i, count, k;       
    ArcNode *p;
    FindID(G, indegree);  /*求各顶点入度*/      
    InitStack(&S);       /*初始化辅助栈*/
    for(i = 0; i < G.vexnum; i++)       
        if(indegree[i] == 0) Push(&S, i);    /*将入度为0的顶点入栈*/
    count = 0;
    while(!StackEmpty(S))
     {  Pop(&S, &i); 	   
        printf("%c", G.vertex[i].data);
	    count++;  /*输出i号顶点并计数*/            p = G.vertexes[i].firstarc;//访问其链表(作为起点的弧)
	    while(p != NULL)
	    {//更新i邻接点的入度
	        k = p->adjvex;             
	       indegree[k]--;      /*i号顶点的每个邻接点的入度减1*/
	       if(indegree[k] == 0)  Push(&S, k);  /*若入度减为0,则入栈*/
	       p = p->nextarc;     
	     }
	  } /*while*/
     if (count < G.vexnum)  return(Error);  /*该有向图含有回路*/
     else  return(Ok);
} 

求顶点入度算法:

void FindID( AdjList G, int indegree[MAX_VERTEX_NUM])
{   int i; ArcNode *p;
    for(i = 0; i < G.vexnum; i++)     
       indegree[i] = 0;  //初始化indegree
    for(i = 0; i < G.vexnum; i++)
    {  p = G.vertexes[i].firstarc;//获取每个顶点的边链表
	   while(p != NULL)
	   {//扫描每个顶点的边链表
	   	   indegree[p->adjvex]++;//p指向的弧入度累加到p->adjex
	       p = p->nextarc;    
	   }     
	}  
} 

用拓扑排序算法求的拓扑序列为:v6,v1,v3,v2,v4,v5。
时间复杂度为O(n+e)。

2、关键路径

AOE-网:
在有向图中,用顶点表示事件,用弧表示活动,弧的权值表示活动所需要的时间。这种方法构造的有向无环图叫做AOE-网。
源点
存在唯一的、入度为零的顶点——起点。
汇点
存在唯一的、出度为零的顶点——终点。
关键路径
从源点到汇点的最长路径的长度即为完成整个工程任务所需的时间,该路径叫做关键路径。
关键活动:
关键路径上的活动叫做关键活动。

事件vi的最早发生时间ve(i):
从源点到顶点vi的最长路径的长度,叫做事件vi的最早发生时间。要保证前序事件和活动全部完成。
求ve(i) 时可从源点开始,按拓扑顺序向汇点递推。

事件vi的最晚发生时间vl(i):
在保证汇点按其最早发生时间发生这一前提下,事件vi的最晚发生时间。
在求出ve(i)的基础上,从汇点开始,按逆拓扑顺序向源点递推,求出vl(i)。

活动ai的最早开始时间e(i):
如果活动ai对应的弧为<j,k>,则e(i)等于从源点到顶点j的最长路径的长度,即:e(i)=ve(j) ——弧起点ve。

活动ai的最晚开始时间l(i):
如果活动ai对应的弧为<j,k>,其持续时间为dut(<j,k>)则有:l(i)=vl(k)- dut(<j,k>) ——弧终点vl-dut。

活动ai的松弛时间(时间余量):
ai的最晚开始时间与ai的最早开始时间之差:l(i)- e(i)。
显然,松弛时间(时间余量)为0的活动为关键活动。

求关键路径的基本步骤:
1、对图中顶点进行拓扑排序,在排序过程中按拓扑序列求出每个事件的最早发生时间ve(i);
2、按逆拓扑序列求每个事件的最晚发生时间vl(i);
3、求出每个活动ai的最早开始时间e(i)和最晚发生时间l(i);
4、找出e(i)=l(i) 的活动ai,即为关键活动。

算法思想:
1、求出各顶点的入度,并将入度为0的顶点入栈S;
2、将各顶点的最早发生时间ve[i]初始化为0;
3、只要栈S不空,则重复下面处理:
a. 将栈顶顶点j出栈并压入栈T(生成逆拓扑序列);
b. 将顶点j的每一个邻接点k的入度减1,如果k入度变为0,则将k入栈S;
c. 根据顶点j的最早发生时间ve[j]和弧<j, k>的权值,更新顶点k的最早发生时间ve[k]。

修改后的拓扑排序算法:

int  ve[MAX_VERTEX_NUM];    /*每个顶点的最早发生时间*/
int TopoOrder(AdjList G, Stack *T) { /* T为返回逆拓扑序列的栈,计算ve数组*/
int count, i, j, k;  ArcNode *p;  int indegree[MAX_VERTEX_NUM];  /*各顶点入度数组*/
Stack  S;    InitStack(T);  InitStack(&S);   /*初始化栈T,  S*/
FindID(G,  indegree);  /*求各个顶点的入度*/
for(i = 0; i < G.vexnum; i++)   if(indegree[i] == 0)   Push(&S, i);    count = 0;  
for(i = 0; i < G.vexnum; i++)   ve[i] = 0;   /*初始化最早发生时间*/
while(!StackEmpty(S)){
       Pop(&S, &j);      Push(T, j);      count++;      p = G.vertex[j].firstarc;
       while(p!=NULL){//更新j后继
	k = p->adjvex;     if(--indegree[k] == 0)  Push(&S, k);   /*若顶点的入度减为0,则入栈*/
	if(ve[j] + p->weight > ve[k])  ve[k] = ve[j] + p->weight;//更新k的ve,取最大值
	p = p->nextarc;     }  /*while*/       } /*while*/
        if(count < G.vexnum)   return(Error);       else  return(Ok);}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
二叉树的遍历算法是指按照某种顺序访问二叉树中的所有节点。常见的遍历算法有三种:前序遍历、中序遍历和后序遍历。下面分别介绍这三种遍历算法应用: 1.前序遍历:先访问根节点,再访问左子树,最后访问右子树。前序遍历应用包括: - 创建二叉树:可以根据前序遍历序列和中序遍历序列构建一棵二叉树。 - 表达式求值:可以根据前序遍历序列将表达式转换为二叉树,然后对二叉树进行后序遍历求值。 - 复制二叉树:可以根据前序遍历序列和中序遍历序列复制一棵二叉树。 2.中序遍历:先访问左子树,再访问根节点,最后访问右子树。中序遍历应用包括: - 二叉搜索树的排序:对于一棵二叉搜索树,中序遍历序列是有序的,因此可以通过中序遍历将二叉搜索树中的元素按照从小到大的顺序输出。 - 表达式转换:可以根据中序遍历序列将中缀表达式转换为后缀表达式。 - 二叉树的线索化:可以通过中序遍历将二叉树线索化,使得每个节点都有一个前驱节点和后继节点。 3.后序遍历:先访问左子树,再访问右子树,最后访问根节点。后序遍历应用包括: - 表达式求值:可以根据后序遍历序列将表达式转换为二叉树,然后对二叉树进行后序遍历求值。 - 后序遍历销毁二叉树:可以通过后序遍历销毁一棵二叉树。 - 计算二叉树的深度:可以通过后序遍历计算一棵二叉树的深度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值