当前位置: 首页 > news >正文

【数据结构与算法-Day 31】图的遍历:深度优先搜索 (DFS) 详解,一条路走到黑的智慧

Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来

Python系列文章目录

PyTorch系列文章目录

机器学习系列文章目录

深度学习系列文章目录

Java系列文章目录

JavaScript系列文章目录

Python系列文章目录

Go语言系列文章目录

Docker系列文章目录

数据结构与算法系列文章目录

01-【数据结构与算法-Day 1】程序世界的基石:到底什么是数据结构与算法?
02-【数据结构与算法-Day 2】衡量代码的标尺:时间复杂度与大O表示法入门
03-【数据结构与算法-Day 3】揭秘算法效率的真相:全面解析O(n^2), O(2^n)及最好/最坏/平均复杂度
04-【数据结构与算法-Day 4】从O(1)到O(n²),全面掌握空间复杂度分析
05-【数据结构与算法-Day 5】实战演练:轻松看懂代码的时间与空间复杂度
06-【数据结构与算法-Day 6】最朴素的容器 - 数组(Array)深度解析
07-【数据结构与算法-Day 7】告别数组束缚,初识灵活的链表 (Linked List)
08-【数据结构与算法-Day 8】手把手带你拿捏单向链表:增、删、改核心操作详解
09-【数据结构与算法-Day 9】图解单向链表:从基础遍历到面试必考的链表反转
10-【数据结构与算法-Day 10】双向奔赴:深入解析双向链表(含图解与代码)
11-【数据结构与算法-Day 11】从循环链表到约瑟夫环,一文搞定链表的终极形态
12-【数据结构与算法-Day 12】深入浅出栈:从“后进先出”原理到数组与链表双实现
13-【数据结构与算法-Day 13】栈的应用:从括号匹配到逆波兰表达式求值,面试高频考点全解析
14-【数据结构与算法-Day 14】先进先出的公平:深入解析队列(Queue)的核心原理与数组实现
15-【数据结构与算法-Day 15】告别“假溢出”:深入解析循环队列与双端队列
16-【数据结构与算法-Day 16】队列的应用:广度优先搜索(BFS)的基石与迷宫寻路实战
17-【数据结构与算法-Day 17】揭秘哈希表:O(1)查找速度背后的魔法
18-【数据结构与算法-Day 18】面试必考!一文彻底搞懂哈希冲突四大解决方案:开放寻址、拉链法、再哈希
19-【数据结构与算法-Day 19】告别线性世界,一文掌握树(Tree)的核心概念与表示法
20-【数据结构与算法-Day 20】从零到一掌握二叉树:定义、性质、特殊形态与存储结构全解析
21-【数据结构与算法-Day 21】精通二叉树遍历(上):前序、中序、后序的递归与迭代实现
22-【数据结构与算法-Day 22】玩转二叉树遍历(下):广度优先搜索(BFS)与层序遍历的奥秘
23-【数据结构与算法-Day 23】为搜索而生:一文彻底搞懂二叉搜索树 (BST) 的奥秘
24-【数据结构与算法-Day 24】平衡的艺术:图解AVL树,彻底告别“瘸腿”二叉搜索树
25-【数据结构与算法-Day 25】工程中的王者:深入解析红黑树 (Red-Black Tree)
26-【数据结构与算法-Day 26】堆:揭秘优先队列背后的“特殊”完全二叉树
27-【数据结构与算法-Day 27】堆的应用:从堆排序到 Top K 问题,一文彻底搞定!
28-【数据结构与算法-Day 28】字符串查找的终极利器:深入解析字典树 (Trie / 前缀树)
29-【数据结构与算法-Day 29】从社交网络到地图导航,一文带你入门终极数据结构:图
30-【数据结构与算法-Day 30】图的存储:邻接矩阵 vs 邻接表,哪种才是最优选?
31-【数据结构与算法-Day 31】图的遍历:深度优先搜索 (DFS) 详解,一条路走到黑的智慧


文章目录

  • Langchain系列文章目录
  • Python系列文章目录
  • PyTorch系列文章目录
  • 机器学习系列文章目录
  • 深度学习系列文章目录
  • Java系列文章目录
  • JavaScript系列文章目录
  • Python系列文章目录
  • Go语言系列文章目录
  • Docker系列文章目录
  • 数据结构与算法系列文章目录
  • 摘要
  • 一、什么是图的遍历?
    • 1.1 遍历的定义与目标
    • 1.2 为何需要遍历
    • 1.3 两大核心策略:DFS 与 BFS
  • 二、深度优先搜索 (DFS) 的核心思想
    • 2.1 “一条路走到黑”的策略
    • 2.2 核心步骤拆解
    • 2.3 借助“辅助工具”:递归与栈
  • 三、DFS 的实现方式
    • 3.1 准备工作:图的表示
    • 3.2 递归实现 (Recursive Implementation)
      • 3.2.1 算法流程
      • 3.2.2 代码示例 (Java)
      • 3.2.3 可视化图解
    • 3.3 迭代实现 (Iterative Implementation using a Stack)
      • 3.3.1 算法流程
      • 3.3.2 代码示例 (Java)
      • 3.3.3 递归 vs. 迭代
  • 四、DFS 的应用场景
    • 4.1 判断图的连通性
      • 4.1.1 连通分量 (Connected Components)
      • 4.1.2 实现思路
    • 4.2 寻找环 (Cycle Detection)
      • 4.2.1 无向图中的环检测
      • 4.2.2 有向图中的环检测
  • 五、复杂度分析
    • 5.1 时间复杂度
    • 5.2 空间复杂度
  • 六、总结


摘要

在上一篇文章中,我们学习了图的两种主要存储方式:邻接矩阵和邻接表。现在,我们拥有了表示图的工具,是时候学习如何“走遍”整个图了。图的遍历是所有图算法的基石,它系统地访问图中的每一个顶点,确保不重不漏。本文将聚焦于两种核心遍历算法之一——深度优先搜索 (Depth-First Search, DFS)。我们将通过生动的类比、清晰的图解和详尽的代码实现,带你领略 DFS “一条路走到黑,再回头另寻他路”的独特智慧。本文内容涵盖 DFS 的核心思想、递归与迭代两种实现方式、复杂度分析,以及在判断连通性、检测环等经典问题中的应用,助你彻底掌握这一强大的图算法。

一、什么是图的遍历?

在我们深入 DFS 之前,首先需要明确“图的遍历”究竟是什么,以及为什么它如此重要。

1.1 遍历的定义与目标

图的遍历 (Graph Traversal) 是指从图中的某个指定顶点出发,按照某种搜索策略,系统地访问图中所有顶点,且每个顶点仅被访问一次的过程。

想象一下,你是一个城市规划师,拿到了一张包含所有路口(顶点)和道路(边)的城市地图。你的任务是巡视每一个路口,检查其交通状况。图的遍历就像你设计一条巡视路线,确保你走过每一个路口,并且不会重复检查同一个路口。

其核心目标有两个:

  1. 完整性:访问到所有可达的顶点。
  2. 唯一性:每个顶点只访问一次。

1.2 为何需要遍历

图的遍历是解决众多图相关问题的基础。许多复杂的图算法,其核心都内嵌了遍历的逻辑。例如:

  • 路径查找:从 A 点到 B 点是否存在路径?
  • 连通性分析:整个网络是否是连通的?它由几个相互隔离的部分(连通分量)组成?
  • 拓扑排序:确定一系列有依赖关系的任务的执行顺序。
  • 寻找关键路径:在项目管理中,找到影响总工期的最长路径。

1.3 两大核心策略:DFS 与 BFS

要遍历一张图,主要有两种经典的策略:

  1. 深度优先搜索 (DFS, Depth-First Search):它像一个执着的探险家,选择一条路就一直走到底,直到无路可走时才返回上一个路口,选择另一条路继续探索。本文的重点。
  2. 广度优先搜索 (BFS, Breadth-First Search):它像一个谨慎的勘探队,从起点出发,先把所有相邻的路口都探查一遍,然后再从这些相邻的路口出发,继续向外围逐层扩展。我们将在下一篇文章中详细介绍。

二、深度优先搜索 (DFS) 的核心思想

2.1 “一条路走到黑”的策略

DFS 的策略正如其名——“深度优先”。它的核心思想可以形象地比喻为“走迷宫”:

你站在迷宫的一个入口,面前有多条岔路。你随便选择一条,然后沿着这条路一直走下去。每到一个新的岔路口,你都继续选择一条新路前进。直到你走到一个死胡同(没有新的路可走),你才会原路返回到上一个岔路口,看看那里是否还有你没走过的路。如果有,就走那条路继续探索;如果所有路都走过了,就再返回到更上一层的岔路口。如此反复,直到你走遍了所有能从入口到达的地方。

这种“不撞南墙不回头”的探索方式,就是 DFS 的精髓。

2.2 核心步骤拆解

为了在算法中实现这种策略,并避免无限循环(尤其是在有环的图中),我们需要一个“备忘录”来记录哪些顶点已经访问过。通常,我们使用一个布尔数组 visited 来完成这个任务。

DFS 的基本步骤如下:

  1. 选择一个起始顶点 u,并将其标记为“已访问”。
  2. u 进行访问操作(例如,打印其值)。
  3. u 的所有邻接点中,寻找一个尚未被访问过的顶点 v
  4. 如果找到了这样的 v,则从 v 出发,递归地执行深度优先搜索。
  5. 如果 u 的所有邻接点都已被访问,则回溯到上一个顶点(在递归实现中,这一步是自动完成的函数返回)。

对于非连通图,我们需要一个主循环来确保所有顶点都被访问。主循环遍历所有顶点,如果发现一个顶点尚未被访问,就从它开始新一轮的 DFS。

2.3 借助“辅助工具”:递归与栈

DFS 的“回溯”特性与计算机科学中的一个基本概念完美契合——栈 (Stack)

  • 递归实现:函数调用的本质就是利用了系统内置的“调用栈”。当我们从顶点 u 递归调用 dfs(v) 时,系统会将 u 的当前状态(如返回地址、局部变量)压入调用栈。当 dfs(v) 执行完毕(即 v 的所有路径都探索完后),函数返回,u 的状态从栈中弹出,程序回到 u 的上下文继续执行,从而自然地实现了回溯。

  • 迭代实现:我们也可以不依赖系统,而是自己手动维护一个栈来模拟递归过程。这种方式可以避免在图深度过大时,递归调用导致的“栈溢出”错误。

三、DFS 的实现方式

下面,我们将使用邻接表作为图的存储结构,分别展示 DFS 的递归和迭代实现。

3.1 准备工作:图的表示

我们先定义一个简单的图类,使用邻接表来表示。这里以 Java 为例:

import java.util.LinkedList;
import java.util.List;class Graph {private int V; // 顶点的数量private List<List<Integer>> adj; // 邻接表public Graph(int v) {V = v;adj = new ArrayList<>(V);for (int i = 0; i < V; i++) {adj.add(new LinkedList<>());}}// 添加一条边public void addEdge(int u, int v) {adj.get(u).add(v);// 如果是无向图,还需要添加反向边// adj.get(v).add(u); }// ... getter 方法省略 ...
}

3.2 递归实现 (Recursive Implementation)

递归是实现 DFS 最自然、最简洁的方式。

3.2.1 算法流程

  1. 创建一个 boolean[] visited 数组,初始化为 false
  2. 编写一个主遍历函数 dfs(), 它遍历图中的所有顶点。如果顶点 i 未被访问,就调用辅助的递归函数 dfsRecursive(i, visited)
  3. dfsRecursive(u, visited) 中:
    • visited[u] 设为 true
    • 打印(或处理)顶点 u
    • 遍历 u 的所有邻接点 v
    • 如果 v 未被访问(!visited[v]),则递归调用 dfsRecursive(v, visited)

3.2.2 代码示例 (Java)

public class DFSRecursive {// 主遍历函数,处理非连通图的情况public void dfs(Graph graph) {int V = graph.getV();boolean[] visited = new boolean[V]; // 访问标记数组// 遍历所有顶点,确保每个连通分量都被访问for (int i = 0; i < V; i++) {if (!visited[i]) {dfsRecursive(i, visited, graph);}}}// 递归辅助函数private void dfsRecursive(int u, boolean[] visited, Graph graph) {// 1. 标记当前顶点为已访问visited[u] = true;System.out.print(u + " "); // 访问顶点// 2. 遍历该顶点的所有邻接点for (int v : graph.getAdj().get(u)) {// 3. 如果邻接点未被访问,则递归访问if (!visited[v]) {dfsRecursive(v, visited, graph);}}}public static void main(String[] args) {Graph g = new Graph(7);g.addEdge(0, 1);g.addEdge(0, 2);g.addEdge(1, 3);g.addEdge(1, 4);g.addEdge(2, 5);g.addEdge(2, 6);// 假设图 g 是有向图System.out.println("深度优先遍历 (递归):");DFSRecursive dfsTraversal = new DFSRecursive();dfsTraversal.dfs(g); // 输出可能为:0 1 3 4 2 5 6 (具体顺序取决于邻接表实现)}
}

3.2.3 可视化图解

让我们用 Mermaid 语法来可视化上述代码中图的 DFS 过程(假设从顶点 0 开始)。

graph TDsubgraph 图结构A[0] --> B[1]A --> C[2]B --> D[3]B --> E[4]C --> F[5]C --> G[6]endsubgraph 遍历过程 (访问顺序: 0 1 3 4 2 5 6)P1(访问 0) --> P2(深入 1)P2 --> P3(深入 3)P3 --> P4(3 无路可走, 回溯到 1)P4 --> P5(从 1 深入 4)P5 --> P6(4 无路可走, 回溯到 1)P6 --> P7(1 无路可走, 回溯到 0)P7 --> P8(从 0 深入 2)P8 --> P9(深入 5)P9 --> P10(5 无路可走, 回溯到 2)P10 --> P11(从 2 深入 6)P11 --> P12(6 无路可走, 回溯到 2)P12 --> P13(2 无路可走, 回溯到 0)P13 --> P14(0 无路可走, 结束)end

3.3 迭代实现 (Iterative Implementation using a Stack)

使用我们自己的栈,可以实现一个非递归版本的 DFS。

3.3.1 算法流程

  1. 创建一个 boolean[] visited 数组和一个 Stack<Integer>
  2. 编写一个主遍历函数 dfs(), 它遍历图中的所有顶点。如果顶点 i 未被访问,就调用辅助的迭代函数 dfsIterative(i, visited, graph)
  3. dfsIterative(startNode, visited, graph) 中:
    • startNode 压入栈中。
    • 当栈不为空时,循环执行:
      • 弹出栈顶顶点 u
      • 如果 u 尚未被访问:
        • 标记 u 为已访问并进行处理(如打印)。
        • u 的所有未访问的邻接点 v 压入栈中。

注意:迭代版本中,一个节点被压入栈时,并不意味着它被“访问”了,只有当它从栈中被弹出并处理时,才算真正被访问。

3.3.2 代码示例 (Java)

import java.util.Stack;public class DFSIterative {public void dfs(Graph graph) {int V = graph.getV();boolean[] visited = new boolean[V];Stack<Integer> stack = new Stack<>();for (int i = 0; i < V; i++) {if (!visited[i]) {// 开始新一轮的DFSstack.push(i);while (!stack.isEmpty()) {int u = stack.pop();// 如果节点已经访问过,则跳过// 这是因为一个节点可能被多次加入栈中,但我们只想访问一次if (visited[u]) {continue;}visited[u] = true;System.out.print(u + " ");// 将邻接点逆序入栈,以保证访问顺序与递归版本相似// (但这不是必须的,任何顺序都符合DFS的定义)List<Integer> neighbors = graph.getAdj().get(u);for (int j = neighbors.size() - 1; j >= 0; j--) {int v = neighbors.get(j);if (!visited[v]) {stack.push(v);}}}}}}public static void main(String[] args) {// 使用与递归示例相同的图Graph g = new Graph(7);g.addEdge(0, 1);g.addEdge(0, 2);g.addEdge(1, 3);g.addEdge(1, 4);g.addEdge(2, 5);g.addEdge(2, 6);System.out.println("\n深度优先遍历 (迭代):");DFSIterative dfsTraversal = new DFSIterative();dfsTraversal.dfs(g); // 输出可能为:0 1 3 4 2 5 6}
}

3.3.3 递归 vs. 迭代

特性递归实现迭代实现 (使用栈)
代码简洁性非常高,代码直观,与 DFS 思想高度吻合。相对复杂,需要手动管理栈。
空间开销隐式的函数调用栈,最坏情况下深度为 O(V)O(V)O(V)显式的栈,最坏情况下大小为 O(V)O(V)O(V)
栈溢出风险当图的深度非常大时,可能导致栈溢出。无此风险,只要内存足够。
控制性对执行流程的控制较少。可以更精细地控制栈内元素和遍历过程。

四、DFS 的应用场景

DFS 的强大之处在于它能够在图的结构上进行深度探索,这使得它在许多问题中都大有可为。

4.1 判断图的连通性

4.1.1 连通分量 (Connected Components)

对于一个无向图,如果从任意顶点 i 到任意顶点 j 都存在路径,则称这个图是连通图。如果一个图不是连通的,它可以被划分为多个连通分量,每个分量内部是连通的。

4.1.2 实现思路

我们可以利用 DFS 轻松地找出图中所有的连通分量。

  1. 初始化一个计数器 count = 0
  2. 遍历从 0 到 V-1 的所有顶点。
  3. 对于每个顶点 i,如果它还没有被访问过:
    • 说明我们发现了一个新的连通分量的起点。
    • count 加 1。
    • 从顶点 i 开始执行一次完整的 DFS。这次 DFS 会访问到该连通分量中的所有顶点。
  4. 遍历结束后,count 的值就是图中连通分量的数量。
// 计算连通分量的伪代码
public int countConnectedComponents(Graph graph) {int V = graph.getV();boolean[] visited = new boolean[V];int count = 0;for (int i = 0; i < V; i++) {if (!visited[i]) {count++;// dfsRecursive or dfsIterative to visit all nodes in this componentdfsRecursive(i, visited, graph); }}return count;
}

4.2 寻找环 (Cycle Detection)

判断一个图中是否存在环是 DFS 的另一个经典应用。

4.2.1 无向图中的环检测

在无向图中,如果在 DFS 过程中,我们从顶点 u 访问到一个邻接点 v,而 v 之前已经被访问过,并且 v 不是 u 在 DFS 树中的父节点,那么我们就找到了一个环。

实现思路:
在 DFS 递归函数中增加一个 parent 参数,即 dfs(u, parent, visited)

  • 当从 u 遍历到邻接点 v 时:
    • 如果 v 未被访问,继续递归 dfs(v, u, visited)
    • 如果 v 已被访问,且 v != parent,则说明 vu 的一个祖先节点(非直接父节点),形成了一个回边 (Back Edge),从而构成了环。

4.2.2 有向图中的环检测

有向图的环检测稍微复杂一些。仅仅使用 visited 数组是不够的。我们需要区分两种“已访问”状态:

  1. 当前递归路径上的访问:正在探索的路径上的点。
  2. 已完成探索的访问:所有子孙节点都已探索完毕的点。

实现思路:
使用一个额外的 recursionStack (或颜色标记法:白色-未访问, 灰色-在递归栈中, 黑色-已完成探索) 布尔数组。

  • 在进入 dfs(u) 时,将 visited[u]recursionStack[u] 都设为 true
  • 当从 u 遍历到邻接点 v 时:
    • 如果 v 未被访问,递归调用 dfs(v)。如果该调用返回 true(表示找到了环),则直接向上返回 true
    • 如果 vrecursionStack[v]true,说明 v 正在当前的递归路径上,我们找到了一个环,返回 true
  • dfs(u) 即将返回时,将 recursionStack[u] 设为 false,表示将 u 从当前探索路径中移除(回溯)。

五、复杂度分析

5.1 时间复杂度

DFS 的时间复杂度取决于图的表示方式。

  • 邻接表 (Adjacency List): 算法需要访问每个顶点一次,并且对于每个顶点,它会遍历其所有的邻接边。因此,总的操作数是所有顶点的度数之和。对于有向图和无向图,所有顶点的度数之和都等于 2E2E2EEEE 为边数)。所以,时间复杂度为 O(V+E)O(V + E)O(V+E),其中 VVV 是顶点数,EEE 是边数。这是分析图算法时最常用的复杂度。

  • 邻接矩阵 (Adjacency Matrix): 即使一个顶点只有一个邻接点,我们也需要检查一整行(VVV 个元素)来找出它的所有邻接点。因此,对于每个顶点,我们都需要 O(V)O(V)O(V) 的时间。总时间复杂度为 O(V2)O(V^2)O(V2)

5.2 空间复杂度

空间复杂度主要由 visited 数组和递归/迭代所用的栈决定。

  • visited 数组需要 O(V)O(V)O(V) 的空间。
  • 递归栈/迭代栈: 在最坏的情况下,图可能是一条长链,递归深度或栈的大小会达到 VVV。因此,栈的空间复杂度为 O(V)O(V)O(V)

综合起来,DFS 的空间复杂度为 O(V)O(V)O(V)

六、总结

本文我们对图的深度优先搜索(DFS)进行了全面而深入的探讨。现在,让我们对核心知识点进行回顾:

  1. 核心思想: DFS 是一种图遍历算法,它遵循“一路走到底,再回头”的策略。它从一个起点开始,沿着一条路径尽可能深地探索,直到到达路径末端,然后回溯到上一个节点,继续探索其他未访问的分支。

  2. 实现方式: DFS 可以通过两种主要方式实现。递归是最自然、简洁的实现,它巧妙地利用了函数调用栈来处理回溯。迭代则需要手动维护一个栈,虽然代码稍显复杂,但能避免深度过大时的栈溢出问题。

  3. 复杂度: 当图以邻接表形式存储时,DFS 的时间复杂度为 O(V+E)O(V + E)O(V+E),空间复杂度为 O(V)O(V)O(V)。这是图算法中非常高效的复杂度。

  4. 关键应用: DFS 不仅仅是一种遍历方法,它还是解决众多图论问题的强大工具,例如高效地计算图的连通分量和在有向图/无向图中检测环的存在。

  5. 与 BFS 的区别: DFS 深入探索分支,而我们将在下一篇文章中学习的广度优先搜索 (BFS) 则以“层”为单位,向外围逐层扩展。两者在不同问题场景下各有优势。

掌握了 DFS,你就拥有了深入探索图结构内部秘密的一把钥匙。在接下来的学习中,我们会看到 DFS 如何在更多高级算法(如拓扑排序)中发挥其不可或缺的作用。


http://www.dtcms.com/a/394802.html

相关文章:

  • C#练习题——LinkedList 的进阶应用与测试
  • 手机CPU型号
  • jdbc相关知识
  • yolov12 导出onnx
  • Linux 环境变量与程序地址空间
  • LeetCode:48.路径总和Ⅲ
  • 计算机网络的性能
  • 深度学习笔试选择题:题组1
  • 统一配置管理根据不同域名展现不同信息或相近信息 Vue3类单例模式封装
  • 人工智能深度学习——循环神经网络(RNN)
  • 单例模式指南:全局资源的安全访问
  • 容器化 Tomcat 应用程序
  • Vue Router【前端】
  • 数据结构——受限线性表之栈
  • 数据结构(1)------ 三要素
  • BaaS(Backend as a Service)概述、平台、项目
  • 区间dp,数据结构优化dp等5种dp,各种trick深度讲解
  • 数据结构笔试选择题:题组1
  • 前端基础:从0到1实现简单网页效果(一)
  • 数据结构|图论:从数据结构到工程实践的核心引擎
  • AI赋能个人效能提升:实战演练工作规划、项目复盘与学习发展
  • 7. Linux RAID 存储技术
  • iOS 上架 App 费用详解 苹果应用发布成本、App Store 上架收费标准、开发者账号与审核实战经验
  • kafka 2.12_3.9.1 版本修复 Apache Commons BeanUtils 访问控制错误漏洞(CVE-2025-48734)
  • 二分查找经典——力扣153.寻找旋转排序数组中的最小值
  • 离散数学之命题逻辑
  • 【Linux命令从入门到精通系列指南】ping 命令详解:网络连通性诊断的终极工具
  • 游戏UI告别“贴图”时代:用Adobe XD构建“活”的设计系统
  • NXP - 用MCUXpresso IDE导入lpcopen_2_10_lpcxpresso_nxp_lpcxpresso_1769.zip中的工程
  • ✅ Python+Django租房推荐系统 双协同过滤+Echarts可视化 租房系统 推荐算法 全栈开发(建议收藏)✅