第六章 图 总结

本章的主要内容是:

图的逻辑结构
图的存储结构及实现
图的连通性
最小生成树
最短路径
AOV网与拓扑排序
AOE网与关键路径

顶点的度:在无向图中,顶点v的度是指依附于该顶点的边数,通常记为TD (v)。
顶点的入度:在有向图中,顶点v的入度是指以该顶点为弧头的弧的数目,记为ID (v);
顶点的出度:在有向图中,顶点v的出度是指以该顶点为弧尾的弧的数目,记为OD (v)。

在具有n个顶点、e条边的无向图G中,各顶点的度之和与边数之和的关系?
在这里插入图片描述
在具有n个顶点、e条边的有向图G中,各顶点的入度之和与各顶点的出度之和的关系?与边数之和的关系?
在这里插入图片描述

邻接矩阵存储无向图的类

const int MaxSize=10; 
template <class T>
class Mgraph{
   public:
      MGraph(T a[ ], int n, int e );   
       ~MGraph( )
       void DFSTraverse(int v); 
       void BFSTraverse(int v);
        ……
   private:
       T vertex[MaxSize]; 
       int arc[MaxSize][MaxSize]; 
       int vertexNum, arcNum; 
};

邻接矩阵中图的基本操作——构造函数 t

emplate <class T>
MGraph::MGraph(T a[ ], int n, int e) {
    vertexNum=n; arcNum=e;
    for (i=0; i<vertexNum; i++) 
        vertex[i]=a[i];
    for (i=0; i<vertexNum; i++)    //初始化邻接矩阵
	   for (j=0; j<vertexNum; j++)
           arc[i][j]=0;             
    for (k=0; k<arcNum; k++) {
        cin>>i>>j;     //边依附的两个顶点的序号
        arc[i][j]=1;  arc[j][i]=1;  //置有边标志    
    }
}

邻接矩阵中图的基本操作——深度优先遍历

⑴ 访问顶点v;
⑵ 从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;
⑶ 重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。
递归定义

int visited[MaxSize];
template <class T>
void MGraph::DFSTraverse(int v){
     cout<<vertex[v]; visited [v]=1;
     for (j=0; j<vertexNum; j++)
         if (arc[v][j]==1 && visited[j]==0)
            DFSTraverse( j );
}

邻接矩阵中图的基本操作——广度优先遍历

⑴ 访问顶点v;
⑵ 依次访问v的各个未被访问的邻接点v1, v2, …, vk;
⑶ 分别从v1,v2,…,vk出发依次访问它们未被访问的邻接点,并使“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”被访问。直至图中所有与顶点v有路径相通的顶点都被访问到。

int visited[MaxSize];
template <class T>
void MGraph::BFSTraverse(int v){     
    front=rear=-1;   //假设采用顺序队列且不会发生溢出
   int Q[MaxSize]; cout<<vertex[v]; visited[v]=1;  Q[++rear]=v; 
    while (front!=rear)    {
         v=Q[++front];   
         for (j=0; j<vertexNum; j++)
            if (arc[v][j]==1 && visited[j]==0 ) {
                  cout<<vertex[j]; visited[j]=1; Q[++rear]=j;
            }
      }
}

增加一个顶点

template <class T>
void MGraph<T>::InsertVex(int num,T name) { 
 if ( num<0|| num>vertexNum) throw "位置";     
 int row, col, numv; 
 numv = vertexNum-1;
vertexNum++;    
for(int i=numv;i>=num;i--)	vertex[i++]=vertex[i];  
vertex[num]=name;    
 for(row=numv;row>=0;row--)  {所有行上num列之后的列后移,增加一列,
    for(col=numv;col>=num;col--)  arc[row][col+1]=arc[row][col];
     arc[row][num]=0;
  }
  for(row=numv;row>=num;row--) 
        for(col=0;col<=numv+1;col++)  arc[row+1][col]=arc[row][col];	
  for(col=0;col<vertexNum;col++)  arc[num][col]=0; 
 

}

删除一个顶点

template <class T>   void MGraph<T>::DeleteVex(int pos){
     if ( pos<0||  pos>MaxSize) throw "位置";   
     int row, col;    
     int numv=vertexNum;    
     for(int i=pos;i<numv;i++)   vertex[i]=vertex[i+1];    
     vertexNum--;                            
     for(row=0;row<numv;row++)   { //删除一列
         for(col=pos;col<numv;col++)	  arc[row][col]=arc[row][col+1];  
    }
    for(row=pos;row<numv;row++) 
	  for(col=0;col<numv;col++)
		  arc[row][col]=arc[row+1][col];      
  } 
}

插入一条边

template <class T>
void MGraph<T>::InsertArc(int i, int j)
{
  if ( i>MaxSize||  j>MaxSize) throw "位置";  
  arc[i][j]=1;
  arc[j][i]=1;
}

删除一条边

template <class T>
void MGraph<T>::DeleteArc(int i, int j)
{
         if ( i>MaxSize||  j>MaxSize) throw "位置";
 
         arc[i][j]=arc[j][i]=0;   
}

图的存储结构及实现:邻接表

邻接表有两种结点结构:顶点表结点和边表结点。

邻接表存储的基本思想:
对于图的每个顶点vi,将所有邻接于vi的顶点链成一个单链表,称为顶点vi的边表(对于有向图则称为出边表)
所有边表的头指针和存储顶点信息的一维数组构成了顶点表。

定义邻接表的结点

struct ArcNode{   
      int adjvex; 
      ArcNode *next;
};

template <class T>
struct VertexNode{
      T vertex;
      ArcNode *firstedge;
};

邻接表存储有向图的类

const int MaxSize=10;    //图的最大顶点数
template <class T>
class ALGraph
{    
   public:
       ALGraph(T a[ ], int n, int e);   
       ~ALGraph;    
       void DFSTraverse(int v);      
       void BFSTraverse(int v);      
   ………
  private:
       VertexNode adjlist[MaxSize];   
       int vertexNum, arcNum;       
};

邻接表中图的基本操作——构造函数

  1. 确定图的顶点个数和边的个数;
  2. 输入顶点信息,初始化该顶点的边表;
  3. 依次输入边的信息并存储在边表中;
    3.1 输入边所依附的两个顶点的序号i和j;
    3.2 生成邻接点序号为j的边表结点s;
    3.3 将结点s插入到第i个边表的头部;
template <class T>
ALGraph::ALGraph(T a[ ], int n, int e)
{   
    vertexNum=n; arcNum=e; 
    for (i=0; i<vertexNum; i++)   
    {
       adjlist[i].vertex=a[i];
       adjlist[i].firstedge=NULL;      
    } 
   

邻接表中图的基本操作——深度优先遍历

template <class T>
void ALGraph::DFSTraverse(int v){        
    cout<<adjlist[v].vertex;  visited[v]=1;
    p=adjlist[v].firstedge;    
    while (p!=NULL)     {
        j=p->adjvex;
        if (visited[j]==0) DFSTraverse(j);
    p=p->next;           
    }
}

邻接表中图的基本操作——广度优先遍历

template <class T>
void ALGraph::BFSTraverse(int v){
   front=rear=-1;   
   cout<<adjlist[v].vertex;    visited[v]=1;   Q[++rear]=v;   
   while (front!=rear)  {
       v=Q[++front];    p=adjlist[v].firstedge;    
       while (p!=NULL)  {
            j= p->adjvex;
            if (visited[j]==0) {
                cout<<adjlist[j].vertex;  visited[j]=1; Q[++rear]=j;
            }
            p=p->next;
       }
    }
}

边集数组

利用两个一维数组
一个数组存储顶点信息,
另外一个数组存储边及其权
数组分量包含三个域:边所依附的两个顶点,权值
各边在数组中的次序可以任意。

边集数组的实现

Struct edge
{ 
    int i;
    int j;
    int weight;
}

将邻接矩阵转化成边集数组

edge edges[M];//边的数据结构类型的变量
 for ( i = 0; i < G->vexnum; i++) { 
	 for (j = 0; j <= G->vexnum; j++)  {
	    if (G->arc[i][j] == 1)   {
	  	    edges[k].begin = i;
  	  	    edges[k].end = j;
	          // edges[k].weight = G->arc[i][j];
               k++;
         }
     }
 }

最小生成树(minimal spanning tree)

生成树的代价:设G=(V,E)是一个无向连通网,生成树上各边的权值之和称为该生成树的代价。
最小生成树:在图G所有生成树中,代价最小的生成树称为最小生成树。

普里姆(Prim)算法

基本思想:
设G=(V, E)是具有n个顶点的连通网,
T=(U, TE)是G的最小生成树,
T的初始状态为U={u0}(u0∈V),TE={ },
重复执行下述操作:
在所有u∈U,v∈V-U的边中找一条代价最小的边(u, v)并入集合TE,同时v并入U,直至U=V。

伪代码

  1. 初始化两个辅助数组lowcost(=arc[0][i])和adjvex(=0)(0是始点);
  2. 输出顶点u0,将顶点u0加入集合U中;
  3. 重复执行下列操作n-1次
    3.1 在lowcost中选取最短边(lowcost[k]),取对应的顶点序号k;
    3.2 输出顶点k和对应的权值;
    3.3 将顶点k加入集合U中(lowcost[k]=0);
    3.4 调整数组lowcost和adjvex;
Void prime(MGraph G){
    for(int i=1;i<G.vertexNu;i++){
        lowcost[i]=G.arc[0][i];  adjvex[i]=0;
    }
    lowcost[0]=0;
    for(i=1;i<G.vertexNum;i+++){
        k=MinEdge(lowcost,G.vertexNum)
        cout<<K<<adjvex[k]<<lowcost[k];
        lowcost[k]=0;
       
for(j=1;j<G.vertexNum;j++)
          if((G.arc[k][j]<lowcost[j]){
              lowcost[j]=G.arc[k][j];
              arcvex[j]=k;
           }
    }
}

克鲁斯卡尔(Kruskal)算法

基本思想:
设无向连通网为G=(V, E),令G的最小生成树为T=(U, TE),其初态为U=V,TE={ },
然后,按照边的权值由小到大的顺序,考察G的边集E中的各条边。
若被考察的边的两个顶点属于T的两个不同的连通分量,则将此边作为最小生成树的边加入到T中,同时把两个连通分量连接为一个连通分量;
若被考察边的两个顶点属于同一个连通分量,则舍去此边,以免造成回路,
如此下去,当T中的连通分量个数为1时,此连通分量便为G的一棵最小生成树

  1. 初始化:U=V; TE={ };
  2. 循环直到T中的连通分量个数为1
    2.1 在E中寻找最短边(u,v);
    2.2 如果顶点u、v位于T的两个不同连通分量,则
    2.2.1 将边(u,v)并入TE;
    2.2.2 将这两个连通分量合并为一个;
    2.3 在E中标记边(u,v),使得(u,v)不参加后续最短边的选取;

算法实现的关键问题

1、图的存储结构
采用边集数组存储图。
2、如何判断一条边所依附的两个顶点在同一个连通分两中(并查集)
定义Parent[i]数组。数组分量的值表示顶点i的双亲节点(初值为-1;)
当一条边(u,v)的两个顶点的根结不同时,这两个结点属于不同的连通分量(利用parent 数组查找一棵树的根节点。当一个结点n的parent==-1,树的根节点即为n)
3. 如何将一条边所依附的两个顶点合并到同一个连通分量中
要进行联通分量的合并 ,其中一个顶点所在的树的根节点为vex1,另一个顶点所在的树的根节点为vex2,则:parent[vex2]=vex1

int main(){
    int arcNum, int vertexNum;
    EdgeNode *edge;
    int *parent;

    cout<<"please input the number of vertexNum:"; cin>>vertexNum;
    cout<<"please input the number of edges:";	cin>>arcNum;
    edge=new EdgeNode[arcNum];	parent=new int[vertexNum];
    for(int i=0;i<arcNum;i++)	{
 	cout<<"Please input the edges:";
	cin>>edge[i].from>>edge[i].to>>edge[i].weight;
    }
    sort(edges, G); //对边集数组进行堆排序,时间复杂性为O(eloge)
    for (i=0;i<vertexNum;i++)
	parent[i]=-1;  //每个节点分属于不同的集合

    int k=0,begin,end,count=0;
    cout<<"next is the MST :"<<endl;
	
for (k=0;k<arcNum;k++)	{
         begin=edge[k].from;	end=edge[k].to;	
         int m,n;
        m=Find(parent,begin);	n=Find(parent,end);
        if(m!=n)	{
            cout<<begin<<","<<end<<","<<edge[k].weight<<endl;
            parent[n]=m;	
            count++;
            if(count==vertexNum-1)	break;
       }
   }
   return 0;
}
int Find(int *parent, int node)
{
	int f;
	f=node;
	while(parent[f]>-1)
		f=parent[f];
	return f;
}

Kruskal算法的时间复杂性分析

边集数组排序,时间复杂性O(eloge)
在e条边中选边,时间复杂性为O(e)
因此时间复杂性为O(eloge)

Dijkstra算法

基本思想:
1、设置一个集合S存放已经找到最短路径的顶点,S的初始状态只包含源点v,
2、对vi∈V-S,假设从源点v到vi的有向边为最短路径(从v到其余顶点的最短路径的初值)。
3、以后每求得一条最短路径v, …, vk,就将vk加入集合S中,并将路径v, …, vk , vi与原来的假设相比较,取路径长度较小者为最短路径。
重复上述过程,直到集合V中全部顶点加入到集合S中。

数据结构
图的存储结构:邻接矩阵存储结构
数组dist[n]:每个分量dist[i]表示当前所找到的从始点v到终点vi的最短路径的长度。初态为:
若从v到vi有弧,则dist[i]为弧上权值;否则置dist[i]为∞。
数组path[n]:path[i]是一个字符串,表示当前所找到的从始点v到终点vi的最短路径。初态为:若从v到vi有弧,则path[i]为vvi;否则置path[i]空串。
数组s[n]:存放源点和已经找到最短路径的终点,其初态为只有一个源点v。

const int MAX=1000;
void  Dijkstra(MGraph g, int v){
       for ( i =0; i<g.vexnum ; i++){
	 dist[i]=g.arcs[v][i];  
               if ( dist[i]!= MAX) 
                      path [i]=g.vertex[v]+g.vertex[i];
               else
                      path[i]=“”;
       }
       S[0]=g.vertex[v]; 
       num=1;  
While (num<g.vextexNum){
    k=0;
    for(i=0;i<G.vertexNum;i++)
           if((dist[i]<dist[k])   k=i
    cout<<dist[k]<<path[k];
    s[num++]=G.vertex[k];                
    for(i=0;i<G.vertexNum;i++)
             if(dist[k]+g.arc[k][i]<dist[i] {
		 dist[i]=dist[k]+g.arc[k][i];
                       path[i]=path[k]+g.vertex[i];
               }
}
}

Floyd算法

基本思想如下:
设图g用邻接矩阵法表示,
求图g中任意一对顶点vi、 vj间的最短路径。
(-1) 将vi到vj 的最短的路径长度初始化为(vi,vj), 然后进行如下n次比较和修正:
(0) 在vi、vj间加入顶点v0,比较(vi, v0, vj)和(vi, vj)的路径的长度,取其中较短的路径作为vi到vj的且中间顶点号不大于0的最短路径。

void Floyd(MGraph G)
{
    for (i=0; i<G.vertexNum; i++)        
       for (j=0; j<G.vertexNum; j++)
       {
          dist[i][j]=G.arc[i][j];
          if (dist[i][j]!=) 
               path[i][j]=G.vertex[i]+G.vertex[j];
          else path[i][j]=""; 
       }
for (k=0; k<G.vertexNum; k++)         
        for (i=0; i<G.vertexNum; i++)       
           for (j=0; j<G.vertexNum; j++)
               if (dist[i][k]+dist[k][j]<dist[i][j]) {
                    dist[i][j]=dist[i][k]+dist[k][j];
                    path[i][j]=path[i][k]+path[k][j];
              }
}

AOV网:

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,称这样的有向图为顶点表示活动的网,简称AOV网。
AOV网特点:
1.AOV网中的弧表示活动之间存在的某种制约关系。
2.AOV网中不能出现回路 。

拓扑序列:

设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1, v2, …, vn称为一个拓扑序列,当且仅当满足下列条件:若从顶点vi到vj有一条路径,则在顶点的拓扑序列中顶点vi必在顶点vj之前。
拓扑排序:对一个有向图构造拓扑序列的过程称为拓扑排序 。

基本思想:
⑴ 从AOV网中选择一个没有前驱的顶点并且输出;
⑵ 从AOV网中删去该顶点,并且删去所有以该顶点为尾的弧;
⑶ 重复上述两步,直到全部顶点都被输出,或AOV网中不存在没有前驱的顶点。

拓扑排序算法——伪代码

  1. 栈S初始化;累加器count初始化;
  2. 扫描顶点表,将没有前驱的顶点压栈;
  3. 当栈S非空时循环
    3.1 vj=退出栈顶元素;输出vj;累加器加1;
    3.2 将顶点vj的各个邻接点的入度减1;
    3.3 将新的入度为0的顶点入栈;
  4. if (count<vertexNum) 输出有回路信息;

void TOpSort(){

int  top=-1, count=0;
for(int i=0;i<vertexnum;i++)
     if(adjlist[i].in==0) s[++top]=i;
while(top!=-1){
    j=s[top--]; cout <<adjlist[j].vertext;   count++;
    p=adjlist[j].firstedge;
    while(p!=NULL){
          k=p->adjvex; adjlist[k].in--;
         if(adjlist[k].in==0) s[top++]=k;
         p=p->next;
      } 
}
If (count<vertexNum) cout<<“有回路”;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值