图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的有向图
指用一个一维数组存储图顶点信息,用一个二维数组存储图中边的信息
邻接矩阵:存储顶点直接邻接关系的二维数组
#define maxvertexnum 100 \\顶点数目
typedef char vertextype; \\顶点的数据类型
typedef int edgetype; \\带权图中边上权值的数据类型
typedef struct
{
vertextype Vex[maxvertexnum]; \\顶点表
edgetype Edge[maxvertexnum][maxvertexnum]; \\邻接矩阵,边表
int vexunm,arcnum; \\当前顶点数和弧数
}
A[i][j]=1或0 表示从顶点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的路径数目
对图的每个顶点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)链表次序不唯一
十字链表中,对应有向图中的每条弧有一个结点,对应每个顶点也有一个结点
tailvex:指示弧尾顶点的编号
headvex:指示弧头顶点的编号
hlink:指向弧头相同的下一个弧结点
tlink:指向弧尾相同的下一个弧结点
info:存放弧的相关信息
弧头相同的就在同一个链表,弧尾相同的在同一个链表
data:顶点的数据信息
firstin:指向以该顶点为弧头的第一个弧结点
firstout:指向以该结点为弧尾的第一个弧结点
顶点结点之间顺序存储,很容易找到头和尾,即出入度
无向图的另一种链式存储,每条边都用一个结点表示
ivex和jvex指示该边依附的两个顶点的编号;ilink指向下一条依附于顶点ivex的边,jlink指向下一条依附于顶点jvex的边;info存放边的相关信息
data:顶点信息
firstedge:指向第一条依附于该顶点的边
所有依附于同一顶点的边串联在同一链表中,每个边结点同时链接在两个链表中
仅抽象考虑
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
基本思想:访问起始结点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);
}
}
}
}
广度优先生成树
遍历过程中可以得到一颗遍历树
邻接矩阵的树唯一,但是邻接表的不唯一
类似树的先序遍历,尽可能“深”的搜索一个树
基本思想:访问起始顶点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才能产生深度优先生成树,否则产生的是深度优先生成森林
无向图调用DFS,BFS的次数等于图的连通分量数
连通有向图分为强连通个非强连通,连通子图也分为强连通分量和非强连通分量
非强联分量一次调用BFS(G,i)或DFS无法访问到该连通分量的所有顶点
一个带权连通无向图G,生成树不同,每棵树的权也可能不同,若T为权值之和最小的那颗生成树,则T为G的最小生成树MST
性质:
(1)最小生成树不唯一
(2)最小生成树的边的权值之和唯一
(3)最小生成树的边数为顶点数减1
根据性质从最小权值的边开始,基于贪心算法的有Prim和Kruskal算法
基本思想:任取一个顶点,找权值最小边并且不是重复顶点的边加入,直到所有顶点加入
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)
是一种按权值的递增次序选择合适的边来构造最小生成树的方法
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|)
一个顶点到另一个顶点的路径的权值之和最短称为最短路径;两点直接的最短路径也包含了路径上其他顶点间的最短路径
单源最短路径:求图中某一顶点到其他各顶点的最短路径,可通过Dijkstra算法
每对顶点间的最短路径:Floyd算法求解
设置一个集合S记录已求得的最短路径的顶点,初始时把源点v0放入S,集合S每并入一个新顶点vi,都要修改源点v0到集合V-S中顶点当前的最短路径长度值。
设置两个辅助数组:
dist[]:记录源点v0到其他各顶点当前的最短路径长度,初态为:若从v0到vi有弧,则dist[i]为弧上的权值,否则置为无穷
path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点
初始S只包含顶点0;邻接矩阵arcs表示带权有向图,arcs[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] (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,无法更新 求出任意两个顶点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) 若一个有向图不存在环,就成为有向无环图,简称为DAG图 例如((a+b)*(b*(c+d)+(c+d)*e))*((c+d)*e) AOV网:若用DAG图表示一个工程,其顶点表示工地,边表示活动i必须先于活动j进行的一种关系,则将这种有向图称为顶点表示活动的网络,边没有权值 拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足以下条件时称为一个图的拓扑排序: (1)每个顶点只出现一次 (2)若顶点A在序列中排在顶点B的前面,则在图中不存在从B到A的路径 拓扑排序是对有向无环图的的顶点的一种排序,使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面,每个AOV网都有一个或多个拓扑排序序列 常见算法步骤: (1)从AOV网中厕一个没有前驱的顶点输出 (2)删除该顶点和它为起点的有向边 (3)重复1,2直到没有顶点为止 邻接表:O(|V|+|E|) 邻接矩阵:O(|V|2) 逆拓扑排序: (1)选一个出度为0 的顶点输出 (2)删除顶点和它以它为终点的有向边 (3)重复1,2直到AOV网空 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()减去弧的持续时间 关键路径上的所有活动都是关键活动 网中的关键路径并不唯一,只有加快所有关键路径上的关键活动才能缩短工期2.2 Floyd算法求各顶点直接最短路径问题
2.3 有向无环图描述表达式
2.4 拓扑排序
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;
}
2.5 关键路径