数据结构——图

一 图的基本概念

1 定义

图G由顶点集V和边集E组成,记为G=(V,E);|V|表示顶点个数,|E|表示边的条数

线性表可以是空表,树可以是空树,但是图不能是空图

(1)有向图

在这里插入图片描述

从顶点1到顶点2,1称为弧尾,2称为弧头

(2)无向图

没有方向的边

在这里插入图片描述

(3)简单图,多重图

简单图:不存在重复的边,不存在顶点到自身的边,如上面两图

多重图:某两个顶点直接的边数大于1条,又允许顶点通过一条边和自身相连

(4)完全图(简单完全图)

对于无向图,有n*(n-1)/2条边的无向图称为完全图,其任意两个顶点之间都存在边

对于有向图,有==n(n-1)==条弧的有向图称为有向完全图,其任意两个顶点之间都存在方向相反的两条弧

在这里插入图片描述

(5)子图

如有两个图G,G`,V’是V的子集,E‘是E的子集,则G’是G的子图

若V(G’)=V(G)的子图G’,则称其为G的生成子图

但不是任意子集都能构成子图,有些可能甚至不是图

(6)连通,连通图和连通分量

连通:无向图中,顶点v到顶点w有路径存在,则v和w连通的

连通图:图G中任意两个顶点都是连通的,否则称为非连通图

无向图中的极大连通子图称为连通分量

如果一个图有n个顶点,边数小于n-1,则必是非连通图;同理,如果图是非联通图,那么最多有n-2条边

在这里插入图片描述

(7)强连通图,强连通分量

强连通:有向图中,一对顶点v和w,从v到w和从w到v都有路径,称他们为强连通

强连通图:图中任何一对顶点都是强连通的

有向图中的极大强连通子图称为有向图的强连通分量

一个n个顶点的强连通图最少有n+1条边

在这里插入图片描述

(8)生成树,生成森林

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

去掉一个边就是非连通图,加一个边就形成回路

非连通图中,连通分量的生成树构成了非连通图的生成森林

在这里插入图片描述

极大连通子图:无向图的连通分量,要求该连通子图包含所有的边

极小连通子图:既要保持图连通,又要使得边数最少的子图

(9)顶点的度,入度和出度

无向图中,顶点v的度是指依附于v的边的条数,记为TD(v),每个顶点都要算一次,n条边就2n的度

有向图中,入度是以顶点v为终点的有向边的数目,记为ID(v),出度是以顶点v为起点的有向边的树,记为OD(v),度就是两者之和,且整个图的ID=OD=n(图的边数)

(10)边的权和网

每条边标上具有某种含义的数值称为该边的权值,这种带有权值的图称为带权图也称网

(11)稠密图,稀疏图

边数多就是稠密,少就是稀疏,是相对概念

(12)路径,路径长度和回路

顶点v到w经过的顶点序列就是路径,路径上的边就是路径长度,第一个顶点和最后一个顶点相同的路径成为回路或环

n个顶点,大于n-1条边的图一定有环

(13)简单路径,简单回路

简单路径:路径序列中顶点不重复出现

简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路

(14)距离

距离:顶点v到w的最短路径长度,不存在就标记为无穷

(15)有向树

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

二 图的存储及基本操作

1 邻接矩阵法

指用一个一维数组存储图顶点信息,用一个二维数组存储图中边的信息

邻接矩阵:存储顶点直接邻接关系的二维数组

#define maxvertexnum 100         \\顶点数目
typedef char vertextype;         \\顶点的数据类型
typedef int edgetype;            \\带权图中边上权值的数据类型
typedef struct
{
    vertextype Vex[maxvertexnum];               \\顶点表
    edgetype Edge[maxvertexnum][maxvertexnum];  \\邻接矩阵,边表
    int vexunm,arcnum;                          \\当前顶点数和弧数
}

A[i][j]=10  表示从顶点i到j存在边就是1,否则是0
对带权图,A[i][j]=w 表示从i到j的权值是w,如不存在边就是0或者无穷

在这里插入图片描述

无向图的邻接矩阵是对称矩阵,并且唯一,可采用压缩存储;第i行非零元素的个数正好是顶点i的度

对有向图的第i行非零是i的出度,第i列非零是入度

邻接矩阵表示法的空间复杂度为O(n2),n是顶点数

要确定边数需要按行和列进行检测,时间慢,适合稠密图

邻接矩阵A,An的元素An[i][j]等于从i到j的长度为n的路径数目

2 邻接表法

对图的每个顶点vi建立一个单链表,第i个单链表中的结点表示依附于vi的边(对于有向图就是以vI为尾的弧),称为边表(对有向图就是出边表)

边表的头指针和顶点的数据信息采用顺序存储(顶点表),所以有顶点表结点和边表结点

在这里插入图片描述

#define maxvertexnum 100        \\顶点数目

typedef struct Arcnode          \\边表结点
{
    int adjvex;                 \\该弧指向顶点的位置
    struct Arcnode *next;       \\指向下一条弧的位置
    \\Infotype info;            \\边权值
}Arcnode;

typedef struct Vnode             \\顶点表结点
{
    Vertextype data;             \\顶点信息
    Arcnode *first;              \\指向第一条依附该顶点的弧的指针
}Vnode,Adjlist[maxvertexnum];

typedef struct
{
    Adjlist vertices;            \\邻接表
    int vexnunm,arcnum;          \\图的顶点数和弧数
}Algraph;

在这里插入图片描述

(1)无向图的存储空间为O(|v|+|2e|),有向图存储空间为O(|V|+|E|)

(2)适合稀疏图,极大的节省空间

(3)若要确定两个顶点间是否存在边,邻接矩阵可以直接查到,邻接表需要在相应的结点对应的边表中查找另一结点,效率较低

(4)有向图邻接表,顶点的出度只需看邻接表的结点个数,但是入度要查整个表

(5)链表次序不唯一

3 十字链表

十字链表中,对应有向图中的每条弧有一个结点,对应每个顶点也有一个结点

在这里插入图片描述

tailvex:指示弧尾顶点的编号

headvex:指示弧头顶点的编号

hlink:指向弧头相同的下一个弧结点

tlink:指向弧尾相同的下一个弧结点

info:存放弧的相关信息

弧头相同的就在同一个链表,弧尾相同的在同一个链表

data:顶点的数据信息

firstin:指向以该顶点为弧头的第一个弧结点

firstout:指向以该结点为弧尾的第一个弧结点

顶点结点之间顺序存储,很容易找到头和尾,即出入度

在这里插入图片描述

4 邻接多重表

无向图的另一种链式存储,每条边都用一个结点表示

在这里插入图片描述

ivex和jvex指示该边依附的两个顶点的编号;ilink指向下一条依附于顶点ivex的边,jlink指向下一条依附于顶点jvex的边;info存放边的相关信息

在这里插入图片描述

data:顶点信息

firstedge:指向第一条依附于该顶点的边

所有依附于同一顶点的边串联在同一链表中,每个边结点同时链接在两个链表中

在这里插入图片描述

5 图的基本操作

仅抽象考虑

Adjacent(G,x,y); \\判断G是否存在边<x,y>
Neighbors(G,x);  \\列出图G中与结点x邻接的边
Insertvertex(G,x);\\插入顶点x
Deletevertex(G,x);\\删除x
Addedge(G,x,y);   \\若边<x,y>不存在,则添加
Removeedge(G,x,y);\\若边<x,y>存在,则删除
Firstneighbor(G,x);\\求顶点x的第一个邻接点,有返回顶点号,没有或不存在x返回-1
Nextneighbor(G,x,y);\\y是x的一个邻接点,返回除y的下一个邻接点,没有返回-1
Get_edge_value(G,x,y);\\获取权值
Set_dege_value(G,x,y,v);\\设置边的权值为v                

三 图的遍历

1 广度优先搜索BFS

基本思想:访问起始结点v,从v出发,依次访问v的各个未访问过的邻接结点w1….wn,然后再依次访问w1….wn的所有未被访问过的邻接结点,然后再循环从结点出发,访问未被访问过的邻接结点,直到全部访问完毕

若还有没被访问的,重新选一个起始结点重复上述过程,直到全部访问完毕

dijkstra算法和prim最小生成树算法应用了这种思想

是一种分层的查找过程,类似树的层序遍历,需要借助队列,伪代码如下:

bool visited[maxvertexnum];              \\标记数组
void BFStraverse(Graph G)
{
    for(int i =0;i<G.vexnum;i++)         \\初始化标记数组
    {
        visited[i]=false;
    }
    Initqueue(Q);
    for(int i =0;i<G.vexnum;i++)        \\从0号顶点开始遍历
    {
        if(!visited[i])
        {
           BFS(G,i);                    \\每个连通分量一次BFS,没被访问过就BFS
        }
    }
}

void BFS(Graph G,int v)                   \\从v出发,遍历G
{ 
    visit(v);                             \\访问v
    visited[v]=true;                      \\置为true防止多次访问
    Enqueue(Q,v);                          \\v入队列
    while(!isempty(Q))                   
    {
        Dequeue(Q,v);                     \\v出队列
        for(w=Firstneighbor(G,v);w>=0; w=Nextneighbor(G,v,w))   \\检测v的所有邻接接点
        {
            if(!vitited[w])                   \\w为v未访问过的邻接接点
            {
                visit(w);       
                visited[w]=true;
                Enqueue(Q,w);                \\访问w后,w再入队列
            }
        }
    }
}

图的广度优先搜索和二叉树的层序遍历基本一致

BFS的算法性能分析

空间复杂度为O(|V|)

时间复杂度:(1)邻接表:O(|V|+|E|)

(2)邻接矩阵:O(|V|2)

BFS算法求解单源最短路径的问题

非带权图的最短路径d(u,v):从u到v的任何路径中最少的边数,没有则置为无穷

void BFSmindistance(Graph G,int u)
{
    \\d[i]表示u到i的最短路径
        for(int i =0;i<G.vextun;i++)
        {
            d[i]=;
        }
    visited[u]=true;d[u]=0;
    Enqueue(Q,u);
    while(!isempty(Q))
    {
        Dequeue(Q,u);                        \\队头元素u出队
        for(w =Firstneighbor(G,u); w>=0; w=Nextnrighbor(G,u,w))
        {
            if(!visited[w])                    \\w为u未访问的邻接结点
            {
                visited[w]=true;
                d[w]=d[u]+1;                  \\路径长度加1
                Enqueue(Q,w);
            }
        }
    }
}

广度优先生成树

遍历过程中可以得到一颗遍历树

邻接矩阵的树唯一,但是邻接表的不唯一

在这里插入图片描述

2 深度优先搜索DFS

类似树的先序遍历,尽可能“深”的搜索一个树

基本思想:访问起始顶点v,从v出发,访问与c邻接且未被访问的任意一个顶点w1,再访问与w1邻接且未被访问的任意一个顶点w2,重复这个过程,不能向下访问的时候,依次退回最近被访问的顶点, 若它还有未被访问的邻接顶点,则再从这个顶点开始搜索,直到图访问完毕

递归算法:

bool visited[maxvertexnum];
void DFStraverse(Graph G)
{
    for(int v=0 ;v<G.vextum;v++)
    {
        visited[v]=false;
    }
    for(int v = 0;v<G.vextum;v++)
    {
        if(!visited[v])
        {
            DFS(G,v);
        }
    }
}

void DFS(Graph G,int v)
{
    visit(v);
    visited[v]=true;
    for(w=Firstneighbor(G,v); w>=0; w=Nextneighbor(G,v,w))
    {
        if(!visited[w])
        {
            DFS(G,w);
        }
    }
}

DFS遍历的序列,邻接矩阵唯一,邻接表不唯一

DFS算法的性能分析

需要一个递归工作栈,空间复杂度为O(|V|)

时间复杂度:(1)邻接矩阵:O(|v|2)

(2)邻接表:O(|V|+|E|)

深度优先的生成树和生成森林

连通图调用DFS才能产生深度优先生成树,否则产生的是深度优先生成森林

3 图的遍历与连通性

无向图调用DFS,BFS的次数等于图的连通分量数

连通有向图分为强连通个非强连通,连通子图也分为强连通分量和非强连通分量

非强联分量一次调用BFS(G,i)或DFS无法访问到该连通分量的所有顶点

四 图的应用

1 最小生成树

一个带权连通无向图G,生成树不同,每棵树的权也可能不同,若T为权值之和最小的那颗生成树,则T为G的最小生成树MST

性质:

(1)最小生成树不唯一

(2)最小生成树的边的权值之和唯一

(3)最小生成树的边数为顶点数减1

根据性质从最小权值的边开始,基于贪心算法的有Prim和Kruskal算法

1.1 Prim(普里姆)算法

基本思想:任取一个顶点,找权值最小边并且不是重复顶点的边加入,直到所有顶点加入

n个顶点,T必有n-1条边

在这里插入图片描述

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};                    \\顶点归入树
    }
}

时间复杂度:O(|V|2)

1.2 Kruskal(克鲁斯卡尔)算法

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

在这里插入图片描述

void Kruskal(G,T)
{
    T=V;                           \\初始化T,仅含顶点
    numS=n;                        \\连通分量数,初始就是顶点总数
    while(numS>1)
    {
        从E中取权值最小的边(v,u)
        if((v,u)属于不同连通分量)
        {
             T=T∪{(u,v)};                \\边归入树
             numS--; 
        }
    }
}

时间复杂度:O(log2|E|)

2 最短路径

一个顶点到另一个顶点的路径的权值之和最短称为最短路径;两点直接的最短路径也包含了路径上其他顶点间的最短路径

单源最短路径:求图中某一顶点到其他各顶点的最短路径,可通过Dijkstra算法

每对顶点间的最短路径:Floyd算法求解

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

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

设置两个辅助数组:

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

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

初始S只包含顶点0;邻接矩阵arcs表示带权有向图,arcs[i] [j]表示边<i,j>的权值,不存在就置为无穷

算法步骤:

(1)初始化:S初始为{0},dist[i]=arcs[0] [i],i=1,2,3,……n-1

(2)从顶点集合V-S中选出vj,满足dist[j]=Min{dist[i]|vi ∈ \in V-S},vj就是当前求得的一条从v0出发的最短路径的终点,S=S ⋃ \bigcup {j}

(3) 修改从v0出发到集合V-S上任意一个顶点Vk可达的最短路径长度,若dist[j]+arcs[j] [k]<dist[k],则更新dist[k]=dist[j]+arcs[j] [k]

(4)重复2-3的操作n-1次,直到所有顶点都包含在S中,基于贪心策略

举例:

在这里插入图片描述

(1)初始化S,v1可达v2和v5,不可达v3和v4;故dist[2]=0,dist[3]=∞,dist[4]=∞;dist[5]=5

(2)选出最小值dist[5],将顶点v5并入集合S,此时找到1-5的最短路径;5加入后,1到集合V-S中可达顶点的最短路径长度可能会产生变化,更新dist[]数组,5可达2,1-5-2的距离8比dist[2]=10小,更新dist[2]=8;5可达3,1-5-3的距离14,更新dist[3]=14;5可达4,1-5-4的距离是7,更新dist[4]=7

(3)再选出最小值,dist[4],顶点4并入S,再更新dist[]数组;4不可达2,不变;4可达3,1-5-4-3距离是13比dist[3]小,更新dist[3]=13

(4)再选出最小的dist[2],顶点2并入S;更新dist[];2可达3,1-5-2-3的距离是9,比dist[3]小,更新dist[3]=9

(5)选出唯一的最小值dist[3],并入S,此时全部顶点并入完毕

无论是邻接矩阵还是邻接表的时间复杂度都是O(|V|2)

但是当边有负权值时,Dijkstra算法失效

在这里插入图片描述

例如图a,0到1和2的距离分别是7和5,那么就会首先并入顶点2,距离为5,再并入顶点1,但是并入顶点1时,从0到2的距离最小实际是2,不是5,但由于已经并入了2,无法更新

2.2 Floyd算法求各顶点直接最短路径问题

求出任意两个顶点i和j之间的最短路径和最短路径长度

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

初始时,任意两个顶点i和j直接存在边,则在边上的权值作为它们之间的最短路径长度;不存在有向边就用∞作为长度。

然后逐步尝试在原路径中加入顶点k作为中间顶点,若增加中间顶点后,得到的路径比原来的路径长度减少了,新路径就代替原路径

算法描述:

定义一个n阶方阵序列A(-1),A(0),A(1)……A(n-1),其中A(k)[i][j]=arcs[i] [j]

A(k)[i][j]=Min{A(k-1)[i][j],A(k-1)[i][k]+A(k-1)[k][j]},k=0,1……n-1

A(0)[i][j]是从顶点i到j,中间顶点是0的最短路径的长度;A(k)[i][j]是从顶点i到j,中间顶点的序号不大于k的最短路径的长度

这是一个迭代的过程,每迭代一次多考虑一个顶点;A(n-1)[i][j]就保存了任意一对顶点之间的最短路径长度

举例:

在这里插入图片描述

在这里插入图片描述

(1)初始化:方阵A(-1)[i][j]=arcs[i] [j]

(2)把0作为中间顶点,对所有顶点有A(-1)[i][j]>A(-1)[i][0]+A(-1)[0][j],把A(-1)[i][j]更新为A(-1)[i][0]+A(-1)[0][j];更新后方阵记为A0

(3)把1作为中间顶点,继续检测所有顶点,例如有A(0)[0][2]>A(0)[0][1]+A(0)[1][2]=10,更新,新方阵记为A1

(4)把2作为中间顶点,继续,更新方阵为A2,A2就是保存的任意顶点对的最短路径长度

时间复杂度:O(|V|3),但代码紧凑,对中等规模的输入来说仍然有效

允许图中有带负权值的边,但不允许与包含带负权值的边组成的回路;也适用于带权无向图

也可以用单源最短路径算法解决每对顶点之间的最短路径问题,轮流将每个顶点作为源点,在所有边权值均非负时,运行一次Dijkstra算法,时间复杂度为O(|V|3)

2.3 有向无环图描述表达式

若一个有向图不存在环,就成为有向无环图,简称为DAG图

例如((a+b)*(b*(c+d)+(c+d)*e))*((c+d)*e)

在这里插入图片描述

2.4 拓扑排序

AOV网:若用DAG图表示一个工程,其顶点表示工地,边表示活动i必须先于活动j进行的一种关系,则将这种有向图称为顶点表示活动的网络,边没有权值

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

(1)每个顶点只出现一次

(2)若顶点A在序列中排在顶点B的前面,则在图中不存在从B到A的路径

拓扑排序是对有向无环图的的顶点的一种排序,使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面,每个AOV网都有一个或多个拓扑排序序列

常见算法步骤:

(1)从AOV网中厕一个没有前驱的顶点输出

(2)删除该顶点和它为起点的有向边

(3)重复1,2直到没有顶点为止

在这里插入图片描述

bool Topologicalsort{Graph G}
{
    Initstack(S);
    int i ;
    for(i=0;i<G.vextum;i++)
    {
        if(indegree[i]==0)        \\所有入度为0的顶点入栈
        {
            Push(S,i);
        }
    }
    int count =0;                 \\记录当前输出的顶点数
    while(!isempty(S))
    {
        Pop(S,i);
        print[count++]=i;              \\输出顶点i
        for(p=G.vertices[i].firstarc; p ; p=p->nextarc) \\所有i执行的顶点的入度减1,并把入度为0的入栈
        {
            v=p->adjvex;
            if(!(--indegree[v]))
            {
                Push(S,v);             
            }
        }
    }
    if(count<G.vexnum)
        return false;                 \\有环路 失败
    else
        return true;
}

邻接表:O(|V|+|E|)

邻接矩阵:O(|V|2)

逆拓扑排序:

(1)选一个出度为0 的顶点输出

(2)删除顶点和它以它为终点的有向边

(3)重复1,2直到AOV网空

2.5 关键路径

AOE网:顶点表示事件,有向边表示活动,边的权值表示完成活动的开销,是有向无环图

性质:(1)只有某顶点的事件发生后,从该顶点出发的各有向边所代表的活动才能开始

(2)进入某顶点的各有向边的活动都结束时,该顶点代表的事件才发生

(3)仅有一个入度为0的顶点,称为开始顶点(源点);仅有一个出度为0的顶点,称为结束顶点(汇点)

(4)有些活动可以并行进行

(3)源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,关键路径的长度就是完成工程的最短时间

寻找关键活动的参量:

(1)事件vk的最早发生时间ve(k)

(2)事件vk的最迟发生时间vl(k)

(3)活动ai的最早开始时间e(i)

(4)活动ai的最迟开始时间l(i)

(5)一个活动ai的最迟开始时间l(i)和最早开始时间e(i)的差额d(i)=l(i)-e(i)

l(i)-e(i)=0,即活动aI是关键活动

算法步骤:

(1)从源点出发,ve(源点)=0,按拓扑有序求其余顶点的最早发生时间ve()

(2)从汇点出发,令vl(汇点)=ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间vl()

(3)根据各顶点的ve()值求所有弧的最早开始时间e()

(4)根据各顶点的vl()值求所有弧的最迟开始时间l()

(5)求AOE网中的所有活动的差额d,找出所有d=0的活动构成关键路径

怎么求ve,ve就是从源点到这个点的耗时最长的数值

怎么求vl,从汇点倒着来,用汇点的值减去汇点到这个点的最小的值(尽量小的到达这个),所以vl ⩾ \geqslant ve

e就是弧起点的顶点的ve

l(i)等于该弧的终点的顶点的vl()减去弧的持续时间

在这里插入图片描述

关键路径上的所有活动都是关键活动

网中的关键路径并不唯一,只有加快所有关键路径上的关键活动才能缩短工期

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青山是哪个青山

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值