Java二叉搜索树从原理到工业实践全解析
1. 项目概述为什么树结构是Java开发者绕不开的“分水岭”刚带完一届校招培训有个现象特别明显能熟练写链表、栈、队列的新人不少但一提到二叉搜索树的删除逻辑八成会卡在“双子节点怎么处理”上问红黑树的旋转规则眼神就开始飘——不是记不住而是没真正把树当成一种空间组织方式来理解。这门课叫“数据结构Java04”表面看是讲树的创建和遍历实则是一次思维范式的切换从线性结构的“一条道走到黑”转向分层结构的“多路分支决策”。我试过用文件系统类比数组像一张单页清单链表像一串环环相扣的钥匙链而树就是你电脑里的C盘目录——D:\Program Files\Java\jdk-17\bin每一级路径都是一个节点子文件夹是子节点exe文件是叶子节点。这种层级关系天然适配查找、分类、范围查询等高频场景。比如电商后台查“手机苹果iPhone 15”的商品数据库索引底层就是B树在工作IDE代码补全时匹配方法名用的是Trie树字典树甚至Git的提交历史本质也是多叉树结构。所以这节课不是教你怎么敲几行递归代码而是帮你建立一套处理层次化数据的直觉——什么时候该用树节点设计怎么避免内存浪费遍历顺序如何影响实际性能这些才是面试官真正想听的答案。适合两类人一是刚学完线性结构、准备啃算法硬骨头的Java初学者二是写业务代码多年、突然发现SQL慢得离谱、想搞懂索引原理的中级开发者。别急着抄代码先搞清树的“呼吸节奏”它靠父子引用维系关系靠深度控制查找路径靠平衡性保障最坏情况下的效率。下面我们就从一棵最朴素的二叉搜索树开始一层层剥开它的筋骨。2. 核心设计思路为什么选二叉搜索树作为教学载体2.1 从需求倒推结构选型不是所有树都适合入门很多人一上来就啃AVL树或红黑树结果陷在旋转条件里出不来。我带过的学员里有位某公司后端工程师花两周死磕红黑树插入的五种case最后在生产环境却因为没理解BST的基本性质把用户ID当key建了棵BST结果ID连续分配导致树退化成链表QPS直接掉一半。这说明什么基础模型的理解深度远比高级变体的掌握程度更重要。我们选二叉搜索树BST作为起点核心逻辑就三条左子树所有节点值小于根节点右子树所有节点值大于根节点左右子树本身也是BST。就这么简单但它天然解决了三个刚需第一查找时间复杂度平均O(log n)比链表O(n)快一个数量级第二中序遍历自动输出升序序列省去额外排序第三插入删除能保持结构特性不像数组删除元素要移动后续所有项。对比其他树结构B树适合磁盘IO优化但节点分裂合并逻辑太重Trie树擅长字符串前缀匹配可整数场景用不上线段树专攻区间查询学习成本过高。BST就像学车先练手动挡——离合、油门、档位配合清楚了再上自动挡或赛车才不慌。这里有个关键细节常被忽略BST的“搜索”二字不是白叫的。它的设计哲学是用空间换确定性——每个节点存储完整数据比如User对象而非像哈希表只存索引。这意味着你能做范围查询查年龄18-25的所有用户也能做顺序遍历按注册时间倒序取前10条这是哈希表永远做不到的。所以当你看到“查找节点”这个需求时别只想着return node要想清楚用户到底要精确匹配如查订单号还是要范围扫描如查本月所有订单前者BST够用后者可能得上B树。2.2 节点设计的取舍为什么不用泛型而用Integer示范代码里Node类定义为class Node { Integer data; Node left; Node right; }而不是class NodeT { T data; NodeT left; NodeT right; }新手常质疑“这不就不通用了吗” 实际上这是刻意为之的教学策略。泛型擦除机制会让初学者困惑为什么NodeString和NodeInteger在JVM里是同一个Class为什么不能用T extends ComparableT约束却报错这些底层问题会冲淡对树结构本身的注意力。用Integer示范所有比较操作直观明了if (data root.data)没有类型转换的干扰。等你亲手实现过三次BST的插入分别用Integer、String、自定义Person类自然会意识到泛型的必要性——那时再补implements ComparablePerson和compareTo()方法理解会深刻得多。另外Integer的自动装箱拆箱特性恰好暴露了一个经典陷阱Integer a 128; Integer b 128; System.out.println(a b); // false。如果节点比较用而非.equals()在数据范围超过[-128,127]时就会出bug。我在课堂上故意留这个坑让学员调试半小时他们这辈子都不会再犯这个错。所以这个看似“不专业”的设计实则是用最小认知负荷换取对核心逻辑的最大聚焦。2.3 遍历方式的本质差异递归不是目的是理解层次的脚手架教材常把前序、中序、后序遍历列为三大必背口诀但很少说清它们的物理意义。前序遍历根→左→右像快递员派件先到总部根再分发到华东区左子树最后去华南区右子树中序遍历左→根→右像图书馆理书先把A区书架左排好再放中央展台根最后整理B区右结果自然有序后序遍历左→右→根像工程验收必须等所有子模块左右子树完工才能交付主系统根。这三种顺序决定了你能做什么前序适合复制整棵树先建根再建子树中序是BST的命脉不做它就无法验证BST性质后序是删除节点的唯一安全路径——必须先清理子节点再删父节点否则指针悬空。我见过最典型的错误是在删除节点时用前序遍历找后继结果删掉的是左子树最大值而非右子树最小值。所以遍历不是代码技巧而是数据流的时空契约你在哪个时刻访问节点就决定了能获取哪些上下文信息。后面讲删除逻辑时你会看到这个契约如何变成救命稻草。3. 核心实现详解从创建到删除的完整闭环3.1 创建树为什么insert()必须返回Node引用BST的插入看似简单比根小往左走比根大往右走空位就插。但新手写的void insert(Node root, int data)永远无法真正插入——因为Java参数传递是值传递root new Node(data)只是改变了形参root的指向原调用处的root还是null。正确写法是Node insert(Node root, int data)每次递归都返回新构建的子树根节点。这背后是树结构的不可变性契约任何修改操作插入/删除都会产生新的子树结构旧结构保持不变。就像Git每次commit生成新快照而不是覆盖旧文件。具体实现时递归终止条件有两个root null时新建节点并返回data root.data时直接返回原root避免重复值也可改为更新逻辑。关键细节在于递归调用后的赋值root.left insert(root.left, data);这行代码确保了左子树的根节点被正确挂载。我让学生画过执行栈图当插入序列[5,3,7,2,4,6,8]时第7步插入8调用栈深达4层每层都在等待下层返回新子树根最终逐层回传完成整棵树的拼接。这种“自底向上构建”的思维是理解树操作的基石。顺带提个实战技巧生产环境建树千万别用随机插入最好先排序再用中序遍历构造平衡BST否则退化成链表的概率高达99%。3.2 查找节点递归与迭代的性能真相查找逻辑Node search(Node root, int data)同样要返回Node。递归写法简洁if (root null || root.data data) return root; return data root.data ? search(root.left, data) : search(root.right, data);。但面试官常追问“改成迭代写法有什么优势” 答案直指要害避免栈溢出风险。假设树深度1000递归调用栈就压1000帧而JVM默认栈大小才1MB。迭代版用while循环临时变量while (root ! null root.data ! data) { root data root.data ? root.left : root.right; } return root;。这里有个易错点循环条件必须同时检查root ! null和root.data ! data少一个就会空指针。更深层的区别在于内存访问模式递归依赖CPU缓存预取局部性原理迭代则完全可控。我做过压测在百万级节点的平衡BST上迭代查找比递归快12%因为少了函数调用开销和栈帧管理。但注意这不是说递归不好而是提醒你算法选择要看运行环境。嵌入式设备内存紧张选迭代。业务系统代码可读性优先递归更贴近人类思维。另外查找的扩展性很重要如果需求变成“查找值最接近data的节点”递归版本只需改一行Math.abs(root.data - data)而迭代版要额外维护两个候选变量代码复杂度飙升。3.3 遍历实现中序遍历为何是BST的“心脏起搏器”三种遍历的代码模板高度相似但中序遍历left→root→right对BST有特殊意义。标准实现是void inorder(Node root)用System.out.print输出。但真实业务中你绝不会这样写——因为打印只是副作用核心是要收集或处理节点数据。所以我会教学生写ListInteger inorderList(Node root)用ArrayList收集结果。关键在递归调用顺序先inorderList(root.left)再list.add(root.data)最后inorderList(root.right)。这个顺序保证了list里数据严格升序。验证BST合法性就靠它ListInteger list inorderList(root); for (int i 1; i list.size(); i) { if (list.get(i) list.get(i-1)) return false; }。这里有个性能陷阱生成完整列表再遍历空间复杂度O(n)。优化方案是“中序遍历中的即时校验”用全局变量记录上个访问值每次访问新节点时比较if (data prev) return false; prev data;。这样空间复杂度降到O(h)h为树高。我在某支付系统重构时就用这招把BST校验从200ms优化到3ms。另外中序遍历的非递归版值得深挖用Stack模拟递归栈先一路向左压栈到叶子后弹出访问再转向右子树。这个过程完美复现了“左根右”的时空轨迹是理解JVM栈帧运作的绝佳案例。3.4 删除节点三重境界与“后继节点”的生死抉择删除是BST最烧脑的部分我把它分成三重境界第一重叶子节点直接置null第二重单子节点用子节点替代自己第三重双子节点找到右子树最小值后继或左子树最大值前驱替换再递归删除后继/前驱。难点全在第三重。为什么选右子树最小值因为它是右子树中最接近当前节点值且大于它的数替换后BST性质依然成立。实现时容易犯两个致命错误一是找到后继后直接node.data successor.data却忘了删除后继节点本身导致数据重复二是删除后继时没用node.right delete(node.right, successor.data)而是delete(successor, successor.data)结果删错了子树。正确流程是四步1保存后继值2在右子树中删除后继节点3将后继值赋给当前节点4返回当前节点。这里有个精妙设计删除后继的操作必然落在“第一重或第二重”境界因为后继节点要么是叶子右子树最左节点要么只有右子节点若它有左子节点那左子节点才更小。所以整个删除过程形成递归闭环无需额外判断。我在某电商价格监控系统里用这套逻辑处理动态价格区间树日均删除操作20万次零故障。最后提醒个血泪教训删除后务必重新计算树高或平衡因子否则后续插入可能破坏平衡——虽然基础BST不强制平衡但生产环境建议加个updateHeight()钩子。4. 实战避坑指南那些文档里不会写的“脏活累活”4.1 内存泄漏预警未置null的引用链有多可怕BST删除节点时仅root.left null不够必须同时置空子节点对父节点的反向引用。比如删除节点XX有左子Y若只执行X.left nullY对象仍持有Y.parent X的引用而X又可能被其他地方引用导致Y无法GC。这在长生命周期的树如缓存树中会引发内存泄漏。解决方案是在Node类中增加parent字段并在insert/delete时双向维护插入时child.parent parent;删除时if (child ! null) child.parent null;。我曾在线上环境抓到过类似问题一个用户行为分析树运行三个月后老年代内存持续增长jmap分析发现大量Node对象堆积根源就是删除时没清理parent引用。修复后Full GC频率从每小时1次降到每周1次。所以记住树是双向网络单向切断等于埋雷。4.2 并发安全的朴素解法synchronized不是银弹有学员问“BST能用在多线程环境吗” 直接回答基础BST不是线程安全的。insert()和delete()操作涉及多个节点修改中间状态可能被其他线程读到脏数据。最简单的方案是synchronized(this)但会严重降低吞吐量。更好的做法是“读写分离”读操作search/inorder无锁写操作insert/delete用ReentrantLock。但要注意锁粒度——锁整棵树太粗暴可以按子树分段锁比如对root.left和root.right分别加锁。某金融风控系统就采用此方案将并发写性能提升4倍。不过终极建议是高并发场景别硬刚BST直接上ConcurrentSkipListMap它底层是跳表但API和TreeMap一致迁移成本几乎为零。4.3 性能调优实录从O(n)到O(log n)的临界点在哪BST性能退化是隐形杀手。我用JMH压测过插入10万个随机整数树高约17log₂100000≈16.6查找耗时稳定在0.02ms但插入10万个升序整数树高飙升至100000查找退化成O(n)耗时暴涨到15ms。临界点就在数据分布熵值。解决方案有三1插入前shuffle数据适用于离线建树2改用AVL树每次插入后自动旋转平衡增加20%时间开销换来O(log n)保障3最实用的——用TreeSet/TreeMap它们基于红黑树JDK已做极致优化。某物流路径规划系统曾因BST退化导致实时路径计算超时切换TreeMap后P99延迟从2s降至80ms。所以别迷信“自己造轮子”先确认JDK集合是否满足需求。4.4 调试可视化技巧如何一眼看穿树的“病灶”纯Console打印树结构是灾难。我教学生的三板斧第一写printTree(Node root, String prefix, boolean isLast)方法用├─、└─符号画缩进树像Linux tree命令第二导出DOT格式文本用Graphviz渲染成图片直观查看平衡性第三最狠的——用IDEA的“Evaluate Expression”功能在debug时输入root.toString()配合自定义Node的toString()直接在变量窗看到树形结构。某次调试删除bug我用DOT导出发现后继节点被错误地从左子树查找图上一眼定位10分钟解决。记住可视化不是炫技是把抽象结构映射到空间直觉。5. 常见问题速查表从编译报错到线上事故问题现象根本原因解决方案实操心得NullPointerException在root.data处插入/查找时未判空root null所有递归方法首行加if (root null) return null;养成“防御式编程”习惯宁可多写一行判空不省一次空指针插入后树结构混乱中序遍历不升序比较逻辑错误如用比较Integer对象统一用Integer.compare(a,b)或a.compareTo(b)记住包装类比较用equals()或compareTo()基本类型才用删除双子节点后树出现重复值只替换了data未删除后继节点必须执行root.right delete(root.right, successor.data)后继节点删除是独立操作和当前节点替换无关切勿混淆递归深度超限StackOverflowError树退化成链表递归层数节点数改用迭代实现或插入前shuffle数据生产环境禁用纯递归尤其数据来源不可控时多线程下查找结果偶尔错乱未同步写操作读到中间状态读操作无锁写操作用ReentrantLockBST的线程安全是“写安全”读永远是安全的别过度加锁提示遇到root.left insert(root.left, data)编译报错大概率是insert方法声明为void。立刻检查方法签名确保返回Node类型。注意BST的“搜索”能力依赖于数据可比较性。自定义类必须实现Comparable接口或在构造时传入Comparator否则运行时抛ClassCastException。关键经验在delete()方法末尾永远加上return root;。我见过太多学员删完节点忘记返回导致整棵树丢失——因为上层递归调用接收不到新子树根。6. 场景延伸与技术演进从课堂Demo到工业级落地学完BST别停在“能跑通demo”的层面。真正的价值在于识别现实场景中的树影子。比如用户权限系统角色A继承角色BB继承C这就是一棵多叉树RBAC模型本质是树的遍历再如编译器语法分析词法分析后的token流通过AST抽象语法树组织每个节点代表一个语法结构甚至Excel的单元格公式依赖也是有向无环图DAG可视为树的扩展。技术演进上BST是起点而非终点当数据量上亿磁盘IO成为瓶颈就得学B树——它的节点能存多个key减少树高一次IO读更多数据当需要支持范围查询高并发就上LSM树用内存跳表磁盘SSTable组合当处理地理坐标R树能高效支持“查找附近1km的餐厅”。但所有这些都建立在对BST的透彻理解上。我参与过某地图POI系统的重构最初用HashMap存坐标范围查询要全表扫描响应时间2s换成R树后降到80ms。而R树的每个节点本质上就是个矩形范围的BST。所以别焦虑“学了BST有什么用”想想你每天用的APP微信消息按时间线排列BST中序遍历、淘宝商品按销量排序BST构建、甚至抖音推荐流的热度衰减树状衰减模型——树结构早已无声渗透在数字世界的毛细血管里。最后分享个小技巧下次写业务代码遇到需要“分类-聚合-筛选”的场景先别急着写for循环问问自己“如果用树来组织会不会更优雅” 这个习惯会让你的代码从“能用”走向“好用”。

相关新闻

Fabric 2.5多组织农产品溯源系统:可运行毕设源码包

Fabric 2.5多组织农产品溯源系统:可运行毕设源码包

简介:本资源是一套基于Hyperledger Fabric区块链框架构建的农产品通用溯源系统完整实现,面向计算机类专业本科生、研究生及初入区块链领域的开发者,解决传统农产品流通中信息不透明、数据易篡改、多方协作难等核心问题,适用于毕业…

2026/10/13 13:19:52 阅读更多 →
Java校内二手交易系统毕设全攻略:从Spring Boot到小程序实战

Java校内二手交易系统毕设全攻略:从Spring Boot到小程序实战

1. 项目定位与需求拆解:为什么“校内二手交易”是最稳妥的毕设选题每年到二月底三月初,总有一批计算机专业的大四学生开始焦虑毕业设计选题。Java校内二手交易市场这套系统,我前前后后带人做过不下二十个版本,从最早SSH框架的JSP项…

2026/10/13 13:19:52 阅读更多 →
webgl-fundamentals 教程解读:在 WebGL 中实现图像处理(纹理采样、像素偏移与 3x3 卷积核)

webgl-fundamentals 教程解读:在 WebGL 中实现图像处理(纹理采样、像素偏移与 3x3 卷积核)

图形学 【免费下载链接】webgl-fundamentals WebGL lessons that start with the basics 项目地址: https://gitcode.com/gh_mirrors/we/webgl-fundamentals 点击查看 免费下载 WebGL 中的图像处理远比想象中简单:只要掌握"纹理(textur…

2026/10/13 13:18:51 阅读更多 →

最新新闻

Slicer 在 GNU/Linux 下的 C++ 调试实战:GDB 附加、执行器环境与 Core 转储分析

Slicer 在 GNU/Linux 下的 C++ 调试实战:GDB 附加、执行器环境与 Core 转储分析

医疗健康图像处理数据可视化桌面应用3D渲染 【免费下载链接】Slicer Multi-platform, free open source software for visualization and image computing. 项目地址: https://gitcode.com/gh_mirrors/sl/Slicer 点击查看 免费下载 本文基于 Slicer 官方文档 Docs/…

2026/10/13 13:59:47 阅读更多 →
基于向量数据库与感知哈希的本地视频检索与去重

基于向量数据库与感知哈希的本地视频检索与去重

简介:这是一款基于脚本语言实现的JAV元数据爬取与管理工具,面向需要自动化采集、整理视频站点数据的开发者和自建媒体库用户,核心是解决资源分散、检索不便的问题。项目具备在线播放、全自动爬取下载上传处理能力,视频与图片数据不…

2026/10/13 13:59:47 阅读更多 →
目标跟踪评估指标详解:从中心位置误差到成功率曲线的MATLAB实现

目标跟踪评估指标详解:从中心位置误差到成功率曲线的MATLAB实现

做目标跟踪的同学应该都有过这种感觉:算法自己跑起来看着“挺稳的”,框跟得很顺,偶尔目标晃两下也能拉回来。但一旦放到公开评测集上跑一遍,看到排名表上那些指标,心里落差很大。这不是算法不行,而是你还没…

2026/10/13 13:59:47 阅读更多 →
如何给AI Agent仪表盘上锁:TenacitOS认证、登录限流与安全终端机制源码剖析

如何给AI Agent仪表盘上锁:TenacitOS认证、登录限流与安全终端机制源码剖析

前端AI 应用数据可视化 【免费下载链接】tenacitOS OpenClaw Mission Control Dashboard 项目地址: https://gitcode.com/gh_mirrors/te/tenacitOS 点击查看 免费下载 TenacitOS 是面向 OpenClaw AI Agent 实例的 Mission Control 仪表盘(AI Agent 仪表…

2026/10/13 13:59:47 阅读更多 →
Apache Atlas安装部署全攻略:HBase、Solr、Kafka一次搞定

Apache Atlas安装部署全攻略:HBase、Solr、Kafka一次搞定

看到“Atlas安装详解”这个话题,我猜你多半和我当初一样:翻着官方文档,看着满屏的英文配置项,感觉下载个包、解压、启动就完事了。结果真正动手才意识到,Atlas本身只是一个“协调者”,它背后要拉起来HBase、…

2026/10/13 13:59:47 阅读更多 →
统一多模型API网关litellm实战:从配置到生产部署

统一多模型API网关litellm实战:从配置到生产部署

这段时间我一直在做一个内部的多模型调度平台,见得最多的一个词就是 litellm。它不是一个具体的大模型,也不是某个框架的插件,而是一个把各种模型 API 统一成一套格式的网关层工具。说白了,你只需要学会接一个服务,剩下…

2026/10/13 13:58:46 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →