数据结构第七章 图

图的基本概念

图的定义

图G由顶点集V和边集E组成,记为G=(V,E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间关系(边)的集合。

注意:线性表可以是空表,树可以是空树,但图不可以是空图。图不能一个顶点都没有,图的顶点集一定不空,但是图的边集可以为空,即图可以只有顶点而没有边。

1)有向图

若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v,w>,其中v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧。

2)无向图

若E是无向边的有限集合时,则图G为无向图。边是顶点的无序对,记为(v,w)或(w,v),其中w,v是顶点。

3)简单图

一个图G若满足:1.不存在重复边;2.不存在顶点到自身的边,则称图G为简单图。

4)多重图

若图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边与自己关联,则G为多重图。

5)完全图(也称简单完全图)

对于无向图,|E|的取值范围是0到n(n-1)/2,有n(n-1)/2条边的无向图称为完全图,即在无向完全图中任意两个顶点之间都存在边。对于有向图,|E|的取值范围是0到n(n-1),有n(n-1)条弧的有向图称为有向完全图,即在有向完全图中任意两个顶点之间都存在方向相反的两条弧。

6)子图

设有两个图G=(v,E)和G’=(v’,E’)若v’是v的子集,且E’是E的子集,则称G’是G的子图。若有满足V(G’)=V(G)的子图G’,则称其为G的生成子图。

7)连通、连通图和连通分量

在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G中任意两个顶点都是连通的,则称图G为连通图,无向图的极大连通子图称为连通分量。

8)强连通图、强连通分量

在有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的。若图中任何一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量。

9)生成树、生成森林

连通图的生成树是包含图中全部顶点的极小连通子图。若图中顶点数为n,则它的生成树含有n-1条边。

10)顶点的度、入度和出度

图中每个顶点的度定义为以该顶点为一个端点的边的数目。

对于无向图,顶点v的度是指依附于该顶点的边的条数,记为TD(v);在具有n个顶点,e条边的无向图中,
∑ i = 1 n T D ( v i ) = 2 e \sum _{i=1}^n TD(v_i) = 2e i=1nTD(vi)=2e
即无向图的全部顶点的度的和等于边数的2倍。

对于有向图,顶点v的度分为入度和出度,入度是以顶点v为终点的有向边的数目,记为ID(v);而出度是以顶点v为起点的有向边的数目,记为OD(v)。顶点v的度等于其入度与出度之和,即TD(v)=OD(v)+ID(v)。在具有n个顶点,e条边的有向图中
∑ i = 1 n I D ( v i ) = ∑ i = 1 n O D ( v i ) = e \sum _{i=1}^n ID(v_i)=\sum _{i=1}^n OD(v_i)=e i=1nID(vi)=i=1nOD(vi)=e
即有向图的全部顶点的入度之和与出度之和相等,并且等于边数。

11)边的权和网

在一个图中,每一条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称网。

12)稠密图、稀疏图

边数很少的图称为稀疏图,反之称为稠密图。一般当图G满足|E|<|V| log |V|时,可以将G视为稀疏图

13)路径、路径长度和回路

顶点v_1到v_n之间的一条路径是指顶点序列v_1、v_2、v_3…v_n,路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。若一个图有n个顶点,并且有大于n-1条边,则该图一定有环。

14)简单路径、简单回路

在路径序列中,顶点不重复出现的路径称为简单路径;除第一个顶点和最后普一个顶点外,其余顶点不重复出现的回路称为简单回路

15)距离

从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v 的距离

16)有向树

一个顶点的入度为0,其余顶点的入度均为1的有向图,称为有向树。

常见考点:

对于n个顶点的无向图G,

  • 所有顶点度数之和为2|E|

  • 若G是连通图,则最少有n-1条边(树),若|E|>n-1,则一定有回路

  • 若G是非连通图,则边最多可能有
    C n − 1 2 C_{n-1}^2 Cn12

  • 无向完全图共有边
    C n 2 C_{n}^2 Cn2

对于n个顶点的有向图G,

  • 所有顶点的出度之和=所有顶点的入度之和=|E|
  • 所有顶点的度之和=2|E|
  • 若G是强连通图,则最少有n条边
  • 有向完全图共有边

2 C n 2 2C_{n}^2 2Cn2

图的存储及基本操作

1.邻接矩阵法

所谓邻接矩阵存储,是指用一个一维数组存储图中顶点的信息,用一个二维数组存储边的信息,存储顶点之间邻接关系的二维数组称为邻接矩阵。

结点数为n的图G=(V,E)的邻接矩阵A是n*n的,将G的顶点编号为v_1,v_2,…,v_n。
A [ i ] [ j ] = { 1 , 若 ( v i , v j ) 或 < v i , v j > 是 E ( G ) 中 的 边 0 , 若 ( v i , v j ) 或 < v i , v j > 不 是 E ( G ) 中 的 边 A[i][j]=\left\{ \begin{array}{lr} 1, &若(v_i,v_j)或<v_i,v_j>是E(G)中的边\\ 0, &若(v_i,v_j)或<v_i,v_j>不是E(G)中的边 \end{array} \right. A[i][j]={1,0,(vi,vj)<vi,vj>E(G)(vi,vj)<vi,vj>E(G)
对于带权图而言,若顶点v_i和v_j之间有边相连,则邻接矩阵中对应项存放该边对应的权值,若顶点v_i与v_j不相连,则用无穷来代表这两条边之间不存在边
A [ i ] [ j ] = { w i j , 若 ( v i , v j ) 或 < v i , v j > 是 E ( G ) 中 的 边 ∞ , 若 ( v i , v j ) 或 < v i , v j > 不 是 E ( G ) 中 的 边 A[i][j]=\left\{ \begin{array}{lr} w_{ij}, &若(v_i,v_j)或<v_i,v_j>是E(G)中的边\\ \infty, &若(v_i,v_j)或<v_i,v_j>不是E(G)中的边 \end{array} \right. A[i][j]={wij,,(vi,vj)<vi,vj>E(G)(vi,vj)<vi,vj>E(G)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OMpz9QtU-1598624101080)(http://myimg.zhengjc.cn/images/2020/08/26/Inked1-12022322341C59_LI.jpg)]

图的邻接矩阵存储结构定义如下:

#define MaxVertexNum 100  //顶点数目的最大值
typedef char VertexType;  //顶点的数据类型
typedef int EdgeType;   //带权图中边上权值的数据类型
typedef struct{
    VertexType Vex[MaxVertexNum];   //顶点表
    EdgeType Edge[MaxVertexNum][MaxVertexNum];   //邻接矩阵,边表
    int vexnum,arcnum;   //图的当前顶点数和弧数
}MGraph;

图的邻接矩阵存储表示法具有以下特点

  • 无向图的邻接矩阵一定是一个对称矩阵(并且唯一)。因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素
  • 对于无向图,邻接矩阵的第i行(或第i列)非零元素的个数正好是第i个顶点的度
  • 对于无向图,邻接矩阵的第i行(或第i列)非零元素的个数正好是第i个顶点的出度[或入度]
  • 用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连
  • 稠密图适合用邻接矩阵的存储表示
  • 设图G的邻接矩阵为A,An的元素An [i] [j]等于由顶点i到顶点j的长度为n的路径的数目。

2.邻接表法

所谓邻接表,是指对图G中的每个顶点v_i建立一个单链表,第i个单链表中的结点表示依附于顶点v_i的边(对于有向图则是以顶点v_i为尾的弧),这个单链表就称为顶点v_i的边表。边表的头指针和顶点的数据信息采用顺序存储,所以在邻接表中存在两种结点:顶点表结点和边表结点。

查看源图像

顶点表结点由顶点域(data)和指向第一条邻接边的指针(firstarc)构成,边表(邻接表)结点由邻接点域(adjvex)和指向下一条邻接边的指针域(nextarc)构成。

图的邻接表存储结构定义如下:

#define MaxVertexNum 100
typedef struct ArcNode{  //边表结点
    int adjvex;  //该弧所指向的顶点的位置
    struct ArcNode *next;   //指向下一条弧的指针
}ArcNode;
typedef struct VNode{   //顶点表结点
    VertexType data;   //顶点信息
    ArcNode *first;   //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{   //邻接表
    AdjList vertices;  //图的顶点数和弧数
    int vexnum,arcnum;  //ALGraph是以邻接表存储的图类型
}ALGraph;

图的邻接表存储方法具有以下特点:

  • 若G为无向图,则所需的存储空间为O(|V|+2|E|);若G为有向图,则所需的存储空间为O(|V|+|E|)。
  • 对于稀疏图,采用邻接表表示将极大地节省存储空间
  • 在邻接表中,给定一顶点,很容易地找出它所有邻边
  • 在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数,但求其顶点的入度则需要遍历全部的邻接表
  • 图的邻接表表示不唯一

3.十字链表

十字链表是有向图的一种链式存储结构,在十字链表中,对应于有向图的每条弧有一个结点,对应于每一个顶点也有一个结点。查看源图像

图的十字链表表示不是唯一的,但一个十字链表表示确定一个图

4.邻接多重表

邻接多重表是无向图的另一种链式存储结构。

查看源图像

5.图的基本操作

图的基本操作是独立于图的存储结构的。图的基本操作主要包括:

  • Adjacent(G,x,y):判断图G是否存在边<x,y>或(x,y)
  • Neighbors(G,x):列出图G中与结点x邻接的边
  • InsertVertex(G,x):在图G插入顶点x
  • DeleteVertex(G,x):在图G删除顶点x
  • AddEdge(G,x,y):若无向边(x,y)或有向边<x,y>不存在,则向图G中添加该边
  • RemoveEdge(G,x,y):若无向边(x,y)或有向边<x,y>存在,则向图G中删除该边
  • FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1
  • NexNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y外顶点x的下一个邻接点的顶点号。若y是x的最后一个邻接点,则返回-1
  • Get_edge_value(G,x,y):获取图G中边(x,y)或<x,y>对应的权值
  • Set_edge_value(G,x,y,v):设置图G中边(x,y)或<x,y>对应的权值为v

图的遍历

广度优先搜索

广度优先搜索类似于二叉树的层次遍历算法。

广度优先算法的伪代码如下:

bool visited[MAX_VERTEX_NUM];  //访问标记数组
void BFSTraverse(Graph G){  //广度优先遍历
    for(i=0;i<G.vexnum;++i)
        visited[i]=FALSE;   //访问标记数组初始化
    InitQueue(Q);  //初始化辅助队列Q
    for(i=0;i<G.vexnum;++i)  //从0号顶点开始遍历
        if(!visited[i])  //对每个连通分量调用一次BFS
            BSF(G,i);   // vi未访问过,从vi开始BFS
}
void BFS(Graph G,int v){  // 从顶点v出发,广度优先遍历图G
    visit(v);  //访问初始顶点v
    visited[v]=TRUE;  //对v做已访问标记
    Enqueue(Q,v);  //顶点v入队列Q
    while(!isEmpty(Q)){
        DeQueue(Q,v);   //顶点v出队列
        for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))  //检测v所有邻接点
            if(!visited[w]){  //w为v的尚未访问的邻接顶点
                visit(w);  //访问结点w
                visited[w]=TRUE;  //对w做已访问标记
                EnQueue(Q,w);  //顶点w入队列
            }
    }
}

BFS算法的性能分析

在最坏情况下,空间复杂度为O(|V|),采用邻接表存储方式时,每个顶点均需要搜索一次,故时间复杂度为O(V),在搜索任一顶点的邻接点时,每条边至少访问一次,故时间复杂度为O(|E|),算法总的时间复杂度为O(|V|+|E|)。采用邻接矩阵存储方式时,查找每个顶点的邻接点需要的时间为O(|V|),算法总的时间复杂度为O(|V|^2)。

广度优先生成树:在广度遍历的过程中,我们可以得到一棵遍历树称为广度优先生成树。一给定图的邻接矩阵存储表示是唯一的,故其广度优先生成树也是唯一的,但由于图的邻接表存储是不唯一的,故其广度优先生成树也不唯一。

深度优先搜素

深度优先搜索类似于树的先序遍历,它的基本思想如下:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一结点w1,再访问与w1邻接且未被访问的任一顶点w2……重复以上过程,当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。

深度优先算法的伪代码如下:

bool visited[MAX_VERTEX_NUM];  //访问标记数组
void DFSTraverse(Graph G){  //深度优先遍历
    for(v=0;v<G.vexnum;++v)
        visited[v]=FALSE;   //访问标记数组初始化
    for(v=0;v<G.vexnum;++v)  //从0号顶点开始遍历
        if(!visited[v])  //对每个连通分量调用一次BFS
            DSF(G,v);   // vi未访问过,从vi开始BFS
}
void DFS(Graph G,int v){  // 从顶点v出发,深度优先遍历图G
    visit(v);  //访问初始顶点v
    visited[v]=TRUE;  //对v做已访问标记
    for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))  //检测v所有邻接点
        if(!visited[w]){  //w为v的尚未访问的邻接顶点
            DFS(G,w)
        }
}

DFS算法的性能分析

深度优先遍历(DFS)是一个递归算法,需要借助一个递归工作栈,其空间复杂度为O(|V|),采用邻接表存储方式时,每个顶点均需要搜索一次,故时间复杂度为O(V),在搜索任一顶点的邻接点时,每条边至少访问一次,故时间复杂度为O(|E|),算法总的时间复杂度为O(|V|+|E|)。采用邻接矩阵存储方式时,查找每个顶点的邻接点需要的时间为O(|V|),算法总的时间复杂度为O(|V|^2)。

深度优先生成树和生成森林:与广度优先搜索一样,深度优先搜索也会产生一棵深度优先生成树。但其生成需要条件,即对连通图调用DFS才能产生深度优先生成树,否则产生的将是深度优先生成森林。与BFS类似,基于邻接表存储的深度优先生成树是不唯一的。

图的应用

1.最小生成树

对于一个带权连通无向图G=(V,E),生成树不同,每棵树的权也可能不同。设R为G的所有生成树的集合,若T为R中边的权值最小的那棵生成树,则T称为G的最小生成树。

最小生成树有如下性质

  • 最小生成树不唯一,即最小生成树的树形不唯一。当G本身是一棵树时,则G的最小生成树是它本身
  • 最小生成树的边的权值之和总是唯一的。
  • 最小生成树的边数为顶点数减1

构造最小生成树的算法有很多,下面是两种比较通用的算法:

1)Prim算法

算法思想:初始时从图中任取一顶点加入树T,此时树中只含有一个顶点,之后选择一个与当前T中顶点集合距离最近的顶点,并将该顶点和相对应的边加入T,每次操作后T中的顶点数和边数都增加1.依此类推,直至图中所有顶点都并入T,的到的T就是最小生成树。此时T中必然有n-1条边。需要注意的是,如果所选取的边在T中会使得T构成回路,则应该丢弃这条边另选其他边。

Prim算法的简单实现如下

void Prim(G,T){
    T=;  //初始化空树
    U={w}; //添加任一顶点w
    while((V-U)!=){  //若树中不含全部顶点
        //设{u,v}是使u∈U与v∈(V-U),且权值最小的边;
        T=T ∪ {(u,v)};  //边归入树
        U=U ∪ {v};  //顶点归入树
    }
}

Prim算法的时间复杂度为O(|V|^2),不依赖与|E|,因此它适合于求解边稠密的图的最小生成树。

2)Kruskal算法

Kruskal算法是一种按权值递增次序选择合适的边来构造最小生成树的办法。

算法思想:初始时为只有n个顶点而无边的非连通图T={V,{}},每个顶点自成一个连通分量,然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入T,否则舍弃此边而选择下一条权值最小的边。(其实也就是选取的边不能是能构成回路的边)依次类推,直至T中所有顶点都在一个连通分量上。

Kruskal算法的简单实现如下

void Kruskal(V,T){
    T=V;  //初始化树T,仅含顶点
    numS=n;  //连通分量数
    while(numS>1){  //若连通分量数大于1
        从E中取出权值最小的边(v,u);
        if(v和u属于T中不同的连通分量){
            T=T∪{(u,v)};  //将此边加入生成树中
            numS--;   //连通分量数-1
        }
    }
}

Kruskal算法的时间复杂度为O(|E|log|E|),因此它适合于边稀疏而顶点较多的树。

2.最短路径

当图是带权图时,把从一个顶点v0到图中其余任意一个顶点vi的路径所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径长度最短的那条路径称为最短路径。

1)Dijkstra算法求单源最短路径问题

Dijkstra算法设置了一个集合S记录已求得的最短路径的顶点,初始时把源点v0放入S,集合S每并入一个新顶点vi,都要修改源点v0到集合V-S中顶点当前的最短路径长度值。

在构造过程中还设置了两个辅助数组:

  • dist[]:记录从源点v0到其他各顶点当前的最短路径长度,它的初始状态为:若从v0到vi有弧,则dist[i]为弧上的权值;否则置dist[i]为∞。

  • path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点。

使用邻接矩阵表示时,时间复杂度为O(|V|2),使用带权的邻接表表示时,时间复杂度仍为O(|V|2)。

需要注意的是,边上带有负权值时,Dijkstra算法并不适用

2)Floyd算法求各顶点之间最短路径问题

Floyd算法的基本思想是:递推产生一个n阶方阵序列A(-1),A(0),…A(k),…A(n-1),其中A^(k)[i] [j]表示从顶点vi到顶点vj的路径长度,k表示绕行第k个顶点的运算步骤。

Floyd算法是一个迭代过程,每迭代一次,在从vi到vj的最短路径上就多考虑了一个顶点;经过n次迭代后,所得到的A^(n-1)中就保存了任意一对顶点之间的最短路径长度。

查看源图像

Floyd算法的时间复杂度为O(|V|^2)。Floyd算法允许图中带有负权值的边,但不允许有包含带负权值的边组成的回路。Floyd算法同样适用于带权无向图

3.有向无环图描述表达式

有向无环图:若有一个有向图中不存在环,则称为有向无环图,简称DAG图。

查看源图像

4.拓扑排序

AOV网:若用DAG图表示一个工程,其顶点表示活动,用有向边<vi,vj>表示活动vi必须先于活动vj进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,记为AOV网。

拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:

​ ① 每个顶点出现且只出现一次

​ ② 若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径。

对一个AOV网进行拓扑排序的一种常用方法的步骤为:

  1. 从AOV网中选择一个没有前驱的顶点并输出
  2. 从网中删除该顶点和所有以它为起点的有向边
  3. 重复1和2直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环

拓扑排序算法的实现如下:

bool TopologicalSort(Graph G){
    InitStack(S);   //初始化栈,存储入度为0的顶点
    for(int i=0;i<G.vexnum;i++)
        if(indegree[i]=0)
            Push(S,i);  //将所有入度为0的顶点进栈
    int count=0;       //计数,记录当前已经输出的顶点数
    while(!IsEmpty(S)){  //栈不空,则存在入度为0的顶点
        Pop(S,i);       //栈顶元素出栈
        print[count++]=i;  //输出顶点i
        for(p=G.vertices[i].firstarc;p;p->nextarc){
            //将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈S
            v=p->adjvex;
            if(!(--indegree[v]))
                Push(S,v);    //入度为0,则入栈
        }
    }
    if(count<G.vexnum)
        return false;   //排序失败,有向图中有回路
    else
        return true;   //拓扑排序成功
}

由于输出每个顶点的同时还要删除以它为起点的边,故拓扑排序的时间复杂度为O(|V|+|E|)。

5.关键路径

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销,称之为用边表示活动的网络,简称AOE网。AOV网和AOE网都是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的,AOE网的边有权值,而AOV网中的边无权值,仅表示顶点之间的前后关系。

AOE网具有以下两个性质

​ ①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始

​ ②只有在进入某顶点的各有向边所代表的活动都已结束时,该结点代表的事件才能发生

在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点)它表示整个工程的开始;网中也仅存在一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。

从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动

在寻找关键活动时所需要用到的几个参量定义:

  • 从开始顶点 v1 出发,令 ve(1)=0,按拓扑有序序列求其余各顶点的可能最早发生时间。Ve(k)=max{ve(j)+dut(<j,k>)} , j ∈ T 。其中T是以顶点vk为尾的所有弧的头顶点的集合(2 ≤ k ≤ n)。如果得到的拓朴有序序列中顶点的个数小于网中顶点个数n,则说明网中有环,不能求出关键路径,算法结束。
  • 从完成顶点出发,令vl(汇点)=ve(汇点),按逆拓扑有序求其余各顶点的允许的最晚发生时间:
    vl(j)=min{vl(k)-dut(<j,k>)} ,k ∈ S 。其中 S 是以顶点vj是头的所有弧的尾顶点集合(1 ≤ j ≤ n-1)。
  • 求每一项活动ai(1 ≤ i ≤ m)的最早开始时间e(i)=ve(j),最晚开始时间l(i)=vl(k)-dut(<j,k>) 。
    若某条弧满足 e(i)=l(i) ,则它是关键活动。


    对于关键路径,需要注意以下几点:
  1. 关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可通过加快关键活动来缩短整个工程的工期。但不能任意缩短,因为一旦缩短到一定程度,该关键活动就可能会变成非关键活动
  2. 网中的关键活动并不唯一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值