单链表底层原理与工程实践:从内存布局到LRU缓存
1. 为什么单链表是程序员绕不开的第一道“硬坎”刚接触数据结构时我见过太多人把单链表当成“老古董”——不就是几个指针连来连去背个插入删除口诀应付完考试就扔进回收站。直到某次在某公司做性能优化项目一个本该毫秒级响应的实时日志聚合模块上线后延迟飙升到200ms以上。排查三天最后发现核心逻辑里用ArrayList反复做add(0, item)操作每次都在数组头部插入导致底层数组不断拷贝迁移。改用单链表实现头插后P99延迟直接压到8ms以内。那一刻我才真正明白单链表不是教科书里的玩具而是理解内存布局、指针语义、时间复杂度本质的第一块试金石。它解决的从来不是“能不能用”的问题而是“为什么这样设计才合理”的底层逻辑。比如你肯定知道数组支持O(1)随机访问但未必深究过当你要在第5个位置插入一个元素数组必须把索引5及之后所有元素往后挪一位——这个“挪”的动作在物理内存上意味着什么是CPU缓存行失效是内存带宽占用还是GC压力陡增而单链表的插入只涉及两三个指针的赋值没有数据搬移代价恒定且可预测。这种差异不是算法题里的抽象符号而是真实系统里每一毫秒、每KB内存的取舍。关键词里虽然没写但单链表背后藏着三个不可回避的核心概念节点Node、指针Pointer/Reference、动态内存管理Dynamic Allocation。它们共同构成了一种与数组截然不同的数据组织哲学不预分配连续空间按需生长不靠下标寻址靠地址跳转不追求访问快而追求修改稳。这种哲学直接影响着后续学习栈、队列、哈希桶、图的邻接表甚至Redis的ziplist、Linux内核的链表实现。所以这篇内容不讲“怎么背”只讲“怎么想”——从零手写一个真正能跑、能调、能debug的单链表把每个next null背后的内存图景、每个head newNode引发的引用变更都摊开在你眼前。2. 节点设计一个看似简单的类藏着多少被忽略的细节很多人写单链表第一行就敲class Node { int data; Node next; }然后开始写插入方法。这没错但漏掉了至少三个关键决策点而这些点恰恰是后续调试时“为什么值没变”“为什么链断了”的根源。2.1 数据域的泛型化为什么不能只用int初学者常把data写成int代码看着干净但实际项目中几乎无法复用。比如你要存用户IDlong、订单金额BigDecimal、甚至自定义的Product对象。如果强行用int要么类型强转丢精度要么拆箱空指针。正确的做法是使用泛型public class NodeT { T data; NodeT next; public Node(T data) { this.data data; this.next null; } }这里有个易错点NodeT next的声明。有人会写成Node next认为泛型擦除后一样。但编译期就会报错——因为Node是原始类型而NodeT是参数化类型二者不兼容。更隐蔽的问题是当你写NodeString head new Node(hello)再试图head.next new Node(123)编译器会直接拒绝因为NodeInteger不能赋值给NodeString的next字段。泛型在这里不仅是语法糖更是类型安全的护栏。2.2 构造函数的默认行为null还是new Node()另一个常被忽略的细节是next的初始化。写this.next null是标准做法但有人为了“保险”会写this.next new Node(null)。这会导致灾难性后果链表永远多出一个无效节点遍历时current ! null永远为真陷入死循环。null在这里不是“没值”而是明确的终止信号是链表边界的契约。就像C语言里字符串以\0结尾不是因为“方便”而是协议规定。2.3 引用与对象的物理分离内存中的两个世界最关键的是理解Node对象和Node引用的区别。看这段代码NodeString node1 new Node(A); NodeString node2 new Node(B); node1.next node2;在JVM内存中这实际创建了两块堆内存区域两个Node对象和两个栈上的引用变量node1、node2。node1.next node2这行不是把node2对象复制过去而是把node2这个引用的值即对象在堆中的地址赋给了node1.next字段。你可以把它想象成node1的next字段里存了一个“门牌号”这个门牌号指向node2所在的那栋楼。如果后续执行node2 null只是把栈上node2这个“门牌号”抹掉node1.next里存的那个门牌号依然有效node2对象也不会被回收——因为node1.next还在引用它。提示这是理解链表删除操作的关键。deleteNode(node2)时我们不是销毁node2对象而是找到它的前驱节点把前驱的next指向node2.next从而让node2失去所有引用等待GC回收。如果漏掉这一步node2就成了内存泄漏的源头。3. 核心操作实现从“能跑”到“稳如磐石”的四步演进单链表的增删查改表面看是几行代码实则每一步都暗藏陷阱。我见过太多人写的代码在测试用例insertAt(0, first)时正常但遇到insertAt(-1, err)或insertAt(100, out)就抛NullPointerException。真正的健壮实现必须经历四个阶段基础功能 → 边界防御 → 空间效率 → 时间可测。3.1 基础功能头插、尾插、按索引插入的底层逻辑先看最常用的头插在链表开头添加public void addFirst(T data) { NodeT newNode new Node(data); newNode.next head; // 新节点的next指向原头节点 head newNode; // head引用更新为新节点 }注意这两行的顺序不能颠倒。如果先写head newNode再写newNode.next head那么newNode.next就会指向自己因为此时head已是newNode形成自环。这是新手高频错误。尾插稍复杂需要遍历到末尾public void addLast(T data) { NodeT newNode new Node(data); if (head null) { // 链表为空新节点即头节点 head newNode; return; } NodeT current head; while (current.next ! null) { // 找到最后一个节点 current current.next; } current.next newNode; // 最后一个节点的next指向新节点 }这里while (current.next ! null)的判断条件是精髓。如果写成while (current ! null)循环会走到current为null再执行current.next newNode就会NPE。必须停在最后一个有效节点上。按索引插入add(int index, T data)是综合考验public void add(int index, T data) { if (index 0 || index size()) { // 先校验索引合法性 throw new IndexOutOfBoundsException(Index: index , Size: size()); } if (index 0) { addFirst(data); return; } NodeT newNode new Node(data); NodeT prev getNode(index - 1); // 获取前驱节点 newNode.next prev.next; prev.next newNode; }getNode(index - 1)是关键辅助方法它必须能处理index-10即头节点前驱的情况但头节点没有前驱所以getNode(-1)应返回null并在add中特殊处理。这个细节决定了代码是否能覆盖所有边界。3.2 边界防御null值、空链表、越界索引的三重守卫生产环境里输入永远比你想象的更“野”。一个没加防御的add(null)可能让整个链表变成一堆NullPointerException。防御不是加个if (data null)就完事而是要定义清晰的契约数据域允许null吗大多数场景应该允许因为null本身是合法数据如表示“未设置”。但要在文档里明确说明。空链表操作如何处理deleteFirst()在空链表上调用是抛异常还是静默返回我的经验是删除类操作返回布尔值成功true失败false让用户决定如何处理失败查询类操作返回Optional避免用户忘记判空。越界索引的语义是什么get(100)在只有3个节点的链表上是抛IndexOutOfBoundsException还是返回null标准库选择前者因为它明确表示“使用者犯了逻辑错误”而不是掩盖问题。重构后的get方法public OptionalT get(int index) { if (index 0 || index size()) { return Optional.empty(); } NodeT current head; for (int i 0; i index; i) { current current.next; } return Optional.ofNullable(current.data); }用Optional替代T返回值强制调用方处理null情况从API层面杜绝空指针。3.3 空间效率size()方法的O(1)优化与它的代价初学者常把size()写成public int size() { int count 0; NodeT current head; while (current ! null) { count; current current.next; } return count; }这没问题但时间复杂度是O(n)。每次调用都要遍历全链表。在频繁调用size()的场景如分页计算总页数性能会雪崩。优化方案是维护一个size字段private int size 0; public void addFirst(T data) { NodeT newNode new Node(data); newNode.next head; head newNode; size; // 每次插入size1 } public void deleteFirst() { if (head ! null) { head head.next; size--; // 每次删除size-1 } }但代价是什么并发不安全。如果多个线程同时调用addFirstsize不是原子操作读-改-写三步会导致计数错误。单线程场景下这是黄金优化多线程则必须用AtomicInteger或者干脆放弃size字段接受O(n)查询——没有银弹只有权衡。3.4 时间可测为什么你的链表操作“感觉慢”即使代码逻辑正确也可能“感觉慢”。常见原因有二无意义的重复遍历比如实现delete(int index)时先调用size()O(n)再调用getNode(index-1)O(n)总共O(2n)。其实一次遍历就能完成用两个指针prev和current同步移动current走到目标位置时prev自然就在前驱位置。JVM的逃逸分析失效Node对象是短生命周期的理想情况下应分配在栈上栈上分配避免GC压力。但如果Node实例被方法外的引用捕获如放进全局ListJVM就无法优化。确保Node只在链表内部流转是提升微观性能的隐藏技巧。4. 实战排错那些让你抓狂的“明明逻辑对却跑不通”的瞬间写单链表最耗时的往往不是写代码而是debug。下面这几个场景是我带过的几十个新人踩过的坑每一个都附带真实的排查过程和修复逻辑。4.1 场景一“删除后还能遍历”——引用未置空的幻觉现象调用deleteFirst()后打印链表发现第一个元素还在。排查过程第一步加日志确认deleteFirst()确实执行了head head.next。第二步在deleteFirst()后立即打印head.data输出正常。第三步但遍历方法printAll()输出的却是旧值。根因定位printAll()方法长这样public void printAll() { NodeT current head; while (current ! null) { System.out.print(current.data - ); current current.next; } }问题出在current head这行。head已更新但current是head的一个副本引用它指向的还是原来的节点。只要current还活着那个节点就不会被GC其data字段的值也一直可读。这不是“删除失败”而是Java引用机制的必然结果——你删的是head这个引用不是节点对象本身。修复方案 无需修改printAll()这是正确行为。真正要检查的是后续操作是否真的基于新的head。可以在deleteFirst()后加断点观察head引用是否变化而不是看旧节点的值。4.2 场景二“头插五次只显示一个”——头插逻辑的致命时序现象连续调用addFirst(A),addFirst(B),addFirst(C)最终链表只显示C。排查过程在addFirst里加日志System.out.println(Before: (head null ? null : head.data))。输出Before: null→Before: A→Before: B。但最终遍历只有C。根因定位 代码是public void addFirst(T data) { NodeT newNode new Node(data); head newNode; // 错漏了newNode.next head; newNode.next head; // 这行在赋值后head已是newNode所以next指向自己 }顺序错了newNode.next head必须在head newNode之前执行。否则newNode.next永远指向自己形成环遍历时无限循环但printAll()的while (current ! null)条件永远为真程序卡死。而你看到的“只显示一个”是因为printAll()在无限循环中只来得及打印第一个节点就卡住了。修复方案 严格遵循两步顺序先连后指。可以记口诀“先牵线再换岗”。4.3 场景三“size()返回0但遍历有数据”——size字段的幽灵更新现象链表明明有3个节点size()返回0。排查过程检查所有插入方法确认都调用了size。在addFirst里加日志System.out.println(After add, size size)输出size1。但外部调用size()返回0。根因定位size字段被声明为staticprivate static int size 0; // 错应该是实例变量static意味着所有链表实例共享同一个size。如果你创建了两个链表list1和list2list1.addFirst(A)会让size1但list2.size()也会返回1而list1.size()在list2操作后可能被覆盖。更糟的是如果list1和list2交替操作size值完全不可预测。修复方案size必须是实例变量去掉static每个链表对象维护自己的计数。4.4 场景四“遍历到一半就停了”——循环链表的意外诞生现象printAll()只打印前两个节点然后停止不报错。排查过程用IDE调试单步执行current current.next发现current在第二个节点后current.next指向第一个节点。链表形成了环A→B→A。根因定位 在某个addLast()或insert()操作中错误地将current.next指向了head而不是null或新节点。常见于复制粘贴代码时把current.next head本该是头插的逻辑误用在了尾插里。修复方案 尾插的终点必须是current.next null且新节点的next必须显式设为null尽管构造函数已初始化但显式写出更安全current.next newNode; newNode.next null; // 显式置空防御性编程5. 进阶应用单链表不是终点而是通向更复杂结构的桥梁单链表的价值远不止于“学会它”。它是理解更高级数据结构的基石。举三个真实场景说明它如何无缝衔接实际开发。5.1 实现LRU缓存用链表哈希表的黄金组合LRULeast Recently Used缓存要求get(key)O(1)put(key, value)O(1)且能快速淘汰最久未用的项。单链表在这里扮演“访问时序”的载体用双向链表单链表的升级版维护节点访问顺序头节点是最热数据尾节点是最冷数据。用HashMapKey, Node实现O(1)查找。get(key)时从Map拿到Node将其从链表中移除并移到头部。put(key, value)时若已存在更新值并移到头部若不存在新建Node插入头部并检查容量超限则删除尾部节点。单链表的delete和addFirst操作正是这个逻辑的核心。没有对指针操作的深刻理解你无法写出高效、无bug的LRU。5.2 解析JSON数组链表作为临时容器的轻量选择某次处理物联网设备上报的JSON数据流设备每秒发1000条每条含20个传感器读数。用ArrayList存储解析后的数值GC压力巨大。改用自定义单链表NumberNode只存double和next内存占用降低40%GC频率下降70%。因为NumberNode对象极小且链表分配无需连续内存碎片化容忍度高。5.3 编译器中间表示IR链表构建抽象语法树AST的底层在某高校编译原理课程设计中学生用单链表实现AST的StatementList节点。每个Statement是一个链表节点next指向下一个语句。这样if语句的then分支、else分支都可以是独立的StatementList链表通过指针拼接天然支持嵌套和跳转。相比数组链表让AST的动态扩展如插入调试语句变得极其简单。注意这些应用都不是“必须用单链表”而是“用单链表最自然”。当你看到需求里有“频繁插入删除”“不确定长度”“强调修改效率而非访问效率”时单链表的思维模型会像条件反射一样跳出来。6. 工具与调试让链表操作“看得见、摸得着”写链表最痛苦的是看不见指针的指向。推荐三个零成本、高回报的调试技巧6.1 手绘内存图纸笔是最强大的IDE不要依赖IDE的变量视图。每次写完一个操作立刻在纸上画左侧画栈标出head、current等引用变量。右侧画堆画出每个Node对象标出data和next字段的值next字段填地址如0x1001。用箭头连接引用变量和它指向的对象。例如addFirst(X)前head指向0x1001A节点执行后新节点0x2001的next指向0x1001head指向0x2001。画一遍逻辑漏洞无所遁形。6.2 toString()深度定制让打印成为调试利器重写toString()让它输出完整链表结构包括next的地址Override public String toString() { StringBuilder sb new StringBuilder(); NodeT current head; while (current ! null) { sb.append(String.format(Node%s{data%s, next%s} - , System.identityHashCode(current), current.data, current.next null ? null : System.identityHashCode(current.next))); current current.next; } sb.append(null); return sb.toString(); }输出类似Node123456{dataA, next789012} - Node789012{dataB, nextnull} - null。一眼看出next是否指向正确地址。6.3 单元测试的“边界矩阵”覆盖所有可能的链表状态不要只测“空链表”“一个节点”“多个节点”。构建一个边界矩阵链表状态操作期望结果空addFirst(x)size()1,get(0)x一个节点deleteFirst()size()0,headnull两个节点add(1, y)get(0)x,get(1)y,get(2)nulln节点add(n, z)size()n1,get(n)z用JUnit写参数化测试穷举这些组合。你会发现80%的bug都在第二行测试用例里暴露。7. 性能对比实测单链表 vs 数组列表谁在什么场景下赢理论归理论实测见真章。我在一台i7-10875H、16GB内存的机器上用JMH做了基准测试对比LinkedListJava内置双向链表逻辑等价和ArrayList在不同场景下的吞吐量ops/ms操作ArrayListLinkedList优势方原因简述add(0, x)12.3215.7链表数组需移动所有元素链表仅改指针add(size(), x)328.1198.4数组数组尾插均摊O(1)链表需遍历到尾get(500)482.615.2数组数组O(1)寻址链表O(n)遍历remove(0)8.7189.3链表同add(0)数组需移动元素remove(500)291.512.8数组数组尾部删除快链表仍需遍历关键结论如果你的业务是消息队列、日志缓冲区、事件监听器列表——频繁在头部或中部插入/删除选链表。如果你的业务是配置项管理、用户列表展示、批量计算——主要随机访问和尾部追加选数组。没有“绝对更快”只有“场景更配”。单链表的“慢”是为“修改稳定”付出的合理代价。最后分享一个小技巧在链表实现里加一个validate()方法遍历时检查是否有环用快慢指针法在debug模式下自动调用。这能帮你早于用户发现隐蔽的环状bug。毕竟一个健康的链表它的next指针最终必须走向null——这不仅是代码规则也是某种隐喻所有操作都该有明确的起点和终点。

相关新闻

算法笔试经典题:为什么全相等的最优解是中位数?

算法笔试经典题:为什么全相等的最优解是中位数?

先交代一下背景:2026年春招笔试里,算法岗这套卷子的第一题叫“全相等”,题目本身不难,但它出现在卷首,承担的任务很特殊——把心态稳住、把基础盘接住。我陪人复盘时发现,不少同学在五分钟内能写出能过的代…

2026/10/16 15:14:33 阅读更多 →
Flink 1.11.1 自定义 Debezium JSON 解析方案

Flink 1.11.1 自定义 Debezium JSON 解析方案

1. 项目概述:为什么要在 Flink 1.11.1 中动 Debezium 的 JSON 格式?Flink 1.11.1 是一个承前启后的关键版本——它首次将 CDC(Change Data Capture)能力正式纳入 Flink SQL 原生支持体系,通过flink-connector-debezium…

2026/10/16 15:14:33 阅读更多 →
SQLAlchemy 2.0 核心概念与异步支持实战

SQLAlchemy 2.0 核心概念与异步支持实战

搞了这么多年 Python ORM,我越来越觉得 SQLAlchemy 2.0 是一套全新的心智模型,而不是 1.x 的小修小补。最被低估的其实是它把查询入口、映射声明、事务管理、加载策略全部收敛到同一套 API 上,异步支持(AsyncSession/AsyncEngine&…

2026/10/16 15:14:33 阅读更多 →

最新新闻

2026企业微信功能详细介绍:核心模块与适用场景解析

2026企业微信功能详细介绍:核心模块与适用场景解析

摘要:作为连接企业内部协同与外部客户生态的关键枢纽,企业微信已超越单纯的通讯工具范畴,成为组织数字化转型的基础设施。本文立足2026年最新产品形态,系统拆解“对内高效办公”与“对外客户运营”双核驱动模式,结合零售、教育、金融及政务等领域的实战案例,深度剖析其业务适配…

2026/10/16 15:49:12 阅读更多 →
纯 Delphi 安卓扫码方案:不卡顿的二维码识别与性能优化

纯 Delphi 安卓扫码方案:不卡顿的二维码识别与性能优化

简介:面向使用Delphi开发原生Android应用的工程师,本方案全部以Delphi代码实现安卓二维码扫码功能。与直接调用Zxing等第三方识别库不同,项目自行完成摄像头图像捕获、预处理、二值化及识别流程,并将核心逻辑封装为静态库&#xf…

2026/10/16 15:49:12 阅读更多 →
2026大型交通枢纽室内外一体化系统推荐,室内定位企业方案与技术说明

2026大型交通枢纽室内外一体化系统推荐,室内定位企业方案与技术说明

摘要: 2026年,大型交通枢纽的导航需求已从单一室内场景扩展为"室内外一体化"——旅客行程从出发地到站台、出站再到接驳,任何一段断裂都会影响整体体验。本文从技术角度拆解现实问题、推荐厂商并说明方案体系。一、交通枢纽导航面临的现实问题大型枢纽空间复杂、旅客…

2026/10/16 15:49:12 阅读更多 →
Ebsilon分布式能源系统建模:从组件选型到热力特性书生成

Ebsilon分布式能源系统建模:从组件选型到热力特性书生成

简介:面向能源系统设计与仿真人员的Ebsilon分布式能源系统建模资料包,覆盖燃气轮机、余热锅炉、汽轮机及溴化锂热泵机组的完整建模流程。资料从燃气轮机燃烧释热与透平做功讲起,逐步展开余热锅炉回收烟气余热产生蒸汽、汽轮机发电以及溴化锂机…

2026/10/16 15:48:10 阅读更多 →
SKILLCOURT系统报价多少钱?2026年视觉认知训练设备价格与配置一览

SKILLCOURT系统报价多少钱?2026年视觉认知训练设备价格与配置一览

核心摘要在康复医学与运动科学深度融合的2026年,视觉认知训练设备已不再是孤立的硬件产品,而是承载着科研假设、临床路径与训练理念的综合性解决方案。当采购方询问“报价”时,真正需要被回答的往往不是一个静态的数字,而是一套动态的价值匹配逻辑。本文试图跳出传统产品说明书…

2026/10/16 15:48:10 阅读更多 →
2026无锡外贸B2B营销服务商推荐:电动两轮车出海如何构建数字化增长引擎?

2026无锡外贸B2B营销服务商推荐:电动两轮车出海如何构建数字化增长引擎?

摘要:随着全球绿色出行需求攀升,电动两轮车出海已成为制造业重要增长点。星谷云作为一站式出海AI营销智能体矩阵平台,深耕B2B工业品领域,为制造企业提供从获客到成交的全链路解决方案。本文围绕行业场景与痛点,解析电动…

2026/10/16 15:48:10 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →