数据结构 - - - 图

本文详细介绍了图的基本概念,包括图的定义、类型、存储结构等,并深入探讨了图的遍历算法及其经典应用,例如最小生成树、最短路径等。

一、图的定义

1、图G由顶点集V和边集E组成,记为G=(V,E), |V|表示图G中顶点的个数,也称图G的阶,|E|表示图G中边的条数。

注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集。


2、无向图、有向图

顶点v的度,是依附于该顶点的边的条数。

无向图中,两个顶点中间无向边用()表示,顺序可以互换。每一条边为两个顶点提供度,当有e条边时,总度为2e。

无向图中,两个顶点中间有向边用<>表示,顺序不同则是不同的边。每一条边为一个顶点提供入度,就为另一个顶点提供出度,当有e条边时,出度==入度,总度为2e。


3、顶点-顶点的关系在这里插入图片描述


4、连通图、强连通图

连通图(图中任意两个顶点都是连通的)
对于n个顶点的无向图G,若是连通图,则最少有n-1条边,若是非连通图,则最多可能有(n*n-1)/2

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


5、图的局部- - -子图
在这里插入图片描述
生成子图:包含原图的所有顶点,去除一些边(有向图的定义相同)


6、连通分量、强连通分量

连通分量(无向图)必须要极大连通子图(子图必须连通,且包含尽可能多的顶点和边)
在这里插入图片描述
强连通分量(有向图),中极大强连通子图
在这里插入图片描述


7、生成树 - - - 树:不存在回路,且连通的无向图(n个顶点的树,必有n-1条边)

连通图的上生成树是由一个图的包含全部顶点的极小连通子图边尽可能的少,但要保持连通)构成:若有n个顶点,生成树中有且只有n-1条边。(树的结点比分支多1)
在这里插入图片描述


8、生成森林

非连通图中,取极大连通子图找出图的连通分量,然后找到每个连通分量的包含每个连通分量的所有顶点极小连通子图,即每个连通分量生成一个树,构成森林。
在这里插入图片描述


二、图的存储

1、邻接矩阵法适合于无向图、有向图

空间复杂度为n^2,适合于存储稠密图,因为无向图是对称矩阵,可以压缩存储。
在这里插入图片描述
在这里插入图片描述

public class Graph {

    private ArrayList<String> vertexList;  // 存储顶点集合
    private int[][] edges;  //存储图对应的邻接矩阵
    private int numOfEdges;

    public static void main(String[] args) {

        //测试图是否创建
        int n=5; // 结点的个数
        String Vertexs[]={"A","B","C","D","E"};
        // 创建图对象
        Graph graph=new Graph(n);
        // 循环的添加顶点
        for (String Vertex:Vertexs){
            graph.insertVertex(Vertex);
        }

        //添加边
        //A-B A-C B-C B-D B-E
        graph.insertEdge(0,1,1);//A-B
        graph.insertEdge(0,2,1);//A-C
        graph.insertEdge(1,2,1);//B-C
        graph.insertEdge(1,3,1);//B-D
        graph.insertEdge(1,4,1);//B-E

        //显示邻接矩阵
        graph.showGraph();

    }

    //构造器
    public Graph(int n) {  // n表示传入的顶点个数
        // 初始化矩阵和vertexList
        edges=new int[n][n];
        vertexList=new ArrayList<String>(n);
        numOfEdges=0;
    }

    //插入结点(顶点)
    public void  insertVertex(String vertex){
        vertexList.add(vertex);
    }

    //添加边
    /*
    * v1表示顶点的下标
    * v2表示顶点的下标
    * weight表示边的权值 0/1  矩阵内元素的数值
    * */
    public void insertEdge(int v1,int v2,int weight){
        edges[v1][v2]=weight;
        edges[v2][v1]=weight;
        numOfEdges++;
    }

    // 图中常用的方法
    // 返回顶点的个数
    public int getNumOfvertex(){
       return vertexList.size();
    }

    // 得到边的数目
    public int getNumOfEdges(){
       return numOfEdges;
    }

    // 返回结点i(下标)对应的数据
    public String getValueByIndex(int i){
        return vertexList.get(i);
    }

    //返回v1和v2对应边的权值
    public int getWeight(int v1,int v2){
        return edges[v1][v2];
    }

    // 显示图对应的矩阵
    public void  showGraph(){
        for (int[] link:edges){
            System.out.println(Arrays.toString(link));
        }
    }

}

在这里插入图片描述

A^n [ i ] [ j ] 表示由i对应的顶点到j对应的顶点长度为n的路径的数量
在这里插入图片描述


2、邻接表法顺序+链式存储) 同理 - - - 树的孩子表示法 (适合于无向图、有向图

在这里插入图片描述
在这里插入图片描述


3、十字链表存储有向图只适合于有向图

在这里插入图片描述
弧- - - (弧尾) → (弧头) 出度顺着绿色线路找,入度顺着橙色线路找。

顶点结点中:
firstin- - -从其他顶点指向该顶点
firstout- - - 从该顶点指向其他顶点


4、邻接多重表存储无向图只适合于无向图

在这里插入图片描述
总结:四种存储方式的对比
在这里插入图片描述


、图的遍历

1、广度优先遍历BFS

要点
① 找到与一个顶点相邻的所有顶点
② 标记哪些顶点被访问过
③ 需要一个辅助队列

FitstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号;若x没有邻接点或图中不存在x,则返回-1.

NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1.

在这里插入图片描述
同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一

同一个图的邻接表表示方式不唯一,因此广度优先遍历序列不唯一


注意:存在问题
如果是非连通图,则无法遍历完所有结点

解决方法

根据visite的false判断,对每一个连通分量调用一次BFS,每调用一次BFS就遍历完一个连通分量
在这里插入图片描述


修改和增添后的代码如下:

修改

1、创建8个顶点

	int n=8; // 结点的个数
    String Vertexs[]={"A","B","C","D","E","F","G","H"};

2、添加边时,生成两个非连通子图

	//添加边
    //A-B A-C B-C B-D B-E
    graph.insertEdge(0,1,1);//A-B
    graph.insertEdge(0,2,1);//A-C
    graph.insertEdge(1,2,1);//B-C
    graph.insertEdge(1,3,1);//B-D
    graph.insertEdge(1,4,1);//B-E

    // 上下是个非连通子图

    graph.insertEdge(5,6,1);//F-G
    graph.insertEdge(5,7,1);//F-H
    graph.insertEdge(6,7,1);//G-H

增加

1、定义数组boolean[],记录某个结点是否被访问

private boolean[] isVisited;   // main函数中定义,然后在构造器中new

2、 定义一个队列,记录顶点出入–因为LinkedList有两个方法故用它来替代队列

private LinkedList queue;	 // main函数中定义,然后在构造器中new

2、增加图遍历之前准备的方法:

getFirstNeighbor 返回当前顶点的第一个邻接顶点,没有则返回-1

/*
* 传入当前顶点v
* 如果有与v相连的,则返回第一个邻接顶点w,否则返回-1
* */

public  int getFirstNeighbor(int v){
    for (int w=0;w<vertexList.size();w++){
        if (edges[v][w]>0){  // 两顶点边值>0;说明两顶点有连接
            return w;    // 返回与v相连的第一个邻接顶点w
        }
    }
    return -1;
}

getNextNeighbor 返回当前顶点第一个邻接顶点的下一个邻接顶点,没有则返回-1

//根据前一个邻接结点的下标来获取下一个邻接结点
/*
* 传入当前顶点v,以及与v相连的第一个邻接顶点w
* 如果有与v相连且在w之后的另一个邻接顶点j则返回,否则返回-1
* */

public int getNextNeighbor(int v,int w){
    for (int j=w+1;j<vertexList.size();j++){  // w与v相连,从w+1开始遍历
        if (edges[v][j]>0){  // 两顶点边值>0;说明两顶点有连接
            return j;     // 返回与v相连的w之后的下一个顶点j
        }
    }
    return -1;
}

3、dfs遍历和解决非连通子图间的遍历

// 解决广度遍历非连通图不能遍历的问题
public void bfsTraverse(boolean [] isVisited){
    for (int v=0;v<vertexList.size();++v){
        isVisited[v]=false;  // 初始化访问标记
    }
    for (int v=0;v<vertexList.size();++v){
        if (!isVisited[v]){  //如果没被访问
            bfs(isVisited,v,queue);
        }
    }
}

// 广度优先遍历算法
public void bfs(boolean[] isVisited,int v,LinkedList queue){
    //首先访问该顶点,输出
    System.out.println(getValueByIndex(v)+"->");
    //将该顶点设置为已经访问
    isVisited[v]=true;

    //从尾部加入
    queue.addLast(v);
    while (!queue.isEmpty()){  //队列非空
        // 去出队列的头结点下标并删除
        queue.removeFirst();

        for (int w=getFirstNeighbor(v);w>=0;w=getNextNeighbor(v,w)) {

            if (!isVisited[w]) {  // 如果w对应的顶点没有被访问,则访问输出
                System.out.println(getValueByIndex(w) + "->");
                isVisited[w] = true; //设为被访问过的状态
                queue.addLast(w);  //加入到队列中
            }
            //否则,获取v的下一个邻接顶点,继续判断访问
        }
    }
}

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

广度优先生成森林
对非连通图的广度优先遍历,可得到广度优先生成森林。

在这里插入图片描述


2、深度优先遍历DFS递归过程

每一个邻接顶点再取递归调用访问其邻接顶点(深处走)
初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点

算法步骤

1)访问初始结点v,并标记结点v为已访问

2)查找结点v的第一个邻接结点w。

3)若w存在,则继续执行4)。如果w不存在,则回到1),将从v的下一个结点继续。

4)若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤1、2、3)。

5)查找结点v的w邻接结点的下一个邻接结点,转到步骤3)


下图中从2出发的深度遍历序列:2、1、5、6、3、4、7、8
在这里插入图片描述


注意:同样存在问题
如果是非连通图,则无法遍历完所有结点

解决方法

根据visite的false判断,对每一个连通分量调用一次DFS,每调用一次DFS就遍历完一个连通分量

在这里插入图片描述


修改和增添后的代码如下:

修改

1、创建8个顶点

	int n=8; // 结点的个数
    String Vertexs[]={"A","B","C","D","E","F","G","H"};

2、添加边时,生成两个非连通子图

	//添加边
    //A-B A-C B-C B-D B-E
    graph.insertEdge(0,1,1);//A-B
    graph.insertEdge(0,2,1);//A-C
    graph.insertEdge(1,2,1);//B-C
    graph.insertEdge(1,3,1);//B-D
    graph.insertEdge(1,4,1);//B-E

    // 上下是个非连通子图

    graph.insertEdge(5,6,1);//F-G
    graph.insertEdge(5,7,1);//F-H
    graph.insertEdge(6,7,1);//G-H

增加

1、定义数组boolean[],记录某个结点是否被访问

private boolean[] isVisited;

2、增加图遍历之前准备的方法:

getFirstNeighbor 返回当前顶点的第一个邻接顶点,没有则返回-1

/*
* 传入当前顶点v
* 如果有与v相连的,则返回第一个邻接顶点w,否则返回-1
* */

public  int getFirstNeighbor(int v){
    for (int w=0;w<vertexList.size();w++){
        if (edges[v][w]>0){  // 两顶点边值>0;说明两顶点有连接
            return w;    // 返回与v相连的第一个邻接顶点w
        }
    }
    return -1;
}

getNextNeighbor 返回当前顶点第一个邻接顶点的下一个邻接顶点,没有则返回-1

//根据前一个邻接结点的下标来获取下一个邻接结点
/*
* 传入当前顶点v,以及与v相连的第一个邻接顶点w
* 如果有与v相连且在w之后的另一个邻接顶点j则返回,否则返回-1
* */

public int getNextNeighbor(int v,int w){
    for (int j=w+1;j<vertexList.size();j++){  // w与v相连,从w+1开始遍历
        if (edges[v][j]>0){  // 两顶点边值>0;说明两顶点有连接
            return j;     // 返回与v相连的w之后的下一个顶点j
        }
    }
    return -1;
}

3、dfs遍历和解决非连通子图间的遍历

// 解决深度遍历非连通图不能遍历的问题
public void dfsTraverse(boolean [] isVisited){
    for (int v=0;v<vertexList.size();++v){
        isVisited[v]=false;  // 初始化访问标记
    }
    for (int v=0;v<vertexList.size();++v){
        if (!isVisited[v]){  //如果没被访问
            dfs(isVisited,v);
        }
    }
}

// 深度优先遍历算法
// v第一次就是0,集合中i=0下对应的顶点“A”
public void dfs(boolean[] isVisited,int v){
    //首先访问该顶点,输出
    System.out.println(getValueByIndex(v)+"->");
    //将该顶点设置为已经访问
    isVisited[v]=true;

    // 遍历
    for (int w=getFirstNeighbor(v);w>=0;w=getNextNeighbor(v,w)){

            if (!isVisited[w]){  // 如果w对应的顶点没有被访问,
                // 递归访问与w相连的第一个邻接顶点
                dfs(isVisited,w);
            }
            //否则,获取v的下一个邻接顶点,继续判断访问
    }
}

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

深度优先生成森林
对非连通图的广度优先遍历,可得到深度优先生成森林。


、图的经典应用

1、最小生成树(最小代价树)研究对象是带权连通无向图

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


最小生成树的特点
最小生成树可能有多个,但边的权值之和总是唯一且最小的;
最小生成树的边数 = 顶点数 - 1. 砍掉一条则不连通,增加一条边则会出现回路;
如果一个连通图本身就是一棵树,则其最小生成树就是它本身
只有连通图才有生成树,非连通图只有生成森林。


Prim算法(普里姆)

从某一个顶点开始构建生成树;
每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度:O(|V|^2)- - - 适合用于边稠密图
在这里插入图片描述


Kruskal算法(克鲁斯卡尔)

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

时间复杂度:O(|E| * log2|E|)- - - 适合用于边稀疏图
在这里插入图片描述


2、最短路径

最短路径问题:

1) 单源最短路径某一个顶点和其他顶点的最短路径

① BFS算法(无权图)

d[ i ]表示从u到i顶点的最短路径;path[ i ]表示最短路径从哪个顶点过来

在这里插入图片描述
BFS算法求单源最短路径的局限性:
只适用于无权图,或所有边的权值都相同的图。


② Dijkstra算法(带权图、无权图)

dist与Prim中lowCast类似

还没有确定的顶点V4的5最小,故令final[5]=ture
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Dijkstra算法求单源最短路径的局限性:
不适用于有负权值的带权图。


2)各顶点间的最短路径每对顶点间的最短路径

Floyed算法(带权图、无权图)
在这里插入图片描述
依次扫描矩阵中所有位置上的元素,在允许当前中转的条件下,把满足条件的进行修改,否则不变

在这里插入图片描述
A表示目前来看,各顶点间的最短路径长度(邻接矩阵)
path表示两个顶点之间的中转点(邻接矩阵)


Floyd可以解决负权图
在这里插入图片描述
Floyd不能解决的问题

Floyd算法不能解决带有”负权回路“的图(有负权值的边组成的回路),这种图有可能没有最短路径。


、有向无环图(DAG)

定义:

若一个有向图不存在环,则称为有向无环图。简称DAG图。


DAG图的应用


1、DAG描述表达式

同层相同的只留一个
在这里插入图片描述
在这里插入图片描述


2、AOV网

定义:

AOV网(Activity on Vertex NetWork,用顶点表示活动的网);用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行

在这里插入图片描述
在这里插入图片描述

当有回路时,就会出现每个顶点都有前驱的情况,就会停止拓扑排序
在这里插入图片描述

每个AOV网都有一种或多种拓扑排序序列
在这里插入图片描述


逆拓扑排序
在这里插入图片描述
在这里插入图片描述
总结

使用邻接表(一个顶点指向其他顶点)要全部遍历才能删除指向出度为0的边(麻烦);

使用邻接矩阵/逆邻接表(所有指向当前顶点的顶点) 比较方便


使用逆拓扑排序实现(DFS算法)
在这里插入图片描述


3、AOE网

定义:

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

注意
顶点表示事件(是一瞬间完成的)

有向边表示活动(是需要一定时间完成的)


AOE网具有以下两个性质:

只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;

只有在进入某顶点的个有向边所代表的活动都已经结束时,该顶点所代表的事件才能发生,另外,有些活动是可以并行进行的。

在这里插入图片描述
在这里插入图片描述
不存在时间余量的活动(即不可以拖延时间的活动)被称为关键活动,由关键活动组成的路径就是关键路径


拓扑排序求出事件最早发生时间
在这里插入图片描述


逆拓扑排序求出事件最晚发生时间
在这里插入图片描述


求的关键活动、关键路径
在这里插入图片描述

关键活动、关键路径的特性

若关键活动耗时增加,则整个工程的工期将增长。
缩短关键活动的时间,可以缩短整个工程的工期。
当缩短到一定程度时,关键活动可能会变成非关键活动。

注意
可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值