【数据结构基础】之图的介绍,生动形象,通俗易懂,算法入门必看

news2025/4/7 13:08:04

前言

在这里插入图片描述

本文为数据结构基础【图】 相关知识,下边将对图的基本概念图的存储结构图的遍历包含广度优先遍历深度优先遍历循环遍历数组最小生成树拓扑排序等进行详尽介绍~

📌博主主页:小新要变强 的主页
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
👉Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)

在这里插入图片描述

目录

文章标题

  • 前言
  • 目录
  • 一、图的基本概念
    • 1️⃣图的定义
    • 2️⃣图的种类
    • 3️⃣邻接点和度
    • 4️⃣路径和回路
    • 5️⃣连通图和连通分量
    • 6️⃣权
  • 二、图的存储结构
    • 1️⃣邻接矩阵
    • 2️⃣邻接表
  • 三、图的遍历
    • 1️⃣广度优先搜索
    • 2️⃣深度优先搜索
  • 四、最小生成树
    • 1️⃣最小生成树概念
    • 2️⃣克鲁斯卡尔(Kruskal)算法
    • 3️⃣普里姆(Prim)算法
  • 五、拓扑排序
    • 1️⃣拓扑排序介绍
    • 2️⃣拓扑排序的算法图解
    • 3️⃣拓扑排序的代码实现
  • 后记


在这里插入图片描述


一、图的基本概念

1️⃣图的定义

定义: 图(graph)是由一些点(vertex)和这些点之间的连线(edge)所组成的;其中,点通常被成为"顶点(vertex)“,而点与点之间的连线则被成为"边或弧”(edege)。通常记为,G=(V,E)。

2️⃣图的种类

根据边是否有方向,将图可以划分为:无向图有向图

🍀(1)无向图
在这里插入图片描述
上面的图G0是无向图,无向图的所有的边都是不区分方向的。G0=(V1,{E1})。其中:

  • (1)V1={A,B,C,D,E,F}。 V1表示由"A,B,C,D,E,F"几个顶点组成的集合。
  • (2)E1={(A,B),(A,C),(B,C),(B,E),(B,F),(C,F), (C,D),(E,F),(C,E)}。E1是由边(A,B),边(A,C)…等组成的集合。其中,(A,C)表示由顶点A和顶点C连接成的边。

🍀(2)有向图
在这里插入图片描述
上面的图G2是有向图。和无向图不同,有向图的所有的边都是有方向的! G2=(V2,{A2})。其中:

  • (1)V2={A,C,B,F,D,E,G}。 V2表示由"A,B,C,D,E,F,G"几个顶点组成的集合。
  • (2)A2={<A,B>,<B,C>,<B,F>,<B,E>,<C,E>,<E,D>,<D,C>,<E,B>,<F,G>}。E1是由矢量<A,B>,矢量<B,C>…等等组成的集合。其中,矢量<A,B)表示由"顶点A"指向"顶点C"的有向边。

3️⃣邻接点和度

🍀(1)邻接点

  • (1)一条边上的两个顶点叫做邻接点。 例如,上面无向图G0中的顶点A和顶点C就是邻接点。
  • (2)在有向图中,除了邻接点之外;还有"入边"和"出边"的概念。顶点的入边,是指以该顶点为终点的边。而顶点的出边,则是指以该顶点为起点的边。例如,上面有向图G2中的B和E是邻接点;<B,E>是B的出边,还是E的入边。

🍀(2)度

  • (1)在无向图中,某个顶点的度是邻接到该顶点的边(或弧)的数目。 例如,上面无向图G0中顶点A的度是2。
  • (2)在有向图中,度还有"入度"和"出度"之分。某个顶点的入度,是指以该顶点为终点的边的数目。而顶点的出度,则是指以该顶点为起点的边的数目。 顶点的度=入度+出度。例如,上面有向图G2中,顶点B的入度是2,出度是3;顶点B的度=2+3=5。

4️⃣路径和回路

  • 路径: 如果顶点(Vm)到顶点(Vn)之间存在一个顶点序列。则表示Vm到Vn是一条路径。
  • 路径长度: 路径中"边的数量"。
  • 简单路径: 若一条路径上顶点不重复出现,则是简单路径。
  • 回路: 若路径的第一个顶点和最后一个顶点相同,则是回路。
  • 简单回路: 第一个顶点和最后一个顶点相同,其它各顶点都不重复的回路则是简单回路。

5️⃣连通图和连通分量

  • 连通图: 对无向图而言,任意两个顶点之间都存在一条无向路径,则称该无向图为连通图。 对有向图而言,若图中任意两个顶点之间都存在一条有向路径,则称该有向图为强连通图。
  • 连通分量: 非连通图中的各个连通子图称为该图的连通分量。

6️⃣权

在学习"哈夫曼树"的时候,了解过"权"的概念。图中权的概念与此类似。
在这里插入图片描述
上面就是一个带权的图。

二、图的存储结构

图的存储结构,常用的是"邻接矩阵"和"邻接表"。

1️⃣邻接矩阵

邻接矩阵是指用矩阵来表示图。它是采用矩阵来描述图中顶点之间的关系(及弧或边的权)。
假设图中顶点数为n,则邻接矩阵定义为:
在这里插入图片描述
下面通过示意图来进行解释。
在这里插入图片描述
图中的G1是无向图和它对应的邻接矩阵。
在这里插入图片描述
图中的G2是无向图和它对应的邻接矩阵。

通常采用两个数组来实现邻接矩阵:一个一维数组用来保存顶点信息,一个二维数组来用保存边的信息。
邻接矩阵的缺点就是比较耗费空间。

2️⃣邻接表

邻接表是图的一种链式存储表示方法。它是改进后的"邻接矩阵",它的缺点是不方便判断两个顶点之间是否有边,但是相对邻接矩阵来说更省空间。
在这里插入图片描述
图中的G1是无向图和它对应的邻接矩阵。

在这里插入图片描述
图中的G2是无向图和它对应的邻接矩阵。

三、图的遍历

对于图而言,我们常用的遍历方式有bfs和dfs两种:

  • bfs:广度优先搜索算法,英文Breadth First Search。广度优先搜索会优先访问当前顶点的所有邻接结点。
  • dfs:深度优先搜索算法,英文Depth First Search。深度优先搜索会优先顺延访问当前节点分支进行访问,直到不能深入,每个节点只访问一次。

1️⃣广度优先搜索

🍀(1)广度优先搜索介绍

  • 广度优先搜索算法(Breadth First Search),又称为"宽度优先搜索"或"横向优先搜索",简称BFS。
  • 它的思想是:从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。
  • 换句话说,广度优先搜索遍历图的过程是以v为起点,由近至远,依次访问和v有路径相通且路径长度为1,2…的顶点。

🍀(2)广度优先搜索图解

无向图的广度优先搜索:

在这里插入图片描述

  • 第1步:访问A。
  • 第2步:依次访问C,D,F。在访问了A之后,接下来访问A的邻接点。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,C在"D和F"的前面,因此,先访问C。再访问完C之后,再依次访问D,F。
  • 第3步:依次访问B,G。在第2步访问完C,D,F之后,再依次访问它们的邻接点。首先访问C的邻接点B,再访问F的邻接点G。
  • 第4步:访问E。 在第3步访问完B,G之后,再依次访问它们的邻接点。只有G有邻接点E,因此访问G的邻接点E。

因此访问顺序是:A -> C -> D -> F -> B -> G -> E

有向图的广度优先搜索:

在这里插入图片描述

  • 第1步:访问A。
  • 第2步:访问B。
  • 第3步:依次访问C,E,F。在访问了B之后,接下来访问B的出边的另一个顶点,即C,E,F。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,因此会先访问C,再依次访问E,F。
  • 第4步:依次访问D,G。 在访问完C,E,F之后,再依次访问它们的出边的另一个顶点。还是按照C,E,F的顺序访问,C的已经全部访问过了,那么就只剩下E,F;先访问E的邻接点D,再访问F的邻接点G。

因此访问顺序是:A -> B -> C -> E -> F -> D -> G

🍀(3)广度优先搜索代码实现

public class Graph {
    /**
     * 定义顶点的抽象
     * @param <T>
     */
    public static class Vertex<T>{
        // 要保存的数据
        private T t;
        // 其他和我管理的邻接节点
        private List<Vertex<T>> neighborList;

        private boolean visited = false;

        public Vertex(T t) {
            this.t = t;
        }
    }

    // bfs 广度优先遍历算法
    public static <T> void bfs(Vertex<T> vertex){
        // 1、定义一个临时存储的空间,使用队列
        Queue<Vertex<T>> queue = new ArrayBlockingQueue<>(8);
        // 2、增加一个用来保存已经遍历过的数据的集合
        HashSet<Vertex<T>> mome = new HashSet<>(8);
        // 3、将第一个顶点放入队列
        queue.add(vertex);

        while (!queue.isEmpty()){
            // 将第一个元素拿出来
            Vertex<T> temp = queue.poll();
            // 进行操作
            if (!mome.contains(temp)){
                System.out.println(temp.t);
                mome.add(temp);
            }
            // 将他所有的邻接节点放进去
            if(temp.neighborList != null){
                queue.addAll(temp.neighborList);
            }
        }

    }
}

2️⃣深度优先搜索

🍀(1)深度优先搜索介绍

  • 图的深度优先搜索(Depth First Search),和树的先序遍历比较类似。
  • 它的思想:假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
  • 深度优先搜索是一个递归的过程。

🍀(2)深度优先搜索图解
无向图的深度优先搜索:
在这里插入图片描述

对上面的图G1进行深度优先遍历,从顶点A开始

在这里插入图片描述

  • 第1步:访问A。
  • 第2步:访问(A的邻接点)C。 在第1步访问A之后,接下来应该访问的是A的邻接点,即"C,D,F"中的一个。但在本文的实现中,顶点ABCDEFG是按照顺序存储,C在"D和F"的前面,因此,先访问C。
  • 第3步:访问(C的邻接点)B。 在第2步访问C之后,接下来应该访问C的邻接点,即"B和D"中一个(A已经被访问过,就不算在内)。而由于B在D之前,先访问B。
  • 第4步:访问(C的邻接点)D。 在第3步访问了C的邻接点B之后,B没有未被访问的邻接点;因此,返回到访问C的另一个邻接点D。
  • 第5步:访问(A的邻接点)F。 前面已经访问了A,并且访问完了"A的邻接点B的所有邻接点(包括递归的邻接点在内)";因此,此时返回到访问A的另一个邻接点F。
  • 第6步:访问(F的邻接点)G。
  • 第7步:访问(G的邻接点)E。

因此访问顺序是:A -> C -> B -> D -> F -> G -> E

有向图的深度优先搜索:
在这里插入图片描述
对上面的图G2进行深度优先遍历,从顶点A开始。

在这里插入图片描述

  • 第1步:访问A。
  • 第2步:访问B。 在访问了A之后,接下来应该访问的是A的出边的另一个顶点,即顶点B。
  • 第3步:访问C。在访问了B之后,接下来应该访问的是B的出边的另一个顶点,即顶点C,E,F。在本文实现的图中,顶点ABCDEFG按照顺序存储,因此先访问C。
  • 第4步:访问E。 接下来访问C的出边的另一个顶点,即顶点E。
  • 第5步:访问D。 接下来访问E的出边的另一个顶点,即顶点B,D。顶点B已经被访问过,因此访问顶点D。
  • 第6步:访问F。 接下应该回溯"访问A的出边的另一个顶点F"。
  • 第7步:访问G。

因此访问顺序是:A -> B -> C -> E -> D -> F -> G

🍀(3)深度优先搜索代码实现

public class Graph {
    /**
     * 定义顶点的抽象
     * @param <T>
     */
    public static class Vertex<T>{
        // 要保存的数据
        private T t;
        // 其他和我管理的邻接节点
        private List<Vertex<T>> neighborList;

        private boolean visited = false;

        public Vertex(T t) {
            this.t = t;
        }
    }
    // dfs 深度优先遍历算法
    public static <T> void dfs(Vertex<T> vertex){
        // 1、定义一个临时存储的空间
        Stack<Vertex<T>> stack = new Stack<>();
        // 2、将第一个顶点放入栈中
        stack.push(vertex);
        while (!stack.isEmpty()){
            // 3、将栈顶的元素取出
            Vertex<T> temp = stack.pop();
            // 4、执行操作
            if(!temp.visited){
                System.out.println(temp.t);
                temp.visited = true;
            }
            // 5、将邻接节点压栈
            if(temp.neighborList != null){
                stack.addAll(temp.neighborList);
            }
        }
    }
}

四、最小生成树

1️⃣最小生成树概念

在含有n个顶点的连通图中选择n-1条边,构成一棵极小连通子图,并使该连通子图中n-1条边上权值之和达到最小,则称其为连通网的最小生成树。
在这里插入图片描述
例如,对于如上图G4所示的连通网可以有多棵权值总和不相同的生成树。

在这里插入图片描述

2️⃣克鲁斯卡尔(Kruskal)算法

🍀(1)克鲁斯卡尔算法介绍

  • 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
  • 基本思想: 按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路。
  • 具体做法: 首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。

🍀(2)克鲁斯卡尔算法图解

以上图G4为例,来对克鲁斯卡尔进行演示(假设,用数组R保存最小生成树结果)。

在这里插入图片描述

  • 第1步:将边<E,F>加入R中。 边<E,F>的权值最小,因此将它加入到最小生成树结果R中。
  • 第2步:将边<C,D>加入R中。 上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果R中。
  • 第3步:将边<D,E>加入R中。 上一步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果R中。
  • 第4步:将边<B,F>加入R中。上一步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳过边<C,F>。将边<B,F>加入到最小生成树结果R中。
  • 第5步:将边<E,G>加入R中。 上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果R中。
  • 第6步:将边<A,B>加入R中。上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路;因此,跳过边<F,G>。同理,跳过边<B,C>。将边<A,B>加入到最小生成树结果R中。

此时,最小生成树构造完成!它包括的边依次是:<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。

🍀(3)克鲁斯卡尔算法代码实现

这里选取"邻接矩阵"对克鲁斯卡尔算法进行说明。

// 边的结构体
private static class EData {
    char start; // 边的起点
    char end;   // 边的终点
    int weight; // 边的权重

    public EData(char start, char end, int weight) {
        this.start = start;
        this.end = end;
        this.weight = weight;
    }
}
// 邻接矩阵边对应的结构体
public class MatrixUDG {

    private int mEdgNum;        // 边的数量
    private char[] mVexs;       // 顶点集合
    private int[][] mMatrix;    // 邻接矩阵
    private static final int INF = Integer.MAX_VALUE;   // 最大值

    ...
}
/*
 * 克鲁斯卡尔(Kruskal)最小生成树
 */
public void kruskal() {
    int index = 0;                      // rets数组的索引
    int[] vends = new int[mEdgNum];     // 用于保存"已有最小生成树"中每个顶点在该最小树中的终点。
    EData[] rets = new EData[mEdgNum];  // 结果数组,保存kruskal最小生成树的边
    EData[] edges;                      // 图对应的所有边

    // 获取"图中所有的边"
    edges = getEdges();
    // 将边按照"权"的大小进行排序(从小到大)
    sortEdges(edges, mEdgNum);

    for (int i=0; i<mEdgNum; i++) {
        int p1 = getPosition(edges[i].start);      // 获取第i条边的"起点"的序号
        int p2 = getPosition(edges[i].end);        // 获取第i条边的"终点"的序号

        int m = getEnd(vends, p1);                 // 获取p1在"已有的最小生成树"中的终点
        int n = getEnd(vends, p2);                 // 获取p2在"已有的最小生成树"中的终点
        // 如果m!=n,意味着"边i"与"已经添加到最小生成树中的顶点"没有形成环路
        if (m != n) {
            vends[m] = n;                       // 设置m在"已有的最小生成树"中的终点为n
            rets[index++] = edges[i];           // 保存结果
        }
    }

    // 统计并打印"kruskal最小生成树"的信息
    int length = 0;
    for (int i = 0; i < index; i++)
        length += rets[i].weight;
    System.out.printf("Kruskal=%d: ", length);
    for (int i = 0; i < index; i++)
        System.out.printf("(%c,%c) ", rets[i].start, rets[i].end);
    System.out.printf("\n");
}

3️⃣普里姆(Prim)算法

🍀(1)普里姆算法介绍

  • 普里姆(Prim)算法,是用来求加权连通图的最小生成树的算法。
  • 基本思想 : 对于图G而言,V是所有顶点的集合;现在,设置两个新的集合U和T,其中U用于存放G的最小生成树中的顶点,T存放G的最小生成树中的边。从所有uЄU,vЄ(V-U) (V-U表示出去U的所有顶点)的边中选取权值最小的边(u, v),将顶点v加入集合U中,将边(u, v)加入集合T中,如此不断重复,直到U=V为止,最小生成树构造完毕,这时集合T中包含了最小生成树中的所有边。

🍀(2)普里姆算法图解
在这里插入图片描述
以上图G4为例,来对普里姆进行演示(从第一个顶点A开始通过普里姆算法生成最小生成树)。

在这里插入图片描述
初始状态:V是所有顶点的集合,即V={A,B,C,D,E,F,G};U和T都是空!

  • 第1步:将顶点A加入到U中。 此时,U={A}。
  • 第2步:将顶点B加入到U中。 上一步操作之后,U={A}, V-U={B,C,D,E,F,G};因此,边(A,B)的权值最小。将顶点B添加到U中;此时,U={A,B}。
  • 第3步:将顶点F加入到U中。 上一步操作之后,U={A,B}, V-U={C,D,E,F,G};因此,边(B,F)的权值最小。将顶点F添加到U中;此时,U={A,B,F}。
  • 第4步:将顶点E加入到U中。 上一步操作之后,U={A,B,F}, V-U={C,D,E,G};因此,边(F,E)的权值最小。将顶点E添加到U中;此时,U={A,B,F,E}。
  • 第5步:将顶点D加入到U中。 上一步操作之后,U={A,B,F,E},V-U={C,D,G};因此,边(E,D)的权值最小。将顶点D添加到U中;此时,U={A,B,F,E,D}。
  • 第6步:将顶点C加入到U中。 上一步操作之后,U={A,B,F,E,D}, V-U={C,G};因此,边(D,C)的权值最小。将顶点C添加到U中;此时,U={A,B,F,E,D,C}。
  • 第7步:将顶点G加入到U中。 上一步操作之后,U={A,B,F,E,D,C}, V-U={G};因此,边(F,G)的权值最小。将顶点G添加到U中;此时,U=V。

此时,最小生成树构造完成!它包括的顶点依次是:A B F E D C G。

🍀(3)普里姆算法代码实现

这里以"邻接矩阵"为例对普里姆算法进行说明。

// 邻接矩阵对应的结构体
public class MatrixUDG {

    private char[] mVexs;       // 顶点集合
    private int[][] mMatrix;    // 邻接矩阵
    private static final int INF = Integer.MAX_VALUE;   // 最大值

    ...
}
/*
 * prim最小生成树
 *
 * 参数说明:
 *   start -- 从图中的第start个元素开始,生成最小树
 */
public void prim(int start) {
    int num = mVexs.length;         // 顶点个数
    int index=0;                    // prim最小树的索引,即prims数组的索引
    char[] prims  = new char[num];  // prim最小树的结果数组
    int[] weights = new int[num];   // 顶点间边的权值

    // prim最小生成树中第一个数是"图中第start个顶点",因为是从start开始的。
    prims[index++] = mVexs[start];

    // 初始化"顶点的权值数组",
    // 将每个顶点的权值初始化为"第start个顶点"到"该顶点"的权值。
    for (int i = 0; i < num; i++ )
        weights[i] = mMatrix[start][i];
    // 将第start个顶点的权值初始化为0。
    // 可以理解为"第start个顶点到它自身的距离为0"。
    weights[start] = 0;

    for (int i = 0; i < num; i++) {
        // 由于从start开始的,因此不需要再对第start个顶点进行处理。
        if(start == i)
            continue;

        int j = 0;
        int k = 0;
        int min = INF;
        // 在未被加入到最小生成树的顶点中,找出权值最小的顶点。
        while (j < num) {
            // 若weights[j]=0,意味着"第j个节点已经被排序过"(或者说已经加入了最小生成树中)。
            if (weights[j] != 0 && weights[j] < min) {
                min = weights[j];
                k = j;
            }
            j++;
        }

        // 经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。
        // 将第k个顶点加入到最小生成树的结果数组中
        prims[index++] = mVexs[k];
        // 将"第k个顶点的权值"标记为0,意味着第k个顶点已经排序过了(或者说已经加入了最小树结果中)。
        weights[k] = 0;
        // 当第k个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。
        for (j = 0 ; j < num; j++) {
            // 当第j个节点没有被处理,并且需要更新时才被更新。
            if (weights[j] != 0 && mMatrix[k][j] < weights[j])
                weights[j] = mMatrix[k][j];
        }
    }

    // 计算最小生成树的权值
    int sum = 0;
    for (int i = 1; i < index; i++) {
        int min = INF;
        // 获取prims[i]在mMatrix中的位置
        int n = getPosition(prims[i]);
        // 在vexs[0...i]中,找出到j的权值最小的顶点。
        for (int j = 0; j < i; j++) {
            int m = getPosition(prims[j]);
            if (mMatrix[m][n]<min)
                min = mMatrix[m][n];
        }
        sum += min;
    }
    // 打印最小生成树
    System.out.printf("PRIM(%c)=%d: ", mVexs[start], sum);
    for (int i = 0; i < index; i++)
        System.out.printf("%c ", prims[i]);
    System.out.printf("\n");
}

五、拓扑排序

1️⃣拓扑排序介绍

  • 拓扑排序(Topological Order)是指,将一个有向无环图(Directed Acyclic Graph简称DAG)进行排序进而得到一个有序的线性序列。
  • 通过简单的例子进行说明:例如,一个项目包括A、B、C、D四个子部分来完成,并且A依赖于B和D,C依赖于D。现在要制定一个计划,写出A、B、C、D的执行顺序。这时,就可以利用到拓扑排序,它就是用来确定事物发生的顺序的。
  • 在拓扑排序中,如果存在一条从顶点A到顶点B的路径,那么在排序结果中B出现在A的后面。

2️⃣拓扑排序的算法图解

拓扑排序算法的基本步骤:

  • 1.构造一个队列Q(queue) 和 拓扑排序的结果队列T(topological);
  • 2.把所有没有依赖顶点的节点放入Q;
  • 3.当Q还有顶点的时候,执行下面步骤:
    • 3.1 从Q中取出一个顶点n(将n从Q中删掉),并放入T(将n加入到结果集中);
    • 3.2 对n每一个邻接点m(n是起点,m是终点):
      • 3.2.1 去掉边<n,m>;
      • 3.2.2 如果m没有依赖顶点,则把m放入Q。

注:顶点A没有依赖顶点,是指不存在以A为终点的边。

在这里插入图片描述

以上图为例,来对拓扑排序进行演示。

在这里插入图片描述

  • 第1步:将B和C加入到排序结果中。顶点B和顶点C都是没有依赖顶点,因此将C和C加入到结果集T中。假设ABCDEFG按顺序存储,因此先访问B,再访问C。访问B之后,去掉边<B,A>和<B,D>,并将A和D加入到队列Q中。同样的,去掉边<C,F>和<C,G>,并将F和G加入到Q中。
    • (1) 将B加入到排序结果中,然后去掉边<B,A>和<B,D>;此时,由于A和D没有依赖顶点,因此并将A和D加入到队列Q中。
    • (2) 将C加入到排序结果中,然后去掉边<C,F>和<C,G>;此时,由于F有依赖顶点D,G有依赖顶点A,因此不对F和G进行处理。
  • 第2步:将A,D依次加入到排序结果中。第1步访问之后,A,D都是没有依赖顶点的,根据存储顺序,先访问A,然后访问D。访问之后,删除顶点A和顶点D的出边。
  • 第3步:将E,F,G依次加入到排序结果中。

因此访问顺序是:B -> C -> A -> D -> E -> F -> G

3️⃣拓扑排序的代码实现

拓扑排序是对有向无向图的排序。下面以邻接表实现的有向图来对拓扑排序进行说明。

// 邻接表对应的结构体
public class ListDG {
    // 邻接表中表对应的链表的顶点
    private class ENode {
        int ivex;       // 该边所指向的顶点的位置
        ENode nextEdge; // 指向下一条弧的指针
    }

    // 邻接表中表的顶点
    private class VNode {
        char data;          // 顶点信息
        ENode firstEdge;    // 指向第一条依附该顶点的弧
    };

    private VNode[] mVexs;  // 顶点数组

    ...
}
/*
 * 拓扑排序
 *
 * 返回值:
 *     -1 -- 失败(由于内存不足等原因导致)
 *      0 -- 成功排序,并输入结果
 *      1 -- 失败(该有向图是有环的)
 */
public int topologicalSort() {
    int index = 0;
    int num = mVexs.size();
    int[] ins;               // 入度数组
    char[] tops;             // 拓扑排序结果数组,记录每个节点的排序后的序号。
    Queue<Integer> queue;    // 辅组队列

    ins   = new int[num];
    tops  = new char[num];
    queue = new LinkedList<Integer>();

    // 统计每个顶点的入度数
    for(int i = 0; i < num; i++) {

        ENode node = mVexs.get(i).firstEdge;
        while (node != null) {
            ins[node.ivex]++;
            node = node.nextEdge;
        }
    }

    // 将所有入度为0的顶点入队列
    for(int i = 0; i < num; i ++)
        if(ins[i] == 0)
            queue.offer(i);                 // 入队列

    while (!queue.isEmpty()) {              // 队列非空
        int j = queue.poll().intValue();    // 出队列。j是顶点的序号
        tops[index++] = mVexs.get(j).data;  // 将该顶点添加到tops中,tops是排序结果
        ENode node = mVexs.get(j).firstEdge;// 获取以该顶点为起点的出边队列

        // 将与"node"关联的节点的入度减1;
        // 若减1之后,该节点的入度为0;则将该节点添加到队列中。
        while(node != null) {
            // 将节点(序号为node.ivex)的入度减1。
            ins[node.ivex]--;
            // 若节点的入度为0,则将其"入队列"
            if( ins[node.ivex] == 0)
                queue.offer(node.ivex);    // 入队列

            node = node.nextEdge;
        }
    }

    if(index != num) {
        System.out.printf("Graph has a cycle\n");
        return 1;
    }

    // 打印拓扑排序结果
    System.out.printf("== TopSort: ");
    for(int i = 0; i < num; i ++)
        System.out.printf("%c ", tops[i]);
    System.out.printf("\n");

    return 0;
}

后记

在这里插入图片描述
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/3416.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

spring启动流程(二):包的扫描流程

在applicationContext的创建中&#xff0c;我们分析了applicationContext的创建过程&#xff0c;在本文中&#xff0c;我们将分析spring是如何进行包扫描的。 依旧是AnnotationConfigApplicationContext的构造方法&#xff1a; public AnnotationConfigApplicationContext(St…

自底向上语法分析(bottom-up parsing)

自底向上语法分析&#xff08;bottom-up parsing&#xff09;自底向上分析概述LR分析概述LR(0)分析增广文法点标记项目LR(0)分析表CLOSURE函数GOTO函数LR(0)自动机的状态集LR(0)分析表构造算法LR(0)自动机的形式化定义LR(0)分析的冲突问题SLR分析SLR算法的关键SLR分析的冲突问题…

U3D热更新技术

作者 : SYFStrive 博客首页 : HomePage &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f937;‍♀️&#xff1a;创作不易转发需经作者同意&#x1f608; &#x1f483;&#xff1a;程…

适用于 Windows 的企业级 Subversion 服务器

适用于 Windows 的企业级 Subversion 服务器。 Subversion 的 Windows 身份验证 Windows 身份验证是 VisualSVN 服务器的一个关键特性。此功能专为 Active Directory 域环境设计&#xff0c;允许用户使用其 Windows 凭据访问 VisualSVN 服务器。 VisualSVN Server 支持两种不同…

【Linux】基础IO ——中

&#x1f387;Linux&#xff1a;基础IO 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看…

这些Java基础知识,诸佬们都还记得嘛(学习,复习,面试都可)

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇将记录几次面试中经常被问到的知识点以及对学习的知识点总结和面试题的复盘。 本篇文章记录的基础知识&#xff0c;适合在学Java的小白&#xff0c;也适合复习中&…

趣说 Mysql内存篇 Buffer Pool

讲解顺序 先说 Mysql InnoDB 内存结构 Buffer PoolPage 管理机制Change BufferLog Buffer Buffer Pool 接上回 说到了 LRU 算法对内存的数据 进行淘汰 LRU 算法本身是 最近最少使用的&#xff0c;但是这样就会出现 分不清楚 哪些是真正多次使用的数据 LRU缺点&#xff1a…

软考重点10 知识产权

软考重点10 知识产权一、著作权1. 著作权的理解&#xff08;1&#xff09;版权&#xff1a;&#xff08;2&#xff09;人身权与财产权2. 知识产权的归属判定3. 知识产权的归属判定&#xff08;1&#xff09;委托创作&#xff08;2&#xff09;合作开发4. 著作权保护对象及范围5…

为什么要有包装类,顺便说一说基本数据类型、包装类、String类该如何转换?

一、前言 开门见山&#xff0c;首先看看八种基本数据类型对应的包装类&#xff1a; 基本数据类型包装类charCharacterbyteByteshortShortintIntegerlongLongfloatFloatdoubleDoublebooleanBoolean 其中Character 、Boolean的父类是Object&#xff0c;其余的父类是Number 二、装…

【软件测试】毕业打工两年,辞职一年后转行月薪18K,软件测试让我发起了第一春......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 小徐&#xff1a; 毕…

C++ 类和对象 日期类的实现

作者&#xff1a;小萌新 专栏&#xff1a;初阶C 作者简介&#xff1a;大二学生 希望能和大家一起进步 本篇博客目标&#xff1a; 完成Date类的实现 梳理剩下两个默认函数 好困 跑个步去 睡醒啦&#xff01; 继续肝 日期类的实现本章目标一. 日期类的实现1.1 Getmonthday的实现…

CNN的实现与可视化

CNN的实现 我们已经实现了卷积层和池化层&#xff0c;现在来组合这些层&#xff0c;搭建进行手写数字识别的CNN。如下图所示&#xff0c;网络的构成是“Convolution - ReLU - Pooling -Affine - ReLU - Affine - Softmax”&#xff0c;我们将它实现为名为SimpleConvNet的类。 …

R语言—向量

向量&#xff08;vector&#xff09; R 语言最基本的数据结构是向量。类似于数学上的集合的概念&#xff0c;由一个或多个元素构成。向量其实是用于存储数值型、字符型、或逻辑型数据的一维数组。 创建向量 c()函数 > a <- 1 #给a赋值1 > a #显示a的值 [1] 1 …

【C++初阶】类和对象终极篇

文章目录一.加const修饰this指针二.cout<<自定义类型的对象的实现1.深入理解cout和<<2流插入和流提取3.友元函数的来源a.作为ostream成员函数b.作为全局函数c.作为Date类的成员函数d.作为Date类的友元函数三.再谈构造函数之初始化列表四.隐式类型转换&explicit…

Linux命令从入门到实战----文件目录类

文章目录pwd显示当前工作路径的绝对路径ls列出目录的内容cd切换目录mkdir 创建一个新的目录删除一个空的目录touch创建新文件cp复制文件或目rm删除文件或目录mv移动文件与目录&#xff0c;重命名文件cat查看文件内容&#xff0c;创建新文件more文件内容分屏查看less分屏显示文件…

【C语言】字符串、字符数组

目录 写在开头 正文 一、字符串的本质 二、输入函数scanf和gets 三、输出函数printf和puts 四、字符串的长度——strlen 五、字符串的复制——strcpy 六、字符串的比较函数 七、实战练习 八、二维字符数组——字符串数组 写在最后 写在开头 看了标题&#xff0c;是…

Kotlin编程实战——类与对象(05)

一 概述 类与继承属性和字段接口(interface )函数式&#xff08;SAM&#xff09;接口可见性修饰符扩展数据类(data class)密封类泛型嵌套类与内部类枚举类对象表达式与对象声明类型别名内联类(inline class)委托委托属性 二 类与继承 类继承(open override )抽象类(abstract)…

MySQL去重中 distinct 和 group by 的区别

今天在写业务需要对数据库重复字段进行去重时&#xff0c;因为是去重&#xff0c;首先想到的是distinct关键字。于是一小时过去了。。。。&#xff08;菜鸟一个&#xff0c;大家轻点骂&#xff09; 我把问题的过程用sql语句演示给大家演示一下 首先我使用的是mybatis-plus&am…

数据结构之哈希表

文章目录 一、概念二、哈希冲突三、如何解决哈希冲突&#xff1f; 1.哈希函数设计2.负载因子调节3.闭散列4.开散列&#xff08;哈希桶&#xff09;四、模拟实现哈希桶总结一、概念 顺序结构以及平衡树中&#xff0c;元素与其存储位置之间没有对应的关系&#xff0c;因此在查找一…

C++多态学习笔记

C多态学习笔记一、多态概述二、多态的作用三、多态发生的三个条件四、多态实现的原理五、接口的定义六、模板方法模式七、虚析构函数和纯虚析构函数7.1 虚析构函数7.2 纯虚析构函数八、重写重载重定义九、父类引用子类对象一、多态概述 同一个操作作用于不同的对象&#xff0c;…