从零开始的图论讲解(1)——图的概念,图的存储,图的遍历与图的拓扑排序

news2025/4/18 6:21:01

目录

前言

图的概念

1. 顶点和边

2. 图的分类

3. 图的基本性质

图的存储

邻接矩阵存图

邻接表存图

图的基本遍历

拓扑排序

拓扑排序是如何写的呢?

1. 统计每个节点的入度

2. 构建邻接表

3. 将所有入度为 0 的节点加入队列

4. 不断弹出队头节点,更新其相邻节点的入度

5. 判断是否存在环

结尾:


前言

本文将从最基础的概念讲起,介绍 图的存储方式和怎么遍历图(BFS和DFS基本遍历),并深入 拓扑排序及其应用,帮助你快速入门图论。目标是让你在短时间内掌握图论的核心知识,并具备独立完成 LeetCode 简单及以上难度的图论题目的能力。博客很长,欢迎大家根据目录各取所需.

这是该系列的第一篇,在后面的博客中,笔者还会讲解 最短路径问题(Dijkstra、Bellman-Ford、SPFA)最小生成树(Kruskal、Prim) 等常见算法,帮助你建立图论基础。

笔者自知水平有限, 本博客的质量无法与专业算法书籍相比。但笔者希望通过 通俗易懂的语言,并结合 数据模拟,帮助零基础的读者快速入门图论,并熟悉常见的图论算法模板。对于需要复习的有基础的读者,也可以把该系列博客当成"模板代码托管所",随时备你复习!

目标是让你 不仅能理解算法,还能够在实际中熟练运用,让图论不再只是抽象的概念,而是可以直观感受到的计算过程。 

图论是计算机科学中的重要分支,在路径规划、网络流分析、任务调度等多个领域有着广泛应用,学好它对于我们提升代码能力和使用数据结构的能力很有帮助。

好了,前言到此为止,希望您怀揣耐心读下去

博客中出现的参考图都是笔者手画的,代码示例也是笔者手敲的!影响虽小,但请勿抄袭

图的概念

话不多说,首先什么是图?图是由一组顶点(Vertex)和一组(Edge)组成的结构,通常用于表示事物之间的关系。比如,社交网络中的人和他们之间的关系可以用图来表示;城市之间的道路、交通网络也可以用图来建模。图论研究的就是这些结构以及如何对图进行操作和分析。

1. 顶点和边

  • 顶点(Vertex):图中的基本元素,表示对象或节点。比如,在社交网络中,每个用户可以看作一个顶点;在城市的道路网中,每个城市可以看作一个顶点。

  • 边(Edge):表示顶点之间的连接关系,通常可以有方向性或者没有方向性。每条边都连接着两个顶点。边可以表示各种关系,比如朋友之间的关系、城市之间的道路等。

2. 图的分类

图的分类可以依据边的方向性、边的权重等多个方面来进行,常见的分类包括:

  • 无向图(Undirected Graph):图中的每条边没有方向,表示两个顶点之间的关系是双向的。例如,社交网络中朋友关系就是一种无向图关系。

  • 有向图(Directed Graph):图中的每条边都有方向,即每条边从一个顶点指向另一个顶点。例如,网页之间的超链接就是一种有向图关系。

  • 加权图(Weighted Graph):图中的边有权重(权值),表示连接两个顶点之间的代价或距离。例如,城市之间的道路距离或者交通时间。

3. 图的基本性质

  • 邻接关系:在图中,如果两个顶点通过边相连,就称它们是邻接的。对于无向图,如果顶点 A 和顶点 B 之间有边,则 A 和 B 是邻接的;而对于有向图,如果从顶点 A 到顶点 B 有一条边,则称 B 是 A 的邻接顶点。

  • 度(Degree):顶点的度是与该顶点相连的边的数量。对于无向图,顶点的度是其邻接边的数目;对于有向图,分为入度(指向该顶点的边数)和出度(从该顶点出发的边数)。请牢记这个概念,因为拓扑排序需要用到它.

  • 连通性:如果图中的每一对顶点都有路径相连,则称图是连通的。无向图中的连通性比有向图更容易理解,因为无向图中不区分边的方向,只要两个顶点之间存在路径就视为连通。图的连通性问题也是一个概念很大的问题,有很多分支,感兴趣的读者们可以自己去了解

图的存储

相信很多初学者在学习图论时,会因为对数据结构理解不够深入而感到困惑,尤其是在理解图的存储方式时可能会遇到不少困难。图的存储方式决定了我们如何在计算机中表示和操作图,因此掌握它至关重要。如果你对数组、链表等基本数据结构还不太熟悉,不用担心,在接下来的内容中,我会用通俗易懂的方式来讲解不同的存储方法,帮助你轻松理解它们的优缺点以及适用场景。

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?

我们主要介绍那么两种,第一种是邻接矩阵,第二种是邻接表,还有一种叫做链式前向星的结构,但是笔者不做介绍.

邻接矩阵存图

首先是邻接矩阵,这是最简单最好理解的存储方法,适用于密度高的图,如果用于稀疏图,那么效果不如邻接表

我们定义二维数组 graph[N][N] 来存图.

如果 图是无向无权值图,那么 graph[i][j] == 1 表示 点 i 到 点 j 有连通

同理 表示 点 j 到 点 i 连通

如果 图是有向无权值图,那么 graph[i][j] == 1 表示 点 i 到 点 j 有连通

graph[j][i] == 1 表示 点 j 到 点 i 有连通

如果两点 a,b之间没有边相连,那么 grapg[a][b] = 0.

如果带权值,二维数组的值就是两点之间的权值,同样分为 无向图与有向图,二维数组的含义与上文同理

例如:

同理,如果矩阵有具体值,那么边就有了权值,这里笔者就不画图了

邻接表存图

不知道各位读者是否对邻接表这个名字很熟悉?是的,之前在介绍哈希表时,笔者就已经介绍过邻接表

[入门JAVA数据结构 JAVADS] 哈希表的初步介绍和代码实现-CSDN博客

邻接表的思想是:每个节点维护一个链表(或数组/列表),记录它连接到的所有节点。对于有权图,我们在记录目标节点的同时,也记录每条边的权值。通俗的说,邻接表就像一个“关系表”,它告诉我们:每个节点直接连接到哪些节点。我们通过这些关系就可以完整地表示出整个图。

以无权有向图为例,假设图如下:

1 → 2  
1 → 3  
2 → 4  

那么在邻接表中,是这么被存储的

1: 2 → 3  
2: 4  
3:  
4:  

若这是有权图(比如边的权值分别为 5、7、2),则可以这样表示:

1: (2,5) → (3,7)  
2: (4,2)  
3:  
4:  

那么,在JAVA语言中,我们用什么数据结构去组织和描述邻接表呢?

通过图示我们可以看到,这种数据结构要求 

  • 能够按“节点编号”快速访问;

  • 每个节点后面还要挂一串“与它相连的边”。

显然,我们可以这么写

List<List<Edge>> graph = new ArrayList<>();
  • 外层 List 的下标表示当前节点编号;

  • 内层 List<Edge> 保存当前节点连接的所有边(每条边都有终点和权值);

  • Edge 是我们自己定义的一个类,表示一条边。

这三个结合起来,我们就可以有效的存储图了,第一层的List下标代表起点,第二层List<Edge> 是一个存储Edge的链表,里面有终点坐标和权值,如果是无权图也可以用

List<Integer>.

 你可以理解成是一个“数组 + 链表”的组合体,既能快速定位每个节点,又能灵活添加边。

我们再定义一个Edge类

class Edge {
    int to;      // 目标节点编号
    int weight;  // 边的权值

    Edge(int to, int weight) {
        this.to = to;
        this.weight = weight;
    }
}

举个例子,有个图如下所示:

1 → 2 (权值3)
1 → 3 (权值5)
2 → 4 (权值2)

他在邻接表中就长这样

graph[1] -> [(2, 3), (3, 5)]
graph[2] -> [(4, 2)]
graph[3] -> []
graph[4] -> []

我们写一个代码简单构建一下 

int n = 4; // 4 个节点,从 1 开始编号
List<List<Edge>> graph = new ArrayList<>();
for (int i = 0; i <= n; i++) {
    graph.add(new ArrayList<>());
}

// 添加边
graph.get(1).add(new Edge(2, 3));
graph.get(1).add(new Edge(3, 5));
graph.get(2).add(new Edge(4, 2));

值得注意的是,上述的例子都是单向图的构建方法, 因为这只是存储了起点和终点,有并不代表终点也可以通往起点,因此,如果是双向图,就要构建两次

例如:  假设 a 点和 b 点是双边互通的,那么就应该这么构建

graph.get(a).add(new Edge(b, w)); // a → b
graph.get(b).add(new Edge(a, w)); // b → a

看到这里的你,哪怕是一名刚刚接触图论的小白,相信也已经不再对“图”这个概念感到陌生了。我们已经了解了图的基本概念、常见分类,以及如何用邻接表在 Java 中高效地存储图结构。

为了节省大家的阅读时间成本,避免重复讲解一些过于基础、但实际中不太常用的内容,接下来的图论部分,我们默认所有图都使用邻接表进行存储。这种方式在实际中应用广泛,简单高效

图的基本遍历

图的遍历是图论中的基本操作之一。无论你是在求连通块、寻找路径,还是在实现更复杂的图算法(比如最短路径、拓扑排序),都绕不开遍历操作。

常见的图遍历方式有两种:

  1. DFS(深度优先搜索)

  2. BFS(广度优先搜索)

这两种遍历中DFS更强调一条路走到黑,而BFS是层层递进的遍历

以下是我给出的代码

import java.util.*;

public class GraphTraversal {
    static int n, m;
    static final int N = 505;
    static List<List<Edge>> graph = new ArrayList<>();

    static boolean[] vis = new boolean[N];

    // 定义一个边的类 (u->v, 权值w)
    static class Edge {
        int v, w;
        Edge(int v, int w) {
            this.v = v;
            this.w = w;
        }
    }

    // 添加一条 u -> v, 权值为 w 的边
    static void addEdge(int u, int v, int w) {
        graph.get(u).add(new Edge(v, w));
    }

    // BFS 遍历
    static void BFS(int start) {
        Arrays.fill(vis,false);
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(start);
        vis[start] = true;
        System.out.print("BFS :");
        System.out.print(start+" ");
        while(!queue.isEmpty())
        {
            int temp = queue.poll();
            for(Edge edge : graph.get(temp))
            {
                if(!vis[edge.v])
                {
                    vis[edge.v] = true;
                    queue.offer(edge.v);
                    System.out.print(edge.v+" ");
                }
            }
        }
    }

    // DFS 遍历
    static void DFS(int node)
    {
            if(vis[node])
            {
                return;
            }
           System.out.print(node+" ");
            vis[node] = true;
            for(Edge x : graph.get(node))
            {
                if(!vis[x.v])
                {
                    DFS(x.v);
                }
            }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();

        // 提前创建 n+1 个 ArrayList,避免越界
        for (int i = 0; i <= n+1000; i++) {
            graph.add(new ArrayList<>());
        }

        for (int i = 0; i < m; i++) {
            int a = scanner.nextInt();
            int b = scanner.nextInt();
            int c = scanner.nextInt();
            addEdge(a, b, c);
        }

        scanner.close();

        // 从 1 号节点开始遍历(你可以改成 0)
        BFS(1);

        Arrays.fill(vis, false); // 重新初始化 vis 数组
        System.out.print("DFS: ");
        DFS(1);
        System.out.println();
    }
}

分别是DFS遍历和BFS遍历,通过vis数据去判断结点是否被遍历过,代码很简单 

我们给一组示例,如图所示:

我们分别通过DFS和BFS遍历,默认1为起始点

6 8
1 2 4
1 3 2
2 4 3
3 4 1
3 5 2
4 6 5
5 6 1
2 5 7

结果如下:

可以看到:

BFS(广度优先搜索)中,我们从节点 1 开始遍历。由于 BFS 的特点是按层次逐层访问图的节点,因此它的遍历过程是按照节点距离起点的“层数”来进行的。具体来说:

  1. 首先输出起始节点 1,这是第一层。

  2. 然后访问与 1 相邻的节点 23,这就是第二层。

  3. 接着,访问与 23 相邻的节点 45,这是第三层。

  4. 最后,访问与 45 相邻的节点 6,这是第四层。

BFS 的核心在于通过队列来保证节点是按照层次顺序被访问的。它总是先访问当前层的所有节点,然后再访问下一层的节点。因此,BFS 是“逐层”访问的。

DFS(深度优先搜索)则不同,它的遍历方式是“沿着一条路径一直走到底,然后再回溯”。因此,它会先访问某个节点的所有相邻节点,直到不能再继续为止,然后再回溯到上一个节点,继续访问其他未访问的邻接节点。

  1. 首先输出起始节点 1

  2. 然后,DFS 会优先选择一个与 1 相邻的节点进行深入。在这个例子中,它会先访问节点 2

  3. 接下来,DFS 会沿着节点 2 的相邻节点继续深入,直到没有新的节点可以访问。此时会回溯到节点 2,然后继续访问其他未访问的相邻节点。

  4. 然后回溯到节点 1,访问与 1 相邻的节点 3,并重复相同的过程,直到所有节点都被访问。

我们来看一道例题:1971. 寻找图中是否存在路径 - 力扣(LeetCode)

这道题就要求我们去遍历图,来判断是否联通

首先我们构建邻接表,然后去遍历判断

首先对于构建邻接表,因为这道题是双向无权图,所以我们可以构建

  List<List<Integer>> graph = new ArrayList<>() 来存储图

BFS写法:  

class Solution {
    public boolean validPath(int n, int[][] edges, int source, int destination) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++)
        {
            graph.add(new ArrayList<>());
        }
        for (int[] edge : edges) {
            int u = edge[0], v = edge[1];
            graph.get(u).add(v);
            graph.get(v).add(u);
        }
        boolean[] vis = new boolean[n];
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(source);
        vis[source] = true;
        while(!queue.isEmpty())
        {
            int temp = queue.poll();
            if(temp == destination)
            {
                return true;
            }
            for(Integer num : graph.get(temp))
            {
                if(!vis[num])
                {
                    vis[num] = true;
                    queue.offer(num);
                }
            }
        }
        return false;
    }
}

首先构建双向邻接表,然后遍历

DFS写法: 和上述同理

class Solution {
  static  boolean[] vis;
    public  boolean DFS(List<List<Integer>> graph,int st,int ed)
    {
        if(st == ed)
        {
            return true;
        }
        vis[st] = true;
        for(Integer num:graph.get(st))
        {
            if(!vis[num])
            {
               boolean pd = DFS(graph,num,ed);
               if(pd == true)
               {
                   return true;
               }
            }
        }
        return  false;
    }
    public boolean validPath(int n, int[][] edges, int source, int destination) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++)
        {
            graph.add(new ArrayList<>());
        }
        for (int[] edge : edges) {
            int u = edge[0], v = edge[1];
            graph.get(u).add(v);
            graph.get(v).add(u);
        }
        vis = new boolean[n];
        boolean pdf = DFS(graph,source,destination);
        return pdf==true?true:false;
    }
}

 当然,这不是该题的最优解法,但是我们可以通过这题了解BFS与DFS是如何遍历图的.

拓扑排序

 拓扑排序可以被看作是BFS,DFS的简单应用,从代码模板上看也是这样的.

"拓扑排序"是图论中一个非常经典的问题,常用于解决“有依赖关系的任务排序问题”。比如学习技术栈,如果我要成为一个合格的JAVA开发工程师,我需要学习很多技术栈

  • 在学习 SpringBoot 之前,必须先掌握 JavaSE 和 JavaEE 的基础;

  • 在学习 MyBatis 前,需要具备一定的数据库基础,比如 SQL;

  • 想要理解分布式系统,还得先了解网络通信、RPC 原理、消息队列等内容;

  • 构建前后端分离项目,也依赖于对前端基础、后端 API 编写等知识的掌握。

但是拓扑排序的前提是图必须是有向且无环的图,如果图中存在环,那么就无法构建出合法的拓扑序列 —— 比如课程 A 依赖课程 B,B 又依赖 A,这样就永远无法开始任何课程。

举个例子:

它拓扑排序的结果应该是:

1 3 2 4 5 6  

拓扑排序是如何写的呢?

大概有这么几个步骤 

1. 统计每个节点的入度

每个节点的入度是指:有多少条边指向它。我们需要用一个数组来记录每个点的入度。这个在前面也提到了

   static  int []  ingrade;//存储入度
    public  static  void addEdge(int u,int v)
    {
        graph.get(u).add(v);
        // 表示 u 到 v 有一条边
        ingrade[v]++;
    }

2. 构建邻接表

我们用邻接表来表示图中每个节点的出边(即它连接到哪些后续节点):

       for(int i = 0;i<=n;i++)
        {
            graph.add(new ArrayList<>());
        }
        for(int i = 0;i<m;i++)
        {
            int a = scanner.nextInt();
            int b = scanner.nextInt();
            addEdge(a,b);
        }

3. 将所有入度为 0 的节点加入队列

这些节点说明它们没有前置依赖,可以作为起点。我们使用一个队列来进行 BFS

4. 不断弹出队头节点,更新其相邻节点的入度

遍历过程中,每访问一个节点,就“移除”它的影响,也就是把它连接的边都删掉,同时更新这些目标节点的入度。

5. 判断是否存在环

如果最终输出的拓扑序列长度少于 n,说明存在环(即有任务间形成了“循环依赖”)

    public  static  void BFS()
    {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        for(int i =1;i<=n;i++)
        {
            if(ingrade[i]==0)//入度为0
            {
                priorityQueue.offer(i);
            }
        }
        while(!priorityQueue.isEmpty())
        {
            int node = priorityQueue.poll();
            result.add(node);
            for(Integer neighbor : graph.get(node))
            {
                ingrade[neighbor]--;//相邻结点入度--
                if(ingrade[neighbor]==0)
                {
                    priorityQueue.add(neighbor);
                }
            }
        }
    }

完整代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Scanner;
//拓扑排序是一种 用于有向无环图(DAG,Directed Acyclic Graph) 的排序方法,它将图中的所有节点排成一个线性序列,使得对于 每一条有向边
//𝑢→𝑣,节点 u 在序列中出现在 v 之前。
public class TopoSortBFS
{
   static  int n,m;
   static List<List<Integer>> graph = new ArrayList<>(); // 用邻接表存储图
   static List<Integer> result = new ArrayList<>();
    public  static  void addEdge(int u,int v)
    {
        graph.get(u).add(v);
        // 表示 u 到 v 有一条边
        ingrade[v]++;
    }
    public  static  void BFS()
    {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        for(int i =1;i<=n;i++)
        {
            if(ingrade[i]==0)//入度为0
            {
                priorityQueue.offer(i);
            }
        }
        while(!priorityQueue.isEmpty())
        {
            int node = priorityQueue.poll();
            result.add(node);
            for(Integer neighbor : graph.get(node))
            {
                ingrade[neighbor]--;//相邻结点入度--
                if(ingrade[neighbor]==0)
                {
                    priorityQueue.add(neighbor);
                }
            }
        }
    }
    static  int []  ingrade;//存储入度
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
         n = scanner.nextInt(); // 读取节点数
         m = scanner.nextInt(); // 读取边数
        ingrade = new int[n+1];
        for(int i = 0;i<=n;i++)
        {
            graph.add(new ArrayList<>());
        }
        for(int i = 0;i<m;i++)
        {
            int a = scanner.nextInt();
            int b = scanner.nextInt();
            addEdge(a,b);
        }
        BFS();
        if(result.size()==n)
        {
            for(Integer i : result)
            {
                System.out.print(i+" ");
            }
        }
        else
        {
            System.out.println(-1);
        }
    }
}

 我们可以看一道例题:0恋爱通关游戏 - 蓝桥云课

在这个例题中,我们需要在一个无环图(DAG)中,从起点出发,依次经历多个关卡,根据不同选择提升好感度,直到到达终点关卡,并判断最终好感度是否达到目标值(≥100)。

从图论的角度来看:

  • 每个关卡可以看成是一个图中的节点

  • 每个选项可以看成是有向边,带有一个权值(即好感度提升值);

  • 整个游戏流程构成了一张有向无环图(DAG),因为题目明确说明“不会再遇到已结束关卡”,即不存在回环;

  • 最终目标是从起点到某个终点路径中,累积最大好感度,看是否能达到通关标准。

因此,这道题本质上就是在一张 DAG 上找最大路径和 的问题。

 为什么是拓扑排序?

这是一个非常典型的拓扑排序应用场景:

  • 只有当一个节点的所有前驱节点都已经被处理完,才能开始计算它的最优值。

    换句话说,我们必须尝试过所有能到达该节点的路径,才能确定哪一条路径带来的值最大(或最小)

  • 所以,我们需要先对整个图进行 拓扑排序,然后按照拓扑序去“刷新”每个点的最大好感度。

题解代码:


import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Scanner;
import java.util.*;
public class Demo46 {
    // Edge 用于存边,a->b好感度为c;b就是达到的关卡,c为好感度
    static class Node{
        int b, c;
        public Node(int b, int c){
            this.b = b; this.c = c;
        }
    }
    static final int N = (int)2e5+10;
    static int[] dis = new int[N]; // 用于存储到达当前关卡的好感度 cArr[i]  表示到 i 点的好感度
    static  List<List<Node>> list = new ArrayList<>();
    static Queue<Integer> queue = new LinkedList<>(); // 入度为0的关卡加入到order列表中,以用于拓扑排序
    static int[] inDegree = new int[N]; // 记录每个关卡的当前入度

    static  int res = 0;

    public static void addEdge(int a,int b,int c)
    {
        list.get(a).add(new Node(b,c));
        inDegree[b]++;
    }
    public static void BFS() {
        Arrays.fill(dis, (int) -2e8);

        // 入度为 0 的点,初始化
        for (int i = 0; i < n; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
                dis[i] = 0;  // 所有入度为 0 的点,都要初始化为 0
            }
        }

        while (!queue.isEmpty()) {
            int st = queue.poll();
            // 终点判断:出度为 0 的节点
            if (list.get(st).isEmpty() && dis[st] >= 100) {
                res++;
            }
            for (Node temp : list.get(st)) {
                int ed = temp.b;
                inDegree[ed]--;
                if (inDegree[ed] == 0) {
                    queue.offer(ed);
                }
                dis[ed] = Math.max(dis[ed], dis[st] + temp.c);
            }
        }
    }

    static int n,m;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();
        for(int j = 0;j<=n;j++)
        {
            list.add(new ArrayList<>());
        }
        while(m!=0)
        {
            m--;
            int a = scanner.nextInt();
            int b = scanner.nextInt();
            int c = scanner.nextInt();
            addEdge(a,b,c);
        }
        BFS();
        System.out.println(res);
    }
}

结尾:

又是一篇万字长文,好久没有花这么长时间(大概写了120-140min)去写一篇博客了,感谢能读到这里的读者!

欢迎大佬私信来拷打我!

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

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

相关文章

SpringBoot框架—启动原理

1.SpringBootApplication注解 在讲解启动原理之前先介绍一个非常重要的注解SpringBootApplication&#xff0c;这个注解在Springboot程序的入口文件Application.java中必须添加。SpringBootApplication是一个整合了三个核心注解的组合注解。 三个核心注解的作用机制&#xff1…

怎么检查网站CDN缓存是否生效

为什么要使用CDN缓存&#xff1f; 网站使用缓存可显著提升加载速度&#xff0c;减少服务器负载和带宽消耗&#xff0c;优化用户体验&#xff0c;增强架构稳定性&#xff0c;助力SEO优化&#xff0c;实现资源高效利用与性能平衡。 通过合理配置 CDN 缓存策略&#xff0c;可降低…

【自然语言处理】深度学习中文本分类实现

文本分类是NLP中最基础也是应用最广泛的任务之一&#xff0c;从无用的邮件过滤到情感分析&#xff0c;从新闻分类到智能客服&#xff0c;都离不开高效准确的文本分类技术。本文将带您全面了解文本分类的技术演进&#xff0c;从传统机器学习到深度学习&#xff0c;手把手实现一套…

vba讲excel转换为word

VBA将excel转换为word Sub ExportToWordFormatted() 声明变量Dim ws As Worksheet 用于存储当前活动的工作表Dim rng As Range 用于存储工作表的使用范围&#xff08;即所有有数据的单元格&#xff09;Dim rowCount As Long, colCount As Long 用于存储数据范围的行数和列数…

ubuntu安装openWebUI和Dify【自用详细版】

系统版本&#xff1a;ubuntu24.04LTS 显卡&#xff1a;4090 48G 前期准备 先安装好docker和docker-compose&#xff0c;可以参考我之前文章安装&#xff1a; ubuntu安装docker和docker-compose【简单详细版】 安装openWebUI 先docker下载ollama docker pull ghcr.nju.edu.c…

基于Flask的勒索病毒应急响应平台架构设计与实践

基于Flask的勒索病毒应急响应平台架构设计与实践 序言&#xff1a;安全工程师的防御视角 作为从业十年的网络安全工程师&#xff0c;我深刻理解勒索病毒防御的黄金时间法则——应急响应速度每提升1分钟&#xff0c;数据恢复成功率将提高17%。本文介绍的应急响应平台&#xff…

spark数据清洗案例:流量统计

一、项目背景 在互联网时代&#xff0c;流量数据是反映用户行为和业务状况的重要指标。通过对流量数据进行准确统计和分析&#xff0c;企业可以了解用户的访问习惯、业务的热门程度等&#xff0c;从而为决策提供有力支持。然而&#xff0c;原始的流量数据往往存在格式不规范、…

list的使用以及模拟实现

本章目标 1.list的使用 2.list的模拟实现 1.list的使用 在stl中list是一个链表,并且是一个双向带头循环链表,这种结构的链表是最优结构. 因为它的实现上也是一块线性空间,它的使用上是与string和vector类似的.但相对的因为底层物理结构上它并不像vector是线性连续的,它并没有…

【今日三题】小乐乐改数字 (模拟) / 十字爆破 (预处理+模拟) / 比那名居的桃子 (滑窗 / 前缀和)

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;每日两三题 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 小乐乐改数字 (模拟)十字爆破 (预处理模拟&#xff09;比那名居的桃子 (滑窗 / 前缀和) 小乐乐改数字 (模拟) 小乐乐改数字…

基于 Qt 的图片处理工具开发(一):拖拽加载与基础图像处理功能实现

一、引言 在桌面应用开发中&#xff0c;图片处理工具的核心挑战在于用户交互的流畅性和异常处理的健壮性。本文以 Qt为框架&#xff0c;深度解析如何实现一个支持拖拽加载、亮度调节、角度旋转的图片处理工具。通过严谨的文件格式校验、分层的架构设计和用户友好的交互逻辑&am…

44、Spring Boot 详细讲义(一)

Spring Boot 详细讲义 目录 Spring Boot 简介Spring Boot 快速入门Spring Boot 核心功能Spring Boot 技术栈与集成Spring Boot 高级主题Spring Boot 项目实战Spring Boot 最佳实践总结 一、Spring Boot 简介 1. Spring Boot 概念和核心特点 1.1、什么是 Spring Boot&#…

虽然理解git命令,但是我选择vscode插件!

文章目录 2025/3/11 补充一个项目一个窗口基本操作注意 tag合并冲突已有远程&#xff0c;新加远程仓库切换分支stash 只要了解 git 的小伙伴&#xff0c;应该都很熟悉这些指令&#xff1a; git init – 初始化git仓库git add – 把文件添加到仓库git commit – 把文件提交到仓库…

idea 打不开terminal

IDEA更新到2024.3后Terminal终端打不开的问题_idea terminal打不开-CSDN博客

【JVM】JVM调优实战

&#x1f600;大家好&#xff0c;我是白晨&#xff0c;一个不是很能熬夜&#x1f62b;&#xff0c;但是也想日更的人✈。如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下&#x1f440;白晨吧&#xff01;你的支持就是我最大的动力&#xff01;&#x1f4…

FPGA_DDR(二)

在下板的时候遇到问题 1&#xff1a;在写一包数据后再读&#xff0c;再写再读 这时候读无法读出 查看时axi_arready没有拉高 原因 &#xff1a; 由于读地址后没有拉高rready,导致数据没有读出卡死现象。 解决结果

【吾爱出品】[Windows] 鼠标或键盘可自定义可同时多按键连点工具

[Windows] 鼠标或键盘连点工具 链接&#xff1a;https://pan.xunlei.com/s/VONSFKLNpyVDeYEmOCBY3WZJA1?pwduik5# [Windows] 鼠标或键盘可自定义可同时多按键连点工具 就是个连点工具&#xff0c;功能如图所示&#xff0c;本人系统win11其他系统未做测试&#xff0c;自己玩…

vue3实战一、管理系统之实战立项

目录 管理系统之实战立项对应相关文章链接入口&#xff1a;实战效果登录页&#xff1a;动态菜单&#xff1a;动态按钮权限白天黑夜模式&#xff1a;全屏退出全屏退出登录&#xff1a;菜单收缩&#xff1a; 管理系统之实战立项 vue3实战一、管理系统之实战立项&#xff1a;这个项…

设计模式 Day 6:深入讲透观察者模式(真实场景 + 回调机制 + 高级理解)

观察者模式&#xff08;Observer Pattern&#xff09;是一种设计结构中最实用、最常见的行为模式之一。它的魅力不仅在于简洁的“一对多”事件推送能力&#xff0c;更在于它的解耦能力、模块协作设计、实时响应能力。 本篇作为 Day 6&#xff0c;将带你从理论、底层机制到真实…

汽车软件开发常用的需求管理工具汇总

目录 往期推荐 DOORS&#xff08;IBM &#xff09; 行业应用企业&#xff1a; 应用背景&#xff1a; 主要特点&#xff1a; Polarion ALM&#xff08;Siemens&#xff09; 行业应用企业&#xff1a; 应用背景&#xff1a; 主要特点&#xff1a; Codebeamer ALM&#x…

AI 越狱技术剖析:原理、影响与防范

一、AI 越狱技术概述 AI 越狱是指通过特定技术手段&#xff0c;绕过人工智能模型&#xff08;尤其是大型语言模型&#xff09;的安全防护机制&#xff0c;使其生成通常被禁止的内容。这种行为类似于传统计算机系统中的“越狱”&#xff0c;旨在突破模型的限制&#xff0c;以实…