环形缓冲区空/满判断:三种方案与面试手写要点
环形缓冲区的实现和空/满判断是我在技术面试里最常问的基础题没有之一。它的代码量不超过五十行看起来就是一个数组加两个下标可每次让候选人在白板上写下来十分钟内能把边界情况全部写对的人真的不多。尤其是“这个缓冲区到底是空还是满”这个点十个里有六七个会卡壳。这篇文章就把环形缓冲区的底层原理、三种主流空满判断方案和面试答题的思路一次讲透顺带把我自己真正踩过的坑也放进去。不管你是准备校招的应届生还是工作两三年想补基础的后端、嵌入式开发这都值得认真看一遍。1. 环形缓冲区是什么为什么非用不可1.1 本质是一个会“绕圈”的定长队列环形缓冲区也叫循环队列circular queue本质上是一块固定大小的连续内存配合两个指针做 FIFO先入先出读写。它和普通队列最大的区别在“回绕”当写指针走到缓冲区末尾时不是停下或扩容而是绕回开头继续写就像操场跑道的场景——跑完一圈回到同一个起点但跑道总长度不变。内存模型上有几个关键点数据还是放在一块线性数组里head 和 tail 只是数组下标不是真的进入“环”。所谓“环”只存在于索引计算上靠取模或位运算把越界的下标折回来。缓冲区容量是固定不变的不会因为写入而动态增长。1.2 什么场景必须用它实际工程里数据生产和消费的速度往往不平衡。典型场景包括串口接收中断把数据一股脑塞进缓冲区主循环慢慢取出来解析中断里不能做耗时操作。音视频采集线程持续写入 PCM 数据播放线程按固定节奏读取两者速度稍微不同步就出杂音。网络驱动收包报文先放进缓冲区应用层再按需取走。日志系统需要临时攒一批日志再批量刷入磁盘。如果不用环形缓冲区一般只有两个选择要么每次写数据都重新分配内存malloc/free 开销大还容易产生碎片要么用无限增长的链表队列内存占用不可控。环形缓冲区用固定内存解决了这两个问题读写几乎不涉及内存分配特别适合对性能和实时性有要求的场景。1.3 为什么面试官偏偏爱考它我站在面试官的角度看原因有三个。第一逻辑简单五分钟能讲清大概思路适合当面试题的起步第二边界条件多head/tail 的更新、回绕处理、空满判断全是易错点能筛出写代码粗不细心的人第三它可以顺藤摸瓜问并发、原子操作、内存屏障等进阶话题一道题能同时考察候选人好几个层次的知识。所以它不只是“一道基础题”更像面试官手里的一个放大镜。2. 核心结构设计两个指针的正确含义2.1 head 和 tail 到底指向哪里数组加两个指针是环形缓冲区最经典的实现方式但指针的指向约定有讲究。我的习惯是这样定义head 指向当前可以读出数据的那个位置也就是队首读数据时从 head 处取取完 head 向后移动。tail 指向下一个可以写入数据的位置也就是队尾的“下一个空位”写数据时先往 tail 处写写完 tail 向后移动。初始化时head 和 tail 都等于 0此时缓冲区为空。把 tail 定义为“下一个空闲位置”而不是“最后一个有效数据”的位置这不是随便选的。这个约定能让判断条件保持统一head tail 时我只需要再结合其他手段去区分空和满而不需要针对“最后一个位置”做各种偏移调整。见过一些实现把 tail 定义成“最后一个数据位置”代码一复杂就容易出现到处 1、-1 的麻烦。需要拎清楚两个容易蒙的概念head tail 只表示两个指针碰上了可能为空也可能为满具体是哪种得看你用哪套判断方案。这是环形缓冲区最容易踩的坑。回绕并不是真的把数据搬到数组开头而是把“索引”通过取模或位运算折回去。2.2 索引怎么“绕圈”取模和位运算写指针前进一格时不能简单写 tail因为 tail 会越过数组边界。通用写法是tail (tail 1) % capacity;我举个例子capacity 为 8tail 当前为 7(7 1) % 8 0tail 就回到数组开头。读取时 head 的更新一模一样。取模写法适用于任意正整数容量代码也最好读。但在嵌入式场景或者追求吞吐的地方取模运算在部分单片机上比较慢。于是有一个经典优化把容量设置成 2 的幂比如 8、16、32然后用位运算代替取模tail (tail 1) (capacity - 1);为什么可以这样因为 capacity 是 2 的幂时capacity - 1 的二进制表示就是低 n 位全是 1比如 8 对应 0x0716 对应 0x0F。tail 自增后跟这个掩码做按位与等价于把超出部分截断结果和取模完全一致。这条优化在很多高性能队列和内核代码里都在用。注意位运算做法要求 capacity 必须是 2 的幂否则结果会错。面试时如果说得出这个前提是很加分的细节。3. 空/满判断三种主流方案逐条拆解3.1 方案一计数器法在结构体里额外增加一个 count 成员记录缓冲区里当前有效元素的个数。每次成功写入一个元素 count1每次成功读出一个元素 count-1。判断逻辑非常直白空count 0满count capacity我直接给一份核心代码结构typedef struct { int *data; int capacity; int head; int tail; int count; // 记录有效元素个数 } ring_buffer_t; int rb_is_empty(ring_buffer_t *rb) { return rb-count 0; } int rb_is_full(ring_buffer_t *rb) { return rb-count rb-capacity; }这个方案的优点是直观、可读性高而且缓冲区所有槽位都能用不浪费任何空间。缺点是每个读写操作都要多维护一个 count在多线程环境下这个 count 会成为并发竞争点需要额外加锁或使用原子变量来保证安全。3.2 方案二预留一个槽位法这是教科书和面试答案里最常见的方案核心思路是故意让缓冲区最多只存 capacity - 1 个元素留下一个空闲位置用于区分空和满。空head tail满(tail 1) % capacity head也就是 tail 再往前一步就追上 head 时判定为满比如 capacity 为 4最多存 3 个元素。从头开始依次写入 3 个元素后head 停在 0tail 停在 3此时 (tail 1) % 4 0 等于 head满。再尝试写第 4 个元素就会被拒绝于是 tail 永远不会真正追到 head。这个方案的好处是不需要额外变量结构体最精简在单生产者单消费者的场景下甚至方便设计成无锁队列。代价是浪费一个槽位而且如果 capacity 为 1这个方案直接用不了。3.3 方案三标志位法既然 head tail 时空和满都有可能那就用一个布尔标志记录“最后一次操作是读还是写”。通常这样定义初始化时把标志置为 READ表示刚初始化完或者刚做过读取此时 head tail 视为空写数据后标志置为 WRITE读数据后标志置为 READ。typedef struct { int *data; int capacity; int head; int tail; int last_op; // 0 表示 READ1 表示 WRITE } ring_buffer_t; int rb_is_empty(ring_buffer_t *rb) { return (rb-head rb-tail) (rb-last_op 0); } int rb_is_full(ring_buffer_t *rb) { return (rb-head rb-tail) (rb-last_op 1); }判定逻辑我验证过一遍capacity 为 4初始 headtail0last_opREAD判空成立。连续写入 4 个元素后 tail 回到 0head 仍为 0head 与 tail 相等但 last_opWRITE判满成立。再连续读 4 个元素head 最后也回到 0head 与 tail 相等且 last_opREAD判空成立。这个方案不浪费槽位也不依赖 count 变量但多维护一个标志位。有一个特别容易犯的错误初始化时如果把 last_op 误设为 WRITE缓冲区一开始就被判定为满所有写入全部失败。3.4 三种方案怎么选我整理了一张对比表面试时可以直接拿来用方案判空条件判满条件空间利用率额外内存推荐场景计数器法count 0count capacity100%count 变量逻辑简单单线程或配合锁使用预留槽位法head tail(tail1)%cap head浪费 1 格无经典方案适合嵌入式、无锁设计标志位法相等且 last_opREAD相等且 last_opWRITE100%last_op 变量不想浪费槽位且愿意维护标志的场景我的个人倾向是如果是面试手写题优先说预留槽位法因为它是教科书标准答案代码最精简最能体现你对边界条件的理解。如果是实际工程项目且缓存空间非常宝贵我会选计数器法或标志位法把空间利用率和代码可读性权衡好。4. 从零手写一个可复用的环形缓冲区4.1 数据结构定义与初始化我以下面的 C 代码为准采用预留槽位法。先定义结构体和初始化函数#include stdio.h #include stdlib.h typedef struct { int *data; int capacity; // 数组总大小实际可存 capacity-1 个元素 int head; // 指向队首有效数据 int tail; // 指向下一个空闲位置 } ring_buffer_t; int rb_init(ring_buffer_t *rb, int *buf, int capacity) { if (rb NULL || buf NULL || capacity 1) { return -1; // capacity 至少为 2预留槽位法才能工作 } rb-data buf; rb-capacity capacity; rb-head 0; rb-tail 0; return 0; }很多新手容易漏掉对 capacity 1 的判断。预留槽位法预留一个空位那至少得有两个槽位才有意义。如果传入 1缓冲区永远既不满也不空逻辑直接混乱。4.2 入队、出队和元素个数核心操作就三个判空、判满、读写。我习惯把判空和判满单独写成函数不要在 push 里直接写判断式这样代码看起来清晰也方便单元测试。int rb_is_empty(ring_buffer_t *rb) { return rb-head rb-tail; } int rb_is_full(ring_buffer_t *rb) { return (rb-tail 1) % rb-capacity rb-head; } int rb_push(ring_buffer_t *rb, int value) { if (rb_is_full(rb)) { return -1; // 队列满拒绝写入 } rb-data[rb-tail] value; rb-tail (rb-tail 1) % rb-capacity; return 0; } int rb_pop(ring_buffer_t *rb, int *out) { if (rb_is_empty(rb)) { return -1; // 队列空无法读取 } *out rb-data[rb-head]; rb-head (rb-head 1) % rb-capacity; return 0; } int rb_size(ring_buffer_t *rb) { return (rb-capacity - rb-head rb-tail) % rb-capacity; }单独说下 rb_size 的计算公式。普通数组里元素个数是 tail - head但环状结构里 head 可能在 tail 后面直接减会出现负数。所以统一先加一个 capacity 再取模比如 head3、tail1、capacity8元素个数就是 (8 - 3 1) % 8 6完全正确。这个公式和回绕的取模思路是一脉相承的。注意push 和 pop 里都要先用判断函数做检查返回值语义要统一。我用 -1 表示操作失败0 表示成功。面试时把这个返回值约定说清楚代码可读性会好很多。4.3 一个简单的正确性测试写完不测等于白写。我最常用的是边界测试法把缓冲区写满再继续写一次确认被拒绝再读空再继续读一次确认也失败。int main() { int buf[4]; ring_buffer_t rb; rb_init(rb, buf, 4); // capacity 4实际最多存 3 个 if (rb_push(rb, 10) ! 0) return 1; if (rb_push(rb, 20) ! 0) return 1; if (rb_push(rb, 30) ! 0) return 1; if (rb_push(rb, 40) 0) return 1; // 这里必须满写入应失败 if (!rb_is_full(rb)) return 1; int v 0; if (rb_pop(rb, v) ! 0 || v ! 10) return 1; if (rb_pop(rb, v) ! 0 || v ! 20) return 1; if (rb_pop(rb, v) ! 0 || v ! 30) return 1; if (rb_pop(rb, v) 0) return 1; // 这里必须空读取应失败 if (!rb_is_empty(rb)) return 1; printf(ring buffer test passed\n); return 0; }注意 capacity 为 4 时最多只有 3 个元素这是预留槽位法最容易让初学者感到意外的地方。出了 bug 先检查是不是在往第 4 个位置写。5. 面试官藏在后面的连环追问5.1 缓冲区满了之后怎么办这是基础题之后第一个高频追问。答案不唯一关键是跟面试官讲清楚你的策略和代价拒绝写入返回失败调用方自己处理。适合保证数据完整性的场景比如配置文件解析。覆盖最旧数据头部前进一格再把新数据写进去。适合实时性要求高的场景比如传感器数据流旧数据晚一点处理就没意义了。阻塞等待消费者取走数据后再继续写。适合生产者消费者速率基本匹配的场景但需要配合多线程同步机制。覆盖最旧数据的实现其实很简单就是在 push 时先判断满不满如果满就先执行一次 head 前进操作。但这里有一个隐含问题如果你用预留槽位法覆盖写入后缓冲区仍然保持满状态所以下一次写入还是要先覆盖这一点要跟面试官说清楚。5.2 多线程下的线程安全和 CAS 优化再往下问就是并发。环形缓冲区在单生产者单消费者SPSC场景下有一个非常诱人的优势只要保证头尾指针的原子可见性理论上可以设计成无锁队列。原因不复杂生产者只写 tail、读 head消费者只读 tail、写 head两个指针各有一个写者天然不冲突。麻烦在于内存可见性普通 C 代码里 tail 的更新不一定立刻对消费者可见因此在真正的无锁设计里要用原子操作加内存屏障比如 C11 的 atomic_load 和 atomic_store 配合 acquire/release 语义。多生产者或者多消费者的情况就麻烦得多。多个生产者同时去更新 tail 会出现竞争要么加锁要么用 CAScompare-and-swap循环去争取更新权。这个深度已经足够撑起面试后半程了能把从基础到并发这一条线顺下来面试官对你的印象会明显不一样。5.3 指针约定变了空/满判断怎么变有些面试官会故意换个约定考你如果我们规定 tail 始终指向最后一个有效数据而不是下一个空闲位置判断条件变成什么样这时候就不能死记公式了。我推导一下如果 tail 指向最后一个有效数据初始状态需要让 head 指向 0tail 设为 -1。判空条件变成 head (tail 1) % capacity判满条件在预留槽位法下变成 (tail 2) % capacity head。我见过不少候选人被这种变体问懵根因是没有真正理解“为什么 tail 指向下一个空闲位置时条件是 (tail1)%cap head”。理解了指针含义后任何变体都可以现场推出来而不需要背公式。面试答题时我建议先说清楚自己的指针约定再给判断条件这样思路是完整的。6. 实战避坑清单与调试心得6.1 最容易犯的五个错误我自己写过无数个环形缓冲区也帮同事排查过相关 bug总结下来高频错误集中在下面几个点错误类型具体表现解决办法忘记回绕读出正确的数据但 tail 或 head 越界每次更新指针都用 (index1)%capacity 或位运算先移动指针再写数据数据写到错误位置读到旧值先写 data[tail]再更新 tail预留槽位法里把 capacity 当可用长度以为能存 capacity 个实际只能存 capacity-1 个初始化时就想清楚最大容量语义标志位法初始值错误last_op 误初始化为 WRITE导致初始满初始化为 READ语义是“上一次操作是读”并发环境没有屏障生产者写的数据消费者看不到使用原子操作和 acquire/release 屏障6.2 调试技巧把环形图画出来环形缓冲区的 bug 跟踪单步断点其实很低效因为问题往往出现在几十次操作之后的回绕瞬间。我调试时最常用的办法是“状态快照”void rb_dump(ring_buffer_t *rb) { printf(head%d tail%d size%d full%d empty%d\n, rb-head, rb-tail, rb_size(rb), rb_is_full(rb), rb_is_empty(rb)); }在每次 push 和 pop 之后打一条快照然后用一张纸把数组画成圆手动标出 head 和 tail 的位置跟打印内容比对。这个笨办法比盯着断点跳转高效得多尤其是回绕那一瞬间看一眼 tail 从 7 变成 0 的过程bug 原因往往直接暴露。6.3 我个人的一个习惯最后分享一个我坚持了很多年的习惯新写任何环形缓冲区之前先画一个等分六格或八格的圆把 head 和 tail 画在对应位置手动推演“写-回绕-满-读-回绕-空”至少三个循环。看起来很笨但这一步帮我挡住了大量的 off-by-one 错误。这个习惯在面试时同样有效——你一边画圆一边讲 head 和 tail 的移动过程比上来就默写代码更能让面试官看到你是真的懂而不是背过题。环形缓冲区的小巧之处在于它的全部难点几乎都集中在“两个指针怎么绕、空满怎么分”这一件事上。把这件事想透后面不管换成计数器、标志位还是换成无锁并发队列思路都是通的。

相关新闻

环形缓冲区核心实现:空满判断、性能优化与典型应用

环形缓冲区核心实现:空满判断、性能优化与典型应用

凡是亲手写过生产者-消费者代码的开发者,几乎都会和环形缓冲区打一次照面。这玩意儿听起来基础,但在音频采集、网络收包、日志异步落盘这些每天都在跑的路径上,它都扮演着核心角色。面试时,环形缓冲区(循环队列&#x…

2026/10/13 4:48:14 阅读更多 →
测试工程师转脑机接口调试:认证路径与实操指南

测试工程师转脑机接口调试:认证路径与实操指南

这两年,我身边聊脑机接口(BCI)的测试工程师明显多了。问得最多的一句话是:搞了五六年功能测试、接口测试,突然想去碰脑机接口这种听起来像科幻片的东西,还考什么调试认证,到底靠不靠谱&#xff…

2026/10/13 4:48:14 阅读更多 →
TensorFlow 2.x自定义Layer四大生命周期详解

TensorFlow 2.x自定义Layer四大生命周期详解

1. 为什么自定义Layer不是“高级技巧”,而是TensorFlow 2.x的日常刚需刚接触TensorFlow 2.x时,我跟很多开发者一样,以为tf.keras.layers.Dense、Conv2D这些内置层就是全部——写模型就是搭积木,调参就是改数字。直到在某次图像增强…

2026/10/13 4:48:14 阅读更多 →

最新新闻

手写Tool Use面试题:从函数到生产级工具调度系统

手写Tool Use面试题:从函数到生产级工具调度系统

1. 面试现场的真实压力:当“手写 Tool Use”成为临场能力试金石“请手写一个 Tool Use 实现。”这句话从面试官嘴里说出来的时候,我正端着一次性纸杯喝第三口温水——不是因为紧张,而是前两轮算法题刚结束,嗓子发干。但这句话一落…

2026/10/13 5:34:33 阅读更多 →
Carla 0.9.15 zlib-1.2.13 编译避坑指南:ABI兼容与符号导出实战

Carla 0.9.15 zlib-1.2.13 编译避坑指南:ABI兼容与符号导出实战

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

2026/10/13 5:34:33 阅读更多 →
Kudu线上集群性能优化实战:内存、IO与RPC调优指南

Kudu线上集群性能优化实战:内存、IO与RPC调优指南

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

2026/10/13 5:34:33 阅读更多 →
电动牙刷BLE上传刷牙时长至家长App的芯片方案与低功耗设计

电动牙刷BLE上传刷牙时长至家长App的芯片方案与低功耗设计

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

2026/10/13 5:34:33 阅读更多 →
开源BOM管理系统:从Excel到可追溯数据库的实战落地

开源BOM管理系统:从Excel到可追溯数据库的实战落地

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

2026/10/13 5:34:33 阅读更多 →
IIS管理器打开全攻略:本机启动、远程连接与排错实战

IIS管理器打开全攻略:本机启动、远程连接与排错实战

搞Windows服务器的同学都清楚,IIS管理器这个窗口,看着不起眼,却是撑起大量Web业务的关键工具。很多人以为它就是个“打开就能用”的图形界面,但在实际运维里,光是怎么把它顺利打开、正常连上远程站点,就能筛…

2026/10/13 5:33:32 阅读更多 →

日新闻

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

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

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

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 阅读更多 →