数据结构(C语言版)摘录--图


第七章 图


图(Graph)是一种较线性表和树更为复杂的数据结构。

图的定义和术语

图是由一个顶点集 V 和一个弧集 VR构成的数据结构。Graph = (V , VR )其中,VR={<v,w>| v,w∈V 且 P(v,w)},<v,w>表示从 v 到 w 的一条弧,并称 v 为 弧尾,w 为 弧头。谓词 P(v,w) 定义了弧 <v,w>的意义或信息。由于“弧”是有方向的,因此称 由顶点集和弧集构成的图为有向图。



若<v, w>∈VR 必有<w, v>∈VR, 则称 (v,w) 为顶点v 和顶点 w 之间存在一条边。 由顶点集和边集构成的图称作无向图。



假设图中有 n 个顶点,e 条边,则:
  • 含有 e=n(n-1)/2 条边的无向图称作完全图
  • 含有 e=n(n-1) 条弧的有向图称作有向完全图
  • 若边或弧的个数 e<nlogn,则称作稀疏图,否则称作稠密图
假若顶点v 和顶点w 之间存在一条边,则称顶点v 和w 互为邻接点,边(v,w) 和顶点v 和w 相关联。和顶点v 关联的边的数目定义为顶点的

在无向图G中,如果从顶点v到顶点v'有路径,则称v和v'是连通的。如果对于图中任意两个顶点vi、vj∈V,vi和vj都是连通的,则称G是 连通图(Connected Graph)。无向图中的极大连通子图称为 连通分量(Connected Component)。

在有向图G中,如果对于每一对vi, vj∈V, vi≠vj,从vi到vj和从vj到vi都存在路径,则称G是 强连通图。有向图中的极大强连通子图称作有向图的 强连通分量

一个连通图的 生成树是一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。

图的存储结构


数组(邻接矩阵)表示法






用两个数组分别存储数据元素(顶点)的信息和数据元素之间的关系(边或弧)的信息。其形式描述如下:

// - - - - - - 图的数组(邻接矩阵)存储表示 - - - - -

#define  MAX_VERTEX_NUM  20    //最大顶点个数

typedef enum {DG, DN, UDG, UDN} GraphKind;  // {有向图,有向网,无向图,无向网}

// 弧的定义
typedef struct ArcCell 
{     
     VRType  adj;           // VRType是顶点关系类型。
                                       // 对无权图,用1或0表示相邻否;
                                       // 对带权图,则为权值类型。

     InfoType  *info;       // 该弧相关信息的指针
} ArcCell,  AdjMatrix[MAX_VERTEX_NUM] [MAX_VERTEX_NUM];

// 图的定义
typedef struct 
{        
      VertexType   vexs[MAX_VERTEX_NUM];      // 顶点向量
      AdjMatrix    arcs;                                                 // 邻接矩阵                    
      int    vexnum, arcnum;                                       // 顶点数,弧数      
      GraphKind   kind;                                                // 图的种类标志             
} MGraph;


邻接表




邻接表(Adjacency List)是图的一种链式存储结构。在邻接表中对图中每个顶点建立一个单链表,每个链表上附设一个表头结点。
表结点:
其中 邻接点域(adjvex)指示与顶点vi邻接的点在图中的位置, 链域(nextarc)指示下一条边或弧的结点, 数据域(info)存储和边或弧相关的信息,如权值等。
头结点:
数据域(data)存储顶点vi的名或其他有关信息, 链域(firstarc)指向链表中第一个结点。

//- - - - 图的邻接表存储表示 - - - -

#define  MAX_VERTEX_NUM  20

// 表结点
typedef struct ArcNode
{
        int              adjvex;                      // 该弧所指向的顶点的位置
        struct ArcNode   *nextarc;      // 指向下一条弧的指针
        InfoType         *info;                    // 该弧相关信息的指针
}

// 头结点
typedef struct VNode
{
        VertexType       data;                // 顶点信息
        ArcNode          *firstarc;           // 指向第一条依附该顶点的弧的指针
}VNode, AdjList[MAX_VERTEX_NUM];

// 图的结构定义
typedef struct
{
        AdjList      vertices;
        int              vexnum, arcnum;        // 图的当前顶点数和弧数
        int              kind;                               // 图的种类标志
}ALGraph;

最小生成树

假设要在 n 个城市之间建立通讯联络网,则连通n个城市只需要修建n-1条线路,如何在最节省经费的前提下建立这个通讯网?
该问题等价于:构造网的一棵 最小生成树(Minimum Cost Spanning Tree),即在e条带权的边中选取n-1条边(不构成回路),使“权值之和”为最小。

构造最小生成树可以有多种算法。其中多数算法利用了最小生成树的下列一种简称为 MST的性质:假设N=(V, {E})是一个连通网,U是顶点集V的一个非空子集。若(u,v)是一条具有最小权值(代价)的边,其中u∈U, v∈V-U,则必存在一棵包含边(u,v)的最小生成树。

可以用 反证法证明之。假设网N的任何一棵最小生成树都不包含(u,v)。设T是连通网上的一棵最小生成树,当将边(u,v)加入到T中时,由生成树的定义,T中必存在一条包含(u,v)的回路。另一方面,由于T是生成树,则在T上必存在另一条边(u',v'),其中u'∈U, v'∈V-U,且u和u'之间,v和v'之间均有路径相通。删去边(u',v'),便可消除上述回路,同时得到另一棵生成树T‘。因为(u,v)的代价不高于(u',v'),则T'的代价亦不高于T,T'是包含(u,v)的一棵最小生成树。由此和假设矛盾。

普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法是两个利用MST性质构造最小生成树的算法。

普里姆算法

假设N=(V,{E})是连通网,TE是N上最小生成树中边的集合。算法从U={u 0} (u0∈V),TE={}开始,重复执行下述操作:在所有u∈U, v∈V-U的边(u,v)∈E中找到一条代价最小的边(u0,v0)并入集合TE,同时v0并入U,直至U=V为止。此时TE中必有n-1条边,则T=(V,{TE})为N的最小生成树。



为实现这个算法需附设一个 辅助数组closedge,以记录从U到V-U具有最小代价的边。对每个顶点vi∈V-U,在辅助数组中存在一个相应分量closedge[i-1],它包括两个域,其中 lowcost存储该边上的权。显然 closedge[i-1].lowcost = Min{ cost(u,vi)|u∈U}。 vex域存储该边依附的在U中的顶点

void MiniSpanTree_PRIM(MGraph G, VertexType u)
{
         // 用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。
         // 记录从顶点集U到V-U的代价最小的边的辅助数组定义:
         //   struct
         //   {
        //               VertexType     adjvex;
        //               VRType          lowcost;
        //     }closedge[MAX_VERTEX_NUM];
         k=LocateVex(G,u);
         for(j=0;j<G.vexnum;++j)      // 辅助数组初始化
         {
                       if(j!=k)
                          closedge[j]={u, G.arcs[k][j].adj};
         }
         closedge[k].lowcost=0;     // 初始,U={u}
         for(i=1;i<G.vexnum;++i)      // 选择其余G.vexnum-1个顶点
        {
                       k=minimum(closedge);
                       //  此时closedge[k].lowcost=MIN{ closedge[vi].lowcost | closedge[vi].lowcost>0, vi∈V-U}
                       printf(closedge[k].adjvex, G.vexs[k]);   //输出生成树的边
                       closedge[k].lowcost=0;                            //第k顶点并入U集
                       for(j=0;j<G.vexnum;++j)
                       {
                                if(G.arcs[k][j].adj<closedge[j].lowcost)            // 新顶点并入U后重新选择最小边
                                           closedge[j]={G.vex[k], G.arcs[k][j].adj};
                       }
          }
} // MiniSpanTree



分析算法,假设网中有n个顶点,则第一个进行初始化的循环语句的频度为n,第二个循环语句的频度为n-1。其中有两个内循环:其一是在closedge[v].lowcost中求最小值,其频度为n-1;其二是重新选择具有最小代价的边,其频度为n。 普利姆算法的时间复杂度为O(n^2),与网中的边数无关,因此适用于求边稠密的网的最小生成树。

克鲁斯卡尔算法

假设连通网N={V,[E]},则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量为止。



上述算法至多对e条边各扫描一次,以堆来存放网中的边,则每次选择最小代价的边仅需O(loge)的时间(第一次需O(e))。又生成树T的每个连通分量可以看成是一个等价类,则构造T加入新的边的过程类似于求等价类的过程,由此可以利用并查集来描述T,使构造T的过程仅需O(eloge)的时间, 由此克鲁斯卡尔算法时间复杂度为O(eloge)(e为网中边的数目),因此它相对于普里姆算法而言,适合于求边稀疏的网的最小生成树。

附:建堆的时间复杂度
建堆的时间复杂度为O(n)
证明如下:
构建堆从叶节点的父节点开始,以树高递减的方向逐层往上,一直到根。
假设堆中共有N个元素,则树高H=log2(N),从最底层开始,为从各节点建堆的复杂度求和:
S = 1 * 2^(H-1) + 2 * 2^(H-2) + ... + (H-1) * 2^1 + H * 2^0  
   = 2^H * ( 1*2^(-1) + 2*2^(-2) + 3*2^(-3) + ... + H*2(-H))
   = 2^(H+1) - 2 - H
将H=log2(N)代入,S = 2N - 2 - log2(N)

数列an=n*2^(-n)求和
Sn=1/2+2/2^2+3/2^3+…+n/2^n
2Sn=1+2/2+3/2^2+4/2^3+…+n/2^(n-1)
2Sn-Sn=1+1/2+1/2^2+1/2^3+…+1/2^(n-1)-n/2^n
Sn=1*(1-1/2^n)/(1-1/2)
=(2^(n+1)-n-2)/2^n



拓扑排序

检查有向图中是否存在回路的方法之一,是对有向图进行拓扑排序。
什么是拓扑排序(Topological Sort)?简单地说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。



拓扑排序的步骤:

(1)在有向图中选一个没有前驱的顶点且输出之。
(2)从图中删除该顶点和所有以它为尾的弧。
重复上述两步,直至全部顶点均已输出,或者当前图中不存在无前驱的顶点为止。后一种情况则说明有向图中存在环。


最短路径


从某个源点到其余各顶点的最短路径

给定带权有向图G和源点v,求从v到G中其余各顶点的最短路径。



如何求得这些路径? 迪杰斯特拉(Dijkstra)提出了一个按路径长度递增的次序产生最短路径的算法。

首先,引进一个辅助向量D,它的每个分量D[i]表示当前所找到的从始点v到每个终点vi的最短路径的长度。它的初态为:若从v到vi有弧,则D[i]为弧上的权值;否则置D[i]为∞。显然,长度为 D[j] = Min{ D[i] | vi∈V} 的路径就是从v出发的长度最短的一条最短路径。此路径为(v, vj)。

那么,下一条长度次短的最短路径是哪一条呢?假设该次短路径的终点是vk,则可想而知,这条路径或者是(v, vk),或者是(v, vj, vk)。它的长度或者是从v到vk的弧上的权值,或者是D[j]和从vj到vk的弧上的权值之和。

一般情况下,假设S为已求得最短路径的终点的集合,则可证明:下一条最短路径(设其终点为x)或者是弧(v, x),或者是中间只经过S中的顶点而最后到达顶点x的路径。这可用反证法来证明。假设此路径上有一个顶点不在S中,则说明存在一条终点不在S而长度比此路径短的路径。但是,这是不可能的。因为我们是按长度递增的次序来产生各最短路径的,故长度比此路径短的所有路径均已产生,它们的终点必定在S中,即假设不成立。

因此,在一般情况下,下一条长度次短的最短路径的长度必是 D[j] = Min{ D[i] | vi∈V-S} 其中,D[i]或者是弧(v, vi)上的权值,或者是D[k] (vk∈S)和弧(vk, vi)上的权值之和。



// - - - - - - - - - - - - - C语言描述的迪杰斯特拉算法 - - - - - - - - - - - - - - -  

void ShortestPath_DIJ(MGraph G, int v0, PathMatrix &P, ShortPathTable &D)
{
             // 用Dijkstra算法求有向网G的v0顶点到其余顶点v的最短路径P[v]及其带权长度D[v]。
             // 若P[v][w]为TRUE,则w是从v0到v当前求得最短路径上的顶点。
             // final[v]为TRUE当且仅当v∈S,即已经求得从v0到v的最短路径。
     
            for(v=0; v<G.vexnum; ++v)
            {
                         final[v]=FALSE;
                         D[v]=G.arcs[v0][v];
                         for(w=0; w<G.vexnum; ++w)
                                    P[v][w]= FALSE;               // 设空路径
                         if( D[v]< INFINITY)
                         {
                                    P[v][v0]=TRUE;
                                    P[v][v]=TRUE;
                         }
             } 

             D[v0]=0;
             final[v0]=TRUE;                                    // 初始化,v0顶点属于S集
             // 开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集
             for(i=1; i<G.vexnum; ++i)                    // 其余G.vexnum-1个顶点
             {
                         min = INFINITY;                         // 当前所知离v0顶点的最近距离
                         for(w=0; w<G.vexnum; ++w)
                         {
                                    if( !final[w])                       // w顶点在V-S中
                                    {
                                                if(D[w]<min)       // w顶点离v0顶点更近
                                                {
                                                          v=w;
                                                          min=D[w];
                                                }
                                    }
                         }
                         final[v] = TRUE;                         // 离v0顶点最近的v加入S集
                         for(w=0; w<G.vexnum; ++w)  // 更新当前最短路径及距离
                         {
                                    if(!final[w] && (min + G.arcs[v][w]<D[w]))            // 修改D[w]和P[w], w∈V-S
                                    {
                                                       D[w] = min + G.arcs[v][w];
                                                       P[w] = P[v];
                                                       P[w][w] = TRUE;          // P[w] =P[v] + P[w]
                                    } // if
                         } // for
             } // for
} // ShortestPath_DIJ

分析这个算法的运行时间。第一个FOR循环的时间复杂度是O(n),第二个FOR循环共进行n-1次,每次执行的时间是O(n)。所以总的 时间复杂度是O(n^2)。

每一对顶点之间的最短路径

解决这个问题的一个办法是:每次以一个顶点为源点,重复执行迪杰斯特拉算法n次。这样,便可求得每一对顶点之间的最短路径。总的执行时间为O(n^3)。



这里要介绍由 弗洛伊德(Floyd)提出的另一个算法。这个 算法的时间复杂度也是O(n^3),但形式上简单些。

弗洛伊德算法仍从图的带权邻接矩阵cost出发,其基本思想是:

假设求从顶点vi到vj的最短路径。如果从vi到vj有弧,则从vi到vj存在一条长度为 arcs[i][j]的路径,该路径不一定是最短路径,尚需进行n次试探。首先考虑路径(vi, v0, vj)是否存在(即判别弧(vi, v0)和(v0, vj)是否存在)。如果存在,则比较(vi, vj)和(vi, v0, vj)的路径长度取长度较短者为从vi到vj的中间顶点的序号不大于0的最短路径,那么(vi, ..., v1, ..., vj)就有可能是从vi到vj的中间顶点的序号不大于1的最短路径。将它和已经得到的从vi到vj中间定点序号不大于0的最短路径相比较,从中选出中间顶点的序号不大于1的最短路径之后,再增加一个顶点v2,继续进行试探。以此类推。在一般情况下,若(vi, ... , vk)和(vk, ..., vj)分别是从vi到vk和从vk到vj的中间顶点的序号不大于k-1的最短路径,则将(vi, ..., vk, ..., vj)和已经得到的从vi到vj且中间顶点序号不大于k-1的最短路径相比较,其长度较短者便是从vi到vj的中间顶点的序号不大于k的最短路径。这样,在经过n次比较后,最后求得的必是从vi到vj的最短路径。按此方法,可以同时求得各对顶点间的最短路径。



先定义一个n阶方阵序列 D^(-1), D^(0), D^(1), ..., D^(k), ... , D^(n-1),其中D^(-1)[i][j] = G.arcs[i][j]     D^(k)[i][j] = Min {D^(k-1)[i][j], D^(k-1)[i][k] + D^(k-1)[k][j]}  0<=k<=n-1
从上述计算公式可见,D^(1)[i][j]是从vi到vj的中间顶点的序号不大于1的最短路径的长度;D^(k)[i][j]是从vi到vj中间顶点的序号不大于k的最短路径的长度;D^(n-1)[i][j]就是从vi到vj的最短路径的长度。

// - - - - - - - - - - - - - - Floyd算法 - - - - - - - - - - - - - - - -

void ShortestPath_FLOYD( MGraph G, PathMatrix &P[], DistancMatrix &D)
{
                 // 用Floyd算法求有向网G中各对顶点v和w之间的最短路径P[v][w]及其
                 // 带权长度D[v][w]。若P[v][w][u]为TRUE,则u是从v到w当前求得最短路径上的顶点。
                 for(v=0; v<G.vexnum; ++v)       // 各对节点之间初始已知路径及距离
                 {
                                for(w=0; w<G.vexnum; ++w)
                                {
                                                D[v][w] = G.arcs[v][w];
                                                for(u=0; u<G.vexnum; ++u)
                                                             P[v][w][u] = FALSE;
                                                if(D[v][w]<INFINITY)       // 从v到w有直接路径
                                                {
                                                             P[v][w][v] = TRUE;
                                                             P[v][w][w] = TRUE;
                                                }
                                 }
                  }
                  for(u=0; u<G.vexnum; ++u)
                  {
                                for(v=0; v<G.vexnum; ++v)
                                {
                                                 for(w=0; w<G.vexnum; ++w)
                                                 {
                                                             if( D[v][u] + D[u][w] < D[v][w])   // 从v经u到w的一条路径更短
                                                             {
                                                                          D[v][w] = D[v][u] + D[u][w];
                                                                          for( i=0; i<G.vexnum; ++i)
                                                                                      P[v][w][i] = P[v][u][i] || P[u][w][i];
                                                             }
                                                  }
                                 }
                   } 
}// ShortestPath_FLOYD


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值