数据结构与算法——图

  • 图的定义和术语
    • 一、图的定义和术语
      • 有向图
      • 无向图
      • 有向完全图和无向完全图
      • 稀疏图和稠密图
    • 二、基本操作
  • 图的存储结构
    • 一、图的数组(邻接矩阵)存储表示
    • 二、图的邻接表存储表示
    • 三、有向图的十字链表存储表示
    • 四、无向图的邻接多重表存储表示
  • 图的遍历
    • 一、深度优先搜索
    • 二、广度优先搜索
    • 三、遍历应用举例
      • 求一条从顶点i 到顶点s 的简单路径
      • 求两个顶点之间的一条路径长度短的路 径
  • 生成树
    • (连通网的)最小生成树
      • 解决方案一:普里姆算法
      • 解决方案二:克鲁斯卡尔算法
      • 两种算法比较
  • 拓扑排序
  • 关键路径
  • 两点之间的最短路径问题
    • 求从某个源点到其余各点的短路径
    • 每一对顶点之间的短路径

图的定义和术语

一、图的定义和术语

  • 图(Graph)——图G是由两个集合V(G)和E(G)组成的。记为G=(V,E)

  • 其中:V(G)是顶点的非空有限集

  • E(G)是边的有限集合,边是顶点的无序对或有序对

  • 权——与图的边或弧相关的树叫权

  • 网)带权的图叫做网

  • 子图——如果图G(V,E)和图G1(v1,E1),满足:
    在这里插入图片描述
    数据结构与算法——图_第1张图片

  • 弧或边带权的图分别称为有向网或无向网
    数据结构与算法——图_第2张图片

  • 对无向图来讲,假若顶点v和顶点w之间存在一条边,则称顶点v和w互为邻接点,边(v,w)和顶点v和w相关联

  • 顶点的度
    在无向图中,顶点的度为与每个顶点相连的边数
    有向图中,顶点的度分为入度和出度
    入度:以该顶点为头的弧的数目
    出度:以该顶点为尾的弧的数目
    数据结构与算法——图_第3张图片
    数据结构与算法——图_第4张图片

  • 路径——路径是顶点的序列,顶点V到顶点V’的路径{V=Vi,0,Vi,1,……Vi,n=V’},

  • 其中属于E,(1

  • 路径长度—— 路径上的边或弧的数目

  • 回路——第一个顶点和最后一个顶点相同的路径

  • 简单路径——序列中的顶点不重复出现的路径

  • 简单回路——除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路
    数据结构与算法——图_第5张图片
    数据结构与算法——图_第6张图片

  • 连通——从顶点V到顶点W有一条路径,则说V和W是连通的

  • 连通图——图中任意两个顶点都是连通的

  • 连通分量——非连通图的每一个连通部分

  • 强连通图——有向图中,如果对每一对Vi,Vj属于V,Vi不等于Vj.从Vi到Vj和从Vj到Vi都存在路径,则称G是强连通图

  • 强连通分量——有向图中的极大连通子图
    数据结构与算法——图_第7张图片
    数据结构与算法——图_第8张图片
    例:
    数据结构与算法——图_第9张图片

  • 假设设一个连通图有n 个顶点和e 条边,其中 n-1 条边和n 个顶点构成一个极小连通子图, 称该极小连通子图为此连通图的生成树

  • 对非连通图,则称由各个连通分量的生成树 的集合为此非连通图的生成森林

数据结构与算法——图_第10张图片
数据结构与算法——图_第11张图片

有向图

  • 有向图——有向图G是由两个集合V(G)和E(G)组成的
  • 其中:V(G)是顶点的非空有限集
  • E(G)是有向边(也称弧)的有限集合,弧是顶点的有序对,记为,vw是顶点,v是弧尾w是弧头

由于"弧“是有方向的,因此称有顶点集和弧集构成的图称为有向图

数据结构与算法——图_第12张图片

  • 对有向图来说
    顶点的出度:以顶点v为弧尾的弧的数目
    顶点的入度:以顶点v为弧头的弧的数目
    数据结构与算法——图_第13张图片
    例如:
    数据结构与算法——图_第14张图片

无向图

  • 无向图——无向图G是由两个集合V(G)和E(G)组成的
  • 其中:V(G)是顶点的非空有限集
  • E(G)是边的有限集合,边是顶点的无序对,记为(v,w)或(w,v),并且(v,w)=(w,v)

属于E(G)必有属于E(G), 则称(v,w) 为顶点v 和顶点w 之间存在一条边

由顶点集和边集构成的图称作无向图

数据结构与算法——图_第15张图片
数据结构与算法——图_第16张图片

有向完全图和无向完全图

假设图中有n个顶点,e条边:

  • 有向完全图——含e=n(n-1)条弧的有向图
  • 无向完全图——含e=n(n-1)/2条边的无向完全图
    数据结构与算法——图_第17张图片

稀疏图和稠密图

假设图中有n个顶点,e条边:
若边或弧的个数e

二、基本操作

CreateGraph(&G,V,VR);   //按定义(V,VR)构造图

DestroyGraph(&G);   //销毁图

LocateVex(G,u);     //若G中存在顶点u 则返回该顶点在图中位置,否则返回其他信息

GetVex(G,V);        返回V的值

PutVex(&G,V,value);     //对v赋值value

// 在一个图中,顶点是没有先后次序的,
// 但当采用某一种确定的存储方式存储后,
// 存储结构中的顶点存储次序构成顶点之间的相对次序

FirstAdjVex(G,v);   //返回v的第一个邻接点 若该顶点
                    //在G中没有邻接点 则返回空
            
NextAdjVex(G,v,w);
                //返回v的(相对于w的)下一个邻接点
                // 若w是v的最后一个邻接点 则返回空

InsertVex(&G,v);    //在图G中增加新顶点v

DeleteVex(&G,v);        //删除G中顶点v及其相关的弧

InsertArc(&G,v,w);      //在G中增添 若G是无向的则删除对称弧

DFSTraverse(G,v,Visit()); //从顶点v起深度优先遍历图G 并对每个顶点调用函数Visit一次且仅一次

BFSTraverse(G,v,Visit());   //从顶点v起广度优先遍历图G 并对每个顶点调用函数Visit一次且仅一次

图的存储结构

一、图的数组(邻接矩阵)存储表示

定义:矩阵的元素为
数据结构与算法——图_第18张图片
有向图的邻接矩阵为非对称矩阵

特点:

  • 无向图的邻接矩阵对称,可压缩存储;有n个顶点 的无向图需存储空间为n(n+1)/2
  • 有向图邻接矩阵不一定对称;有n个顶点的有向图 需存储空间为n²
  • 无向图中顶点Vi的度TD(Vi)是邻接矩阵A中第i行“1” 的个数之和
  • 有向图中,顶点Vi的出度是A中第i行“1”的个数之 和
  • 顶点Vi的入度是A中第i列“1”的个数之和

网的邻接矩阵定义:
数据结构与算法——图_第19张图片
邻接矩阵表示法的优缺点:
优点:容易实现图的操作,如:求某顶点的度、判 断顶点之间是否有边、找顶点的邻接点等等

缺点::n个顶点需要 n*n 个单元存储边;空间效率为 O(n2)。对稀疏图而言尤其浪费空间。

邻接矩阵实现:

typedef struct AreCell { //弧的定义
    VRType adj;     //VRType是顶点关系类型
            //对无权图 用1或0表示是否相邻
            //对有权图 则为权值类型
    InfoType *info; //  该弧相关信息的指针

}ArcCell;

AdjMatrix[MAX_VERTEX_NUM] [MAX_VERTEX_NUM];

typedef struct {    //图的定义
    VertexType  vexs[MAX_VERTEX_NUM]; //    顶点信息
    AdjMatrix arcs;         //邻接矩阵
    int vexnum,arcnum;      //顶点数,弧数
    GraphKind kind;         //图的种类标志

}MGraph;


//邻接矩阵的存储表示
//用两个数组分别存储顶点表和邻接矩阵
#define MaxInt 32767            //表示极大值 即∞
#define MAX_VERTEX_NUM100      //最大顶点数 
typedef char VerTexType;       //假设顶点的数据类型为字符型
typedef int VRType; (最简单的)    //假设边的权值类型为整型

typedef struct{ 
        VerTexType vexs[MVNum];    //顶点表 
        AdjMatrix arcs; //邻接矩阵 
        //或ArcCell arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; 
        //VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
        int vexnum,arcnum;     //图的当前顶点数和边数 
}AMGraph; 

采用邻接矩阵表示法创建无向网:
算法思想:

  1. 输入总顶点数和总边数
  2. 依次输入点的信息存入顶点表中
  3. 初始化邻接矩阵,使每个权值初始化为极大值
  4. 构造邻接矩阵

算法描述:

Status CreateUDN(AMGraph &G) {
    //采用邻接矩阵表示法,创建无向网G
    cin>>G.vexnum>>G.arcnum;        //输入总顶点数 总边数
    for(i = 0;i<G.vexnum; ++i) 
        cin>>G.vexs[i];             //依次输入顶点的信息
    for(i = 0;i<G.vexnum; ++i)      //初始化邻接矩阵 边的权值均置为极大值
        for(j = 0; j<G.vexnum; ++j)
            G.arcs[i][j] = MaxInt;
    for(k = 0; k<G.arcnum; ++k) {       //构造邻接矩阵
        cin>>v1>>v2>>w;                 //输入一条边依附的顶点和权值
        i = LocateVex(G,v1);
        j = LocateVex(G,v2);            //确定v1和v2在G中的位置
        G.arcs[i][j].adj = w;       //边的权值置为w
        G.arcs[j][i].adj = G.arcs[i][j].adj;    //置的对称边的权值为w 
    }
    ewruen OK;
}//CreateUDN 

int LocateVex(MGraph G,VertexType u) {
    //存在则返回u在顶点表中的下标;否则返回-1 
    int i; 
    for(i=0;i<G.vexnum;++i) 
        if(u==G.vexs[i]) 
            return i; 
        return -1; 
}

二、图的邻接表存储表示

数据结构与算法——图_第20张图片
有向图的邻接表:

数据结构与算法——图_第21张图片

有向图的逆邻接表:
在有向图的逆邻接 表中,对每个顶点,链接的是指向该顶点的弧。

特点:

  • 表结点与边数的关系
  • 无向图中顶点Vi的度为第i个单链表中的结点数
  • 有向图中:
    • 顶点Vi的出度为第i个单链表中的结点个数
    • 顶点Vi的入度为整个单链表中邻接点域值是i 的结点个数

邻接表的实现:
在这里插入图片描述

typedef struct ArcNode {
    int adjvex;     //该弧所指向的顶点的位置
    struct ArcNode *nextarc; // 指向下一条弧的指针 
    InfoType  *info;   // 该弧相关信息的指针 
}ArcNode;

在这里插入图片描述

typedef structVNode {
    VertexType  data;   // 顶点信息 
    ArcNode  *firstarc; // 指向第一条依附该顶点的弧 

}VNode, AdjList[MAX_VERTEX_NUM];


在这里插入图片描述

typedef struct { 
    AdjList  vertices; 
    int vexnum, arcnum; 
    intkind;          // 图的种类标志 

}ALGraph;

采用邻接表表示法创建无向网:
算法思想:

  1. 输入总顶点数和总边数
  2. 依次输入点的信息存入顶点表中,使每个表头结 点的指针域初始化为NULL
  3. 创建邻接表

算法描述:

Status CreateUDG(ALGraph &G) {
    //采用邻接表表示法 创建无向图G
    cin>>G.vexnum>>G.arcnum;        //输入总顶点数 总边数
    for(i = 0;i<G.vexnum; ++i) {    //输入各点 构造表头结点
        cin>>G.vertices[i].data;    //输入顶点值
        G.vertices[i].firstarc = NULL;  //初始化表头结点的指针域为NULL

    }
    for(k = 0;k<G.arcnum;++k) { //输入各边 构造邻接表 
        cin>>v1>>v2;            //输入一条边依附的两个顶点
        i = LocateVex(G,v1);
        j = LocateVex(G, v2); 
        p1=new ArcNode;               //生成一个新的边结点*p1 
        p1->adjvex=j;                   //邻接点序号为j 
        p1->nextarc= G.vertices[i].firstarc;  G.vertices[i].firstarc=p1; 
        //将新结点*p1插入顶点vi的边表头部 
        p2=new ArcNode; //生成另一个对称的新的边结点*p2 
        p2->adjvex=i;                   //邻接点序号为i 
        p2->nextarc= G.vertices[j].firstarc;  G.vertices[j].firstarc=p2;  
        // /将新结点*p2插入顶点vj的边表头部 

    }
    return OK;
}

邻接表表示法的优缺点:
优点:空间效率高,容易寻找顶点的邻接点
缺点:判断两顶点间是否有边或弧,需要搜索两结点对应的单链表,没有邻接矩阵方便

邻接矩阵与邻接表表示法的关系:

  1. 联系:邻接表中每个链表对应于邻接矩阵中的一行, 链表中结点个数等于一行中非零元素的个数。
  2. 区别:
    对于任一确定的无向图,邻接矩阵是唯一的(行列 号与顶点编号一致),但邻接表不唯一(链接次序 与顶点编号无关)。
    邻接矩阵的空间复杂度为O(n2),而邻接表的空间复 杂度为O(n+e)。
  3. 用途:邻接矩阵多用于稠密图;而邻接表多用于稀 疏图

三、有向图的十字链表存储表示

数据结构与算法——图_第22张图片

typedef struct ArcBox { //弧的结构表示
    int tailvex.headvex;
    InfoType *info;
    struct ArcBox *hlink,*tlink;

}ArcBox;

数据结构与算法——图_第23张图片

typedef struct VexNode {    //顶点的结构表示
    VeertexType data;
    ArcBox *firstin,*firstout;
}VexNode;

有向图的结构表示(十字链表)

typedef struct {
    VexNode xlist[MAX_VERTEX_NUM];
    // 顶点结点(表头向量)
    int vexnum, arcnum; //有向图的当前顶点数和弧数 
}OLGraph;

数据结构与算法——图_第24张图片

四、无向图的邻接多重表存储表示

边的结构表示:

typedef struct EBox {
    VisitIf mark;       //访问标记
    int ivex,jvex;      //该边依附的两个顶点的位置
    struct EBox *ilink,*jlinkl;     //分别指向依附这两个顶点的下一条边
    InfoType     *info;          // 该边信息指针 
}

顶点的结构表示:

typedef struct VexBox { 
    VertexType  data;
    EBox  *firstedge; // 指向第一条依附该顶点的边 

}VexBox;

无向图的结构表示:

typedef struct {  // 邻接多重表
    VexBox  adjmulist[MAX_VERTEX_NUM]; 
    int vexnum, edgenum;    
    
}AMLGraph;

数据结构与算法——图_第25张图片

图的遍历

从图中某个顶点出发游历图,访遍 图中其余顶点,并且使图中的每个顶点 仅被访问一次的过程。

一、深度优先搜索

连通图的深度优先搜索遍历
从图中某个顶点V0 出发,访问此顶 点,然后依次从V0的各个未被访问的邻接 点出发深度优先搜索遍历图,直至图中所 有和V0有路径相通的顶点都被访问到

数据结构与算法——图_第26张图片

从深度优先搜索遍历连通图的过 程类似于树的先根遍历
如何判别V的邻接点是否被访问:

解决的办法是:为每个顶点设立一 个“访问标志visited[w]”

void DFS(Graph G,int v) {
    //从第v个顶点出发,深度优先搜索遍历连通图G 
    visited[v] = TRUE;
    VisitFunc(v);
    for(w = FirstAdjVex(G,v);w>=0; w=NextAdjVex(G,v,w)) 
        if (!visited[w])  DFS(G, w); // 对v的尚未访问的邻接顶点w // 递归调用DFS 

}

非连通图的深度优先搜索遍历

首先将图中每个顶点的访问标志设 为FALSE, 之后搜索图中每个顶点,如 果未被访问,则以该顶点为起始点,进 行深度优先搜索遍历,否则继续检查下 一顶点。

bool visited[MAX];// 访问标志数组 

Status (*VisitFunc)(int v);
//函数变量,是 一个静态分配的指针,VisitFunc指向一 个函数,
//这个函数原型为Status fun(int v),即返回类型为Status,
//形参类型为 int,VisitFunc是定义的指向具有这种函数 原型的指针。

void DFSTraverse(Graph G, Status(*Visit)(int v)){
     // 对图G 作深度优先遍历
     VisitFunc = Visit;   //使用全局变量VisitFunc,使DFS不必设函数指针参数 
     for(v=0; v<G.vexnum; ++v) 
        visited[v] = FALSE; // 访问标志数组初始化 
    for(v=0; v<G.vexnum; ++v) 
        if(!visited[v])  
            DFS(G, v); // 对尚未访问的顶点调用DFS
}

时间复杂度分析:

遍历图的过程实质上是对每个顶点查找其邻 接点的过程。其耗费的时间取决于所采用的 存储结构。当用邻接矩阵做存储结构时,查 找每个顶点的邻接点所需时间为O(n^2),当 以邻接表做存储结构时,找邻接点所需时间 为O(e)。因此,当以邻接表作存储结构时,深 度优先搜索遍历图的时间复杂度为O(n+e)

注意:

  • 每次调用前一定要判断起点的visited标志
  • 这个算法还可以求图G的连通性、连通分量 的个数以及每个连通分量的顶点个数(注 意是无向图)

二、广度优先搜索

数据结构与算法——图_第27张图片

从图中的某个顶点V0出发,并在访问此 顶点之后依次访问V0的所有未被访问过的 邻接点,之后按这些顶点被访问的先后次 序依次访问它们的邻接点,直至图中所有 和V0有路径相通的顶点都被访问到。 若此时图中尚有顶点未被访问,则另选 图中一个未曾被访问的顶点作起始点,重 复上述过程,直至图中所有顶点都被访问 到为止。

void BFSTraverse(Graph G,Status (*Visit)(int v)) {
    for(v=0; v<G.vexnum; ++v)
        visited[v] = FALSE;  //初始化访问标志 
    InitQueue(Q);// 置空的辅助队列Q
    for( v=0;  v<G.vexnum;  ++v )
        if (!visited[v]){// v 尚未访问
            visited[v] = TRUE;  Visit(v);    // 访问v 
            EnQueue(Q, v);// v入队列 
            while(!QueueEmpty(Q))  {
                DeQueue(Q, u);        // 队头元素出队并置为u 
                for(w=FirstAdjVex(G, u); w>=0; w=NextAdjVex(G,u,w)) 
                    if ( ! visited[w]){ 
                        visited[w]=TRUE;  Visit(w); 
                        EnQueue(Q, w);// 访问的顶点w入队列 
                    } // if 
            } // while
        }   
}

时间复杂度分析:

遍历图的过程实质上是通过边或弧找邻接点 的过程,因此广度搜索遍历图的时间复杂度 和深度优先搜索遍历相同,两者不同之处仅 仅在于对顶点的访问顺序不同。

三、遍历应用举例

求一条从顶点i 到顶点s 的简单路径

数据结构与算法——图_第28张图片

结论:

  1. 从顶点i 到顶点s ,若存在路径,则从 顶点i 出发进行深度优先搜索,必能搜索 到顶点s
  2. 遍历过程中搜索到的顶点不一定是路 径上的顶点
  3. 由它出发进行的深度优先遍历已经完 成的顶点不是路径上的顶点需要被删除
voidDFSearch( int v, int s, char*PATH)  { 
    // 从第v个顶点出发递归地深度优先遍历图G, 
    // 求得一条从v到s的简单路径,并记录在PATH中 
    visited[v] = TRUE;   // 访问第v 个顶点 
    Append(PATH, getVertex(v));// 第v个顶点加入路径 
    for(w=FirstAdjVex(v);  w>=0&&!found; w=NextAdjVex(v) ) 
        if(w=s) {
            found = TRUE;  
            Append(PATH, w); 
            Output(PATH);
        }
        else if (!visited[w])  
            DFSearch(w, s, PATH); 
        if (!found)  Delete (PATH,v);// 从路径上删除第v个顶点
}

求两个顶点之间的一条路径长度短的路 径

若两个顶点之间存在多条路径,则其中必有 一条路径长度短的路径。如何求得这条 路径?
数据结构与算法——图_第29张图片
数据结构与算法——图_第30张图片

  1. 将链队列的结点改为“双链”结点。即 结点中包含next 和prior两个指针
  2. 修改入队列的操作。插入新的队尾结点 时,令其prior域的指针指向刚刚出队列的 结点,即当前的队头指针所指结点;
  3. 修改出队列的操作。出队列时,仅移动 队头指针,而不将队头结点从链表中删除。
typedef DuLinkList QueuePtr; 
void InitQueue(LinkQueue &Q) {
    Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode));
    Q.front->next = Q.rear->prior = NULL;
}

void EnQueue( LinkQueue &Q, QelemType e ) { 
    p = (QueuePtr) malloc(sizeof(QNode)); 
    p->data = e;  p->next = NULL;
    p->prior = Q.front;
    Q.rear->next = p;  Q.rear = p;
}

void DeQueue( LinkQueue &Q, QelemType &e ) { 
    Q.front = Q.front->next;  
    e = Q.front->data
}

void BFSSearch(int v,int s){ 
    for(v=0; v<G.vexnum; ++v) 
    visited[v] = FALSE;  //初始化访问标志 
    InitQueue(Q);// 置空的辅助队列Q 
    if (!visited[v]){// v 尚未访问 
        visited[v] = TRUE;  
        Visit(v);    // 访问v 
        EnQueue(Q, v);// v入队列 
        while(!QueueEmpty(Q))  { 
            DeQueue(Q, u);      // 队头元素出队并置为u 
            for(w=FirstAdjVex(G, u); w>=0&&!found;   w=NextAdjVex(G,u,w)) 
                if ( ! visited[w]){ 
                    visited[w]=TRUE;  
                    Visit(w); 
                    EnQueue(Q, w);// 访问的顶点w入队列 
                    if (w==s) {
                        outshortpath(Q);
                        found=TRUE;
                    } 
                } // if 
                if (found) 
                    break; 
        } // while 
    }
}

生成树

定义:
所有顶点均由边连接在一起,但不 存在回路的图叫生成树
深度优先生成树与广度优先生成树
生成森林:
非连通图每个连通分量的生成 树一起组成非连通图的生成森林。

说明:

  • 一个图可以有许多棵不同的生成树
  • 所有生成树具有以下共同特点:
    • 生成树的顶点个数与图的顶点个数相同
    • 生成树是图的极小连通子图
    • 一个有n个顶点的连通图的生成树有n-1条边
    • 生成树中任意两个顶点间的路径是唯一的
    • 在生成树中再加一条边必然形成回路
    • 含n个顶点n-1条边的图不一定是生成树

(连通网的)最小生成树

假设要在n 个城市之间建立通讯 联络网,则连通n 个城市只需要修建 n-1条线路,如何在最节省经费的前 提下建立这个通讯网?

该问题等价于:

构造网的一棵小生成树,即:在e 条 带权的边中选取n-1 条边(不构成回路), 使“权值之和”为小

解决方案一:普里姆算法

算法的基本思想:

取图中任意一个顶点v 作为生成树的根, 之后往生成树上添加新的顶点w。在添加 的顶点w 和已经在生成树上的顶点v 之间 必定存在一条边,并且该边的权值在所有 连通顶点v 和w 之间的边中取值最小。之 后继续往生成树上添加顶点,直至生成树 上含有n-1 个顶点为止。

数据结构与算法——图_第31张图片
一般情况下所添加的顶点应满足下列条件:
在生成树的构造过程中,图中n 个 顶点分属两个集合:已落在生成树上的 顶点集U和尚未落在生成树上的顶点集 V-U,则应在所有连通U中顶点和V-U中 顶点的边中选取权值最小的边

算法步骤:

  • 设N=(V,{E})是连通网,TE是N上小生成树 中边的集合
  • 1、初始令U={u0},(u0属于V), TE为空
  • 2、在所有u属于U,v属于V-U的边(u,v)属于E中,找一 条代价小的边(u0,v0)
  • 3、将(u0,v0)并入集合TE,同时v0并入U
  • 4、重复2和3步直至U=V为止,则T=(V,{TE}) 为N的小生成树

算法实现:使用邻接矩阵表示图

//设置一个辅助数组,对当前V-U集 中的每个顶点,
//记录和顶点集U中顶点 相连接的代价最小的边:

struct { 
    VertexType  adjvex;  // U集中的顶点序号 
    VRType     lowcost;  // 边的权值
}closedge[MAX_VERTEX_NUM];

void MiniSpanTree_P(MGraph G, VertexType u) { 
    //用普里姆算法从第u个顶点出发构造网G的最小生成树
    k = LocateVex ( G, u ); 
    for ( j=0; j<G.vexnum; ++j )  // 辅助数组初始化 
        if(j!=k) 
            closedge[j] = { u, G.arcs[k][j].adj }; 
    closedge[k].lowcost = 0;      // 初始,U={u} 
    for(i=0; i<G.vexnum; ++i) {
        // 继续向生成树上添加顶点
        k = minimum(closedge);  // 求出加入生成树的下一个顶点(k) 
        printf(closedge[k].adjvex, G.vexs[k]); // 输出生成树上一条边 
        closedge[k].lowcost = 0;    // 第k顶点并入U集 
        for(j=0; j<G.vexnum; ++j) //修改其它顶点的最小边
            f (G.arcs[k][j].adj < closedge[j].lowcost) 
                closedge[j] = { G.vexs[k], G.arcs[k][j].adj };
    }
}

时间复杂度分析:

假设网中有n个顶点,则第一个进行初始化的 循环语句的频度为n,第二个循环语句的频度 为n-1。其中有两个内循环:其一是在 closedge[v].lowcost中求小值,其频度为n-1 ;其二是重新选择具有小代价的边,其频 度为n。由此,普里姆算法的时间复杂度为 O(n^2),与网中的边数无关,因此适用于求 边稠密的网的小生成树。
算法评价:T(n)=O(n²)

解决方案二:克鲁斯卡尔算法

考虑问题出发点:
为使生成树上边的权 值之和达到最小,则应使生成树中每一条 边的权值尽可能地小。

具体做法:
先构造一个只含n 个顶点的子图 SG,然后从权值最小的边开始,若它的添 加不使SG 中产生回路,则在SG 上加上这 条边,如此重复,直至加上n-1 条边为止。

算法描述:

构造非连通图ST=( V,{ } ); 

k = i = 0;    // k 计选中的边数 
while(k<n-1) { 
    ++i; 
    检查边集E 中第i 条权值最小的边(u,v);(u,v)加入ST后不使ST中产生回路, 
    则输出边(u,v);且k++; 
}

算法的时间复杂度:

克鲁斯卡尔算法的时间复杂度为O(eloge),适 合于求边稀疏的网的小生成树。

两种算法比较

Prim算法:归并顶点,与边数无关,适于稠密网
Kruskal算法:归并边,适于稀疏网

拓扑排序

假设以有向图表示一个工程的施工图或程序 的数据流图,则图中不允许出现回路。 检查有向图中是否存在回路的方法之一,是 对有向图进行拓扑排序。

什么是拓扑排序:

对有向图进行如下操作:按照有向图给出的次序关系,将图中顶 点排成一个线性序列,对于有向图中没有 限定次序关系的顶点,则可以人为加上任 意的次序关系。

一个AOV网的拓扑序列不是唯一的
数据结构与算法——图_第32张图片
这种用顶点表示活动,用弧表示活动间的 优先关系的有向图称为顶点表示活动的网 (Activity On Vertex Network),简称AOV网

如何进行拓扑排序:

  1. 从有向图中选取一个没有前驱 的顶点,并输出之
  2. 从有向图中删去此顶点以及所 有以它为尾的弧
  3. 重复上述两步,直至图空,或者图不空但找 不到无前驱的顶点为止。

数据结构与算法——图_第33张图片
算法描述:

取入度为零的顶点v; 

while (v<>0){ 
    printf(v);  
    ++m; 
    w=FirstAdj(v); 
    while (w<>0) { 
        inDegree[w]--; 
        
        w=nextAdj(v,w); 
    } 
    取下一个入度为零的顶点v; 
} 
if m<n  printf(“图中有回路”);

说明:

  • 以邻接表作存储结构
  • 为避免每次都要搜索入度为零的顶点,在算法 中设置一个“栈”,以保存“入度为零”的顶点

算法实现如下:

Status TopologicalSort(ALGraph G){ 
    FindInDegree(G,indegree); //对各顶点求入度 
    InitStack(S); 
    for( i=0; i<G.vexnum; ++i) 
        if (!indegree[i])  Push(S, i); 
            //入度为零的顶点入栈
            count=0;      //对输出顶点计数 
            while(!EmptyStack(S)) { 
                Pop(S, i); printf(i,G.vertices[i].data);++count;  
                for(p=G.vertices[i].firstarc;p;p=p->nextarc){ 
                    k=p->adjvex;  //取出弧头顶点在图中的位置 
                    if(!(--indegree[k])  Push(S, k); 
                    //弧头顶点的入度减一,新产生的入度为零的顶点入 栈
                } 
            } 
            if(count<G.vexnum) 
                return ERROR;   //图中有回路”) 
            else 
                return OK; 
}

关键路径

假设以有向网表示一个施工流图,弧上的权 值表示完成该项子工程所需时间。 问:哪些子工程项是“关键工程”? 即:哪些子工程项将影响整个工程的完成期限?

整个工程完成的时间为:从有向图的源点到汇点 的最长路径

数据结构与算法——图_第34张图片

这种用边表示活动的网,称AOE网。AOE网 是一个带权的有向无环图,其中,顶点表 示事件,权表示活动持续的时间

如何求关键活动:

  • “事件(顶点)” 的最早发生时间ve(j) ve(j) = 从源点到顶点j的最长路径长度
  • “事件(顶点)” 的最迟发生时间vl(k) vl(k) =在保证汇点在ve(汇点) 时刻完成 的前提下,事件k允许的最迟开始时间
假设第i 条弧为<j, k> 
则对第i 项活动而言 
“活动()”的最早开始时间ee(i) 
    ee(i) = ve(j); 
    “活动()”的最迟开始时间el(i) 
    el(i) = vl(k)dut(<j,k>);

事件发生时间的计算公式: 
    ve(源点) = 0ve(k) = Max{ve(j) + dut(<j, k>)}

vl(汇点) = ve(汇点)vl(j) = Min{vl(k)dut(<j, k>)}

数据结构与算法——图_第35张图片

数据结构与算法——图_第36张图片

算法的实现要点:

  • 显然,求ve的顺序应该是按拓扑有序的次 序
  • 而求vl的顺序应该是按拓扑逆序的次序
  • 因为拓扑逆序序列即为拓扑有序序列的 逆序列
  • 因此应该在拓扑排序的过程中, 另设一个“栈”记下拓扑有序序列

两点之间的最短路径问题

求从某个源点到其余各点的短路径

算法的基本思想:

依最短路径的长度递增的次序求得 各条路径

路径长度最短的最短路径的特点:
在这条路径上,必定只含一条弧,并且 这条弧的权值最小。
下一条路径长度次短的最短路径的特点:
它只可能有两种情况:或者是直接从源 点到该点(只含一条弧);或者是从源点经 过顶点v1,再到达该顶点(由两条弧组成)。

再下一条路径长度次短的最短路径的特点:
它可能有三种情况:或者是直接从源点到 该点(只含一条弧);或者是从源点经过顶点 v1,再到达该顶点(由两条弧组成);或者是 从源点经过顶点v2,再到达该顶点。

其余最短路径的特点:
它或者是直接从源点到该点(只含一条 弧);或者是从源点经过已求得最短路径 的顶点,再到达该顶点。

求最短路径的迪杰斯特拉算法:
设置辅助数组Dist,其中每个分量Dist[k] 表示当前所求得的从源点到其余各顶点k 的最短路径

一般情况下, Dist[k] = <源点到顶点k 的弧上的权值> 或者= <源点到其它顶点的路径长度> +<其它顶点到顶点k 的弧上的权值>。

数据结构与算法——图_第37张图片

每一对顶点之间的短路径

弗洛伊德算法的基本思想是:
从v i到v j的所有 可能存在的路径中,选 出一条长度最短的路径。

数据结构与算法——图_第38张图片
以此类推,则vi至vj的最短路径应该是上述这些路径中,路径长度最小者。

你可能感兴趣的:(数据结构与算法)