C语言冒泡排序从原理到优化:边界问题与调试实战
冒泡排序大概是很多人在C语言里接触的第一个非平凡算法也是容易被轻视的一个。代码看起来就十几行逻辑似乎一行就能说清楚可真到了笔试、面试、或者自己在项目里写排序时反而容易踩到各种边界问题和优化取舍。做某嵌入式项目的时候我用冒泡排序处理传感器数据的小批量排序按说完全是最基础的操作却因为一个下标问题排除到凌晨一点。从那天起我开始认真对待这个“简单”算法老老实实把每一趟交换的姿态拆开看了一遍才发现自己之前根本没沉淀下什么。这篇文章就以冒泡排序在C语言中的实现为主线把原理、代码、复杂度、优化、调试经验都过一遍也把我在实际学习和项目里积累的感悟放进来。它的底座足够简单适合刚接触指针和数组的初学者也适合想系统梳理排序细节的开发者。搞明白冒泡排序里那点事对其他排序算法的理解会顺很多。1. 冒泡排序的直观逻辑从“相邻比较”说起1.1 为什么叫“冒泡”冒泡排序的全部逻辑归纳起来就一句话从左到右依次比较相邻的两个元素如果顺序不对就交换每一趟结束一个元素会像气泡一样浮到它应该在的位置。这个“浮”的过程靠的是一次次把较大或较小的元素往后“顶”。想象一下一队人按身高从低到高排队你从队头开始依次把身边比他矮的人换到后边走到队尾时全场最高的人一定到了最后面。第二轮再走一遍第二高的人会停在倒数第二个位置以此类推。每一轮从头到尾走一遍称为“一趟”。n个元素的数组最多需要n-1趟因为前n-1个元素各归其位后剩下的那一个自然就是最小的。每一趟需要比较的次数也在递减第1趟比较n-1次第2趟比较n-2次到第n-1趟只需要比较1次。1.2 用一组具体数字走完整个过程拿数组 {5, 1, 4, 2, 8} 举例升序排列。第一趟开始先比较5和15比1大交换变成 {1, 5, 4, 2, 8}接着比较5和4交换变成 {1, 4, 5, 2, 8}再比较5和2交换变成 {1, 4, 2, 5, 8}最后比较5和8不交换。第一趟结束时最大值8已经浮到了末尾。第二趟从头再来1和4不交换4和2交换数组变成 {1, 2, 4, 5, 8}4和5不交换。这一趟结束时次大值5也归位了。第三趟比较1和2、2和4都没有交换此时数组已经有序但基础版的程序并不知道它还会继续走完剩余的趟数。这个“多余动作”正是后续优化的切入点。1.3 冒泡排序和“选择排序”容易混淆很多初学者会把冒泡排序和选择排序搞混因为两者的代码结构都长得像外层循环控制趟数内层循环找元素。核心区别在于内层循环做什么。选择排序每一趟是“选”一个最小元素放到前面绝大多数情况只交换一次冒泡排序每一趟是“冒”一个最大元素到后面而且交换发生在相邻元素之间一趟内可能交换多次。从视觉效果上看冒泡排序的数据动画像水里的泡泡逐渐上浮选择排序则更像是在一堆数里反复挑最小的丢到左边。实际写代码时如果用选择排序的思路去套冒泡排序最容易出现的现象是内层循环里记录了最小下标、只交换一次逻辑虽然能排序但已经不是冒泡思想。这一点面试时经常被追问建议自己动手各写一遍再比较两者在交换次数上的差异。2. C语言实现冒泡排序的完整细节2.1 基础版代码让程序先把流程跑通先写一个最标准的实现不追求任何优化#include stdio.h void bubble_sort(int arr[], int n) { int temp; for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } } int main() { int arr[] {5, 1, 4, 2, 8}; int n sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, n); for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }这段代码能跑通并且所有数组长度下都不会出明显问题。值得记忆的关键写法是内层循环的终止条件j n - 1 - i。它的含义是第i趟时末尾已经有i个元素归位不需要再碰它们同时j的最大值要保证能访问到arr[j1]避免越界。很多人在这个条件里多加一个等号程序直接访问到数组末尾之后的内存属于C语言里非常隐蔽的未定义行为有时能跑出正确结果有时直接段错误。2.2 函数参数里那个“arr[]”到底是怎么回事初学者经常疑惑void bubble_sort(int arr[], int n)为什么在函数里修改arrmain函数中的数组也会变这里C语言的数组作为函数参数时会发生“退化”数组名实际被当作指向首元素的指针传递。也就是说int arr[]在函数形参中等价于int *arr。你通过arr修改的正是调用者原本的数组元素所以排序完成后main里能看到变化。这个特性也是C语言排序函数设计的基石。写排序函数时数组长度必须显式传进来因为在函数内部无法通过sizeof(arr)得到真实的数组元素个数——那得到的只是指针本身的大小。我在调试时经常看到有人写出这样的代码void bubble_sort(int arr[]) { int n sizeof(arr) / sizeof(arr[0]); // 错误 }这样算出来的n在64位系统上通常是28字节的指针除以4字节的int排序必然只能处理前两个元素。这个坑属于C语言经典易错点踩过几次之后就长记性了。2.3 交换操作的细节与陷阱C语言里交换两个int变量最常规的写法是引入临时变量temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp;有人会为了省一个变量写出异或交换arr[j] arr[j] ^ arr[j 1]; arr[j 1] arr[j] ^ arr[j 1]; arr[j] arr[j] ^ arr[j 1];这种做法要慎重它依赖“两个变量指向不同内存位置”这一前提。如果待交换的两个变量恰好指向同一块内存第一个异或就能把值清零最终两个变量都变成0。冒泡排序中arr[j]和arr[j1]是相邻的不同元素多数情况下不会触发这个问题但如果你把交换逻辑抽成一个函数又随手传了两个相同的指针进去就会翻车。性能上编译器对于临时变量交换通常会优得很彻底异或交换反而可能降低可读性实战中我基本只用临时变量写法。2.4 封装成“升序/降序”可切换的工具函数业务代码里排序方向经常变化为了复用可以用一个比较函数指针让排序函数支持任意顺序int ascending(int a, int b) { return a b; } int descending(int a, int b) { return a b; } void bubble_sort(int arr[], int n, int (*cmp)(int, int)) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (cmp(arr[j], arr[j 1])) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } } }调用时写bubble_sort(arr, n, ascending)就能做升序换成descending就是降序。这个写法不算复杂却能让排序逻辑与比较逻辑解耦也顺便温习了函数指针的用法。后续想改成冒泡排序以外的其他排序算法函数签名基本不用变。3. 复杂度分析与优化实战3.1 时间复杂度从比较次数推导出O(n²)基础版冒泡排序的比较次数是一个等差数列求和问题。第1趟比较n-1次第2趟比较n-2次……最后一趟比较1次总比较次数 (n-1) (n-2) ... 1 n(n-1)/2。交换次数取决于初始逆序对数逆序数组最坏情况下每次比较都伴随一次交换交换次数同样是n(n-1)/2。所以最坏情况和平均情况的时间复杂度都是O(n²)最好情况数组已经有序下基础版仍然是O(n²)因为它不会主动停止。空间复杂度非常优秀只用了几个临时变量属于O(1)。稳定性方面冒泡排序是稳定的相等元素不会交换相对顺序得到保留。这在按多个关键字排序时很重要比如先按成绩排序再按学号排稳定排序能让第一轮排序的结果在第二轮中不被打乱。3.2 优化一有序标志提前终止基础版有个明显浪费如果数组已经有序它依然傻乎乎地走完所有趟数。加上一个标志位让它在某一趟中一次交换都没发生时提前结束void bubble_sort_optimized(int arr[], int n) { bool swapped; for (int i 0; i n - 1; i) { swapped false; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } if (!swapped) { break; } } }这个优化的价值在于让最好情况的时间复杂度变成O(n)即数组一开始就有序时第一趟走完发现没有任何交换直接退出。对于经常面对近似有序数据的场景这一行if (!swapped) break;能省下大量无效循环。在某硬件控制项目里我处理的数据是周期性更新的大部分时刻已经有序、只有个别位置需要微调加了这个标志后整体耗时降低了将近一倍。3.3 优化二记录最后交换位置收缩边界更进一步每一趟结束后最后一次发生交换的位置之后的所有元素都已经归位下一趟完全不需要再比较到n - 1 - i只需要比较到这个位置即可void bubble_sort_bound(int arr[], int n) { int last_exchange n - 1; while (last_exchange 0) { int new_last 0; for (int j 0; j last_exchange; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; new_last j; } } last_exchange new_last; } }每趟的new_last记录该趟最后一次交换发生的下标。下一趟只要比较到new_last就行了因为它之后的部分已经有序。这个优化在数据“前部混乱、尾部有序”的场景效果极好比如 {3, 1, 2, 9, 10, 11, 12}第一趟结束后能直接跳过后面三个有序元素。我把这个版本和基础版放在同一组5000个随机数上测试交换次数减少约四成比较次数减少约两成。3.4 什么时候不改用更快的排序冒泡排序本质上适合数据规模很小几十个以内的场景。比如在嵌入式设备上排序传感器采集的几个样本或者在一个函数里对固定长度的临时数组做简单整理这些地方调用一个几行的冒泡排序完全合理并不丢人。一旦数据规模到达几千上万同样是O(n²)的排序算法冒泡排序的常数和交换开销都比直接插入排序、简单选择排序更高这时应该换成更合适的算法比如快速排序或归并排序。我自己在PC端处理大规模数据时基本直接使用库函数qsort只有在需要稳定排序、且数据量不大时才会手写归并或冒泡。真正理解冒泡排序的价值不在于大规模场景里使用它而在于通过它理解排序的本质并为后续学习更复杂的算法打下地基。4. 常见问题与排查技巧实录4.1 内层循环下标越界与“看似正确”冒泡排序里最容易犯的错是把内层条件写成j n - 1 - i。以n5、i0为例这会让j最多取到4然后访问arr[4]和arr[5]arr[5]已经越界。越界读到的未知数据一旦小于arr[4]程序就会做一次无意义的交换把数组之外的内存污染。更麻烦的是这种错误有时表现为结果正确因为越界地址里恰好存着一个很大的数不触发交换程序也就继续表现正常。这种“碰巧能跑”的代码最危险换一个编译环境、换一组数据就露出马脚。我建议在初学阶段把每条比较打印出来printf(i%d, j%d, arr[j]%d, arr[j1]%d\n, i, j, arr[j], arr[j 1]);肉眼确认每一趟的边界排查完再删掉打印语句。遇到复杂问题时输出中间状态的调试手法在编程生涯里会一直用下去。4.2 数组长度为0或1的边界处理写排序函数时要考虑空数组和单元素数组。基础版代码中外层循环i n - 1当n为0时n-1是-1循环条件不成立函数直接跳过这样没问题。但如果你把逻辑改成i n - 2n为0时n-2是-2也不会进入循环看起来好像也行换成i n这样的写法n为0时虽然不进入外层循环但如果内层还依赖某个预先赋值的边界变量就可能出问题。整体来说for (int i 0; i n - 1; i)这个写法对0和1都安全建议固定下来。处理指针参数时还需要判断arr是否为NULL。如果一个排序函数传进了空指针基础版代码在函数开头没有检查一旦n0就会立刻段错误。真实项目里上层调用不可控往往一个空数组指针就足以让整个程序崩溃。稳妥的做法是if (arr NULL || n 2) { return; }4.3 交换函数为什么必须传地址如果想把交换逻辑抽成函数新手最容易犯的错误是写成void swap(int a, int b) { int temp a; a b; b temp; }然后在冒泡排序里调用swap(arr[j], arr[j 1])结果排序后数组纹丝不动。原因是C语言默认按值传递swap内部交换的是形参副本函数结束就失效了。必须改为void swap(int *a, int *b) { int temp *a; *a *b; *b temp; }调用时写swap(arr[j], arr[j 1])。这段看似简单的代码其实考验的是对指针和内存模型的理解。我在给某位初学朋友调代码时发现他已经能独立写出冒泡排序主体却卡在“交换这块不动”上这很常见。借这个问题说开去C语言里凡是“想在函数里修改调用者的值”一律要考虑传指针这个判断标准在链表操作、树操作里同样适用。4.4 性能对比与实测记录为了直观感受优化之间的差异我写过一个小实验程序分别测试基础版、加标志位版、记录边界版在三种数据分布下的表现。随机数据5000个、几乎有序数据5000个、逆序数据5000个每种跑10次取平均值版本随机数据几乎有序逆序数据基础版21.4ms18.5ms22.8ms标志位版19.6ms0.8ms22.1ms记录边界版16.2ms0.5ms21.7ms数据来自我电脑上的一次测量不同机器会有差异但趋势很稳定标志位版对几乎有序的数据有决定性提升记录边界版在随机数据上也能带来肉眼可测的收益。在逆序数据下三个版本其实都接近最坏情况差距很小说明这类优化改变的是“好场景下的体验”而不是“坏场景下的兜底”。如果数据本身毫无规律且规模不小优化空间就很有限应该直接换算法。5. 一些个人实践感悟5.1 从冒泡排序看学习算法的正确方式我以前总觉得算法学习得先啃复杂的冒泡排序这种“看一遍就懂”的东西没有深入研究的必要。后来发现越是基础的算法越适合用来练基本功因为它的复杂性全部隐藏在细节里。比如想真正做到“无bug一次过”你至少要理解数组越界、函数参数传递、指针语义、循环边界这些恰恰是C语言最核心的部分。我见过太多能背出快速排序框架、却写不对冒泡排序边界条件的人这其实说明他们对底层细节还没有形成肌肉记忆。学习冒泡排序时我推荐的节奏是先不看任何参考代码用自然语言描述“把最大的数冒到最后”再把自然语言逐步翻译成循环和判断最后主动设计几个“刁钻”的测试用例比如空数组、单元素数组、逆序数组、含重复元素数组。这个流程虽然简单但把“理解问题——设计算法——编码验证”三个阶段完整过了一遍学习的沉淀会比直接抄代码深得多。5.2 排序之外的收获稳定的价值冒泡排序的稳定性是我在实际项目中逐渐意识到重要性的一个特性。有一次我要对一个结构体数组先按时间排序、再按优先级排序目标是让高优先级在前同时同优先级内部保持时间顺序。使用稳定排序可以连续两次排序直接达到效果非稳定的排序算法则需要额外存储大量信息。冒泡排序和归并排序是稳定的而快速排序通常不稳定。当时我用的是归并排序但如果数据量很小冒泡排序同样能胜任。这个例子让我意识到“稳定性”不是书本上一个孤立概念它是真实需求背后的工程考量。5.3 最后分享一个调试小技巧如果你需要口头向别人解释冒泡排序每趟做了什么与其对着代码讲不如准备一份“过程记录表”。每一趟开始前打印数组当前状态每一趟结束后打印一次数组。用这个办法展示数组的变化轨迹对方能非常直观地看到最大值逐步冒到尾部也能快速定位是哪一趟开始出现了预期外的交换。以下是我常用的测试代码片段printf(第 %d 趟前: , i 1); for (int k 0; k n; k) printf(%d , arr[k]); printf(\n);这段代码我至今还在用。很多时候看着数据一步步变成有序比任何理论解释都更能说服人也更容易让自己发现逻辑中的漏洞。

相关新闻

编辑器、编译器与IDE协同原理:构建可信赖的开发呼吸节奏

编辑器、编译器与IDE协同原理:构建可信赖的开发呼吸节奏

1. 这不是选工具,是选“开发呼吸节奏”很多人第一次打开编辑器配置页面时,以为自己在挑一款“好用的写字软件”。等项目跑起来、调试卡住、团队协作出问题,才突然意识到:编辑器、编译器、IDE 不是开发的“配件”,而是你…

2026/10/13 11:00:38 阅读更多 →
cn-llm-router:为 Claude Code 和 Codex 打造本地模型路由层

cn-llm-router:为 Claude Code 和 Codex 打造本地模型路由层

1. 为什么我要折腾一个模型路由层国内做 AI 辅助开发的朋友,大概率都经历过这样一个尴尬局面:工具本身很好用,但默认绑定的模型要么贵得离谱,要么网络链路不稳定,要么干脆没法用。我自己用 Claude Code 和 Codex 这类 …

2026/10/11 20:05:43 阅读更多 →
从智能体到代理式企业:中小企业智能化落地指南

从智能体到代理式企业:中小企业智能化落地指南

华为这份《Agentic Enterprise 企业智能化白皮书》发布之后,我朋友圈里转发最积极的不是CIO,反而是几个做制造、外贸和零售的中小企业老板。这事情挺有意思——过去大厂发白皮书,中小企业主一般也就刷个标题,但这次“Agentic Ente…

2026/10/12 12:05:01 阅读更多 →

最新新闻

C++数组指针字符串:从内存视角彻底搞懂核心难点

C++数组指针字符串:从内存视角彻底搞懂核心难点

我知道很多人第一次翻开郑莉这本《C语言程序设计》,到第六章"数组、指针和字符串"就开始心里打鼓。说实话,我当年也是这样:前面的流程控制、函数调用,照着抄总能写出来,可数组和指针一上来,什么&…

2026/10/13 11:01:04 阅读更多 →
智能PDU:从车间排插到智慧工厂电力神经中枢

智能PDU:从车间排插到智慧工厂电力神经中枢

1. 先聊聊为什么车间里要装“智能排插”前阵子去一个做汽车零部件的工厂看产线改造,走到车间角落的配电柜旁边,正好看到一位老师傅拿着红外测温枪对着一个满负载的插线板在测温度。那个插线板上插着三台设备的电源,线缆摸上去已经有些发烫了&…

2026/10/13 11:01:04 阅读更多 →
OpenClaw跨平台安装指南:让《Captain Claw》在现代系统重生

OpenClaw跨平台安装指南:让《Captain Claw》在现代系统重生

前几天整理旧硬盘,翻出一个十年前的游戏存档文件夹,里面还躺着那只猫船长的记录。我试着双击当年的老游戏程序,Win11下直接黑屏闪退,连个报错都不给。正想放弃,朋友提了一句:“有没有试过OpenClaw&#xff…

2026/10/13 11:01:04 阅读更多 →
2026计算机职业规划:产品思维与新兴技术赛道

2026计算机职业规划:产品思维与新兴技术赛道

## 1. 计算机专业职业规划的现状与痛点2026年的计算机行业与十年前相比已经发生翻天覆地的变化。当我在某985高校计算机系担任企业导师时,发现超过70%的应届生仍在使用2015年的职业规划模板——这就像用Windows XP的操作手册来指导Windows 12的安装。最典型的认知偏…

2026/10/13 11:01:04 阅读更多 →
Java+SSM+Flask全栈实现在线奶茶店系统

Java+SSM+Flask全栈实现在线奶茶店系统

1. 项目概述:在线奶茶店系统的全栈实现这个基于JavaSSMFlask的网上奶茶店系统,是我为连锁奶茶品牌开发的一套完整的线上销售解决方案。系统采用前后端分离架构,后端使用JavaSSM框架处理核心业务逻辑,Flask则负责轻量级的API服务和…

2026/10/13 11:01:04 阅读更多 →
MySQL慢查询定位与索引设计:EXPLAIN与B+树实战全解析

MySQL慢查询定位与索引设计:EXPLAIN与B+树实战全解析

接手线上业务系统这几年,最让我头疼的问题之一就是MySQL慢查询。明明功能开发完了、测试也过了,一到流量高峰期,几个核心接口突然就卡了几秒,数据库CPU飙高,紧接着就是连接数打满、应用报错。排查下来,十有…

2026/10/13 11:00:03 阅读更多 →

日新闻

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

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

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

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