中缀转后缀表达式实战:调度场算法与栈的深度解析
1. 从一道被问烂的面试题说起中缀表达式到底难在哪很多人第一次接触“中缀转后缀”是在数据结构课的栈章节或者某场笔试的选择题里。题目通常长这样给一个(AB)*C-D让你写出它的后缀形式。大部分人背了个口诀——“遇到操作数直接输出遇到运算符看栈顶优先级左括号压栈右括号弹到左括号”——然后就能把答案凑出来。但如果你真的动手写过一个完整的转换器就会发现事情远没有口诀那么轻巧多位数字怎么处理负号和一元的正负号怎么区分右结合运算符比如幂运算的优先级比较为什么要用“大于等于”还是“大于”空格、小数点、函数名、逗号分隔符又该怎么兼容我自己第一次写这个转换器是在做一个计算器小工具的时候。当时想得很简单觉得半小时就能搞定结果在“负号识别”和“右结合”这两个点上反复翻车调试了整整一个下午。后来陆陆续续在几个不同项目里重写过这个模块才慢慢把里面的坑摸清楚。所以这篇内容不打算给你复述一遍教材而是想从一个真正写过、踩过坑的人的角度把中缀转后缀这件事从头到尾拆开讲透它背后的调度场算法是怎么运转的每一步为什么这么设计多位数字和一元符号这些边界怎么处理以及怎么把它扩展成一个能算浮点、能带函数、能报错的实用模块。如果你正在准备数据结构相关的笔试面试或者想给自己的小项目加一个表达式求值功能又或者只是单纯想搞明白“栈”这个东西在真实场景里到底怎么用那这篇内容应该能帮到你。我会尽量把每个决策背后的“为什么”讲清楚而不是只丢给你一段能跑但看不懂的代码。全文会围绕中缀表达式、后缀表达式、调度场算法、运算符优先级、栈这几个核心概念展开从原理到实现再到排错一层层往下走。2. 调度场算法把“人脑的直觉”翻译成“机器的步骤”2.1 为什么需要后缀表达式这个中间形态先回答一个最根本的问题我们为什么不直接对中缀表达式求值非要先转成后缀原因在于中缀表达式的结构是隐式的。12*3这个式子人一眼就知道先算2*3因为脑子里自动套用了“先乘除后加减”的规则。但机器没有这种直觉它看到的只是一串字符。如果直接扫描求值你就得同时处理“当前该算哪个运算符”和“括号改变了优先级”这两件事逻辑会纠缠在一起非常容易出错。后缀表达式也叫逆波兰表达式的好处是它把优先级信息编码进了顺序本身。12*3转成后缀是1 2 3 * 你从左往右扫描遇到数字就压栈遇到运算符就弹出两个数算一下再压回去全程不需要考虑任何优先级规则。也就是说转换阶段把“结构”这件事一次性解决掉求值阶段就退化成一个极其简单的循环。这种“把复杂问题拆成两个简单阶段”的思路是工程里非常常见的手法。提示后缀表达式求值时栈里存的永远是“还没被消费的操作数”。遇到运算符就消费栈顶两个这个特性决定了它天然适合用栈来实现。2.2 调度场算法的核心一个输出队列加一个运算符栈调度场算法Shunting-yard algorithm是 Dijkstra 提出的名字来源于铁路调车场——车厢通过道岔被重新排列顺序和运算符通过栈被重新排序是一个道理。它的核心结构只有两个一个输出队列或者直接用字符串/列表用来按顺序存放最终的后缀结果一个运算符栈用来临时存放还没轮到输出的运算符。整个算法的规则可以浓缩成一句话操作数直接进输出运算符进栈之前先把栈顶那些“优先级不低于自己”的运算符弹出去然后再进栈左括号无条件进栈右括号则把栈里直到左括号为止的运算符全部弹出。这里有个关键细节也是很多人第一次写会忽略的地方优先级比较时到底用“大于”还是“大于等于”取决于运算符的结合性。对于左结合的运算符加减乘除都是栈顶优先级大于等于当前运算符时就要弹出对于右结合的运算符比如幂运算^栈顶优先级严格大于当前运算符时才弹出。这个差别看起来很小但它直接决定了2^3^2这种式子算出来是(2^3)^264还是2^(3^2)512。数学上幂运算是右结合的所以正确答案是 512如果你用错了比较符号结果就错了。2.3 用一张表把优先级和结合性定死在动手写代码之前我强烈建议先把运算符的优先级和结合性用一张表固定下来。这张表是整个算法的“配置中心”后面所有比较逻辑都依赖它。下面是我在实际项目里常用的一套配置运算符优先级结合性说明-1左结合加减*/%2左结合乘除取模^3右结合幂运算(0—左括号特殊处理)——右括号触发弹出有了这张表代码里的优先级比较就变成了一个纯粹的查表操作逻辑清晰后续要加新运算符比如位运算、逻辑运算也只需要改这张表不用动主流程。这是我在重构时体会最深的一点把易变的配置和稳定的流程分开代码的可维护性会好很多。3. 手写一个能跑的中缀转后缀从骨架到细节3.1 先搭骨架主循环长什么样理解了算法之后代码骨架其实很直接。我用 Python 写一版因为它的列表天然可以当栈和队列用读起来最接近伪代码。核心循环大概是这个结构def infix_to_postfix(tokens): output [] op_stack [] for token in tokens: if is_number(token): output.append(token) elif token (: op_stack.append(token) elif token ): while op_stack and op_stack[-1] ! (: output.append(op_stack.pop()) op_stack.pop() # 弹出左括号但不输出 else: # 运算符 while (op_stack and op_stack[-1] ! ( and should_pop(op_stack[-1], token)): output.append(op_stack.pop()) op_stack.append(token) while op_stack: output.append(op_stack.pop()) return output这段代码里should_pop就是前面说的优先级比较函数它需要同时考虑优先级和结合性。主循环结束后别忘了把栈里剩下的运算符全部弹出——这是新手最容易漏的一步漏了的话像12这种式子转出来就只剩1 2运算符丢了。3.2 分词比转换本身更容易翻车的一步上面代码的输入是tokens也就是已经分好词的列表。但真实场景里你拿到的是123.5*(4-1)这样一整串字符串。分词这一步往往比转换本身更容易出问题因为它要处理很多“看起来简单但边界很多”的情况。我踩过的坑主要集中在三个地方。第一是多位数字不能看到一个字符是数字就当成一个 token得一直往后读直到遇到非数字字符为止否则12会被拆成1和2。第二是小数点3.14要作为一个整体而且小数点只能出现一次3.1.4应该报错。第三是一元正负号-35里的-是负号不是减号3*-2里的-也是负号。判断规则是如果一个或-出现在表达式开头或者紧跟在另一个运算符或左括号后面那它就是一元符号。处理一元符号有两种常见做法。一种是在分词阶段就把它识别出来转成一个特殊 token比如u-然后在转换时把它当成高优先级的一元运算符处理。另一种是干脆在分词时就把-3合并成一个负数 token。我个人更推荐第一种因为它更通用也能处理-(34)这种负号作用于整个括号的情况。下面是一个分词的核心逻辑片段def tokenize(s): tokens [] i 0 while i len(s): ch s[i] if ch.isdigit() or ch .: j i while j len(s) and (s[j].isdigit() or s[j] .): j 1 tokens.append(s[i:j]) i j elif ch in -*/%^(): # 判断是否为一元符号 if ch in - and (not tokens or tokens[-1] in -*/%^(): tokens.append(u ch) # 标记为一元 else: tokens.append(ch) i 1 elif ch.isspace(): i 1 else: raise ValueError(f非法字符: {ch}) return tokens这段代码里u和u-就是一元运算符的标记。它们的特点是优先级非常高比^还高而且是一元运算求值时只需要弹一个操作数。把这些细节在分词阶段就处理好后面的转换逻辑就能保持干净。3.3 优先级比较函数结合性到底怎么体现回到should_pop这个函数它是整个算法里最需要想清楚的地方。逻辑是这样的def should_pop(top, current): # 一元运算符优先级最高 if top.startswith(u): return True if current.startswith(u): return False prec {: 1, -: 1, *: 2, /: 2, %: 2, ^: 3} if prec[top] prec[current]: return True if prec[top] prec[current]: # 左结合弹出右结合不弹 return not is_right_assoc(current) return False关键就在prec[top] prec[current]这一支如果当前运算符是左结合的加减乘除栈顶同优先级的要弹出如果是右结合的幂则不弹。这就是为什么2^3^2能正确转成2 3 2 ^ ^而不是2 3 ^ 2 ^。我当初就是在这里用了统一的“大于等于”导致幂运算结果一直不对查了半天才反应过来是结合性问题。注意一元运算符的处理要放在最前面判断因为它的优先级比所有二元运算符都高而且它永远不会被其他运算符“弹出”只会等自己该输出的时候输出。4. 那些教材不会告诉你的边界情况与排错实录4.1 括号不匹配怎么检测、怎么报错教材上的例子永远是括号配平的但真实输入里用户少打一个右括号是家常便饭。如果不做检测程序要么崩溃要么给出一个莫名其妙的结果。我的做法是在转换过程中维护一个计数器或者直接检查栈状态遇到)时如果栈里找不到对应的(说明右括号多了直接报错主循环结束后如果栈里还残留(说明左括号多了也要报错。报错信息最好能带上位置比如“第 5 个字符处的右括号没有匹配的左括号”这样用户能快速定位。我在一个给非技术用户用的小工具里加了这个提示之后反馈明显好了很多——用户不怕报错怕的是报错看不懂。4.2 除零和非法运算转换阶段管不管一个常见的设计困惑是除零、负数开方这类错误应该在转换阶段检查还是求值阶段检查我的答案是转换阶段不管求值阶段再管。因为转换阶段只关心“结构”不关心“数值”把职责分开更清晰。1/0转成后缀是1 0 /这个转换本身没有任何问题问题出在求值的时候除数为零。同理(-1)^0.5的合法性也应该在求值时判断。这种职责分离的好处是转换器可以复用于任何求值策略甚至可以用来做符号化简不局限于数值计算。4.3 一个真实的排查链路结果总是少一个运算符有一次我写完后缀转换测试12*3得到1 2 3 * 正确测试(12)*3得到1 2 3 *也正确。但测试123时结果变成了1 2 3最后一个不见了。这个 bug 很典型我当时的排查过程是这样的第一步确认分词没问题123被正确分成了[1,,2,,3]。第二步手动模拟算法1进输出进栈2进输出第二个来了比较栈顶的同优先级左结合应该弹出栈顶的进输出然后新的进栈。第三步3进输出循环结束把栈里剩下的弹出进输出。理论上结果应该是1 2 3 。问题出在哪我回头检查代码发现主循环结束后那个while op_stack的收尾循环被我写在了return后面等于根本没执行。这就是典型的“逻辑想对了但代码写漏了”。这个坑提醒我收尾步骤一定要单独测因为它不在主循环里很容易被忽略。后来我养成了一个习惯每写完一个转换函数至少测三类用例无括号的、有括号的、以及会触发收尾弹出的也就是栈里最后还有运算符的。4.4 空格和大小写输入清洗的隐形工作量真实输入里用户可能写成1 2 * 3也可能写成12*3甚至1 2* 3。分词阶段统一跳过空白字符就能解决。但如果表达式里包含函数名比如max(1,2)那还要处理大小写问题——MAX和max应该视为同一个函数。我的做法是在分词时把标识符统一转成小写然后在函数表里用小写键查找。这些看起来是小事但如果不处理用户就会觉得“这个工具怎么这么难用”。5. 从能跑到好用扩展成支持函数和逗号的完整方案5.1 函数调用怎么融进调度场算法基础版调度场算法只处理二元运算符但实际表达式里经常有max(1,2)、sin(3.14)这样的函数调用。函数调用的处理方式和左括号类似但有几个额外细节函数名本身也是一个 token遇到它时直接压栈和左括号一样函数名后面的左括号也要压栈但它的作用是标记参数列表的开始逗号,是参数分隔符遇到它时要把栈里直到左括号为止的运算符全部弹出但不弹出左括号遇到右括号时如果栈顶是函数名说明这个函数调用结束了把函数名弹出并输出。这里的关键是区分“普通左括号”和“函数调用的左括号”。我的做法是在栈里用一个特殊标记或者干脆在遇到函数名时记录一个状态。下面是一个处理逗号的片段elif token ,: while op_stack and op_stack[-1] ! (: output.append(op_stack.pop()) # 不弹出左括号它还要继续标记参数列表处理完逗号之后参数之间的运算符就被正确隔离了。比如max(12, 3*4)会转成1 2 3 4 * max求值时先算出两个参数再调用函数逻辑完全自洽。5.2 一元运算符在求值阶段怎么处理前面分词时我们把一元正负号标记成了u和u-转换阶段它们会作为高优先级运算符正常参与。到了求值阶段需要特殊处理遇到u-时只弹一个操作数取负后再压回去。这一点如果忘了求值时会因为栈里操作数不够而报错。我在第一次实现时就犯过这个错-35求值时弹出两个数结果把5和某个不存在的值凑一起了直接崩掉。后来在求值循环里加了一个判断if op.startswith(u): a stack.pop() stack.append(-a if op u- else a) else: b stack.pop() a stack.pop() stack.append(apply(op, a, b))这个细节很小但它是“能跑”和“跑对”之间的分界线。5.3 用测试用例把边界钉死写完一个转换器我最推荐的做法是准备一组覆盖各种边界的测试用例每次改代码都跑一遍。下面这张表是我自己常用的用例集你可以直接拿去用输入期望后缀考察点12*31 2 3 * 基本优先级(12)*31 2 3 *括号改变优先级1231 2 3 左结合、收尾弹出2^3^22 3 2 ^ ^右结合-353 u- 5 一元负号3*-23 2 u- *运算符后的一元负号max(12,3*4)1 2 3 4 * max函数与逗号1(2报错括号不匹配每次重构或者加新功能跑一遍这张表基本能覆盖 90% 的回归问题。这个习惯帮我省了很多“改了一个地方另一个地方又坏了”的时间。6. 写在最后几个我反复用到的实操心得关于中缀转后缀这件事我最后再分享几个从实际项目里攒下来的经验都是那种“知道了能少走弯路”的小点。第一个是先把分词和转换分开写、分开测。很多人喜欢把分词逻辑塞进转换循环里边扫描边判断代码会变得很难读出错了也不好定位。分开之后分词器只负责把字符串变成 token 列表转换器只负责重排 token各自都能单独测试排查问题时能快速缩小范围。第二个是优先级表一定要集中管理。不要在各个函数里散落着if op or op -这种判断全部收敛到一张字典或者一个配置对象里。这样以后要加位运算、逻辑运算改一处就够了不会漏。第三个是永远不要相信用户的输入。括号可能不配平可能有多余的运算符可能有非法字符可能有连续两个小数点。转换器要做的不是“假设输入正确”而是“遇到错误能给出清晰提示”。我在一个项目里因为没做输入校验用户输入了一个空字符串程序直接抛了个索引越界异常体验很差。后来加了空输入和非法字符的检查报错信息也写清楚了用户自己就能改。第四个是右结合这个点值得单独记一笔。它不常出现但一旦出现就是隐蔽的 bug。除了幂运算赋值运算符、三元运算符在某些语言里也是右结合的。如果你以后要扩展这个转换器去处理更复杂的表达式记住“同优先级时左结合弹出、右结合不弹”这条规则能帮你避开很多坑。这套东西我前后在计算器、公式解析、规则引擎几个场景里都用过核心逻辑几乎没变过变的只是运算符表和函数表。把调度场算法吃透你会发现它不只是一道面试题而是一个真正能落地的、解决“表达式结构解析”这类问题的通用工具。

相关新闻

JavaFX+SSM混合架构实战:内嵌Jetty跑通SpringMVC+MyBatis完整流程

JavaFX+SSM混合架构实战:内嵌Jetty跑通SpringMVC+MyBatis完整流程

简介:本资源是一个基于JavaFX与SSM(SpringSpringMVCMyBatis)框架融合开发的企业级ERP系统完整项目,面向Java桌面应用开发者、企业级Web后端学习者及希望打通GUI前端与服务层协同开发的进阶学习者。项目实现了从JavaFX高交互UI界面…

2026/10/16 3:48:09 阅读更多 →
Linux RPC报错errno 113排查:clnt_create与port mapper连接失败详解

Linux RPC报错errno 113排查:clnt_create与port mapper连接失败详解

1. 从一个报错说起:为什么这个RPC连接问题值得单独聊clnt_create: RPC: Port mapper failure - Unable to receive: errno 113 (No route to host)——这行报错,凡是在Linux环境下折腾过NFS挂载、做过RPC服务调试、或者维护过分布式存储集群的人&#xf…

2026/10/16 3:48:09 阅读更多 →
STM32读取多摩川TS5668N21绝对式编码器:RS485驱动实现

STM32读取多摩川TS5668N21绝对式编码器:RS485驱动实现

简介:这套针对STM32读取多摩川TS5668N21编码器的工程资料,适合嵌入式开发者、电机控制与工业自动化方向学习者使用。内容基于STM32F429平台,完整展示ADM485协议下UART收发配置、A/B/Z相脉冲捕获、位置增量与零位处理,以及Hall信号…

2026/10/16 3:47:08 阅读更多 →

最新新闻

汇编子程序与数制转换:深入8086栈机制与寄存器契约

汇编子程序与数制转换:深入8086栈机制与寄存器契约

/* 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 4:29:20 阅读更多 →
Android温湿度监控APP开发实战:串口蓝牙数据采集与Room存储告警

Android温湿度监控APP开发实战:串口蓝牙数据采集与Room存储告警

简介:这份资源是面向物联网与移动应用开发学习者的温湿度监控手机APP完整代码包,适合具备一定Android基础、希望打通传感器采集到手机端展示全链路的开发者参考。内容围绕DHT11/DHT22等传感器数据采集、微控制器处理、无线模块上传云端,以及A…

2026/10/16 4:29:20 阅读更多 →
融合物理先验与域自适应的恶劣天气目标检测方法

融合物理先验与域自适应的恶劣天气目标检测方法

/* 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 4:29:20 阅读更多 →
ONVIF设备支持H265完整开发指南:能力上报、RTSP与RTP打包改造

ONVIF设备支持H265完整开发指南:能力上报、RTSP与RTP打包改造

/* 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 4:29:20 阅读更多 →
深入AQS:doReleaseShared如何用PROPAGATE守住共享锁唤醒链

深入AQS:doReleaseShared如何用PROPAGATE守住共享锁唤醒链

/* 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 4:29:20 阅读更多 →
FPGA高速安全状态机设计:编码、时序优化与安全机制

FPGA高速安全状态机设计:编码、时序优化与安全机制

/* 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 4:28:20 阅读更多 →

日新闻

个人 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/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 9:49:14 阅读更多 →