数据结构——图 笔记合集(C语言)完结

文章中其实有很多图来帮助理解,但是因为外链的原因,我电脑上的图不能直接拉过来,要完整版的可以评论我直接发PDF版本。个人笔记,仅供参考。

定义

G G G(Graph)由顶点集 V V V(Vertex)和边集 E E E(Edge)组成,记为 G = ( V , E ) G = (V, E) G=(V,E)

V = { v 1 , v 2 , . . . , v n } V = \{v_1, v_2,...,v_n\} V={ v1,v2,...,vn},则用 ∣ V ∣ |V| V表示图 G G G中顶点的个数,也称图 G G G的阶, E = { ( u , v ) ∣ u ∈ V , v ∈ V } E = \{(u,v)|u\in V,v\in V\} E={ (u,v)uV,vV},用 ∣ E ∣ |E| E表示图 G G G中边的条数。

顶点的度、入度、出度

对于无向图,顶点 v v v的度是指依附于该顶点的边的条数,记为 T D ( v ) TD(v) TD(v)

对于有向图,入度是以顶点 v v v为终点的有向边的数目,记为 I D ( v ) ID(v) ID(v)

​ 出度是以顶点 v v v为起点的有向边的数目,记为 O D ( v ) OD(v) OD(v);

顶点-顶点的关系描述

· 路径

· 回路——第一个顶点和最后一个顶点相同的路径称为回路或环

· 简单路径——在路径序列中,顶点不重复出现的路径成为简单路径

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

· 路径长度——路径上边的数目(可以有多种结果)

· 点到点的距离——从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v到距离;若不存在路径,则记该距离位无穷 ( ∞ ) (\infty) ()

· 无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的;一个无向图中任意两个顶点都是连通的则称为连通图

· 有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的;一个有向图中任何一对顶点都是强连通的,则称为强连通图

!对于n个顶点的无向图G,若G是连通图,则最少有n-1条边;若G是非连通图,则最多可能有 C n − 1 2 C^2_{n-1} Cn12条边。

!对于n个顶点的有向图G,若G是强连通图,则最少有n条边(形成回路)

研究图的局部——子图

生成子图是包含了原图的所有顶点。

连通分量

无向图中的极大连通子图称为连通分量。(包含尽可能多的顶点和边)

强连通分量

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

生成树

连通图的生成树是包含途中全部顶点的一个极小连通子图。

若图中顶点数为n,则它的生成树含有n-1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。

生成森林

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

几种特殊形态的图

无向完全图 a n d and and 有向完全图

图的存储

邻接矩阵法

#define MaxVertexNum 100
typedef struct{
    
    
  char Vex[MaxVertexNum];
  int Edge[MaxVertexNum][MaxVertexNum];
  int vexnum, arcnum;
}MGraph;

带权图

#define MaxVertexNum 100     //顶点数目的最大值
#define INFINITY Max_int     //宏定义常量“无穷”
typedef char VertexType;     //顶点的数据类型
typedef int EdgeType;        //带权图中边上权值的数据类型
typedef struct{
    
    
  VertexType Vex[MaxVertexNum];                //顶点
  EdgeType Edge[MaxVertexNum][MaxVertexNum];   //边的权
  int vexnum, arcnum;                          //图的当前顶点数和弧数
}MGraph;

性能分析

空间复杂度: O ( n 2 ) O(n^2) O(n2)——只和顶点数相关,和实际的边数无关

适合用于存储稠密图

邻接矩阵法的性质

设图G的临界矩阵为***A***,则***A*** n ^n n的元素 A n [ i ] [ j ] A^n[i][j] An[i][j]等于由顶点 i i i到顶点 j j j的长度为 n n n的路径的数目

邻接表

顺序 + 链式存储

十字链表法

只能存储有向图

邻接多重表

图的基本操作

主要考察邻接矩阵和邻接表。

图的遍历

广度优先遍历

分为广度优先遍历(Breadth-First-Search, BFS)和深度优先遍历(Depth-First-Search, DFS)。

bool visited[MAX_VERTEX_NUM];   //访问标记数组

//广度优先遍历
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)l w>=0; w=NextNeighbor(G, v, w))
      //检测所有邻接点
      if(!visited[w]){
    
              //顶点v出队列
        visited(w);             //访问顶点w
        visited[w] = TRUE;      //对w做已访问标记
        EnQueue(Q, w);          //顶点w入队列
      }//if
  }//while
}

上述代码,如果是非连通图,则无法遍历完所有结点。所有对上述代码进行一些修改,借助visited数组来判断是否遍历完所有对结点。

bool visited[MAX_VERTEX_NUM];   //访问标记数组
void BFSTraverse(Graph G){
    
          //对图G进行广度优先遍历
  for(i=0; i<G.vexnum; ++i)
    visited[i] = FALSE;         //访问标记数组初始化
  InitQuene(Q);                 //初始化辅助队列Q
  for(i=0; i<G.vexnum; ++i)     //从0号顶点开始遍历
    if(!visited[i])             //对每个连通分量调用一次BFS
      BFS(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)l w>=0; w=NextNeighbor(G, v, w))
      //检测所有邻接点
      if(!visited[w]){
    
              //顶点v出队列
        visited(w);             //访问顶点w
        visited[w] = TRUE;      //对w做已访问标记
        EnQueue(Q, w);          //顶点w入队列
      }//if
  }//while
}

综上有一个结论:对于无向图,调用BFS函数的次数=连通分量数

空间复杂度 = O ( ∣ V ∣ ) O(|V|) O(V)

邻接矩阵时间复杂度 = O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

邻接表时间复杂度 = O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(V+E)

广度优先生成树

将未遍历的边去掉,就变成了一棵树。

广度优先生成树由广度优先遍历过程确定。由于邻接表的表示方式不唯一。因此基于邻接表的广度优先生成树也不唯一。基于邻接矩阵的广度优先生成树是唯一的。

广度优先生成森林

是有多个连通分量的情况。

深度优先遍历

bool visited[MAX_VERTEX_NUM];   //访问标记数组
void DFSTraverse(Graph G){
    
          //对图G进行深度优先遍历
  for(v=0; v<G.vexnum; ++v)
    visited[v] = FALSE;         //初始化已访问标记数据
  for(v=0; v<G.vexnum; ++v)     //本代码中是从v=0开始遍历
    if(!visited[v])
      DFS(G, v);
}

void DFS(Graph G, int v){
    
           //从顶点v出发,深度优先遍历图G
  visit(v);                     //访问顶点v
  visited[v] = TRUE;            //设已访问标记
  for(w=FirstNeighbor(G, v); w>=0; w=NextNeighor(G, v, w))
    if(!visited[w]){
    
                //w为尚未访问的邻接顶点
      DFS(G, w);
    } //if
}

空间复杂度 = O ( ∣ V ∣ ) O(|V|) O(V)

邻接矩阵时间复杂度 = O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

邻接表时间复杂度 = O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(V+E)

类似于广度优先生成树和森林,深度优先遍历也存在生成树和生成森林。

最小生成树

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

Prim算法

从某一个顶点开始构建生成树,每次讲代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。

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

实现算法是构建两个数组: i s J o i n isJoin isJoin l o w C o s t lowCost lowCost

Kruskal算法

每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通。

时间复杂度: O ( ∣ E ∣ l o g 2 ∣ E ∣ ) O(|E|log_2|E|) O(Elog2E)

实现算法是将各条边按权值排序,执行E轮,每轮判断两个顶点是否属于同一集合。

最短路径 BFS算法

BFS算法适用于求单源最短路径且是无权图。

//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(Graph G, int u){
    
    
  //d[i]表示从u到i结点的最短路径
  for(i=0; i<G.vexnum; ++i){
    
    
    d[i] = -1;       //初始化路径长度
    path[i] = -1;    //最短路径从哪个顶点过来
  }
  d[u] = 0;
  visited[u] = TRUE;
  EnQueue(Q, u);
  while(!isEmpty(Q)){
    
          //BFS算法主过程
    DeQueue(Q, u);         //队头元素u出队
    for(w=FirstNeighbor(G, u); w>=0; w=NextNeighbor(G, u, w))
      if(!visited[w]){
    
         //w为u的尚未访问的邻接顶点
        d[w] = d[u] + 1;   //路径长度加一
        path[w] = u;       //最短路径应从u到w
        visited[w] = TRUE; //设已访问标记
        EnQueue(Q, w);     //顶点w入队
      }//if
  }//while
}

Dijkstra算法

不适用于有负权值的带权图。

初始:从 V 0 V_0 V0开始,初始化三个数组:

final[VEXNUM];    //标记个顶点是否已找到最短路径
dist[VEXNUM];     //最短路径长度
path[VEXNUM];     //路径上的前驱

循环遍历所有结点,找到还没确定最短路径。且dist最小的顶点 V i V_i Vi,令final[i] = true。

多次遍历直到final中所有的元素为True。

最短路径 Floyd算法

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

使用动态规划思想,将问题的求解氛围多个阶段。
若 A ( k − 1 ) [ i ] [ j ] > A ( k − 1 ) [ i ] [ k ] + A ( k − 1 ) [ k ] [ j ] 则 A ( k ) [ i ] [ j ] = A ( k − 1 ) [ i ] [ k ] + A ( k − 1 ) [ k ] [ j ] ; p a t h ( k ) [ i ] [ j ] = k 否 则   A ( k ) 和 p a t h ( k ) 保 持 原 值 若 A^{(k-1)}[i][j] > A^{(k-1)}[i][k] + A^{(k-1)}[k][j] \\ 则 A^{(k)}[i][j] = A^{(k-1)}[i][k] + A^{(k-1)}[k][j]; \\ path^{(k)}[i][j] = k \\ 否则\ A^{(k)}和path^{(k)}保持原值 A(k1)[i][j]>A(k1)[i][k]+A(k1)[k][j]A(k)[i][j]=A(k1)[i][k]+A(k1)[k][j];path(k)[i][j]=k A(k)path(k)
实现代码:

//准备工作:根据图的信息初始化矩阵A(邻接矩阵)和path
for(int k=0; k<n; k++){
    
                     //考虑以Vk作为中转点
  for(int i=0; i<n; i++){
    
                   //遍历整个矩阵,i为行号,j为列号
    for(int j=0; j<n; j++){
    
    
      if(A[i][j] > A[i][k] + A[k][j]){
    
      //以Vk为中转点的路径更短
        A[i][j] = A[i][k] + A[k][j];    //更新最短路径长度
        path[i][j] = k;                 //中转点
      }
    }
  }
}

时间复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)

有向无环图

若有一个有向图中不存在环,则称为有向无环图,简称DAG图(Directed Acyclic Graph)

DAG描述表达式

( ( a + b ) ∗ ( b ∗ ( c + d ) ) + ( c + d ) ∗ e ) ∗ ( ( c + d ) ∗ e ) ((a + b)*(b*(c + d))+(c + d)*e)*((c + d)*e) ((a+b)(b(c+d))+(c+d)e)((c+d)e)

拓扑排序

AOV网

AOV网(Activity On Vertex Network,用顶点表示活动的网)

用DAG图(有向无环图)表示一个工程。顶点表示活动。有向边 < V i , V j > <V_i,V_j> <Vi,Vj>表示活动 V i V_i Vi必须先于 V j V_j Vj进行。

拓扑排序的定义

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

  1. 每个顶点出现且只出现一次。
  2. 若顶点 A A A在序列中排在顶点 B B B的前面,则在图中不存在从顶点 B B B到顶点 A A A的路径。

拓扑排序的实现

  1. 从AOV网中选择一个没有前驱(入度为0)的顶点并输出。
  2. 从网中删除该顶点和所有以它为起点的有向边。
  3. 重复1和2直到当前AOV网为空或当前网中不存在无前驱的顶点为止。

每个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=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 ∣ ) O(|V|+|E|) O(V+E)

用邻接矩阵存储图,时间复杂图为 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

逆拓扑排序

  1. 从AOV网中选择一个没有后继(出度为0)的顶点并输出。
  2. 从网中删除该顶点和所有以它为终点的有向边。
  3. 重复1和2直到当前AOV网为空。

逆拓扑排序的实现(DFS算法)

在顶点退栈前输出。

思考:如果存在回路,则不存在逆拓扑排序,如何判断回路?

答:在每次深度加一后判定指针所指结点是否指向visited为TRUE的结点,若指向,则说明存在回路,排序失败!

关键路径

AOE网

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)。

性质

  1. 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
  2. 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。另外,有些活动时可以并行进行的。

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

关键路径

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

完成整个工程的最短时间就是关键路径的长度。

事件 v k v_k vk的最早发生时间 v e ( k ) ve(k) ve(k)——决定了所有从 v k v_k vk开始的活动能够开工的最早时间

活动 a i a_i ai的最早开始时间 e ( i ) e(i) e(i)——指该活动弧的起点所表示的事件的最早发生时间

事件 v k v_k vk的最迟发生时间 v l ( k ) vl(k) vl(k)——指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间

活动 a i a_i ai的最迟开始时间 l ( i ) l(i) l(i)——指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差

活动 a i a_i ai的时间余量 d ( i ) = l ( i ) − e ( i ) d(i)=l(i)-e(i) d(i)=l(i)e(i),表示在不增加完成整个工程所需总时间的情况下,活动 a i a_i ai可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成, d ( i ) = 0 d(i) = 0 d(i)=0 l ( i ) = e ( i ) l(i) = e(i) l(i)=e(i)的活动 a i a_i ai是关键活动。由关键活动组成的路径是关键路径。

求关键路径的步骤

  1. 求所有事件的最早发生时间 v e ( ) ve() ve()

    按拓扑排序序列,依次求各个顶点的 v e ( k ) ve(k) ve(k)

    v e ( 源 点 ) = 0 ; ve(源点) = 0; ve()=0; v e ( k ) = M a x { v e ( j ) + W e i g h t ( v j , v k ) } ve(k) = Max\{ve(j) + Weight(v_j, v_k)\} ve(k)=Max{ ve(j)+Weight(vj,vk)}

  2. 求所有事件的最迟发生时间 v l ( ) vl() vl()

    按逆拓扑排序序列,依次求各个顶点的 v l ( k ) vl(k) vl(k)

    v l ( 汇 点 ) = v e ( 汇 点 ) ; vl(汇点) = ve(汇点); vl()=ve(); v l ( k ) = M i n { v l ( j ) − W e i g h t ( v k , v j ) } vl(k) = Min\{vl(j) - Weight(v_k, v_j)\} vl(k)=Min{ vl(j)Weight(vk,vj)}

  3. 求所有活动的最早发生时间 e ( ) e() e()

    e ( i ) = v e ( k ) e(i) = ve(k) e(i)=ve(k)

  4. 求所有活动的最迟发生时间 l ( ) l() l()

    l ( i ) = v l ( j ) − W e i g h t ( v k , v j ) l(i) = vl(j)-Weight(v_k,v_j) l(i)=vl(j)Weight(vk,vj)

  5. 求所有活动的时间余量 d ( ) d() d()

猜你喜欢

转载自blog.csdn.net/weixin_42731543/article/details/106037670