C++递归精解:汉诺塔问题从原理到实战,掌握算法核心思想
1. 项目概述从经典问题到编程实战汉诺塔一个听起来有点神秘的名字对于很多初学编程的朋友来说它就像一道绕不过去的坎。我第一次接触它是在大学的数据结构课上看着老师用递归在黑板上画着一个个移动步骤当时只觉得“哦懂了”但等到自己动手写代码时才发现脑子里一团乱麻。后来在无数次面试和实际项目中我发现汉诺塔问题远不止是一个简单的算法题它是理解递归思想、函数调用栈、乃至计算机解决问题思维方式的一把绝佳钥匙。今天我们就用 C 这把“手术刀”来彻底解剖汉诺塔递归函数从最底层的原理到一行行可运行的代码再到那些教科书上不会告诉你的调试技巧和性能思考手把手带你把这个经典问题吃透、玩转。简单来说汉诺塔问题描述的是有三根柱子我们通常称为 A、B、C其中一根柱子A上从下往上按照大小顺序摞着 N 个圆盘。目标是把所有圆盘从 A 柱移动到 C 柱并且在移动过程中任何时候、任何一根柱子上都不能出现大盘子压在小盘子上面的情况。每次只能移动一个盘子。这个问题用递归来解决代码会异常简洁但其背后蕴含的思维过程却值得反复咀嚼。无论你是正在啃《C Primer》的新手还是想巩固递归概念的进阶者亦或是准备技术面试的求职者这篇解析都将为你提供一条清晰的路径让你不仅写出代码更能理解每一行代码背后的“灵魂”。2. 核心思路拆解递归思想的降维打击面对汉诺塔问题最直接的暴力枚举思路很快就会因为盘子数量 N 的增大而变得不可能移动步数是 2^N - 1N64 时就是个天文数字。递归为我们提供了一种“分而治之”的降维打击策略。其核心思想可以概括为不要一开始就想着怎么移动 N 个盘子而是思考如何把移动 N 个盘子的问题转化为移动 N-1 个盘子的问题。2.1 递归分解三步走战略假设我们要将 N 个盘子从 A 柱起点借助 B 柱辅助移动到 C 柱目标。递归解法将其分解为三个清晰的步骤第一步移开“大山”。将 A 柱上面的 N-1 个盘子看作一个整体借助 C 柱作为辅助移动到 B 柱。此时A 柱上只剩下最大的那个第 N 号盘子。第二步移动“基石”。将 A 柱上剩下的那个最大的盘子第 N 号直接移动到 C 柱。这一步是直接的、一次性的操作。第三步合拢“小山”。现在B 柱上有 N-1 个盘子A 柱是空的C 柱上有一个最大的盘子。我们的目标变成了将 B 柱上的这 N-1 个盘子借助 A 柱此时它是空的可以作为辅助柱移动到 C 柱上。看到这里递归的魔力就显现了。第一步和第三步本质上都是“将 M 个盘子从一个柱子移动到另一个柱子借助第三个柱子”的原问题只不过盘子数量 M 变成了 N-1起点、目标和辅助柱的角色发生了轮换。这就构成了递归调用。注意很多初学者在这里会困惑于“辅助柱”概念的动态变化。请记住在每一次递归函数调用中“起点”、“目标”、“辅助”这三个角色是根据本次调用的任务来临时定义的而不是固定属于 A、B、C 某根柱子。这是理解递归函数参数含义的关键。2.2 递归基终止条件的确定任何递归函数都必须有一个明确的终止条件否则将无限调用下去导致栈溢出。汉诺塔的递归基非常简单当只需要移动 1 个盘子时。这时我们不需要再分解了直接将它从起点柱移动到目标柱即可。在代码中这通常对应着if (n 1)的判断。这个思路看似简单但却是整个递归大厦的基石。它保证了无论最初 N 有多大递归最终都会一层层“剥洋葱”似的回到移动 1 个盘子的最简单情况然后逐层返回组合成完整的移动序列。3. 代码实现与逐行精讲理论清晰后我们来看 C 的实现。代码非常简短但每一行都值得深究。#include iostream using namespace std; // 递归函数声明将 n 个盘子从 source 移动到 target借助 auxiliary void hanoi(int n, char source, char target, char auxiliary) { // 递归基如果只有一个盘子直接移动 if (n 1) { cout Move disk 1 from source to target endl; return; // 本次函数调用结束返回上一层 } // 步骤1将上面 n-1 个盘子从 source 移动到 auxiliary借助 target hanoi(n - 1, source, auxiliary, target); // 步骤2将最大的第 n 号盘子从 source 移动到 target cout Move disk n from source to target endl; // 步骤3将 auxiliary 上的 n-1 个盘子移动到 target借助 source hanoi(n - 1, auxiliary, target, source); } int main() { int numDisks; cout Enter the number of disks: ; cin numDisks; // 调用递归函数初始将 numDisks 个盘子从 A 移到 C借助 B hanoi(numDisks, A, C, B); // 计算并输出总步数 long long totalMoves (1LL numDisks) - 1; // 使用左移和长整型避免溢出 cout \nTotal moves required: totalMoves endl; return 0; }3.1 函数签名与参数设计void hanoi(int n, char source, char target, char auxiliary)这是递归函数的核心。参数设计体现了抽象思维int n当前需要移动的盘子数量。它是递归深度的度量。char source当前这批盘子的起点柱子。char target当前这批盘子的目标柱子。char auxiliary当前可用的辅助柱子。这里的关键是理解source,target,auxiliary是形参它们的角色在每次递归调用中都会根据实际任务而改变与主函数中传入的 ‘A‘, ’B‘, ’C‘ 没有永恒的绑定关系。3.2 递归调用与栈帧变化我们以n3为例拆解一下调用过程这是理解递归运行机制的最佳方式。主函数调用hanoi(3, A, C, B)。含义把3个盘子从A移到C借助B。进入函数n3不满足n1执行步骤1的递归调用hanoi(2, A, B, C)。注意参数位置此时sourceA,targetB,auxiliaryC。这意味着我们进入了一个新的子问题“把2个盘子从A移到B借助C”。同时主调函数n3的那一层的执行被“挂起”它的现场变量值、执行位置被压入调用栈。在hanoi(2, A, B, C)中n2继续分解。步骤1hanoi(1, A, C, B)。又一个子问题“把1个盘子从A移到C借助B”。n2这一层也被挂起压栈。在hanoi(1, A, C, B)中n1满足执行递归基打印Move disk 1 from A to C。然后return返回到调用它的地方即n2那一层。n2那一层从步骤1调用返回继续执行步骤2打印Move disk 2 from A to B。接着执行步骤3hanoi(1, C, B, A)。注意参数此时sourceC上一步移动后1号盘在CtargetBauxiliaryA。这又是一个移动1个盘子的调用打印Move disk 1 from C to B。然后返回。n2的任务完成返回到调用它的地方即最初的n3那一层。n3那一层从步骤1调用返回执行自己的步骤2打印Move disk 3 from A to C。接着执行步骤3hanoi(2, B, C, A)。这又是一个“移动2个盘子”的子问题其内部会再次递归分解为两次移动1个盘子的操作。整个过程会重复类似步骤2-7的逻辑。通过这个过程你可以清晰地看到“栈”这种数据结构是如何支撑递归的每一次函数调用都会产生一个栈帧保存当前状态遇到递归调用就压入新栈帧遇到return或函数结束就弹出栈帧回到上一层继续执行。这种“后进先出”的特性完美匹配了递归“深入最底层然后逐层返回”的执行流程。实操心得在 IDE如 VS Code中调试递归函数时一定要善用调用栈Call Stack窗口。你可以清晰地看到当前执行到了哪一层递归每一层的参数n,source,target,auxiliary分别是什么值。这是可视化理解递归过程最强大的工具没有之一。4. 从理论到实战可视化、步数与性能分析理解了基本代码后我们可以做一些更有趣的扩展让这个程序不仅仅是输出文本。4.1 输出优化与步骤编号基础的输出只说明了移动哪个盘子。我们可以增加一个全局计数器为每一步移动编号让输出更清晰。#include iostream using namespace std; int stepCounter 0; // 全局步数计数器 void hanoi(int n, char source, char target, char auxiliary) { if (n 1) { stepCounter; cout Step stepCounter : Move disk 1 from source to target endl; return; } hanoi(n - 1, source, auxiliary, target); stepCounter; cout Step stepCounter : Move disk n from source to target endl; hanoi(n - 1, auxiliary, target, source); } int main() { int numDisks 3; hanoi(numDisks, A, C, B); cout Total steps: stepCounter (Expected: ( (1 numDisks) - 1 ) ) endl; return 0; }4.2 总步数公式与溢出风险汉诺塔移动 N 个盘子所需的最少步数是2^N - 1。这是一个指数级增长。在代码中计算总步数时必须警惕整数溢出问题。int main() { int numDisks; cout Enter number of disks: ; cin numDisks; // 危险当 numDisks 31 时对32位int会溢出。 // int totalMoves (1 numDisks) - 1; // 安全做法使用更大范围的类型并采用幂运算。 long long totalMoves (1LL numDisks) - 1; // 使用LL后缀确保为long long类型 // 或者使用 pow 函数但要注意返回的是浮点数需转换 // long long totalMoves (long long)pow(2, numDisks) - 1; cout Theoretical minimum moves: totalMoves endl; // ... 调用 hanoi 函数 }注意事项1 n是左移运算在 C 中相当于计算2^n。但1默认是int类型当n较大时如n312^31超过了int的最大正值约21亿会发生溢出结果是未定义的。使用1LL n可以确保以long long类型进行计算能安全计算到n632^63-1。n64时long long也会溢出。这是算法问题本身的性质决定的程序需要处理这种边界情况比如提示用户输入过大的 N 可能不现实。4.3 非递归栈模拟实现探秘虽然递归实现优雅但理解其等价的非递归实现能让你对问题有更深的认识。汉诺塔的非递归算法通常显式地使用一个栈来模拟递归过程其规则基于一个有趣的数学事实对于 N 个盘子最小的移动序列中奇数步总是移动最小的盘子且移动方向是固定的当 N 为奇数时最小盘按 A-C-B-A 循环N 为偶数时按 A-B-C-A 循环。#include iostream #include stack #include vector using namespace std; // 非递归实现使用栈模拟递归过程 void hanoiIterative(int n, char source, char target, char auxiliary) { // 创建一个自定义结构体来模拟递归调用帧 struct Frame { int n; char src, dst, aux; bool stage; // false 表示还未处理“移动前n-1个”的阶段true 表示已处理待处理“移动后n-1个” Frame(int _n, char s, char d, char a) : n(_n), src(s), dst(d), aux(a), stage(false) {} }; stackFrame stk; stk.push(Frame(n, source, target, auxiliary)); // 初始帧 int step 0; while (!stk.empty()) { Frame f stk.top(); if (f.n 1) { // 递归基 step; cout Step step : Move disk 1 from f.src to f.dst endl; stk.pop(); // 这个帧任务完成 } else { if (!f.stage) { // 对应递归函数中“步骤1”之前的状态 // 需要先处理移动前 n-1 个盘子 f.stage true; // 标记为已进入下一阶段 // 将“移动 n-1 个盘子从 src 到 aux”作为新任务压栈 stk.push(Frame(f.n - 1, f.src, f.aux, f.dst)); } else { // 对应递归函数中执行完“步骤1”准备执行“步骤2”和“步骤3” // 先执行“步骤2”移动第 n 号盘子 step; cout Step step : Move disk f.n from f.src to f.dst endl; // 然后安排“步骤3”移动 n-1 个盘子从 aux 到 dst // 当前帧任务完成弹出 stk.pop(); // 将“步骤3”作为新任务压栈 stk.push(Frame(f.n - 1, f.aux, f.dst, f.src)); } } } cout Total steps (iterative): step endl; }这个非递归版本完全模拟了递归函数的调用栈它帮助我们理解递归本质上是一种自动的栈管理。在内存紧张或递归深度可能很大的场景下虽然汉诺塔本身递归深度就是NN大了步数更多得不可行非递归实现有时是必要的。不过对于汉诺塔教学而言递归版本的无与伦比的清晰性使其仍是首选。5. 常见问题、调试技巧与深度思考在实际编写和教学过程中我遇到了许多共性问题。这里总结一下希望能帮你避开这些坑。5.1 递归函数不终止或逻辑错误问题表现程序陷入无限循环或者移动步骤违反规则大盘子压小盘子。排查思路首要检查递归基确保if (n 1)这个条件判断正确并且里面有return语句。我曾见过有人写成if (n 1)赋值或者忘了return导致无限递归。检查递归调用参数这是最容易出错的地方。对照“三步走”战略仔细核对每一次hanoi调用时source,target,auxiliary三个参数的位置是否正确。第一步hanoi(n-1, source, auxiliary, target)。目标是移走上面n-1个所以target是auxiliary。第三步hanoi(n-1, auxiliary, target, source)。目标是把n-1个从B移到C所以source是auxiliarytarget是targetauxiliary是原来的source。使用小数据测试永远从n1,n2,n3开始测试。手动推导出正确的移动序列与程序输出对比。n3时正确移动序列是7步这是一个非常好的测试用例。5.2 理解递归的“对称性”与“自相似性”汉诺塔递归代码呈现出完美的对称性函数体内两次递归调用hanoi(n-1, ...)像一对翅膀包裹着中间那个移动第n号盘子的操作。这种结构反映了问题的自相似性解决N个盘子的问题依赖于先解决两个N-1个盘子的问题虽然起点和目标不同。你可以把递归函数想象成一个负责解决“移动N个盘子从X到Y”的万能工人。当他接到任务时他发现自己一个人搬不动N个于是他找来一个和自己一模一样的工人递归调用让他把上面N-1个盘子搬到“临时仓库”辅助柱。等那个工人干完他自己动手把最底下那个最大的盘子第N号搬到最终目的地。最后他再叫来另一个万能工人又一次递归调用让他把“临时仓库”里的N-1个盘子搬到最终目的地压在那个最大的盘子上。每一个工人都遵循同样的工作手册函数体只是拿到的任务单参数不同。这种“自我复制”来解决问题的模式就是递归的精髓。5.3 性能与可扩展性讨论虽然递归解法在代码简洁性和思维清晰度上满分但我们也要清醒认识其局限性时间复杂度O(2^N)。这是问题本身的下限任何算法都无法更好。所以汉诺塔问题是一个典型的“指数时间”问题N稍大如30以上步数就超过10亿在现实中无法完成传说中64层金盘的世界末日问题。空间复杂度递归深度为N所以栈空间复杂度是O(N)。对于现代计算机只要N不是特别大比如几千这通常不是问题。但如果你显式地用栈模拟非递归栈的空间使用也是O(N)。“可视化”更大N的运行对于N大于10的情况打印每一步是不现实的。我们可以修改程序只计数而不打印或者每移动100万步打印一个进度点来感受指数增长的恐怖。void hanoiCountOnly(int n, char s, char t, char a, long long count) { if (n 1) { count; // 可以在这里添加 if (count % 1000000 0) 来打印进度 return; } hanoiCountOnly(n-1, s, a, t, count); count; // 移动第n号盘子 hanoiCountOnly(n-1, a, t, s, count); }5.4 教学与面试中的应用在面试中汉诺塔常被用来考察候选人对递归的理解。面试官可能不会满足于你写出代码而会追问“如果不允许使用递归你怎么实现”考察栈的应用和对递归本质的理解“移动N个盘子的最少步数是多少为什么”考察数学归纳法和对递归公式的理解“递归解法的空间复杂度是多少调用栈最深有多少层”考察对递归执行模型的理解“如何优化这个程序对于打印步骤而言”可能引导到非递归实现或者讨论尾递归——虽然C编译器一般不做尾递归优化但可以讨论概念在教学中汉诺塔是一个绝佳的教具。我通常会让学生先手动模拟N2,3的情况画出递归调用树然后在调试器中单步跟踪观察调用栈和参数的变化。这个过程能极大地加深对“函数调用”、“栈帧”、“参数传递”这些核心概念的理解。最后我个人最深的体会是汉诺塔递归之美在于它用极简的代码映射了一个复杂的分解过程。它像一面镜子照出了我们解决复杂问题时应有的思维模式不要试图一口吞下整个问题而是定义好清晰的子问题移动N-1个盘子找到那个最简不可分的基本操作移动1个盘子然后相信递归的力量让问题像多米诺骨牌一样自动层层解决。当你真正内化了这种思维你会发现它不仅能用来解算法题更能应用到软件设计、系统架构乃至处理生活事务中。这就是一个经典问题带给我们的超越代码本身的财富。

相关新闻

软件体系结构设计:从分层到微服务的实践指南

软件体系结构设计:从分层到微服务的实践指南

1. 软件体系结构的基本概念软件体系结构(Software Architecture)是软件系统的高层结构设计,它定义了系统各组件之间的关系、交互方式以及整体组织原则。就像建筑师在设计房屋时需要先绘制蓝图一样,软件体系结构就是软件系统的&quo…

2026/10/7 11:27:11 阅读更多 →
Strarling分布式系统设计:CAP定理实践与架构解析

Strarling分布式系统设计:CAP定理实践与架构解析

1. 从Strarling看分布式系统的设计哲学第一次接触Strarling这个项目时,我正被公司自研的分布式存储系统折磨得焦头烂额。那是个典型的"大泥球"架构——各种临时方案像补丁一样层层叠加,性能监控数据像过山车般忽高忽低。直到某天深夜&#xff…

2026/10/18 1:14:08 阅读更多 →
嵌入式网络核心:EMAC与MDIO原理、配置与调试实战指南

嵌入式网络核心:EMAC与MDIO原理、配置与调试实战指南

1. 项目概述:EMAC与MDIO在嵌入式网络中的核心角色 在嵌入式系统开发中,实现稳定可靠的以太网通信是许多项目的基石。无论是工业控制、智能家居网关还是车载信息娱乐系统,网络功能都离不开两个核心硬件模块:以太网媒体访问控制器&a…

2026/10/18 3:20:18 阅读更多 →

最新新闻

857个AI模型仅3.6%公开安全测试:AI安全测试的行业真相与落地实践

857个AI模型仅3.6%公开安全测试:AI安全测试的行业真相与落地实践

1. 一组数字背后的行业真相857个模型,只有3.6%公开了安全测试结果。这个比例我第一次看到的时候,盯着屏幕愣了好几秒。3.6%是什么概念?大概就是你在一个坐满100个人的会议室里扫一圈,只有不到4个人手里拿着体检报告,剩…

2026/10/19 17:41:48 阅读更多 →
逆序对计数:归并排序的O(n log n)解法与Go实现

逆序对计数:归并排序的O(n log n)解法与Go实现

1. 逆序对的定义:为什么这一题能拦住很多人 我第一次认真接触“逆序对”是在处理一批排序统计相关的小工具时。当时我需要量化一组序列到底“有多乱”,排序本身只能给出结果顺序,却丢失了原始乱序程度。后来才意识到,逆序对数量正…

2026/10/19 17:41:48 阅读更多 →
TensorFlow分布式FFT:大规模频谱分析的实战指南

TensorFlow分布式FFT:大规模频谱分析的实战指南

先聊一个有点反常识的现象:FFT(快速傅里叶变换)本身是个再经典不过的数学工具,单条 16384 点的信号,就算是纯 Python 调库也快得几乎感觉不到延迟,那为什么还会有人执着地把“TensorFlow 中的分布式快速傅里…

2026/10/19 17:41:48 阅读更多 →
Qwen-Image-2512本地部署实战:消费级显卡跑通AI绘图全流程

Qwen-Image-2512本地部署实战:消费级显卡跑通AI绘图全流程

讲真,本地跑AI绘图这件事,我前前后后折腾过不少模型,显卡从16G换到24G,环境装了一遍又一遍,踩的坑能写一箩筐。这次拿到Qwen-Image-2512的权重,本来没抱太大期待,结果跑通第一张图的时候确实愣住…

2026/10/19 17:41:48 阅读更多 →
开源CLI代码审查工具:Git集成+本地LLM驱动的实时代码质量门禁

开源CLI代码审查工具:Git集成+本地LLM驱动的实时代码质量门禁

1. 项目概述:一个真正能落地的开源代码审查 CLI 工具“open-code-review”这个名字乍一听像某个 GitHub 上刚起步的玩具项目,但如果你最近在团队里被 PR 堆得喘不过气、被“已阅”式评论糊弄过、或者自己写完代码不敢合入、反复删改 commit message——那…

2026/10/19 17:41:48 阅读更多 →
JSP+Servlet+JDBC图书管理系统实战:源码拆解与避坑指南

JSP+Servlet+JDBC图书管理系统实战:源码拆解与避坑指南

简介:针对Java课程设计与大作业中的常见选题,这套下载资源提供了一套完整可运行的图书管理系统源码与配套数据库。项目内包含前端页面、后端业务逻辑、数据库脚本以及相关配置文件,实现了图书信息的录入、删除、修改、查询和借阅归还等主要功…

2026/10/19 17:40:46 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 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/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 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/19 2:54:50 阅读更多 →
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/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练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/18 12:16:16 阅读更多 →