Java 算法实践(五):二叉树遍历与常见算法题

二叉树(Binary Tree) 是计算机科学中最基础的层次结构。二叉树的核心思维模式是递归(Recursion) 。虽然迭代(Iteration)也能解决问题,但递归通常能更直观地映射树的自相似性(Self-Similarity)。

二叉树算法的核心难点在于:如何将宏观的逻辑(处理整棵树)拆解为微观的操作(处理当前节点、左子树、右子树)

在 Java 中,二叉树的节点定义如下:

java 复制代码
public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}

一、 递归思维:深度优先搜索 (DFS)

深度优先搜索 (DFS) 是二叉树遍历的基础算法。它通过递归方式访问节点,先深入子树直到叶子节点,再回溯处理上级节点。这种方法模拟了栈的压入和弹出操作,适用于需要逐层深入探索的场景,如路径求解或结构分析。

在二叉树中,DFS 的递归实现依赖于函数调用栈的天然机制。每个递归调用处理一个节点,并将子问题委托给子调用。理解 DFS 需要掌握递归的本质:定义清晰的终止条件和单层处理逻辑,以避免无限递归或栈溢出风险。

1.1 三种遍历序的本质

三种遍历序根据处理当前节点 (Root) 的时机不同而区分:

  • 前序遍历 (Pre-order)Root → \rightarrow → Left → \rightarrow → Right
    • 处理顺序:先访问当前节点,然后递归左子树和右子树。
    • 特性:适合在遍历开始时立即获取节点信息,形成从根到叶的顺序序列。
    • 应用:用于序列化二叉树、构建前缀表达式或复制树结构,其中根节点信息需优先处理。
  • 中序遍历 (In-order) :Left → \rightarrow → Root → \rightarrow → Right
    • 处理顺序:先递归左子树,然后访问当前节点,最后递归右子树。
    • 特性:产生从左到右的有序访问,尤其在有序树中体现为升序序列。
    • 应用:在二叉搜索树 (BST) 中用于生成有序节点值列表,或验证树结构的有序性。
  • 后序遍历 (Post-order) :Left → \rightarrow → Right → \rightarrow → Root
    • 处理顺序:先递归左子树和右子树,然后访问当前节点。
    • 特性:自底向上汇总信息,当前节点处理依赖于子树结果。
    • 应用:计算树的高度、节点总数或依赖子树计算的属性,如子树大小或平衡检查。

选择遍历序取决于问题需求:前序强调根优先,中序强调有序,后序强调汇总。实际编码中,可通过调整处理位置的代码行来切换顺序。

1.2 递归函数的编写模板

递归函数设计需定义终止条件和单层逻辑。终止条件 处理空节点,单层逻辑假设子树已处理完成。模板提供了一个通用框架,可根据遍历序插入具体操作。

通用递归模板

java 复制代码
void traverse(TreeNode root) {
    if (root == null) {
        return;  // 终止条件:空节点直接返回,避免进一步递归
    }
    
    // 前序位置:在此处执行对当前节点的处理,例如收集值或修改属性
    
    traverse(root.left);  // 递归深入左子树
    
    // 中序位置:在此处执行对当前节点的处理,适用于有序操作
    
    traverse(root.right); // 递归深入右子树
    
    // 后序位置:在此处执行对当前节点的处理,汇总子树结果
}

注意事项

  • 终止条件 :始终检查 root == null,返回适当值(如 0 或 null),防止空指针异常。
  • 单层逻辑:假设子调用已返回正确结果,仅关注当前节点的计算或操作。这有助于简化复杂问题的分解。
  • 参数传递:若需累积结果,可添加参数(如 List 收集节点值)或使用全局变量,但优先使用返回值以保持函数纯净。

1.3 复杂度分析

  • 时间复杂度:DFS 遍历所有节点,因此为 O(N),其中 N 是节点数。每个节点访问常数次。
  • 空间复杂度:O(H),H 为树高。最坏情况下(链状树)为 O(N),平均为 O(log N)(平衡树)。迭代版本可优化为 O(1) 额外空间,但实现更复杂。

二、 迭代思维:显式栈模拟

虽然递归代码简洁,但在处理极深层级的树中,Java 虚拟机栈(JVM Stack)的大小是有限的(通常几 MB)。过深的递归会导致 StackOverflowError

将递归转化为迭代(Iteration),本质上是在堆内存 (Heap) 中维护一个显式的 Stack 来模拟系统调用栈。

2.1 前序遍历的迭代实现

题目链接LeetCode 144. Binary Tree Preorder Traversal

题目描述:给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

逻辑推演

前序遍历的顺序是:根 (Root) → \rightarrow → 左 (Left) → \rightarrow → 右 (Right)

使用栈(LIFO,后进先出)来模拟。

  1. 处理根:每次从栈顶弹出一个节点,它就是当前需要处理的"根"。
  2. 入栈顺序 :因为栈是后进先出的,为了保证"左子树"先于"右子树"被处理,需要反向入栈
    • 先压入 右孩子 (Right)
    • 再压入 左孩子 (Left)
    • 这样下一轮循环 pop 出来的就是左孩子。

Java 代码

java 复制代码
public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) return res;

    // 使用 Deque 接口的实现类 ArrayDeque 作为栈,性能优于 Stack 类
    Deque<TreeNode> stack = new ArrayDeque<>();
    stack.push(root);

    while (!stack.isEmpty()) {
        // 弹出栈顶,记为当前根节点
        TreeNode node = stack.pop();
        // 立即处理(前序:根先处理)
        res.add(node.val);

        // 栈是 LIFO,所以先压右,后压左
        // 这样下一次 pop 出来的就是左子节点
        if (node.right != null) {
            stack.push(node.right);
        }
        if (node.left != null) {
            stack.push(node.left);
        }
    }
    return res;
}

2.2 中序遍历的迭代实现

题目链接LeetCode 94. Binary Tree Inorder Traversal

题目描述:给你二叉树的根节点 root ,返回它节点值的 中序 遍历。

逻辑推演

中序遍历的顺序是:左 (Left) → \rightarrow → 根 (Root) → \rightarrow → 右 (Right)

这比前序遍历复杂,因为我们访问到根节点(Root)时,不能立即处理它,必须先把它存起来,去处理它的左子树。等到左子树全部处理完了,才能回来处理这个根节点。

指针双重角色

我们需要一个指针 curr 来遍历树。

  1. 访问阶段 (Push)curr 一直向左走 (curr = curr.left),沿途将经过的节点压入栈中。
  2. 回溯阶段 (Pop) :当 curr 为空,说明最近一个压栈节点的左子树处理完毕。弹出栈顶节点(即为"根"),记录其值,然后让 curr 转向右子树 (curr = curr.right)。

Java 代码

java 复制代码
public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode curr = root;

    // curr != null: 表示还有左子树没走完,需要继续入栈
    // !stack.isEmpty(): 表示还有父节点待处理(回溯)
    while (curr != null || !stack.isEmpty()) {
        
        // 尽力向左走,模拟递归调用的压栈过程
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;
        }
        
        // 左边走到头了,弹出栈顶
        // 这个弹出的节点,就是"左-根-右"中的"根"
        curr = stack.pop();
        
        // 处理当前节点
        res.add(curr.val);
        
        // 转向右子树
        // 如果右子树为空,下一轮循环会直接继续弹栈(回溯到上一层)
        // 如果右子树非空,下一轮循环会处理右子树的左分支
        curr = curr.right;
    }
    return res;
}

三、 层序遍历:广度优先搜索 (BFS)

与深度优先搜索 (DFS) 沿树的深度方向遍历不同,广度优先搜索 (BFS) 严格按照树的层级(Level)进行遍历。即只有当第 k k k 层的节点全部被访问处理完毕后,才会开始访问第 k + 1 k+1 k+1 层的节点。

这种遍历方式在解决以下两类问题时具有绝对优势:

  1. 分层处理:需要按层打印、计算每一层的平均值/最大值、填充下一节点的指针。
  2. 最短路径/最小深度:在无权图中,BFS 首次到达目标节点时的路径长度即为最短路径(例如求二叉树的最小深度)。

3.1 核心数据结构:队列 (Queue)

BFS 的核心在于利用 队列 (Queue)先进先出 (FIFO) 特性。

  • 顺序保证:父节点先入队,子节点后入队。这保证了上层节点一定比下层节点先被处理。
  • 实现类选择 :在 Java 中,Queue 是一个接口。虽然 LinkedList 实现了该接口,但在算法题及高性能场景下,推荐使用 ArrayDeque
    • ArrayDeque :基于数组实现,内存连续,缓存命中率高,且没有 LinkedList 那样频繁的节点(Node)对象创建与销毁开销。
    • LinkedList :基于链表实现,虽然入队出队也是 O ( 1 ) O(1) O(1),但涉及额外的对象分配,在数据量大时 GC 压力较大。

3.2 分层逻辑详解:为何需要 size

题目链接LeetCode 102. Binary Tree Level Order Traversal

题目描述:给你二叉树的根节点 root ,返回其节点值的 层序遍历 。(即逐层地,从左到右访问所有节点)。

在标准的 BFS 模版中,最关键的一行代码是 int currentLevelSize = queue.size();

逻辑推演

如果直接遍历队列直到为空,我们将得到一个所有节点的线性序列,但无法区分哪些节点属于同一层。为了实现"分层",需要在每一轮处理前进行快照

  1. 锁定当前层 :在进入内层循环之前,queue 中包含的元素恰好 是当前这一层的所有节点(且不包含下一层的节点)。
  2. 固定次数遍历 :获取此时的 queue.size(),记为 N N N。这意味着接下来的 N N N 次出队操作 (poll),处理的都是当前层的节点。
  3. 动态入队 :在处理这 N N N 个节点的过程中,将它们的子节点(下一层)加入队列。由于队列的 FIFO 特性,这些新加入的节点会排在当前层节点的后面,不会影响当前内层循环的执行次数
  4. 迭代:当内层循环结束时,当前层所有节点已出队,队列中剩下的全是下一层的节点。

Java 代码

java 复制代码
public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();
    if (root == null) return res;

    // 使用 ArrayDeque 作为队列实现
    Queue<TreeNode> queue = new ArrayDeque<>();
    queue.offer(root);

    while (!queue.isEmpty()) {
        // 在遍历当前层之前,先记录当前队列的大小
        // 这个 size 代表了"这一层有多少个节点"
        int currentLevelSize = queue.size();
        
        // 存储当前层结果的列表
        List<Integer> levelList = new ArrayList<>();

        // 仅处理当前层的 currentLevelSize 个节点
        for (int i = 0; i < currentLevelSize; i++) {
            TreeNode node = queue.poll();
            levelList.add(node.val);

            // 将下一层的节点加入队列末尾
            // 这些新节点不会在本次 for 循环中被处理,因为 i < currentLevelSize 限制了次数
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        
        // 当前层处理完毕,收集结果
        res.add(levelList);
    }
    return res;
}

3.3 复杂度分析

  • 时间复杂度O ( N ) O(N) O(N)。每个节点进队一次,出队一次,操作总数为线性。
  • 空间复杂度O ( W ) O(W) O(W) ,其中 W W W 是树的最大宽度。
    • 在最坏情况下(满二叉树),最后一层的节点数约为 N / 2 N/2 N/2。因此 BFS 的空间复杂度在最坏情况下为 O ( N ) O(N) O(N)。
    • 这与 DFS 的空间复杂度不同(DFS 取决于树的高度 H H H)。对于非常宽但很浅的树,DFS 空间更优;对于非常深但很窄的树,BFS 空间更优。

四、 常见核心题型解析

掌握了 DFS 和 BFS 的基本框架后,解题的关键在于:明确递归函数到底要返回什么给父节点 ,以及在当前节点需要做什么计算

4.1 场景一:子树信息的收集与整合(后序遍历)

这类问题的共同特征是:当前节点的计算结果依赖于左右子树的计算结果。因此,必须先递归处理子树,拿到返回值后,再处理当前节点(即后序遍历逻辑)。

例题 1:二叉树的最大深度

题目链接LeetCode 104. Maximum Depth of Binary Tree

题目描述:给定一个二叉树,找出其最大深度。

思路解析

  • 递归定义 :函数 maxDepth(root) 的物理意义是"返回以 root 为根的树的高度"。
  • 逻辑分解
    1. 左子树信息:递归计算左子树高度。
    2. 右子树信息:递归计算右子树高度。
    3. 当前节点整合 :当前树的高度 = Math.max(左, 右) + 1(加 1 是加上当前节点这一层)。

Java 代码

java 复制代码
public int maxDepth(TreeNode root) {
    // 递归终止条件: 空节点高度为 0
    if (root == null) return 0;
    
    // 获取子树高度
    int leftDepth = maxDepth(root.left);
    int rightDepth = maxDepth(root.right);
    
    // 2. 整合并返回
    return Math.max(leftDepth, rightDepth) + 1;
}
例题 2:二叉树的直径

题目链接LeetCode 543. Diameter of Binary Tree

题目描述:给定一棵二叉树,你需要计算它的直径长度。直径定义为任意两个节点路径长度中的最大值(这条路径可能不经过根节点)。

思路解析

这道题展示了一个核心技巧:递归函数的返回值题目的最终答案不一定相同。

  • 问题分析 :路径是"左臂 + 右臂"。对于任意节点 u,穿过该节点的最长路径 = u 的左子树最大深度 + u 的右子树最大深度节点数 计算子树高度,左右子树高度之和恰好等于以当前节点为根的路径的边数(即直径定义))。
  • 变量分离
    • 递归返回值 :必须返回以当前节点为根的子树深度,供父节点计算使用。
    • 全局最大值 :在递归过程中,计算每个节点的"穿过该节点的路径长度",并更新全局变量 maxDiameter

Java 代码

java 复制代码
// 全局变量记录最大直径
private int maxDiameter = 0;

public int diameterOfBinaryTree(TreeNode root) {
    maxDepth(root);
    return maxDiameter;
}

// 该函数的返回值依然是"深度",但在过程中更新直径
private int maxDepth(TreeNode root) {
    if (root == null) return 0;

    int left = maxDepth(root.left);
    int right = maxDepth(root.right);

    // 直径 = 左深度 + 右深度
    // 在后序位置更新全局最大值
    maxDiameter = Math.max(maxDiameter, left + right);

    // 返回给父节点的是:单侧最长链的长度(深度)
    return Math.max(left, right) + 1;
}

4.2 场景二:BST 的特性应用(中序遍历)

二叉搜索树 (BST) 的核心性质是:中序遍历的结果是一个严格递增的有序序列。所有 BST 相关题目,优先考虑中序遍历。

例题 1:验证二叉搜索树

题目链接LeetCode 98. Validate Binary Search Tree

题目描述:给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

思路解析

  • 错误做法 :仅判断 root.left.val < root.valroot.right.val > root.val。这无法保证右子树中的所有节点都大于根节点(例如右子树中有一个极小值)。
  • 正确做法 :利用中序遍历的单调递增性。
    • 维护一个指针 prev,指向中序遍历中上一个访问的节点。
    • 在访问当前节点 root 时,检查 prev.val < root.val 是否成立。
    • 如果不成立,说明破坏了递增顺序,返回 false

Java 代码

java 复制代码
// 记录前一个遍历的节点
private TreeNode prev = null;

public boolean isValidBST(TreeNode root) {
    if (root == null) return true;

    // 递归左子树
    if (!isValidBST(root.left)) return false;

    // 当前节点逻辑:必须严格大于前一个节点
    if (prev != null && prev.val >= root.val) {
        return false;
    }
    prev = root; // 更新 prev 指针

    // 递归右子树
    return isValidBST(root.right);
}
例题 2:二叉搜索树中第 K 小的元素

题目链接LeetCode 230. Kth Smallest Element in a BST

题目描述:给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素。

思路解析

  • 由于 BST 中序遍历是有序的,第 k 小的元素就是中序遍历序列中的第 k 个元素。
  • 我们不需要生成整个数组,只需维护一个计数器 rank
  • 每当遍历到一个节点,rank++。当 rank == k 时,当前节点即为答案,记录结果并停止递归。

Java 代码

java 复制代码
private int rank = 0;
private int result = 0;

public int kthSmallest(TreeNode root, int k) {
    traverse(root, k);
    return result;
}

private void traverse(TreeNode root, int k) {
    if (root == null) return;

    // 先去左边
    traverse(root.left, k);

    // 中序位置:处理当前节点
    rank++;
    if (rank == k) {
        result = root.val;
        return; // 找到后可提前结束
    }

    // 再去右边
    traverse(root.right, k);
}

4.3 场景三:复杂路径问题与状态定义

这类题目通常涉及"路径",且路径的定义灵活(不一定从根开始,也不一定在叶子结束 )。解题关键在于区分 "贡献给父节点的收益""在当前节点内部计算的路径收益"

例题 1:路径总和 III

题目链接LeetCode 437. Path Sum III

题目描述:给定一个二叉树的根节点 root ,求该树中有多少条路径,这条路径上所有节点值相加等于 targetSum。路径不需要从根节点开始,也不需要在叶子节点结束,但必须向下(父节点到子节点)。

思路解析

这道题需要双重递归。

  1. 外层递归 (pathSum):遍历树的每一个节点。对于每一个节点,都把它当作"路径的起点"。
  2. 内层递归 (countPaths) :计算以指定节点为起点 ,向下延伸且和为 target 的路径数量。
    • 在内层递归中,每经过一个节点,target 减去当前值。若 target == 0,说明找到一条路径(注意:找到后需继续向下,因为后续节点和可能为 0)。

Java 代码

java 复制代码
public int pathSum(TreeNode root, long targetSum) {
    if (root == null) return 0;
    
    // 以当前 root 为起点的路径数量
    int pathsFromRoot = countPaths(root, targetSum);
    
    // 递归:去左子树找起点 + 去右子树找起点
    int pathsFromLeft = pathSum(root.left, targetSum);
    int pathsFromRight = pathSum(root.right, targetSum);
    
    return pathsFromRoot + pathsFromLeft + pathsFromRight;
}

// 计算以 node 为起点,和为 target 的路径数
private int countPaths(TreeNode node, long target) {
    if (node == null) return 0;
    
    int count = 0;
    // 找到一条,count + 1,但不能 return,需继续向下(可能后面有正负抵消)
    if (node.val == target) {
        count++;
    }
    
    count += countPaths(node.left, target - node.val);
    count += countPaths(node.right, target - node.val);
    
    return count;
}
例题 2:二叉树中的最大路径和

题目链接LeetCode 124. Binary Tree Maximum Path Sum

题目描述:路径被定义为一条从树中任意节点出发,沿父子 - 节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。求路径和的最大值。

思路解析

  • 逻辑分解
    对于任意节点 u,经过它的最大路径和 = u.val + 左子树提供的最大收益 + 右子树提供的最大收益
  • 贡献值计算(返回值)
    子树只能贡献一条 分支给父节点(因为路径不能分叉)。所以 gain(u) = u.val + max(leftGain, rightGain)
  • 负数截断
    如果子树计算出的收益是负数,则直接舍弃(贡献值记为 0),因为加上负数只会让路径和变小。

Java 代码

java 复制代码
private int maxSum = Integer.MIN_VALUE;

public int maxPathSum(TreeNode root) {
    maxGain(root);
    return maxSum;
}

// 计算以 node 为根的子树,向父节点提供的最大单边贡献
private int maxGain(TreeNode node) {
    if (node == null) return 0;

    // 递归计算左右子树的贡献,如果是负数则截断为 0
    int leftGain = Math.max(maxGain(node.left), 0);
    int rightGain = Math.max(maxGain(node.right), 0);

    // 更新全局最大值(这是"内部路径",可以同时包含左右子树)
    int priceNewPath = node.val + leftGain + rightGain;
    maxSum = Math.max(maxSum, priceNewPath);

    // 返回给父节点(只能选一条边:要么走左,要么走右)
    return node.val + Math.max(leftGain, rightGain);
}

例题比较

这个题与"二叉树的直径"在遍历框架上相似,都在后序位置利用左右子树的返回值更新全局最大值。但在核心逻辑上存在两点关键差异:

  1. 度量维度的转变 :"直径"计算的是路径长度 (边的数量),所有节点权重视为 1,且深度恒为非负数。本题计算的是节点值之和,节点值可能为负数,因此路径越长不一定和越大。
  2. 负数剪枝策略 :这是这个题的难点。在"直径"问题中,累加子树深度总是产生正向增益,因此无需舍弃。但在本题中,如果子树返回的最大路径和是负数 ,连接该子树反而会减少当前路径的总和。因此必须在获取子树贡献时进行剪枝int gain = Math.max(recursive_call(), 0)。这意味着如果子树贡献为负,需要选择断开该分支(将其贡献视为 0),仅保留当前节点或另一侧非负的分支。

五、 总结与最佳实践

5.1 复杂度分析

二叉树算法的复杂度取决于节点的数量 N N N 和树的形状。

  • 时间复杂度
    无论是深度优先搜索 (DFS) 还是广度优先搜索 (BFS),标准遍历都需要访问每个节点一次,因此时间复杂度通常为 O ( N ) O(N) O(N)
  • 空间复杂度
    空间消耗主要来自维持遍历状态的辅助结构(栈或队列)。
    • DFS (递归或栈迭代) :空间复杂度取决于树的高度 。在最坏情况(链状树)下为 O ( N ) O(N) O(N),在平衡树情况下为 O ( log ⁡ N ) O(\log N) O(logN)。
    • BFS (队列) :空间复杂度取决于树的最大宽度 。在最坏情况(满二叉树)下,最后一层的节点数约为 N / 2 N/2 N/2,因此空间复杂度为 O ( N ) O(N) O(N)。

5.2 算法选择策略

在解决二叉树问题时,应根据题目的具体需求选择合适的遍历方式:

  1. 二叉搜索树 (BST) 相关问题
    优先选择 中序遍历。利用 BST 中序遍历结果为有序数组的特性,可以有效地解决验证、查找、第 K 小元素等问题。
  2. 依赖子树信息的问题
    优先选择 后序遍历 (DFS)。例如计算树的高度、直径或最大路径和。这类问题通常需要先获取左右子树的计算结果,整合后再返回给父节点。
  3. 层级或最短路径问题
    优先选择 层序遍历 (BFS)。例如求二叉树的最小深度、计算每一层的平均值或右视图。
  4. 递归与迭代的选择
    递归代码逻辑清晰,易于实现,是首选方案。但在处理节点数量极大且深度很深的树时,为了防止系统栈溢出,应考虑使用显式栈(Deque)将递归转换为迭代。

5.3 注意事项

编写二叉树代码时,需要注意以下几个关键点以保证代码的正确性:

  1. 递归终止条件
    所有递归函数必须包含终止条件 ,通常是判断当前节点是否为 null。这是防止无限递归和栈溢出的基础。
  2. 空指针检查
    在访问 node.leftnode.right 之前,必须确保 node 本身不为 null
  3. 叶子节点的界定
    区分"空节点"与"叶子节点"。某些题目明确要求路径终止于叶子节点,此时需要同时判断 left == nullright == null
  4. 全局变量与返回值的区别
    在复杂递归中(如二叉树直径),递归函数的返回值往往是提供给父节点计算使用的(如子树深度),而题目的最终答案(如最大直径)可能需要在遍历过程中通过更新全局变量或成员变量来获得。
相关推荐
星火开发设计1 小时前
序列式容器:list 双向链表的特性与用法
开发语言·前端·数据结构·数据库·c++·链表·list
知识即是力量ol1 小时前
口语八股——计算机网络篇(终篇)
java·计算机网络·面试·八股
一条大祥脚1 小时前
Z函数/拓展KMP
算法
洛_尘2 小时前
测试6:自动化测试--概念篇(JAVA)
java·开发语言·测试
追随者永远是胜利者2 小时前
(LeetCode-Hot100)39. 组合总和
java·算法·leetcode·职场和发展·go
追随者永远是胜利者2 小时前
(LeetCode-Hot100)34. 在排序数组中查找元素的第一个和最后一个位置
java·算法·leetcode·职场和发展·go
爱凤的小光2 小时前
VisionMaster软件---脚本梳理
java·服务器·网络
键盘鼓手苏苏4 小时前
Flutter for OpenHarmony:markdown 纯 Dart 解析引擎(将文本转化为结构化 HTML/UI) 深度解析与鸿蒙适配指南
前端·网络·算法·flutter·ui·html·harmonyos