数据结构学习记录(三)

一、知识要点

1、图的基本概念

  • 图的定义和术语

    • 图的定义

      • 图(Graph)是由两个集合构成,一个是非空但有限的顶点集合V,另一个是表述顶点之间边的集合E(可能是$\emptyset$)。图可表示为G = (V ,E ).
      • 每条边是一顶点对(v, w)且v,w $\in$ V。通常用|V|表示顶点的数量,|E|表示边的数量。
      • 在图中,至少要有一个顶点,但边集可以为空。
    • 图的相关术语

      • 无向图:无向图中顶点之间的所有边都可互通,不用标方向,起点和终点次序并不重要。用圆括号(v, w)表示无向图

      • 有向图:有向图中所有边都有方向,即<v, w>不同于<w, v>,无向图用尖括号<>表示

      • 简单图:所有边不重合的图叫简单图。

      • 邻接点:在无向图中,一条边连接的两个顶点叫邻接点;在有向图中,如果<v, w>是一条边,那么可以说起点v邻接到终点w

      • 路径、简单路径和回路

        • 从顶点v沿着边可到顶点w,那么这些边就是路径,路径长度是这条路径所包含的边数
        • 在路径序列中,顶点不重复出现的路径称为简单路径
        • 第一个顶点和最后一个顶点相同的路径称为回路或环。若一个图有n个顶点,并且有大于n − 1条边,则此图一定有环。
        • 除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路
      • 无向完全图:在无向图中,任意两个结点都有边相连,那么这个无向图称为无向完全图。可以证明:如果有n个结点,那么有n(n - 1) / 2条边。

      • 有向完全图:在有向完全图中任意两个顶点之间都存在方向相反的两条弧。可以证明:如果有n个结点,那么有n(n - 1) 条边。

      • 顶点的度、入度和出度:顶点的度是指依附于某节点的边数。在有向图中,一某顶点为终点的边数称为入度,反之则为出度。度 = 入度 + 出度。

      • 稠密图、稀疏图

      • 边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的。一般当图G满足∣ E ∣ < ∣ V ∣ l o g ∣ V ∣时,可以将G视为稀疏图。

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

      • 子图:设有两个图G = ( V , E ) 和G ′ = ( V ′ , E ′ ), 若V ′是V的子集,且E ′ 是E的子集,则称G ′是G的子图。

      • 连通、连通图和连通分量:在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。无向图中的极大连通子图称为连通分量。若一个图有n个顶点,并且边数小于n − 1,则此图必是非连通图。

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

      • 生成树:连通图的生成树是包含其所有结点的一个极小联通子图,若图中结点数为n,那么其生成树有n-1条边。在非连通图中,连通分量的生成树构成了非连通图的生成森林。

  • 图的抽象结构

    • 类型名称:图(Graph)
    • 数据对象集:一非空的顶点集合Vertex和一个边集合Edge,每条边用对应的一对定点表示。
    • 操作集:对于任意的图G $\in$ Graph, 顶点V $\in$ Vertex, 边E $\in$ Edge, 以及任一访问顶点的函数Visit( ),我们主要有以下操作
      • Graph Create_Graph(int Vertex_Num):构造一个有Vertex_Num个结点但没有边的图。
      • void Insert_Edge(Graph G, Edge E):在G中增加新边E。
      • void Delete_Edge(Graph G, Edge E):在G中删除边E。
      • bool Is_Empty(Graph G):如果G为空则返回true,否则返回false。
      • void DFS(Graph G, Vertex V, (*Visit)(Vertex)):在图G中,从顶点V出发进行深度优先遍历
      • void BFS(Graph G, Vertex V, (*Visit)(Vertex)):在图G中,从顶点V出发进行广度优先遍历

2、图的储存结构

  • 邻接矩阵

    • 图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图结点数据信息,一个二维数组(邻接矩阵)储存图中的边的信息。

    • 假设邻接矩阵A是一个n * n的方阵,若(vi, vj)或<vi, vj>是图中的边,那么A[i] [j]为1,否则为0.

    • 在无向图中:

      • 无向图的邻接矩阵一定是一个对称矩阵(即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的)。 因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
      • 对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度 TD(vi)。比如顶点v1的度就是1 + 0 + 1 + 0 = 2
      • 求顶点vi 的所有邻接点就是将矩阵中第i行元素扫描一遍, A [ i ] [ j ]为 1就是邻接点。
    • 在有向图中

      • 主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称。
      • 有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之和。顶点v1的出度为2,即第v1行各元素之和
    • 在网图中,A[ i ] [ j ]存的是边的权值。

    • //邻接矩阵的存储结构
      #define MaxVertexNum 100	//定义顶点的最大数
      typedef char VertexType;	//顶点的数据类型
      typedef int EdgeType;	//带权图中边上权值的数据类型
      struct GNode
      {
          VertexType Vex[MaxVertexNum];	//顶点表
          EdgeType Edge[MaxVertexNum][MaxVertexNum];`//邻接矩阵,边表
          int vexnum;	//顶点数
          int arcnum;	//边数
      };
      typedef struct GNode *MGraph;	//图类型
      
      //创建图
      
      #defind IFINITY 65535	//将无穷设为无符号整数最大值
      typedef int Vertex;	//用顶点下标表示顶点,为整型
      
      //边的定义
      struct ENode
      {
          Vertex V1, V2;	//有向边<V1, V2>
          WeightType Weight;	//边的权值
      };
      typedef struct ENode *Edge;
      
      //初始化一个有VertexNum个顶点但没有边的图
      MGraph CreateGragh(int VertexNum)
      {
          Vertex V, W;
          MGraph Graph;
          
          Graph = (MGraph)malloc(sizeof(struct GNode));	//建立图
          Graph->vexnum = VertexNum;
         	Graph->arcnum = 0;
          //初始化邻接矩阵
          for(V = 0; V < Graph->vexnum; V++)
              for(W = 0; W < Graph->arcnum; W++)
                  Graph->G[V][W] = INFINITY;
          
          return Graph;
      }
      
      //插入边
      void InsertEdge(MGraph Graph, Egde E)
      {
          Graph->G[E->V1][E->V2] = E->Weight;
      }
      
      //构建图
      MGraph BuildGraph()
      {
          MGraph Graph;
          Edge E;
          Vertex V;
          int Nv, i;
          
          printf("输入顶点个数:");
          scanf("%d", &Nv);
          
          Graph = CreateGraph(Nv);	//初始化图
          
          printf("输入边数:");
          scanf("%d", &(Graph->arcnum));
          if(Graph->arcnum != 0)	//如果有边
          {
              E = (Edge)malloc(sizeof(struct ENode));
              //读入边
              for(i = 0; i < Graph->arcnum; i++)
              {
                  printf("输入起点、终点、权重:");
                  scanf("%d %d %d", &E->V1, &E->V2, &E->Weight);
                  //插入边
                  InsertEdge(Graph, E);
              }
          }
          //读入顶点数据
          for(V = 0; V < Graph->vexnum; V++)
          {
              printf("输入顶点数据:");
              scanf("%c", &Graph->Vex[V]);
          }
          return Graph;
      }
      
    • 稠密图适合使用邻接矩阵的存储表示。

  • 邻接表

    • 邻接表是图的一种顺序储存与链式储存相结合的储存方法
  • 邻接表有两种结点结构:

    • 顶点表:由顶点数据域(Data)指向第一条邻接边的指针域(FirstEdge)构成。

    • 边表:由邻接点域(AdjV)指向下一条邻接边的指针域(Next)构成。如果是网图结点,还需要增设一个权值域(Weight)。

    • 顶点表用数组按下标存储,每个顶点将所有邻接于该顶点的其他顶点链成一个单链表。

    • 图的邻接表储存特点

      • 若无向图有V个顶点和E条边,那么他的邻接表需要V个头节点和2E个表结点,如果图相当稀疏的话,用邻接表比邻接矩阵节省空间
    • 若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低

      • 在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数;但求其顶点的入度则需要遍历全部的邻接表。
    • #define MaxVertexNum 100	//最大顶点数设为100
      typedef int Vertex;		//用顶点下标表示顶点
      typedef int WeightType;		//边的权值设为整型
      typedef char DataType;		//顶点存储数据类型设为字符型
      
      //边的定义
      struct ENode
      {
          Vertex V1, V2;		//边<v1, v2>
          WeightType Weight;	//边的权值
      };
      typedef struct ENode *Edge;		//设置为边结构
      
      //边表的定义
      struct AdjVNode
      {
          Vertex V;				//顶点下标
          WeightType Weight;		//边权值
          struct AdjVNode *Next;	//指向下一邻接结点的指针
      };
      typedef struct AdjVNode *AdjV;	//设置为邻接点结点结构
      
      //顶点表头定义
      struct VNode
      {
          AdjV FristEdge;		//指向下一邻接结点的指针
          DataType Data;		//结点权值
      };
      typedef struct VNode AdjList[MaxVertexNum];	//设置为邻接表结构
      
      //图节点定义
      struct GNode
      {
          int VertexNum;	//顶点数
          int EdgeNum;	//边数
          AdjList AL;		//邻接表
      };
      typedef struct GNode *LGraph;	//设置为邻接表图类型
      
      //创建没有边的图
      LGraph CreateLGraph(int VertexNum)
      {
          int i;
          LGraph G;	//创建图
          G = (LGraph)malloc(sizeof(strcut GNode));	//赋予空间
          G->VertexNum = MaxVertexNum;	//输入顶点数
          G->EdgeNum = 0;	//边为0
          //给每个表头置空
          for(i = 0; i < G->VertexNum; i++)
              G->AL[i] = NULL;
          
          return G;
      }
      
      //插入边
      void InsertEdge(LGraph G, Edge E)
      {
          //当图为有向图时,插入边<v1, v2>
          AdjV NewNode;	//建立V2结点
          NewNode = (AdjV)malloc(sizeof(struct AdjVNode));	//赋予空间
          NewNode->V = E->V2;		//存入下标
          NewNode->Weight = E->Weight;	//存入边权值
          //将邻接结点插入头节点V1
          NewNode->Next = G->AL[E->V1].FirstEdge;
          G->AL[E->V1].FirstEdge = NewNode;
          
          //如果图为无向图,还需把V1插入V2
          AdjV New2;
          New2 = (AdjV)malloc(sizeof(struct AdjVNode));
          New2->V = E->V1;
          New2->Weight = E->Weight;
          New2->Next = G->AL[E->V2].FirstEdge;
          G->AL[E->V2].FirstEdge = New2;
      }
      
      //创建图·
      LGraph BuildLGraph()
      {
          int i, VertexNum;
          LGraph G;
          Edge E;
          
          printf("请输入顶点数:");
          scanf("%d", &VertexNum);
          G = CreateLGraph(VertexNum);
          
          printf("请输入边数:");
          scanf("%d", &G->EdgeNum);
          
          if(G->EdgeNum != 0)
          {
              E = (Edge)malloc(sizeof(struct ENode));
              for(i = 0; i < G->EdgeNum; i++)
              {
                  scanf("%d%d%d", &E->V1, &E->V2, &E->Weight);
                  InsertEdge(G, E);
              }
          }
          
          return Graph;
      }
      
    
    

3、图的遍历

  • 深度优先搜索

    • 深度优先搜索也称DFS算法,它类似于树的先序遍历,他从某个顶点v0出发,依次递归探索未被探索的顶点。

    • //假设图以邻接表的形式储存
      
      //Visited数组记录顶点的探索情况,预先将所有顶点设置为未探索
      bool Visited[MaxVertexNum] = {false};
      
      //访问顶点函数
      void Visit(Vertex V)
      {
          printf("正在访问结点%d\n", V);
      }
      
      //DFS函数深度优先搜索
      void DFS(LGraph G, Vertes V)
      {
          //从顶点V出发对图G进行DFS搜索
          AdjV W;
          
          //访问节点V
          Visit(V);
          //标记该节点
          Visited[V] = true;
          
          //对V的邻接点进行访问
          for(W = G->AL[V].FirstEdge; W; W->Next)
          {
              //当顶点未被访问时
              if(Visited[W->V] == false)
                  //递归访问
                  DFS(G, W->V);
          }
      }
      
  • 广度优先搜索

    • 广度优先搜索也称BFS算法,它类似于树的按层次遍历的过程,就是构造一个队列,每访问一个顶点就把该顶点的所有邻接顶点放入队列。

    • //假设图以邻接矩阵的方式存储
      
      //Visited数组记录顶点的探索情况,预先将所有顶点设置为未探索
      bool Visited[MaxVertexNum] = {false};
      
      //判断<V,W>是否是边
      bool IsEdge(MGraph G, Vertex V, Vertex W)
      {
          return (G->Edge[V][W] < INFINITY ? true : false);
      }
      
      //以S为出发点对图进行BFS搜索
      void BFS(MGraph G, Vertex S)
      {
          Queue Q;
          Vertex V, W;
          
          //创建一个空队列
          Q = CreateQueue();
          Visit(S);	//访问该顶点
          //标记该顶点
          Visited[S] = true;
          //S入队
          AddQ(Q, S);
          
          //层序遍历顶点
          while(!IsEmpty(Q))
          {
              //当Q不为空时
              V = DeleteQ(Q);	//弹出顶点
              //找到Q的所有邻接点放入队列
              for(W = 0; W < MGraph->VertexNum; W++)
              {
                  if(!Visit[W] && IsEdge(G, V, W))
                  {
                      Visit(W);
                      Visited[W] = true;
                      AddQ(W);
                  }
              }
          }
      }
      

4、最小生成树

  • 生成树的构建与最小生成树的概念

    • 对连通图不同的遍历,就可能得到不同的生成树。
    • 如果无向连通图是一个网图,那么它的所有生成树中必有权值总和最小的生成树,称为最小生成树(MST),当然最小生成树也未必是唯一的l
    • 有两种常用的构造最小生成树的方法:Prim算法和Kruskal算法。
  • 构建最小生成树的Prim算法

    • 从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把当前形成的连通分量当做一个整体或者一个点看待,然后重复“找最短的边并添加”的操作。

    • 原图为稠密图的时候用Prim算法好一点

    • //最小生成树肯定是稀疏图,所以最小生成树用邻接表储存
      //这里用邻接矩阵储存原图
      
      #defind ERROR -1	//错误标记,表示生成树不存在
      
      //prime算法,将图的最小生成树保存到邻接表储存的图MST中,返回最小权值或错误信息
      int Prim(MGraph G, LGraph MST)
      {
          //定义一个数组用来存放下标代表的顶点到其父结点的权值
          //定义最小权值和
          WeightType dist[MaxVertexNum], TotalWeight = 0;
          //定义一个数组来存放下标代表的顶点的父结点
          Vertex parant[MaxVertexNum], V, W;
          int VCount;		//VCount表示收录顶点的数量
          Edge E;
          
          //将所有顶点的父结点初始化为V0,并把每个顶点到v0的权值存入
          for(V = 0; V < G->VertexNum; V++)
          {
              dist[V] = G->Edge[0][V];
              parant[V] = 0;
          }
          VCount = 0;
          
          //创建一个空的邻接表来表示MST
          MST = CreateLGragh(G->VertexNum);
          E = (Edge)malloc(sizeof(struct ENode));
          
          dist[0] = 0;	//表示V0已被收录
          VCount++;
          parant[0] = -1;		//表示当前树根为V0
          
          //核心代码,收录最小权值并更新未收录顶点的父结点
          while(1)
          {
              V = FindMinDist(G, dist);	//找到dist中的最小权值
              if(V == ERROR)
                  break;	//如果最小权值不存在,则结束算法
              //将V和相应的边<parant[V], V>收录进MST
              E->V1 = parent[V];
              E->V2 = V;
              E->Weight = dist[V];
              InsertEdge(MST, E);
              TotalWeight += dist[V];
              dist[V] = 0;	//标记V顶点已被收录
              VCount++;
              
              //找到与当上一个被收录顶点相邻的的所有未收录顶点
              //如果有未收录顶点到上一个被收录节点的边权值比dist数组存的数小,那么更新dist数组,并更新该未收录顶点的父结点
              for(W = 0; W < G->VertexNum; V++)
              {
                  if(G->Edge[V][W] < dist[W])
                  {
                      dist[W] = G->Edge[V][W];
                      parant[W] = V;
                  }
              }
          }//while结束
          
          //如果收录的顶点未满就返回错误信息
          if(VCount < G->VertexNum)
              return ERROR;
          //否则返回最小权值总和
          return TotalWeight;
      }
      
      //返回dist中未收录顶点最小顶点
      Vertex FindMinDist(MGraph G, WeightType dist[])
      {
          Vertex MinV, V;
          WeightType MinDist = INFINITY;
          
          for(V = 0; V < G->VertexNum; V++)
          {
              if(dist[V] != 0 && dist[V] < MinDist)
              {
                  MinDist = dist[V];
                  MinV = V;
              }
          }
          if(MinDist < INFINITY)
              return MinV;
          else
              return ERROR;
      }
      
  • 构建最小生成树的Kruskal算法

    • 当原图是稀疏图时,用Kruskal算法比prim算法好

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

    • //这里用邻接表储存原图和最小生成树
      
      int Kruskal(LGraph G, LGraph MST)
      {
          //初始化空MST
          MST = CreateLGraph(G->VertexNum);
          //设置权重和
          WeightType Vcount = 0;
          //设置parant数组,并将其初始化
          int parant[G->VertexNum] = -1;
          //设置一个边集,把图的所有边存入边集
          Edge E[G->EdgeNum];
          CreateEdge(G, E);
          //构造一个包含原图所有边的最小堆
          MinHeap H = BuildMinHeap(E);
          //while循环构造最小生成树
          while(MST->EdgeNum < G->Vertex - 1 && G->EdgeNum != 0)
          {
              //从最小堆取一个边
              Edge E0 = DeleteMinHeap(H);
              //原图边集中删掉这个边
              DeleteEdge(G, E0);
              //确认这个边是否会让MST形成回路
              if(!Find(parant, E0))
              //是则丢弃该边
              //否则将这个边加入MST,并标记顶点
              {
                  InsertLGraph(MST, E);
                  if(parant[E->V1] == -1)
                      parant[E->V1] = E->V2;
                  if(parant[E->V2] == -1)
                      parant[E->V2] = E->V1;
              }
              //增加权重和
              Vcount += E->Weight;
              //增加MST收集的边数
          }
          //如果MST收集的边不足则返回错误信息
          if(MST->EdgeNum != G->NertexNum)
              return ERROR;
          else
              return Vcount;
          //否则返回权重和
      }
      
      //将图G的边存入边集E中
      void CreateEdge(LGraph G, Edge E[])
      {
          Vertex V, W = 0;
          for(V = 0; V < G->VerNum; V++)
          {
              AdjV H = G->AL[V]->FirstEdge;
              while(H != NULL)
              {
                  E[W]->V1 = V;
              	E[W]->V2 = H->V;
                  E[W]->Weight = H->Weight;
                  W++;
                  H = H->Next;
              }
          }
      }
      
      //构造一个包含原图所有边的最小堆
      MinHeap BuildMinHeap(E[]);
      {
          MinHeap H = CreateMinHeap(G->EdgeNum);
          int i;
          for(i = 0; i < G->EdgeNum; i++)
          {
              H->Data[i] = E[i];
          }
          return H;
      }
      

5、最短路径

  • 单源最短路径

    • 从一个源点到其他各项顶点最短路径问题称为“单源最短问题”,该问题可描述为图G中的顶点V0到其余各顶点的最短路径。

    • Dijkstra算法可以求单源最短路径,这个算法和prim算法几乎一样,都是用dist数组存最短边,parent数组存父结点

    • //利用Dijkstra算法求单源最短路径
      //这里图用邻接矩阵存储
      #defind ERROR -1	//设置报错符号
      #defind INF	65535	//指定权值为无穷的边为INF
      
      //构建最短路径结构
      struct MLNode
      {
          //源顶点和终顶点
          Vertex HomeV;
          Vertex EndV;
          //包含最短路径中所有经过的顶点的数组
          Vertex *SumV;
          //顶点个数
          int VerNum;
          //路径总长度
          WeightType SumLength;
      };
      typedef struct MLNode *MinL;
      //求最短路径函数,函数形参是图、起始顶点和最终顶点
      MinL MinLength(MGraph G, Vertex HV, Vertex EV)
      {
          //创建一个长度为G->VertexNum的一个parent数组,用来存每个顶点对应的父结点
          int parent[G->VertexNum];
          //创建一个长度为VertexNum的一个dist数组,用来存每个顶点到其父结点的权值
          int dist[G->VertexNum];
          //利用Dijkatra算法得到parent数组和dist中的元素
          Dijkatra(G, HV, dist, parent);
          //调用FindSumV函数得到MinL->SumV和Min->VerNum
          Vertex *SV;
          int VN = FindSumV(EV, parent, SV);
          //调用FindSumL函数得到SumLength
          WeightType SL = FindSumL(dist, SV);
          //初始化MinL
          MinL ML = CreateMinL(ML, HV, EV, SV, VN, SL);
          return ML;
      }
      
      //Dijkatra算法,构造dist数组和parent数组
      void Dijkatra(MGraph G, Vertex HV, int dist[], int parent[])
      {
          Vertex V, W;
          //设置一个mark数组,用来标记已收录的顶点
          int mark[G->VertexNum];
          //初始化dist数组,将每个顶点到源顶点的权值存入
          //初始化parent数组,将与源节点邻接的顶点的父结点设为源节点,不邻接的设为-1
          //初始化mark数组,将所有顶点设置为未收录
          for(V = 0; V < G->VertexNum; V++)
          {
              dist[V] = G->Edge[HV][V];
              if(dist[V] < INF)
                  parent[V] = HV;
              else
                  parent[V] = -1;
              mark[V] = 0;
          }
          //标记源顶点
          dist[HV] = 0;
          mark[V] = 1;
          //利用while函数循环收录顶点
          while(1)
          {
              //调用FindMinDist函数找与父结点最小邻接边的顶点V
              V = FindMinDist(G, dist, mark);
              //没找到则结束循环
              if(V == ERROR)
                  break;
              //找到了则标记该顶点V已收录
              mark[V] = 1;
              //利用for循环找能够让dist变小的顶点
              for(W = 0; W < G->VertexNum; W++)
              {
                  //如果循环到的邻接顶点的邻接边小于其父结点的邻接边
                  if(G->Edge[V][W] < dist[W])
                  {
                      //将该边替换
                      dist[W] = G->Edge[V][W];
                      //更新parent数组
                      parent[W] = V;
                  }
              }
          }
      }
      
      //从dist数组找未收录最小顶点
      Vertex FindMinDist(MGraph G, dist[], mark[])
      {
          int i, minV = 0;
          int flag = 0;
          for(i = 0; i < G->VertexNum; i++)
          {
              if(mark[i] && dist[minV] > dist[i])
              {
                  flag = 1;
                  minV = i;
              }
          }
          if(flag == 1)
              return minV;
          else
              return ERROR;
      }
      
  • 每一对顶点之间的最短路径

    • 在稠密图中,求得每一顶点最短路径有一个算法比Dijkatra算法效率更高,也更简单,他是Floyd算法

    • Floyd算法的大概流程是

      • 找一个初始顶点H,如果某个从A到B的路径比先A到H,再H,到B的路径长,那就可以说H是A到B的一个中转顶点,然后更新A到B的最短路径
      • 依次用H结点循环所有起点到终点的路径,就可以得到初级的每一个顶点之间的最短路径。
      • 然后再找别的顶点看看能不能充当初级最短路径图的中转顶点,然后又更新初级路径图,从此往复,直到循环完所有顶点,找到每对顶点最终最短路径图,和最终中转顶点表
    • //Floyd算法求各个顶点最短路径
      //这里图用邻接矩阵储存
      void Floyd(MGraph G, WeightType dis[][MaxVertexNum], Vertex path[][MaxVertexNum])
      {
          Vertex i, j, K;	
          
          //初始化最短路径图dis,将图G复制到dis
          for(i = 0; i < G->VertexNum; i++)
              for(j = 0; j < G->VertexNum; j++)
              {
                  dis[i][j] = G->Edge[i][j];
                  //初始化中转顶点表path
                  path[i][j] = -1;
              }
          
          //循环每个中转顶点
          for(k = 0; k < G->VertexNum; k++)
              //循环每条邻接边,找到他们的中转顶点
              for(i = 0; i < G->VertexNum; i++)
                  for(j = 0; j < G->VertexNum; j++)
                  {
                      //如果i到j的路径比i到k再到j的路径长,就更新i到j的路径
                      if(dis[i][k] + dis[k][j] < dis[i][j])
                      {
                          dis[i][j] = dis[i][k] + dis[k][j];
                          //更新中转顶点
                          path[i][j] = k;
                      }
                  }
          //最终path[i][j]存的中转顶点是邻接于顶点i的
      }
      
      //利用中转顶点表来找最短路径路程
      void Find(MGraph G, path[][MaxVertexNum], Vertex VH, Vertex VE);
      {
          printf("%d ", HV);
          Vertex k = path[VH][VE];
          while(K != -1)
          {
              printf("-> %d ", k);
              k = path[k][HE];
          }
          printf("-> %d\n", HE);
      }
      

6、拓扑排序

  • 拓扑排序是对有向无环图进行排序,它的排序目的是为了排列顶点的先后顺序

  • 拓扑排序的基本步骤:找到任意一个入度为0的顶点,并从图中删除该顶点以及与其相邻的所有边,然后对改变后的图重复此操作。如果每个顶点入度都大于一,那么必定存在回路。

  • 可用队列来储存入度为0的顶点

  • //拓扑排序排列有向无环图
    //这里用邻接表表示图
    bool TopSort(LGraph G, Vertex TopOrder[])
    {
        //对图G进行拓扑排序,排序后的顶点放入TopOrder数组
        //遍历图,得到所有顶点的入度,并存入Indegree数组
        Vertex Indegree[G->VertexNum];
        Vertex V;
        AdjV W;
        for(V = 0; V < G->VertexNum; V++)
        	for(W = G->AL[V].FirstEdge; W == NULL; W = W->Next)
            {
                Indegree[W->V]++;
            }
        //创建队列,并将所有入度为0的顶点入队
        Queue Q = CreateQueue(G->VertexNum);
        for(V = 0; V < G->VertexNum; V++)
        {
            if(Indegree[V] == 0)
                AddQ(Q, V);
        }
        //核心代码
        //设置一个计数器来观察所有出过队的顶点数量
        int cnt = 0;
        //while循环来实现拓扑排序
        while(!IsEmpty(Q))	//当队列为空时说明没有入度为0的顶点,退出循环
        {
            //出队一个顶点,把它存入TopOrder数组,并将计数器加一
            V = DeleteQ(Q);
            TopOrder[cnt] = V;
            cnt++;
            //把这个顶点的邻接顶点的入度数减一,并且把入度减少后度数为0的邻接顶点入队
            for(W = G->AL[V].FirstEdge; W; W = W->Next)
            {
                Indegree[W->V]--;
                if(Indegree[W->V] == 0)
                    AddQ(Q, W->V);
            }
        }
        //如果计数器计算的顶点小于原图中顶点数量,说明原图有回路,返回false
        if(cnt < G->VertexNum)
            return false;
        else
            return tru
        //结束算法
    }
    

7、关键路径计算

  • AOE网:即边表示活动的网。

    • AOE网是一个带权的有向无环图,其中顶点表示事件,弧表示活动,权值表示为活动持续的时间
    • AOE中入度为0的点称为源点出度为0的点称为汇点
    • 用AOE网可以表示一个工程,源点表示接到任务,汇点表示完成任务,完成某一个任务就到达某一个顶点,边表示完成该任务需要的时间。
  • ETV:表示事件最早发生的时间,也就是顶点最早发生时间。

  • LTV:事件最晚需要开始时间,如果超过了这个时间就会延误工期。

  • 关键路径:ETV和LTV相同的顶点是关键顶点,因为关键顶点不能延误,从源点到汇点和经过的关键顶点形成的路径就是关键路径

  • //关键路径的计算
    //用拓扑排序可求得顶点的ETV和LTV
    //这里用邻接表储存图
    
    //拓扑排序求ETV和LTV
    bool TopSort(LGraph G, int ETV[], int LTV[])
    {
    	//建立一个TopOrder数组,用来存排序好的顶点
    	Vertex TopOrder[G->VertexNum];
    	//遍历图,得到所有顶点的入度,并存入Indegree数组
        Vertex Indegree[G->VertexNum];
    	Vertex V;
    	AdjV W;
        for(V = 0; V < G->VertexNum; V++)
        	for(W = G->AL[V].FirstEdge; W == NULL; W = W->Next)
            {
                Indegree[W->V]++;
            }
    	//创建队列,并将所有入度为0的顶点入队,更新ETV数组
        Queue Q = CreateQueue(G->VertexNum);
        for(V = 0; V < G->VertexNum; V++)
        {
            if(Indegree[V] == 0)
            {    
    			AddQ(Q, V);	
    		}
        }
    	//核心代码
        //设置一个计数器来观察所有出过队的顶点数量
        int cnt = 0;
    	//while循环来实现拓扑排序
        while(!IsEmpty(Q))	//当队列为空时说明没有入度为0的顶点,退出循环
        {
            //出队一个顶点,把它存入TopOrder数组,并将计数器加一
            V = DeleteQ(Q);
            TopOrder[cnt] = V;
            cnt++;
    		//遍历顶点V的邻接顶点,如果V的开始时间加活动时间大于邻接顶点的开始时间,则替换邻接顶点时间
    		for(W = G->AL[V].FirstEdge; W; W = W->Next)
    		{
    			if(ETV[V] + W->Weight > ETV[W->V])
    				ETV[W->V] = ETV[V] + W->Weight;
    		}	
            //把这个顶点的邻接顶点的入度数减一,并且把入度减少后度数为0的邻接顶点入队
            for(W = G->AL[V].FirstEdge; W; W = W->Next)
            {
                Indegree[W->V]--;
                if(Indegree[W->V] == 0)
                    AddQ(Q, W->V);
            }
        }
    	//如果计数器计算的顶点小于原图中顶点数量,说明原图有回路,返回false
        if(cnt < G->VertexNum)
            return false;
    	//根据TopOrder数组从最后元素开始计算LTV到下标为0的元素
    	//初始化LTV数组,将汇点值赋予LTV数组
    	for(V = 0; V < G->VertexNum; V++)
    	{
    		LTV[V] = TopOrder[G->VertexNum - 1];
    	}
    	for(V = G->VertexNum - 2; V >= 0; V--)
    	{
    		//遍历顶点V的邻接顶点,如果邻接点最晚时间减活动时间小于V的最晚时间,则替换V顶点时间
    		for(W = G->AL[V].FirstEdge; W; W = W->Next)
    		{
    			if(LTV[W->V] - W->Weight < LTV[V])
    				LTV[V] = LTV[W->V] - W->Weight;
    		}
    	}
    	//算法结束
    	return true;
    }
    
    
    //求关键路径函数,将关键顶点存入CriticalV[]数组中
    bool CriticalPath(LGraph G, Vertex CriticalV[])
    {
    	//设置ETV数组存每个顶点最早开始时间
    	//设置LTV数组存每个顶点最晚需开始时间
    	int ETV[G->VertexNum] = {0};
    	int LTV[G->VertexNum];
    	//利用拓扑排序得到ETV和LTV
    	if(TopSort(G, ETV, LTV) == false)
    		return false; 
    	//将每个顶点的ETV和LTV比较,如果相等就放入CriticalV数组
    	Vertex V;
    	int cnt = 0;
    	for(V = 0; V < G->VertexNum; V++)
    	{
    		if(ETV[V] == LTV[V])
    		{
    			CriticalV[cnt] = V;
    			cnt++;
    		}
    	}
    	return true;
    }
    

8、应用实例

  • 六度空间理论:“你和任何一个陌生人之间所间隔的人不会超过六个”。

  • 六度分隔理论的论证:在人际关系网络图G中,任意两个顶点之间都有一条路径长度不超过6的路径。可以采用广度优先算法对图G进行6层遍历,统计这些路径长度不超过6的顶点数,计算这些顶点数与所有顶点数的比例

  • #define SIX 6
    //标记顶点是否被访问的数组
    int Visited[MaxVertexNum] = {0};
    
    //以S点出发,对图G进行6次广搜,返回路径顶点数
    int SDS_BFS(LGraph G, Vertex S)
    {
    	Vertex V;
    	AdjV W;
    	//创建空队列
    	Queue Q;
    	Q = CreateQueue(MaxVertexNum);
    	//设置两个计数器,Count计数顶点数,Level计数当前层数
    	int Count = 1, Level = 0;
    	//设置一个顶点Last表示每层最后的顶点,Tail表示每层待更新的层尾顶点
    	Vertex Last, Tail;
    	Last = S;
    	//将S放入空队列
    	AddQ(Q, S);
    	//标记S;
    	Visited[S] = 1;
    
    	//利用循环广搜六次
    	while(!IsEmpty(Q))
    	{
    		//弹出V
    		V = DeleteQ(Q);
    		//将V的邻接点加入队列
    		for(W = G->AL[V].FirstEdge; W; W = W->Next)
    		{
    			if(Visited[W->V] == 0)
    			{
    				Visited[W->V] = 1;
    				//入队
    				AddQ(Q, W->V);
    				//统计人数
    				Count++;
    				//更新层尾
    				Tail = W->V;
    			}
    		}
    		//如果顶点V是层尾顶点则层数加一
    		if(V == Last)
    		{
    			Level++;
    			//更新下一层层尾顶点
    			Last = Tail;
    		}
    		//如果到了六层,那么退出循环
    		if(Level == SIX)
    			break;
    	}
    	//删除队列
    	DestoryQueue(Q);
    	return Count;
    }
    
    //检验六度空间理论
    void Six_Degrees_of_Separation(LGraph G)
    {
    	Vertex V;
    	int Count;
    	for(V = 0; V < G->VertexNum; V++)
    	{
    		Count = SDS_BFS(G, V);
    		printf("%d, %.2lf%%\n", V, 100.0 *(double)Count/ (double)G->VertexNum);
    	}
    }
    

二、感想

这一章学得真久/(ㄒoㄒ)/~~

posted on 2023-09-18 11:56  嗷呜ニャー~  阅读(11)  评论(0编辑  收藏  举报