《大话数据结构》第七章 图


第七章 图

图的定义

定义:图是由顶点的有穷非空集合顶点之间边的集合组成,通常表示为:G(V, E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

注意

  1. 图中的数据元素,称为顶点(Vertex)
  2. 图结构中不允许没有顶点
  3. 任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边表示

无向边:两个顶点之间的边没有方向,就称这边为无向边(Edge),用无序偶对 ( v i , v j ) (v_i,v_j) (vi,vj)表示。
如果所有的顶点之间都是无向边,则这个图为无向图(Undirected graphs)。

有向边:顶点间的边有方向,则称这条边为有向边,也成为弧(Arc)。
如果所有的边都是有向边,则该图称为有向图(Directed graphs)。
由A到D的有向边,A是弧尾,D是弧头,<A, D>表示弧,不可写成<D, A>

注意:无向边是小括号(),有向边是尖括号 <>

简单图:若不存在顶点到其自身的边,且同一条不重复出现,则称这样的图为简单图,下面这两个都不是:

无向完全图:如果无向图中任意两个顶点之间都存在边,则称该图为无向完全图。
n个顶点的无向完全图有 n × ( n − 1 ) 2 \frac{n\times (n-1)}{2} 2n×(n1)条边。

有向完全图:如果有向图中任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。
n个顶点的有向完全图有 n × ( n − 1 ) n\times (n-1) n×(n1)条边。

稠密图:很多条边或弧的图。
稀疏图:很少条边或弧的图。

:有些图的边或弧具有相关数字,这种与图的边或弧相关的数叫做权(Weight)。

:这些权可以表示点之间的距离或耗费,带权的图通常称为网(Network)。

子图:A被B包含,则A是B的子图,下面的右边都是左边的子图。


图的顶点和边间关系

无向的邻接和依附:如果两个点 ( v , v ′ ) (v,v') (v,v)之间有边,则称这两个顶点互为邻接点(Adjacent),即 v , v ′ v,v' v,v相邻接。边依附于顶点 v , v ′ v,v' v,v,或者说边和顶点相关联。

顶点的度:和该顶点相关联的边的数目。
边的数量就是各顶点度数和的一半。

有向的邻接:如果有弧 < v , v ′ > < v,v' > <v,v>存在,则称顶点 v v v邻接到顶点 v ′ v' v,顶点 v ′ v' v邻接自顶点 v v v(以头为主)。弧和顶点相关联。

入度:以顶点v为头的弧的数目称为v的入度(InDegree),记为ID(v)。

出度:以v为结尾的弧的数目称为v的出度(OutDegree),记为OD(v)。

:顶点的度 TD = ID + OD

无向图的路径:从顶点 v v v v ′ v' v的路径是一个顶点序列 ( v = v i , 0 , v i , 1 , . . . , v i , m = v ′ ) (v=v_{i,0},v_{i,1},...,v_{i,m} = v') (v=vi,0,vi,1,...,vi,m=v),其中 ( v i , j − 1 , v i , j ∈ E , 1 ≤ j ≤ m ) (v_{i,j-1},v_{i,j} \in E, 1 \le j \le m) (vi,j1,vi,jE,1jm)

B到D四种路径:

有向图的路径:路径也是有向的,B到D只有两种路径

有向图中根结点到任意结点的路径是唯一的。

路径的长度:路径上的边或弧的数目。

回路/环:第一个顶点到最后一个顶点相同的路径。

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

简单回路/环:除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。

途中的粗线都是环,左侧是简单环,右侧因为C顶点重复出现所以不是简单环。


连通图相关术语

连通图:无向图中,如果顶点之间有路径,那么称这两个顶点是连通的。如果对于图中的任意顶点都是连通的,则称该图为连通图(Connected Graph)。

左边不是连通图,右边是:

连通子图:无向图中的极大连通子图成为连通分量,要注意:

  1. 要是子图
  2. 子图要是连通的
  3. 连通子图含有极大顶点数
  4. 具有极大顶点数的连通子图包含依附于这些顶点的所有边

下面图2和图3是图1的连通分量,图4虽然是图1的子图,但是不满足连通子图的极大的顶点数

非连通图可以有连通分量

极大连通子图:包含了原图和子图顶点关联的所有边
极小连通子图:包含子图连通必不可少的边。

强连通图:有向图中,如果每一对顶点间都存在路径,则称G十强连通图。

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

下面图1不是强连通图,图2是图1的极大连通子图

连通图的生成树:一个连通图的生成树是一个极小的连通子图,它含有图中全部的n各顶点,但只有足以构成一棵树的n-1条边。


图1不是,因为边太多了,还可以砍掉一些;
图2和图3是;
图4虽然是n-1条边,但是不连通。

有向树:如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树。

有向图的生成森林:由若干棵有向数组成,含有图中全部顶点,但是只有构成有向树不相交必须个数的弧。

图1是有向图,去掉一些弧后,分解为两棵有向树,右边的两棵有向树就是图1有向图的生成森林。


图的存储结构

图的抽象数据类型


邻接矩阵

结构:图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧信息。

nxn,右边就设为1,没有就为0,这样不也是浪费很多空间吗

无向图

无向图的边数组是一个对称矩阵
优点:容易判定两点间有无边;容易计算度;

有向图

有向图的矩阵并不对称。
顶点 v 1 v_1 v1的入度是第 v 1 v_1 v1列各数之和,出度是第 v 1 v_1 v1行个数之和。

有权图

邻接矩阵存储结构

typedef char VertexType;
typedef int EdgeType;
#define MAXVEX 100
#define INFINITY 65535    // 用65535表示无穷

typedef struct MGraph
{
    VertexType vexs[MAXVEX];    // 顶点表
    EdgeType arc[MAXVEX][MAXVEX];    // 邻接矩阵
    int numVertexes, numEdges;    // 当前顶点数和边数
} MGraph;

创建无向网图

// 建立无向网图的邻接矩阵表示
void CreateMGraph(MGraph *G)
{
    int i, j, k, w;
    printf("输入顶点数和边数:\n");
    scanf("%d, %d", &G->numVertexes, &G->numEdges);
    for (i=0; i < G->numVertexes; i++)    // 读入顶点信息,建立顶点表
        scanf(&G->vexs[i]);
    for (i=0; i < G->numVertexes; i++)    // 初始化邻接矩阵
        for (j=0; j < G->numEdges; j++)
            G->arc[i][j] = INFINITY;
    for (k=0; k < G->numEdges; k++)    // 建立邻接矩阵
    {
        printf("输入前后下标i,j和权重w");
        scanf("%d, %d, %d", &i, &j, &w);
        G->arc[i][j] = w;
        G->arc[j][i] = G->arc[i][j];    // 无向图,矩阵对称
    }
}

时间复杂度
创建的复杂度: O [ n + n 2 + e ] O[n+n^2+e] O[n+n2+e]
初始化邻接矩阵G->arc的复杂度: O [ n 2 ] O[n^2] O[n2]


邻接表

当边数相对于顶点数较少的时候,邻接矩阵很浪费存储空间,如下面的例子:

结构:数组和链表相结合的存储方法称为邻接表。

处理方法

  1. 顶点用一个一维数组存储,每个数据元素还要存储指向第一个邻接点的指针,以便于查找该顶点的边信息
  2. 每个顶点的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点 v 1 v_1 v1的边表,有向图则称为顶点 v 1 v_1 v1作为弧尾的出边表

无向图

adjvex存放邻接点域,为结点下标。

有向图

有向图的逆邻接表,对每个顶点 v i v_i vi都建立一个链接为 v i v_i vi为弧头的表。

逆的就是左边为弧尾,右边为弧头

某个顶点的入度或出度可以通过链表长度判断

对于带权值的,还可以再加个数据域

结构定义

typedef char VertexType;
typedef int EdgeType;

typedef struct EdgeNode    // 边表结点
{
    int adjvex;    // 邻接点域,存储顶点对应下标
    EdgeType weight;
    struct EdgeNode *next;
} EdgeNode;

typedef struct VertexNode    // 顶点表结点
{
    VertexType data;
    EdgeNode *firstedge;
} VertexNode, AdjList[MAXVEX];

typedef struct GraphAdjList
{
    AdjList adjList;
    int numVertexes, numEdges;
} GraphAdjList;

无向图的邻接表

void CreateALGraph(GraphAdjList *G)
{
    int i, j, k;
    EdgeNode *e;
    printf("输入顶点数和边数");
    scanf("%d, %d", &G->numVertexes, &G->numEdges);
    for (i=0; i < G->numVertexes; i++)    // 建立顶点表
    {
        scanf(&G->adjList[i].data);    // 输入顶点信息
        G->adjList[i].firstedge = NULL;    // 先把边表设为空
    }
    for (k=0; k < G->numEdges; k++)    // 建立边表
    {
        printf("输入边上的顶点序号");
        scanf("%d, %d", &i, &j);
        // 给i后边的链表添加元素j,使用头插法
        e = (EdgeNode *) malloc(sizeof(EdgeNode));    // 生成边表结点
        e->adjvex = j;    // 邻接序号为j
        e->next = G->adjList[i].firstegde;    // e指针指向当前顶点指向的结点
        G->adjList[i].firstedge = e;    // 将当前顶点指针指向e
        // 因为是无向所以是对称的,也要个j后面加i
        e = (EdgeNode *) malloc(sizeof(EdgeNode));
        e->adjvex = i;
        e->next = G->adjList[j].firstedge;
        G->adjList[j].firstegde = e;
    }
}

代码说明

  • 先建立顶点表结点,同时输入数据,把指向结点设为NULL。
  • 边表链表中的每个元素之间是没有关联的,每个元素是指向这个链表的顶点的邻接点,所以如果有顶点的邻接点就用头插法插到链表中。
  • 因为是无向图,所以是对称的,要做两遍插入。
  • 时间复杂度为 O [ n + e ] O[n+e] O[n+e]

十字链表

思想:把邻接表和逆邻接表结合起来。

顶点表结点结构

firstin表示入边表头指针,指向该顶点的入边表中第一个结点;
firstout表示出边表头指针,指向出边表中的第一个结点。

边表结点结构

talvex是指弧起点在顶点表的下标;
headvex是指弧终点在顶点表中的下标;
headlink是指入边表的指针域,指向终点相同的下一条边;
taillink是指边表指针域,指向起点相同的下一条边;
网可以再加个weight域。

加入v0指向v1,然后在v0的指向v1的表的taillink中存放另一个指向v1的顶点?

核心理解

  • 通过firstin再加headlink可以找到该顶点的所有入边
  • 通过firstort再加taillink可以找到该顶点的所有出边

实现思路
比如输入 < v 1 , v 0 > < v_1, v_0 > <v1,v0>,此时先建立v1指向v0,新建一个边表结点,让v1的fisrtout指向新边表结点,给边表结点的tailvex和headvex赋值,同时取b0的fisrtin指向,如果为空就赋值给firstin,如果不为空就一直乡下找headlink,直到找到为空的headlink,将新边表结点赋值给空的headlink。

优点

  • 求以某顶点为头/尾的弧很方便,也很容易得到出度和入度。
  • 时间复杂度和邻接表相同。

邻接多重表

如果要对边进行操作,那么邻接表就不方便了,如下图,如果要去掉 ( v 0 , v 2 ) (v_0, v_2) (v0,v2)这条边,对右边的表的操作比较繁琐。

边表结点结构

其中ivex和jvex是和某边相关的两个顶点在顶点表中的下标。
ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。


首先要记住几点

  • ilink会指向 v i v_i vi顶点的另一条边,而在另一条边中, v i v_i vi顶点肯定就不是 i i i了,此时是新边的 j j j,所以ilink指向的结点的 j j j就是原来的 i i i
  • 如果有 n n n个结点,那么右图就会有 2 n 2n 2n条连线。

流程
跟上边的邻接表差不多,之前的邻接表是一个顶点连接结点的链表,而邻接多重是采用指向的方法,一个顶点后面只有一个结点,通过结点来指向,这样对边操作就很方便,如果要删除 ( v 0 , v 2 ) (v_0,v_2) (v0,v2),只需要将6,9的链接设为^即可。


边集数组

结构:边集数组是由两个一维数组组成,一个存储顶点信息;另一个存贮边的信息,这个边数组的每个数据元素由:一条边的起点下标(begin)、终点下标(end)、权(weight)组成。

这个是最好理解的了,直接暴力存储

边数组结构


JAVA实现邻接表/矩阵的创建

出错的点:

  • 见了一个类的数组,但是没有对每个数组元素做类的初始化
  • 输入的下标和顶点表中不匹配,所以多加了个位置
package Graph;

import java.util.Scanner;

class VertexList{
    public char data;
    public adjNode firstNode;
    public int weight;

    public VertexList(char data, int weight) {
        this.data = data;
        this.weight = weight;
        firstNode = null;
    }
}

class adjNode{
    public int data;
    public adjNode next;

    public adjNode() {
        next = null;
    }
}

public class Base {
    public static void main(String[] args) {
        int[][] adjMat  = CreAdjMat();
        VertexList[] vertexLists = CreaAdjList();

    }

    private static VertexList[] CreaAdjList() {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入顶点个数和边数:");
        int numVer = scanner.nextInt();
        int numEdge = scanner.nextInt();
        VertexList[] vertexLists = new VertexList[numVer+1];
        System.out.println("num ver" + numVer);
        // 初始化顶点表
        for (int i = 1; i < numVer+1; i++) {
            System.out.println("输入顶点:");
            char data = scanner.next().charAt(0);
            System.out.println("输入顶点权重:");
            int weight = scanner.nextInt();
            // 忘了初始化了,要先初始化才能赋值
            vertexLists[i] = new VertexList(data, weight);
        }
        // 输入边
        // 注意下标,这里我把长度都+1了
        for (int i = 0; i < numEdge; i++) {
            System.out.println("输入边的顶点序号:");
            int s = scanner.nextInt();
            int e = scanner.nextInt();
            // 两个顶点都接入边,先从s开始
            adjNode node1 = new adjNode();
            node1.data = e;
            // 用头插法,原本的放在新的后面
            node1.next = vertexLists[s].firstNode;
            // 新的插在顶点后
            vertexLists[s].firstNode = node1;

            // 同样的方法再处理e
            adjNode node2 = new adjNode();
            node2.data = s;
            node2.next = vertexLists[e].firstNode;
            vertexLists[e].firstNode = node2;
        }

        return vertexLists;

    }

    private static int[][] CreAdjMat() {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入的顶点数和边数");
        int numVer = scanner.nextInt();
        int numEdge = scanner.nextInt();
        int[] vertexes = new int[numVer];
        int[][] edge = new int[numVer][numVer];
        for (int i = 0; i < numVer; i++) {
            for (int i1 = 0; i1 < numVer; i1++) {
                edge[i][i1] = 0;
            }
        }
        for (int i = 0; i < numEdge; i++) {
            System.out.println("输入边:");
            int v1 = scanner.nextInt();
            int v2 = scanner.nextInt();
            edge[v1][v2] = 1;
        }
        return edge;
    }
}

图的遍历

定义:从图中某一顶点出发遍历图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫图的遍历(Traversing Graph)。
通常有两种遍历方案:一种是深度优先遍历,一种是广度优先遍历。

深度优先遍历

DFS:深度优先遍历(Depth First Search),也有称为深度优先搜索,简称DFS。
类似找钥匙的时候一个屋一个屋地搜,每个屋彻底搜完再搜下一个,类似于树的前序遍历


右图过程
从图中的某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相同的顶点都被访问到。
对于非连通图,如果经过一次深度优先遍历后仍有顶点没有被访问,则另选途中一个未被访问的顶点作初始点,重复上述过程,直至图中所有顶点都被访问到为止。

思路
深度优先就是一条道走到黑,抓到一个输出一个,所以用递归的方法来在一个点的邻接域中做文章,然后邻接域中的点再作为顶点,如此递归。每个DFS中标记已经见过。

邻接矩阵遍历代码
邻接矩阵就是 n × n n\times n n×n标记0或1那个

typedef int Boolean;
Boolean visited[MAX];    // 访问标志的数组


// 邻接矩阵的深度优先递归算法
// 打印所有和i点相邻接的没访问过顶点
void DFS(MGraph G, int i)
{
    int j;
    visited[i] = TRUE;
    printf("%c", G.vexs[i]);    // 打印顶点
    for (j=0; j<G.numVertexes; j++)
        // 和i相邻接 且 没访问过
        if (G.arc[i][j]==1 && !visited[j])
            DFS(G, j);
}

// 邻接矩阵的深度遍历操作
void DFSTraverse(MGraph G)
{
    int i;
    for (i=0; i<G.numVertexes; i++)
        visited[i] = FALSE;    // 所有顶点状态初始化为未访问
    for (i=0; i<G.numVertexes; i++)
        if (!visited[i])    // 对未访问过的idan调用DFS,如果是连通图,只会执行一次
            DFS(G, i);
}

感觉这个方法比较简单,相当于粗暴的对每个顶点遍历,再遍历顶点的邻接点,从某点出就是深度的意思吗?

邻接表结构的遍历代码

void DFS(GraphAdjList GL, int i)
{
    EdgeNode *p;
    visited[i] = TRUE;
    printf("%c", GL->adjList[i].data);    // 操作
    p = GL->adjList[i].firstedge;
    while (p)
    {
        // 思路差不多,顺着链表一直往下找,同时每次找到的新顶点都做DFS
        if (!visited[p->adjvex])
            DFS(GL, p->adjvex);
        p = p->next;
    }
}

void DFSTraverse(GraphAdjList GL)
{
    int i;
    for (i=0; i<GL->numVertexes; i++)
        visited[i] = FALSE;    // 初始为未访问
    for (i=0; i<GL->numVertexes; i++)
        if(!visited[i])
            DFS(GL, i);
}

两种结构遍历的时间复杂度

  • 邻接矩阵需要访问矩阵中的所有元素,因此为 O [ n 2 ] O[n^2] O[n2]
  • 邻接表需要的时间取决于顶点 n 和边 e 的数量,为 O [ n + e ] O[n+e] O[n+e]

对于点多边少的稀疏图来说,邻接表的效率更高。


广度优先遍历

BFS:广度优先遍历(Breadth First Search),又称为广度优先搜索,简称BFS。
类似找钥匙先把每个房间大概看一遍,慢慢扩大范围,类似于树的层序遍历

重构了一下左边的图,变成了右边,边和顶点的关系是不变的:
选择A为第一层;选择A的邻接点BF为第二层;选择BF的邻接点CIGE为第三层;选择CIGE的邻接点DH为第四层。


思路
广度优先就是平向来做,一层一层的处理,通过同一顶点的邻接点都入栈来完成,而不是从一个邻接点一直深入。入栈的时候才可标记为已经见过。

邻接矩阵遍历代码

void BFSTraverse(MGraph G)
{
    int i, j;
    Queue Q;
    for (i=0; i<G.numVertexes; i++)
        visited[i] = FALSE:
    InitQueue(&Q);    // 初始化一个辅助队列
    for (i=0; i<G.numVertexes; i++)
    {
        if (!visited[i])    // 处理未被访问的
        {
            visited[i] = TRUE;
            printf("%c", G.vexs[i]);
            EnQueue(&Q, i);    // 将此顶点入队列
            while (!QueueEmpty(Q))    // 若当前队列不为空
            {
                DeQueue(&Q, &i);    // 将队中元素出队列,赋值给i
                for (j=0; j<G.numVertexes; j++)
                {
                    // 判断两点间是否存在边
                    if (G.arc[i][j]==1 && !visited[j])
                    {
                        visited[j] = TRUE;
                        printf("%c", G.vexs[j]);    // 打印顶点
                        EnQueue(&Q, j);    // 将找到的点加入队列
                    }
                }
            }
        }
    }
}

流程
这种方法利用了队列的先进先出,遍历的时候读取到的邻接点都放入队列中,这样就能一批一批地处理,而不是像深度那样抓住一个查到底。

邻接表遍历代码

void BFSTraverse(GraphAdjList GL)
{
    int i;
    EdgeNode *p;
    Queue Q;
    for (i=0; i<GL->numVertexes; i++)
        visited[i] = False;
    InitQueue(&Q);
    for (i=0; i<GL->numVertexes; i++)
    {
        if (!visited[i])
        {
            visited[i] = TRUE;
            printf("%c", GL->adjList[i].data);
            EnQueue(&Q, i);
            while (!QueueEmpty(Q))
            {
                DeQueue(&Q, &i);
                p = GL->adjList[i].firstedge;    // 找到该顶点的表头指针
                // 把一串链表都送进队列
                while (p)
                {
                    if (!visited[p->adjvex])
                    {
                        visited[p->adjvex] = TRUE;
                        printf("%c", GL->adjList[p->adjvex].data);
                        EnQueue(&Q, p->adjvex);
                    }
                    p = p->next;
                }
            }
        }
    }
}

JAVA实现深度/广度遍历

package Graph;

import java.util.ArrayList;
import java.util.List;

class Queue{
    private int top;
    private int max;
    private int[] list;

    public Queue(int max) {
        this.top = 0;
        this.list = new int[max];
    }

    public void push(int idx){
        list[top++] = idx;
    }

    public int pop(){
        top--;
        return list[top];
    }

    public int Empty(){
        if (top==0){
            return 1;
        }
        else {
            return 0;
        }
    }
}

public class Travers {
    public static void main(String[] args) {
        // 手动邻接矩阵
        // 手动邻接表
        VertexList[] vertexList = new VertexList[10];
        int[][] edgeList = new int[16][2];
        // 初始化
        int[][] edgeMat = new int[10][10];
        InitVerAjd(vertexList, edgeList);
        // 创建邻接表
        CreateAdjList(vertexList, edgeList);
        // 测试创建是否正确
        // System.out.println(vertexList[3].data);
        // System.out.println(vertexList[3].firstNode.data);
        // System.out.println(vertexList[3].firstNode.next.data);
        // System.out.println(vertexList[3].firstNode.next.next.data);

        // 创建邻接矩阵
        CreateAdjMat(edgeList, edgeMat);

        // 邻接矩阵的深度遍历
        System.out.println("邻接矩阵的深度遍历------------");
        TraverMatDFS(vertexList, edgeMat);

        // 邻接表的深度遍历
        System.out.println("邻接表的深度遍历------------");
        TraverListDFS(vertexList, edgeList);

        // 邻接矩阵的广度遍历
        System.out.println("邻接矩阵的广度遍历------------");
        TraverMatBFS(vertexList, edgeMat);

        // 邻接表的广度遍历
        System.out.println("邻接表的广度遍历------------");
        TraverListBFS(vertexList, edgeList);

    }

    private static void TraverListBFS(VertexList[] vertexList, int[][] edgeList) {
        int len = vertexList.length;
        int[] visit = new int[len];
        for (int i = 1; i < len; i++) {
            visit[i] = 0;
        }
        Queue que = new Queue(len-1);
        for (int i = 1; i < len; i++) {
            if (visit[i] == 0){
                que.push(i);
                visit[i] = 1;
                System.out.println(vertexList[i].data);
                while (que.Empty() == 1){
                    int idx = que.pop();
                    adjNode q = vertexList[idx].firstNode;

                    // 顺着邻接表做广度
                    while (q != null){
                        if (visit[q.data]==0){
                            // 邻接入栈
                            que.push(q.data);
                            visit[q.data] = 1;
                            System.out.println(vertexList[q.data].data);
                        }
                        q = q.next;
                    }

                }

            }
        }

    }

    private static void TraverMatBFS(VertexList[] vertexList, int[][] edgeMat) {
        int len = vertexList.length;
        int[] visit = new int[len];
        for (int i = 1; i < len; i++) {
            visit[i] = 0;
        }
        Queue que = new Queue(len-1);
        for (int i = 1; i < len; i++) {
            if (visit[i] == 0){
                que.push(i);
                visit[i] = 1;
                System.out.println(vertexList[i].data);
                // 栈里有就一直找
                while (que.Empty() == 0){
                    int idx = que.pop();
                    // 并不是从这里标记是否看见,入栈的时候才能标记
                    // 从i的邻接点下手,没见过的都入栈

                    for (int i1 = 1; i1 < len; i1++) {
                        // 没见过且有邻接
                        if (visit[i1]==0 && edgeMat[idx][i1]==1){
                            // 打印且入栈
                            System.out.println(vertexList[i1].data);
                            que.push(i1);
                            visit[i1] = 1;
                        }
                    }
                }
            }
        }
    }

    private static void TraverListDFS(VertexList[] vertexList, int[][] edgeList) {
        int len = vertexList.length;
        int[] visit = new int[len];
        for (int i = 1; i < len; i++) {
            visit[i] = 0;
        }
        for (int i = 1; i < len; i++) {
            if (visit[i]==0){
                DFSList(vertexList, visit, i);
            }
        }
    }

    private static void DFSList(VertexList[] vertexList, int[] visit, int idx) {
        visit[idx] = 1;
        System.out.println(vertexList[idx].data);
        // 往后找
        adjNode p = vertexList[idx].firstNode;
        while (p != null){
            if (visit[p.data]==0){
                DFSList(vertexList, visit, p.data);
            }
            p = p.next;
        }

    }

    // 深度优先邻接矩阵用
    private static void DFSMat(VertexList[] vertexList, int[][] edgeMat, int[] visit, int idx){
        int len = edgeMat.length;
        // 这两个在循环的外面,每次调用DFS会打印一个
        // 打印和控制都在循环外面进行
        visit[idx] = 1;
        System.out.println(vertexList[idx].data);

        for (int i = 1; i < len; i++) {
            if(edgeMat[idx][i]==1 && visit[i]==0){
                DFSMat(vertexList, edgeMat, visit, i);
            }
        }
    }

    // 深度优先邻接矩阵遍历
    private static void TraverMatDFS(VertexList[] vertexList, int[][] edgeMat) {
        int len = vertexList.length;
        int[] visit = new int[len];
        for (int i = 0; i < len; i++) {
            visit[i] = 0;
        }
        for (int i = 1; i < len; i++) {
            if(visit[i]==0){
                DFSMat(vertexList, edgeMat, visit, i);
            }

        }
    }

    private static void CreateAdjMat(int[][] edgeList, int[][] edgeMat) {
        int len0 = edgeMat.length;
        for (int i = 0; i < len0; i++) {
            for (int i1 = 0; i1 < len0; i1++) {
                edgeMat[i][i1] = 0;
            }
        }
        int len = edgeList.length;
        for (int i = 0; i < len; i++) {
            int s = edgeList[i][0];
            int e = edgeList[i][1];
            edgeMat[s][e] = 1;
            edgeMat[e][s] = 1;
        }
    }


    private static void CreateAdjList(VertexList[] vertexList, int[][] edgeList) {
        int len = edgeList.length;
        for (int i = 1; i < len; i++) {
            int s = edgeList[i][0];
            int e = edgeList[i][1];
            adjNode node1 = new adjNode();
            node1.data = s;

            node1.next = vertexList[e].firstNode;
            vertexList[e].firstNode = node1;

            adjNode node2 = new adjNode();
            node2.data = e;
            node2.next = vertexList[s].firstNode;
            vertexList[s].firstNode = node2;
        }
    }

    private static void InitVerAjd(VertexList[] vertexList, int[][] edgeList) {
        vertexList[1] = new VertexList('A',0);
        vertexList[2] = new VertexList('B',0);
        vertexList[3] = new VertexList('C',0);
        vertexList[4] = new VertexList('D',0);
        vertexList[5] = new VertexList('E',0);
        vertexList[6] = new VertexList('F',0);
        vertexList[7] = new VertexList('G',0);
        vertexList[8] = new VertexList('H',0);
        vertexList[9] = new VertexList('I',0);
        // 1
        edgeList[1][0] = 1;
        edgeList[1][1] = 2;
        // 2
        edgeList[2][0] = 2;
        edgeList[2][1] = 3;
        // 3
        edgeList[3][0] = 3;
        edgeList[3][1] = 4;
        // 4
        edgeList[4][0] = 4;
        edgeList[4][1] = 5;
        // 5
        edgeList[5][0] = 5;
        edgeList[5][1] = 6;
        // 6
        edgeList[6][0] = 6;
        edgeList[6][1] = 1;
        // 7
        edgeList[7][0] = 2;
        edgeList[7][1] = 7;
        // 8
        edgeList[8][0] = 7;
        edgeList[8][1] = 6;
        // 9
        edgeList[9][0] = 2;
        edgeList[9][1] = 9;
        // 10
        edgeList[10][0] = 3;
        edgeList[10][1] = 9;
        // 11
        edgeList[11][0] = 9;
        edgeList[11][1] = 4;
        // 12
        edgeList[12][0] = 4;
        edgeList[12][1] = 8;
        // 13
        edgeList[13][0] = 7;
        edgeList[13][1] = 4;
        // 14
        edgeList[14][0] = 8;
        edgeList[14][1] = 5;
        // 15
        edgeList[15][0] = 7;
        edgeList[15][1] = 8;
    }
}

最小生成树

定义:把构造连通网的最小代价生成树称为最小生成树(Minimum Cost Spanning Tree)。

Prim算法

lowcost数组的作用
长度为顶点个数的一维数组,初始化下标为0的位置的值为0,其他全为无穷大,下标指向的内容为0时,说明下标代表的顶点已经在最小生成树中了。
这个数组用来存放所有未在最小生成树中的顶点中,距离已经在最小生成树中的顶点的最小距离,简单来说就是未在最小生成树中的顶点和最小生成树的最小距离。最小生成树中尚未有邻接点的顶点的值,设置为无穷大。

adjvex数组的作用
lowcost中存放的是顶点和最小生成树的最小距离,adjvex数组来记录离最小生成树最近的点最小生成树中哪个点距离最近,数组离存放对应的最小生成树的点,以此来生成边。

算法流程

  1. 创建一个lowcost和一个adjvex数组,长度都为顶点数,lowcost全部初始化为无穷,然后第一个值设为0,表示 v 0 v_0 v0结点已经在最小生成树中;adjvex数组全部初始化为0;
  2. 对每个顶点进行遍历,每次找lowcost中的权值最小的位置,然后用adjvex来知道是距离最小生成树中哪个点最近,这时就能生成一个边(adjvex[k], k);
  3. 设lowcost[k]=0,就是将该点纳入最小生成树中;
  4. 此时再循环所有的顶点,如果有离最小生成树距离更近的,就更新adjvex的值,记录对应位置。

代码实现

void MiniSpanTree_Prim(MGraph G)
{
    int min, i, j, k;
    int adjvex[MAXVEX];    // 保存相关下标
    int lowcost[MAXVEX];    // 保存权值
    // 初始化
    lowcost[0] = 0;
    adjvex[0] = 0;
    for (i=1; i<G.numVertexes; i++)
    {
        lowcost[i] = G.arc[0][i];
        adjvex[i] = 0;
    }
    for (i=1; i<G.numVertexes; i++)
    {
        min = INFINITY;
        j = 1;
        k = 0;
        while (j < G.numVertexes)    // 循环全部的点
        {
            // 找权值最小的点
            if (lowcost[j]!=0 && lowcost[j]<min)
            {
                min = lowcost[j];
                k = j;
            }
            j++;
        }
        // k离最小生成树中最近的点是adjvex[k]
        printf("(%d, %d)", adjvex[k], k);    // 打印当前顶点边中权值最小边
        lowcost[k] = 0;    // 当前顶点的权值设置为0, 表示此顶点已经完成任务
        // 新点进去,检测是否会让剩下的点离最小生成树更近
        for (j=1; j<G.numVertexes; j++)
        {
            // 寻找刚找出的k的邻接点中,离最小生成树更近的点
            if (lowcost[j]!=0 && G.arc[k][j] <lowcost[j])
            {
                // 若下标为k顶点某邻接边权值小于此前选中的点,就更新
                lowcost[j] = G.arc[k][j];    // 将较小权值存入lowcost
                // 因为选出的点离k最近,所以要更新一下
                adjvex[j] = k;    // 将下标为k的顶点存入adjvex
            }
        }
    }
}

时间复杂度 O [ n 2 ] O[n^2] O[n2]


Kruskal算法

Prim算法是从顶点入手的,而Kruskal是从边开始入手,这里用到了边集数组结构

边的结构代码

typedef struct Edge
{
    int begin;
    int end;
    int weight;
};

将边按权重排序:

算法流程

  1. 建一个parent数组,用来判断是否构成环路
  2. 每条边遍历
  3. 检测是否构成环,没有的话就用边的头作下标,尾作值,放到parent中

案例
按照代码来,到了i=6的时候,已经获得的边如图粗体所示(24和26权重的边应该是印错了):

在这之前都是n!=m,这是相当于两个连通的边集合纳入到了最小生成树中;
i = 7时,到了边 ( v 5 , v 6 ) (v_5,v_6) (v5,v6),此时两个Fine都会返回6,可知 ( v 5 , v 6 ) (v_5,v_6) (v5,v6)会在A中构成一个环路,所以不要这条边;
i = 8时,到了边 ( v 1 , v 2 ) (v_1,v_2) (v1,v2)也会让A构成环路,所以也不要;
i = 9时,连接 ( v 6 , v 7 ) (v_6,v_7) (v6,v7),此后的均会构成环路,最小生成树此时得到:

思路
直接从边入手,找最小的,把所有的构不成环路的最小的连接起来,前面的连接是没有问题的,到了n=m的时候,此时这个边想进去集合已经来不及了,有跟他作用相同但是更短的边在里面。

时间复杂度 O [ e log ⁡ e ] O[e\log e] O[eloge]

两种方法对比

  • Kruskal主要针对边展开,边数少的时候效率高,所以对稀疏图有很大优势
  • Prim对稠密图,边很多的情况会更好些

JAVA实现Prim算法

public class MinTree {

    public static void main(String[] args) {
        VertexList[] vertexLists = new VertexList[10];
        int[][] edgelist = new int[16][3];
        int[][] edgeMat = new int[10][10];
        InitVerAjd(vertexLists, edgelist);
        CreateAdjMat(edgelist, edgeMat);
        // Prim需要用到邻接权值矩阵
        // 权值矩阵是没有问题的
        // for (int i = 1; i < 10; i++) {
        //     System.out.println("第 " + i + "行");
        //     for (int j = 1; j < 10; j++){
        //         System.out.println(edgeMat[i][j]);
        //     }
        // }
        // Prim(vertexLists, edgeMat);
        Kruskal(vertexLists, edgelist, edgeMat);

    }

    private static void Prim(VertexList[] vertexLists, int[][] edgeMat) {
        int len = vertexLists.length;
        int[] adjvex = new int[len];
        int[] lowcost = new int[len];
        for (int i = 1; i < len; i++) {
            // 赋值第一个点的邻接边的权重
            // 这里搞错了
            lowcost[i] = edgeMat[1][i];
            // 初始化为1, 就是生活树中只有第一个顶点
            adjvex[i] = 1;
        }
        // 设为0表示已经进树了
        lowcost[1] = 0;
        // 每次选出一个顶点
        for (int i = 1; i < len-1; i++) {
            int min = Integer.MAX_VALUE;
            int k = 0;
            // 不会算第一个点本身
            for (int j = 2; j < len; j++) {
                if (lowcost[j]!=0 && lowcost[j]<min){
                    min = lowcost[j];
                    k = j;
                }
            }
            // 该点被选出
            lowcost[k] = 0;
            System.out.println("第" + i + "条边:" + adjvex[k] + "->" + k);
            // 更新lowcost和adjvex
            for (int m = 2; m < len; m++) {
                // 看有无更近
                if (lowcost[m]!=0 && lowcost[m]>edgeMat[k][m]){
                    // 更近就更新
                    lowcost[m] = edgeMat[k][m];
                    // 得记录和那个更近
                    adjvex[m] = k;
                }
            }
        }
    }

    // 这次的矩阵带全值了
    public static void CreateAdjMat(int[][] edgeList, int[][] edgeMat) {
        // 长度搞错了
        int lenVer = edgeMat.length;
        for (int i = 1; i < lenVer; i++) {
            for (int i1 = 1; i1 < lenVer; i1++) {
                // 不相连的距离设为无穷大
                edgeMat[i][i1] = Integer.MAX_VALUE;
            }
        }
        int len = edgeList.length;
        for (int i = 1; i < len; i++) {
            int s = edgeList[i][0];
            int e = edgeList[i][1];
            edgeMat[s][e] = edgeList[i][2];
            edgeMat[e][s] = edgeList[i][2];
        }
    }

    private static void InitVerAjd(VertexList[] vertexList, int[][] edgeList) {
        vertexList[1] = new VertexList('A',0);
        vertexList[2] = new VertexList('B',0);
        vertexList[3] = new VertexList('C',0);
        vertexList[4] = new VertexList('D',0);
        vertexList[5] = new VertexList('E',0);
        vertexList[6] = new VertexList('F',0);
        vertexList[7] = new VertexList('G',0);
        vertexList[8] = new VertexList('H',0);
        vertexList[9] = new VertexList('I',0);
        // 1
        edgeList[1][0] = 1;
        edgeList[1][1] = 2;
        edgeList[1][2] = 10;
        // 2
        edgeList[2][0] = 2;
        edgeList[2][1] = 3;
        edgeList[2][2] = 18;
        // 3
        edgeList[3][0] = 3;
        edgeList[3][1] = 4;
        edgeList[3][2] = 22;
        // 4
        edgeList[4][0] = 4;
        edgeList[4][1] = 5;
        edgeList[4][2] = 20;
        // 5
        edgeList[5][0] = 5;
        edgeList[5][1] = 6;
        edgeList[5][2] = 26;
        // 6
        edgeList[6][0] = 6;
        edgeList[6][1] = 1;
        edgeList[6][2] = 11;
        // 7
        edgeList[7][0] = 2;
        edgeList[7][1] = 7;
        edgeList[7][2] = 16;
        // 8
        edgeList[8][0] = 7;
        edgeList[8][1] = 6;
        edgeList[8][2] = 17;
        // 9
        edgeList[9][0] = 2;
        edgeList[9][1] = 9;
        edgeList[9][2] = 12;
        // 10
        edgeList[10][0] = 3;
        edgeList[10][1] = 9;
        edgeList[10][2] = 8;
        // 11
        edgeList[11][0] = 9;
        edgeList[11][1] = 4;
        edgeList[11][2] = 21;
        // 12
        edgeList[12][0] = 4;
        edgeList[12][1] = 8;
        edgeList[12][2] = 16;
        // 13
        edgeList[13][0] = 7;
        edgeList[13][1] = 4;
        edgeList[13][2] = 24;
        // 14
        edgeList[14][0] = 8;
        edgeList[14][1] = 5;
        edgeList[14][2] = 7;
        // 15
        edgeList[15][0] = 7;
        edgeList[15][1] = 8;
        edgeList[15][2] = 19;
    }
}

最短路径

定义:对于网图来说,最短路径,是指两顶点之间经过的边上的权值之和最少的路径,并且我们称路径上的第一个点是源点,最后一个顶点是终点。


Dijkstra算法

思路

  • 一步步求出所有顶点的最短路径, 后面的顶点路径寻找是建立在前面的寻找的基础上的,通过不断遍历最近点的邻接点来更新到v0点的距离。
  • 会进行很多次最近点的邻接点和当付钱最近距离的比较,以此来找到最优解。


算法流程

  1. 新建三个数组,P,D,final,这三个的长度都是顶点数,P用来存放最短路径,里面的值指向最短路径的前一个顶点;D中存放v0到下标位置点的最短路径距离;final来记录当前顶点是否已经算过最短路径
  2. 初始化三个数组,D使用邻接矩阵的v0行初始化,final全为0表示没被计算过,P全为0表示未有路径。
  3. 一个大循环,里面套了两个小循环,大循环对每个顶点进行遍历
  4. 第一个小循环用来寻找D中未被计算过的最小距离和对应的顶点,用final来标记是否计算过,k值用来保存顶点位置
  5. 第二个小循环遍历所有的顶点,让v0到k点的最小距离+k到各个顶点的距离之前记录D中的v0到各个顶点的距离比较,如果能更近就更新,同时保存路径到P

实现代码

#define MAXVEX 9
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX];    // 存储最短路径下标的数组
typedef int ShortPathTable[MAXVEX];    // 存储到各点最短路径的权值和

// P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx *P, ShortPathTable *D)
{
    int v, w, k, min;
    int final[MAXVEX];    // final[w]=1 表示求得顶点v0至vw的最短路径
    for (v=0; v<G.numVertexes; v++)    // 初始化数据
    {
        final[v] = 0;    // 全部顶点初始化为未知最短路径状态
        (*D)[v] = G.matrix[v0][v];    // 和v0邻接的顶点的权值
        (*P)[v] = 0;    // 初始化路径数组P为0
    }
    (*D)[v0] = 0;    // v0到v0的路径
    final[v0] = 1;
    // 主循环,求v0到各个顶点的最短路径
    for (v=1; v<G.numVertexes; v++)
    {
        min = INFINITY;
        for (w=0; w<G.numVertexes; w++)    // 寻找离v0最近的顶点
        {
            // 为什么不能重复找呢
            // 因为如果不限定final的话,就一直是最小那个,无法向后推进
            if (!final[w] && (*D)[w]<min)    // 从未被找到的点里找
            {
                k = w;
                min = (*D)[w];
            }
        }
        // 最近的点k被考虑过
        final[k] = 1;
        //
        for (w=0; w<G.numVertexes; w++)
        {
            // 如果经过k顶点的路径比现在这条路径的长度短的话
            // k是剩余点中离v0最近的
            if (!final[w] && (min+G.matrix[k][w] < (*D)[w]))
            {
                // 说明找到了最短路径,修改D[w]和P[w]
                (*D)[w] = min + G.matrix[k][w];
                (*P)[w] = k;
            }
        }
    }
}

结果图

时间复杂度:遍历嵌套遍历,为 O [ n 2 ] O[n^2] O[n2],如果要求所有顶点到所有顶点的最短路径,就是 O [ n 3 ] O[n^3] O[n3]


Floyd算法

依次计算所有顶点经过某点后到达另一顶点的最短路径。

思路

  • D数组存距离,P数组存路径
  • 三个遍历,K表示中转顶点,考虑中转比直线更近的情况;V表示起始顶点;W表示结束顶点
  • K在最外层,每个顶点之间都计算了中转,这里只要一个中转就能表达所有情况,因为中转的起点是之前计算出来的最短路径,已经积累了很多中转
  • 最里层的循环判断是否中转更近,是的话就更新距离和路径,让原本值为w的P[v][w]指向中转点,即改变了起点的后期,把中转点插到了原本的起点和终点之间,这样起点指向的就是第一个中转点了。
  • 最后的一个遍历可让路径一直最短,从前到后一点点积累中转点,到最后总有不可再插入中转点的时候,此时path[k][w]=w,路径的寻找就结束了。

最短路径计算代码实现

typedef int Pathmatrix[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];

void ShortestPath_Floyd(MGraph G, Pathmatrix *p, ShortPathTable *D)
{
    int v, w, k;
    for (v=0; v<G.numVertexes; ++v)
    {
        for (w=0; w<G.numVertexes; ++v)    // 初始化D与P
        {
            (*D)[v][w] = G.matrix[v][w];
            (*P)[v][w] = w;
        }
    }
    for (k=0; k<G.numVertexes; ++k)
    {
        for (v=0; v<G.numVertexes; ++v)
        {
            for (w=0; w<G.numVertexes; ++w)
            {
                // 比较中转近还是直接近
                if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w])
                {
                    (*D)[v][w] = (*D)[v][k] + (*D)[k][w];
                    (*P)[v][w] = (*P)[v][k];    // 路径设置为中转点k
                }
            }
        }
    }
}

求最短路径
利用P来求出两个点之间的最短路径,比如求v0到v8,设P[0][8]=m,m为0-8之间的一个点,然后再以m为起点,得到P[m][8]=n,n是m和8之间的一个中转点,再以n为起点,得到P[n][8]=…,这个过程中得到的点就是路径。

最短路径显示代码

for (v=0; v<G.numVertexes; ++v)
{
    for (w=v+1; w<G.numVertexews; w++)
    {
        // 求v和w之间的路径
        printf("v%d-v%d weight: %d",v,w,D[v][w]);
        k = P[v][w];
        printf("path: %d", v);    // 打印源点
        while(k!=w)
        {
            printf(" -> %d", k);
            k = P[k][w];
        }
        printf(" -> %d\n", w);
    }
    printf("\n");
}

使用环境
要求所有顶点到所有顶点的最短路径问题时,使用Floyd。


JAVA实现Dijkstra和Floyd算法

注意:设置最大值的时候不要使用数据类型的最大值(如Integer.MAX_VALUE),因为又可能出现min+Integer.MAX_VALUE数据溢出变成负数的情况。

public class ShortesPath {
    public static void main(String[] args) {
        VertexList[] vertexLists = new VertexList[10];
        int[][] edgeList = new int[17][3];
        int[][] edgeMat = new int[10][10];
        InitShortPathVerAjd(vertexLists, edgeList);
        utils.CreateAdjMat(edgeList, edgeMat);
        // Dijkstra算法
        Dijkstra(edgeMat);
        // Floyd算法
        Floyd(edgeMat);
    }

    private static void Floyd(int[][] edgeMat) {
        int len = edgeMat.length;
        // 距离数组和路径数组,因为这次是两层遍历方法,所以用二维数组
        int[][] Dis = edgeMat.clone();
        int[][] Path = new int[len][len];
        // 初始化Path为终点
        for (int v = 1; v < len; v++) {
            for (int w = 1; w < len; w++) {
                // v表示起点,w表示终点
                Path[v][w] = w;
            }
        }
        // k为中转点
        for (int k = 1; k < len; k++) {
            // 所有的顶点之间都做中转
            for (int v = 1; v < len; v++) {
                for (int w = 1; w < len; w++) {
                    // 如果中专的距离更近,就更新距离和路径为中赚
                    if (Dis[v][w] > Dis[v][k]+Dis[k][w]){
                        Dis[v][w] = Dis[v][k]+Dis[k][w];
                        // 相当于改变了起点的后驱,把中转点插到了原本的起点和顶点之间
                        Path[v][w] = Path[v][k];
                    }
                }

            }
        }
        FloydForeach(Path);
    }

    private static void FloydForeach(int[][] path) {
        int v = 1;
        int w = 8;
        int k = path[v][w];
        System.out.println(v + "到" + w + "的最短路径");
        System.out.printf((k-1)+" ");
        while (k!=w){
            // 起点随着中转带你一直在变,终点不变
            k = path[k][w];
            System.out.printf("-> " + (k-1) + " ");
        }
        System.out.println("-> " + k);
    }

    private static void Dijkstra(int[][] edgeMat) {
        int len = edgeMat.length;
        for (int i = 1; i < len; i++) {
            for (int j = 1; j < len; j++) {
                System.out.printf(edgeMat[i][j] + " ");
            }
            System.out.println("");
        }
        System.out.println("----------------");
        // 创建数组,假设下标从1开始
        int[] Final = new int[len];
        int[] Dis = new int[len];
        int[] Path = new int[len];
        // 初始化
        for (int i = 1; i < len; i++) {
            // 全部为未知
            Final[i] = 0;
            // 以第一个顶点为起点
            Dis[i] = edgeMat[1][i];
            // 路径都设为第一个顶点
            Path[i] = 1;
        }
        Final[1] = 1;
        Dis[1] = 0;
        // 大遍历
        for (int i = 1; i < len; i++) {
            // 找离v0最近的没被考虑过的点
            int min = 65535;
            // 用来记录最近
            int k = 0;
            for (int w = 1; w < len; w++) {
                if (Final[w]==0 && Dis[w] < min){
                    k = w;
                    min = Dis[w];
                }
            }
            // 设k顶点为已被考虑过
            Final[k] = 1;
            // 看是否能通过k更近
            for (int w = 1; w < len; w++) {
                if (Final[w]==0 && (min+edgeMat[k][w] < Dis[w])){
                    // 更近就更新距离和路径
                    System.out.println("更新前到顶点"+w +"的距离: " + Dis[w]);

                    Dis[w] = min + edgeMat[k][w];
                    System.out.println("更新后到顶点"+w +"的距离: " + Dis[w]);
                    // 更新前驱点,通过k连接w会更近
                    Path[w] = k;
                }
            }
        }
        for (int i = 1; i < len; i++) {
            System.out.println(i-1+"的前驱是"+(Path[i]-1));
        }
    }

    private static void InitShortPathVerAjd(VertexList[] vertexList, int[][] edgeList) {
        vertexList[1] = new VertexList('A',0);
        vertexList[2] = new VertexList('B',0);
        vertexList[3] = new VertexList('C',0);
        vertexList[4] = new VertexList('D',0);
        vertexList[5] = new VertexList('E',0);
        vertexList[6] = new VertexList('F',0);
        vertexList[7] = new VertexList('G',0);
        vertexList[8] = new VertexList('H',0);
        vertexList[9] = new VertexList('I',0);
        // 1
        edgeList[1][0] = 1;
        edgeList[1][1] = 0;
        edgeList[1][2] = 1;
        // 2
        edgeList[2][0] = 0;
        edgeList[2][1] = 2;
        edgeList[2][2] = 5;
        // 3
        edgeList[3][0] = 1;
        edgeList[3][1] = 2;
        edgeList[3][2] = 3;
        // 4
        edgeList[4][0] = 1;
        edgeList[4][1] = 3;
        edgeList[4][2] = 7;
        // 5
        edgeList[5][0] = 1;
        edgeList[5][1] = 4;
        edgeList[5][2] = 5;
        // 6
        edgeList[6][0] = 2;
        edgeList[6][1] = 4;
        edgeList[6][2] = 1;
        // 7
        edgeList[7][0] = 2;
        edgeList[7][1] = 5;
        edgeList[7][2] = 7;
        // 8
        edgeList[8][0] = 3;
        edgeList[8][1] = 4;
        edgeList[8][2] = 2;
        // 9
        edgeList[9][0] = 4;
        edgeList[9][1] = 5;
        edgeList[9][2] = 3;
        // 10
        edgeList[10][0] = 3;
        edgeList[10][1] = 6;
        edgeList[10][2] = 3;
        // 11
        edgeList[11][0] = 4;
        edgeList[11][1] = 6;
        edgeList[11][2] = 6;
        // 12
        edgeList[12][0] = 4;
        edgeList[12][1] = 7;
        edgeList[12][2] = 9;
        // 13
        edgeList[13][0] = 7;
        edgeList[13][1] = 5;
        edgeList[13][2] = 5;
        // 14
        edgeList[14][0] = 6;
        edgeList[14][1] = 7;
        edgeList[14][2] = 2;
        // 15
        edgeList[15][0] = 6;
        edgeList[15][1] = 8;
        edgeList[15][2] = 7;
        // 16
        edgeList[16][0] = 7;
        edgeList[16][1] = 8;
        edgeList[16][2] = 4;

        // 搞错了,应从1开始
        for (int i = 0; i < 16; i++) {
            edgeList[i+1][0] += 1;
            edgeList[i+1][1] += 1;
        }
    }
}

拓扑排序

AOV网:在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网(Activity On Vertex Network)。
AOV网中的弧表示活动之间存在的某种制约关系,且不允许存在回路,下面是一个简单的例子:

拓扑序列

  • 设G=(V, E)是一个具有n个顶点的有向图,V中的顶点是有序列的,如果满足从 v i v_i vi v j v_j vj之间有一个条路经,则在顶点序列中 i i i j j j前面的化,就称这样的顶点序列为一个拓扑序列。
  • 对于一个AOV网,拓扑序列可能不止一条。
  • 所谓的拓扑排序,其实就是一个有向图构造拓扑序列的过程。

拓扑排序算法

对AOV网进行拓扑排序的思路:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点和以此顶点为尾的弧,继续重复,直到输出全部顶点或AOV网中不存在入度为0的顶点为止。

数据结构

AOV网

邻接表数据结构

结构代码

typedef struct EdgeNode    // 边表结点
{
    int adjvex;    // 邻接点域,存储该顶点对应的下标
    int weight;    // 用于存储权值,非网图不需要
    struct EgdeNode *next;    // 链域,指向下一个邻接点
}EdgeNode;

typedef struct VertexNode    // 顶点表结点
{
    int in;    // 顶点入度
    int data;    // 顶点域
    EgdeNode *firstedge;
}VertexNode, AdjList[MAXVEX];

typedef struct graphAdjList
{
    AdjList adjList;
    int numVertexes, numEdges;
}graphAdjList, *GraphAdjList;

思路

  • 在算法中用栈来处理入度为0的顶点,目的是为了避免每个查找时都要遍历找入度为0的顶点。
  • 从入度为0开始遍历,入度为0表示该点不会作为弧头,所以把这种点放在前面不会有问题,每打印一个,就相当于抹掉一个点,此时这个点的邻接点的入度就-1,如果入度减到了0,那么该点此时放入栈也是安全的,后面不会有让该点作为弧头的弧。

理解

  • 感觉先输出的都是后面的点啊?
    不是的,后面进去的都是通过前一个点(该点已被打印)的消除而入度为0的,而且都是跟在后面的邻接点。
  • 那么最初的几个入度为0如何保证呢?还是说从弧找顺序而不是从顺序中找弧?
    应该是第一种,先给弧,再看顺序,所以入度为0的几个顺序无所谓。
// 拓扑排序,若GL无回路,则输出序列
Status TopologicalSort(GraphAdjList GL)
{
    EdgeNode *e;
    int i, k, gettop;
    int top=0;    // 用于栈指针下标
    int count=0;    // 统计输出的顶点个数
    int *stack;    // 建栈存储入度为0的顶点
    stack = (int *)malloc(GL->numVertexes *sizeof(int));
    for (i=0; i<GL->numVertexes; i++)
        if (GL->adjList[i].in == 0)
            stack[++top] = i;    // 入度为0的顶点入栈
    while(top != 0)
    {
        gettop = stack[top--];    //出栈打印
        printf("%d -> ", GL->adjList[gettop].data);
        count++;    // 统计输出顶点数
        for (e=GL->adjList[gettop].firstedge; e ;e=e->next)
        {
            // 对顶点的弧边遍历
            k = e->adjvex;
            if (!(--GL->adjList[k].in))    // k号顶点的邻接点的入度减1
                stack[++top] = k;    // 入度变成0了则入栈,以便下次循环输出
        }
    }
    if (count < GL-<numVertexes)    // 数量不够,则存在环
        return ERROE;
    else
        return OK;
}

JAVA实现拓扑排序

顶点类

public class aovNode {
    public int in;
    public int data;
    public adjNode firstedge;


    public aovNode(int in, int data) {
        this.in = in;
        this.data = data;
    }
}

排序实现

public class TopologySort {
    public static void main(String[] args) {
        // 14个顶点
        int len = 6;
        aovNode[] verList = new aovNode[len+1];
        // 这里是有向边, 七条边
        int[][] edgeList = new int[8][2];
        InitTopoplogy(verList, edgeList);
        CrateAdjList(verList, edgeList);
        Sort(verList, edgeList);
    }

    private static void Sort(aovNode[] verList, int[][] edgeList) {
        int len = verList.length;
        // 做一个栈存放入度为0的点
        int[] stack = new int[len];
        int top = 0;
        int count = 0;
        int ver = 0;
        adjNode e = null;
        for (int i = 1; i < len; i++) {
            if (verList[i].in == 0){
                stack[top++] = i;
            }
        }
        // 开始遍历,对点进行处理
        while (top != 0){
            // 在这里打印
            ver = stack[--top];
            System.out.println(ver + "->");
            count++;
            // 对该点的邻接点进行处理
            for (e = verList[ver].firstedge; e!=null; e=e.next){
                // 如果邻接点的入度-1为0的话,就入栈
                if ((--verList[e.data].in) == 0){
                    stack[top++] = e.data;
                }
            }
        }
        // 如果能输出所有点,说明无环
        if (count == len-1){
            System.out.println("无环");
        } else {
            System.out.println("有环");
        }
    }

    private static void CrateAdjList(aovNode[] verList, int[][] edgeList) {
        int len = edgeList.length;
        for (int i = 1; i < len; i++) {
            int s = edgeList[i][0];
            int t = edgeList[i][1];
            adjNode adj = new adjNode();
            adj.data = t;
            adj.next = verList[s].firstedge;
            verList[s].firstedge = adj;
        }
    }

    private static void InitTopoplogy(aovNode[] verList, int[][] edgeList) {
        // 初始化顶点的值和入度
        verList[1] = new aovNode(0,1);
        verList[2] = new aovNode(1,2);
        verList[3] = new aovNode(2,3);
        verList[4] = new aovNode(2,4);
        verList[5] = new aovNode(2,5);
        verList[6] = new aovNode(0,6);
        // 初始化边
        edgeList[1][0] = 1;
        edgeList[1][1] = 2;
        edgeList[2][0] = 1;
        edgeList[2][1] = 3;
        edgeList[3][0] = 2;
        edgeList[3][1] = 5;
        edgeList[4][0] = 3;
        edgeList[4][1] = 4;
        edgeList[5][0] = 5;
        edgeList[5][1] = 3;
        edgeList[6][0] = 5;
        edgeList[6][1] = 4;
        edgeList[7][0] = 6;
        edgeList[7][1] = 5;
    }
}

关键路径

AOE网:在AOV网的弧上加上权值表示活动的持续时间,这样的网叫做AOE网(Activity On Edge Network)。

路径长度:路径上各个活动所持续的时间之和。

关键路径:从源点(入度为0)到汇点(出度为0)具有最大长度的路径。

关键活动:在关键路径上的活动。

算法原理
如果所有活动的最早开始时间和最晚开始时间相同,就意味着此活动为关键活动,活动间的路径为关键路径,若不等则不是。
定义如下参数:

  1. 事件的最早发生时间evt:即顶点 v k v_k vk的最早发生时间
  2. 事件的最晚发生时间ltv:即顶点 v k v_k vk的最晚发生时间,也就是每个顶点对应的时间最晚需要开始的时间,超出此时间将会延误整个工期
  3. 活动的最早开工时间ete:即弧 a k a_k ak的最早发生时间
  4. 活动的最晚开工时间lte:即弧 a k a_k ak的最晚发生时间,也就是不推迟同期的最晚开工时间
    由1和2可以求得3和4,然后再根据ete[k]是否与lte[k]相等来判断 a k a_k ak是否是关键活动。

关键路径算法

全局变量

  • 求etv就是从头到尾找拓扑序列的过程
  • stack2用来存储拓扑序列
int *etv, *ltv;    // 事件最早发生时间和最迟发生时间数组
int *stack2;    // 用于存储拓扑排序的栈,吃stack吐出来的
int top2;    // 用于stack2的指针

改进后的求拓扑序列算法
关键点在于最后的求各顶点事件最早发生时间值,邻接点作为弧的尾点,开始时间取的是各个弧头和弧长的最大值,因为只有前置事件都完成了,才能进入下一事件。

Status TopologicalSort(GraphAdjList GL)
{
    EdgeNode *e;
    int i, k, gettop;
    int top = 0;
    int count = 0;
    int *stack;
    stack = (int *) malloc(GL->numVertexes * sizeof(int));
    for (i=0; i<GL->numVertexes; i++)
        if (0 == GL->adjList[i].in)
            stack[++top] = i;
    top2 = 0;
    etv = (int *) malloc(GL->numVertexes * sizeof(int));
    for (i=0; i<GL->numVertexes; i++)
        etv[i] = 0;    // 初始化为0
    stack2 = (int *) malloc(GL->numVertexes * sizeof(int));
    while(top != 0)
    {
        gettop = stack[top--];
        count++;
        stack2[++top2] = gettop;    // 弹出的顶点压入拓扑序列栈
        for (e=GL->adjList[gettop].firstedeg; e; e=e->next)
        {
            k = e->adjvex;
            if (!(--GL->adjList[k].in))
                stack[++top] = k;
            // 求各顶点事件最早发生时间值
            // 这里的意思就是k要等前面忙完,取前面的最充裕时间
            if ((etv[gettop] + e->weight) > etv[k])
                etv[k] = etv[gettop] + e->weight;
        }
    }
    if (count < GL->numVertexes)
        return ERROR;
    else
        return OK;
}

关键路径代码
注意:

  • 邻接表里的weight是对应的弧的值,也就是两点间的时间。
  • 一是求最晚发生时间,这里是倒序,从最后的点开始,取的是最小最晚时间,也就是让上一个活动开始的尽可能早,因为上个活动结束后剩下的时间里,要保证接下来的时间够完成所有任务,而不是只完成剩下的任务中耗时最短的那个,所以必须保证剩下的任务中耗时最长的可以完成,所以这里用的是小于号,意思就是最晚中的最早。
  • 求lte的时候,当ete=lte时,说明这个任务线是时间最紧的,最早最晚时间是被这个任务约束的,而不等的时候,一定是 lte>ete 的,这种情况的任务执行的时候,时间是有剩余的。
void CriticalPath(GraphAdjList GL)
{
    EgdeNode *e;
    int i, gettop, k, j;
    int ete, lte;    // 活动的最早和最晚发生时间
    TopologicalSort(GL);    //计算数组etv和stack2的值
    ltv = (int *) malloc(GL->numVertexes * sizeof(int));;    // 事件最晚发生时间
    for (i=0; i<GL->numVertexes; i++)
        // 全部初始化成最大时间,同时也方便后面的比较
        ltv[i] = etv[GL->numVertexes - 1]while (top2 != 0)    // 计算ltv
    {
        gettop = stack2[top2--];    // 拓扑序列后进先出
        for (e=GL->adjList[gettop].firstedge; e; e=e->next)
        {
            // 求各顶点时间的最迟发生时间ltv的值
            k = e->adjvex;
            // 求各顶点事件最晚发生时间ltv
            // 这里是要比较最晚开始的,因为可能好多任务交叉的,所以一定要比较,而不是用减法往前推就行
            // 这里要用最小,因为如果用最大,有些任务就完不成了
            if (ltv[k]-e->weight < ltv[gettop])
                ltv[gettop] = ltv[k] - e->weight;
        }
    }
    // 求ete,lte和关键活动
    // 有了最早和最晚,遍历所有点和其邻接点比较即可
    for (j=0; j<GL->numVertexes; j++)
    {
        for (e=GL->adjList[j].firstedge; e; e=e->next)
        {
            // 让点与邻接点时间比较
            k = e->adjvex;
            ete = etv[j];    // 活动最早发生时间
            lte = ltv[k] - e->weight;    // 活动最迟发生时间
            if (ete == lte)    // 两者相等即在关键路径上
                printf("<v%d, v%d>length: %d ",
                GL->adjList[j].data, GL->adjList[k].data, e->weight);
        }
    }
}

JAVA实现关键路径

结点类

public class adjWNode {
    int data;
    int weight;
    adjWNode next;

    public adjWNode(int data, int weight) {
        this.data = data;
        this.weight = weight;
    }
}

class aovNode{
    public int in;
    public int data;
    public adjWNode firstedge;


    public aovNode(int in, int data) {
        this.in = in;
        this.data = data;
    }
}

主代码

package Graph.KeyPath;

public class KeyPath {
    public static void main(String[] args) {
        aovNode[] verList = new aovNode[10];
        int[][] edgeList = new int[13][3];
        InitKeyPath(verList, edgeList);
        CreateAdjList(verList, edgeList);
        // 栈存放排序,这里要用到从后往前的方法,所以用栈来存储
        int[] stack = new int[10];
        // 记录各个活动最早开始时间的数组
        int[] evt = new int[10];
        // 用新的拓扑排序来初始化上面两个数组
        // top为stack数组的top
        int top = TopologySave(verList, stack, evt);
        keyPath(stack, top, evt, verList);
    }

    private static void keyPath(int[] stack, int top, int[] evt, aovNode[] verList) {
        int len = verList.length;
        // 先求一波最晚时间
        int[] ltv = new int[len];
        // 初始化为最大时间
        for (int i = 0; i < len; i++) {
            ltv[i] = evt[len-1];
        }
        // 从后往前
        while (top != 0){
            int gettop = stack[--top];
            // 对邻接点下手
            for (adjWNode e=verList[gettop].firstedge; e!=null; e=e.next){
                // 更新时间,越晚越好
                // 邻接点 - 路径时间 最小,为了保证所有任务都能完成
                if (ltv[e.data] - e.weight < ltv[gettop]){
                    ltv[gettop] = ltv[e.data] - e.weight;
                }
            }
        }
        // 最早和最晚都有了,遍历比较就行了
        // 这样可得到关键点
        for (int i = 0; i < len; i++) {
            if (evt[i] == ltv[i]){
                System.out.println(i);
            }
        }
        System.out.println("---------");

        // 这样可得到路径
        for (int i = 0; i < len; i++) {
            for (adjWNode e=verList[i].firstedge; e!=null; e=e.next){
                if (evt[i] == ltv[e.data]-e.weight){
                    System.out.println(i + "->" + e.data);
                }
            }
        }

    }

    private static int TopologySave(aovNode[] verList, int[] stack2, int[] evt) {
        int len = verList.length;
        int[] stack = new int[len];
        int count = 0;
        int top = 0;
        int top2 = 0;
        for (int i = 0; i < len; i++) {
            if (verList[i].in == 0){
                stack[top++] = i;
            }
        }
        // 初始化evt
        for (int i = 0; i < len; i++) {
            evt[i] = 0;
        }
        while(top!=0){
            int gettop = stack[--top];
            count++;
            // 存起来
            stack2[top2++] = gettop;
            for (adjWNode e=verList[gettop].firstedge; e!=null; e=e.next){
                if((--verList[e.data].in)==0){
                    stack[top++] = e.data;
                }
                // 这里还得记下最早时间
                // e.weight就是从gettop到e.data需要的时间
                if (evt[gettop] + e.weight > evt[e.data]){
                    evt[e.data] = evt[gettop] + e.weight;
                }
            }
        }
        if (count == len){
            System.out.println("无环");
        } else {
            System.out.println("有环");
        }
        return top2;
    }

    private static void CreateAdjList(aovNode[] verList, int[][] edgeList) {
        for (int i=0; i< edgeList.length; i++){
            int s = edgeList[i][0];
            int e = edgeList[i][1];
            adjWNode node = new adjWNode(e, edgeList[i][2]);
            node.next = verList[s].firstedge;
            verList[s].firstedge = node;
        }
    }

    private static void InitKeyPath(aovNode[] verList, int[][] edgeList) {
        verList[0] = new aovNode(0,0);
        verList[1] = new aovNode(1,1);
        verList[2] = new aovNode(1,2);
        verList[3] = new aovNode(2,3);
        verList[4] = new aovNode(2,4);
        verList[5] = new aovNode(1,5);
        verList[6] = new aovNode(1,6);
        verList[7] = new aovNode(2,7);
        verList[8] = new aovNode(1,8);
        verList[9] = new aovNode(2,9);

        edgeList[0][0] = 0;
        edgeList[0][1] = 1;
        edgeList[0][2] = 3;
        edgeList[1][0] = 0;
        edgeList[1][1] = 2;
        edgeList[1][2] = 4;
        edgeList[2][0] = 1;
        edgeList[2][1] = 3;
        edgeList[2][2] = 5;
        edgeList[3][0] = 1;
        edgeList[3][1] = 4;
        edgeList[3][2] = 6;
        edgeList[4][0] = 2;
        edgeList[4][1] = 3;
        edgeList[4][2] = 8;
        edgeList[5][0] = 2;
        edgeList[5][1] = 5;
        edgeList[5][2] = 7;
        edgeList[6][0] = 3;
        edgeList[6][1] = 4;
        edgeList[6][2] = 3;
        edgeList[7][0] = 4;
        edgeList[7][1] = 6;
        edgeList[7][2] = 9;
        edgeList[8][0] = 4;
        edgeList[8][1] = 7;
        edgeList[8][2] = 4;
        edgeList[9][0] = 5;
        edgeList[9][1] = 7;
        edgeList[9][2] = 6;
        edgeList[10][0] = 6;
        edgeList[10][1] = 9;
        edgeList[10][2] = 2;
        edgeList[11][0] = 7;
        edgeList[11][1] = 8;
        edgeList[11][2] = 5;
        edgeList[12][0] = 8;
        edgeList[12][1] = 9;
        edgeList[12][2] = 3;
    }
}

思维导图

在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值