<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">小怪兽的博客</title>
    <updated>2026-07-25T22:20:43+08:00</updated>
        <id>https://lymboy.com</id>
        <link rel="alternate" type="text/html" href="https://lymboy.com" />
        <link rel="self" type="application/atom+xml" href="https://lymboy.com/atom.xml" />
    <rights>Copyright © 2026, 小怪兽的博客</rights>
    <generator uri="https://halo.run/" version="1.5.1">Halo</generator>
            <entry>
                <title><![CDATA[Spring AI的工具调用循环，本质就是ReAct：一次源码级拆解]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/springai-de-gong-ju-diao-yong-xun-huan--ben-zhi-jiu-shi-react-yi-ci-yuan-ma-ji-chai-jie" />
                <id>tag:https://lymboy.com,2026-07-25:springai-de-gong-ju-diao-yong-xun-huan--ben-zhi-jiu-shi-react-yi-ci-yuan-ma-ji-chai-jie</id>
                <published>2026-07-25T22:20:43+08:00</published>
                <updated>2026-07-25T22:20:43+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="spring-ai%E7%9A%84%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8%E5%BE%AA%E7%8E%AF%EF%BC%8C%E6%9C%AC%E8%B4%A8%E5%B0%B1%E6%98%AFreact%EF%BC%9A%E4%B8%80%E6%AC%A1%E6%BA%90%E7%A0%81%E7%BA%A7%E6%8B%86%E8%A7%A3" tabindex="-1">Spring AI的工具调用循环，本质就是ReAct：一次源码级拆解</h1><p>在<code>ChatModel</code>实现类里打断点调试Spring AI的工具调用，你会碰到一件怪事：日志里明明看到模型被连续吐了好几次<code>tool_call</code>，可断点只跳进来一次。是断点打错了地方吗？不是。循环压根不在<code>ChatModel</code>里，这是Spring AI 2.0相对1.x最容易让老用户懵的一处架构变化。</p><p>这篇文章想说清楚三件事：Spring AI的调用链到底长什么样；工具调用的循环具体藏在哪一层、怎么实现的；这套机制是不是标准的ReAct，Reason、Act、Observation三段一样不少，而不只是长得像。全文过了一遍Spring AI 2.0.0的源码，结论都能对着代码验证，但正文只提类名，不贴文件路径和行号，方便阅读。</p><h2 id="%E4%B8%80%E5%BC%A0%E5%9B%BE%E7%9C%8B%E5%85%A8%E8%B2%8C" tabindex="-1">一张图看全貌</h2><p>Spring AI的调用链看着乱，是因为它把三件事叠在了一起：责任链（Advisor Chain）、四层嵌套的可观测性埋点、藏在其中一个Advisor里的ReAct循环。三件事拆开看，每一件都不复杂。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/spring-ai-diagram-1-macro-architecture.png" alt="Spring AI 2.0调用链宏观架构" /></p><p>图上从左到右是一次调用的静态结构：<code>ChatClient</code>接进来一句<code>.prompt(msg).call()</code>，<code>DefaultChatClient</code>把它组装成请求，攒出一条责任链，链上依次挂着用户自己加的Advisor（Memory、RAG、日志之类，可选）、<code>ToolCallingAdvisor</code>、最后是链的终结节点<code>ChatModelCallAdvisor</code>，由它去调具体的<code>ChatModel</code>发起一次真正的模型API调用。</p><p>真正有意思的是<code>ToolCallingAdvisor</code>和<code>ChatModelCallAdvisor</code>之间那两条来回的箭头，那才是运行时真正的循环。一次<code>.call()</code>背后可能是好几轮模型调用，日志里看到的是同一次调用，背地里模型API可能被打了三五次。这也是文章开头那个「断点为什么只跳一次」的答案：循环发生在更外层的<code>ToolCallingAdvisor</code>，断点得下在那一层才看得全。</p><h2 id="%E9%A1%BE%E9%97%AE%E9%93%BE%E6%98%AF%E8%B4%A3%E4%BB%BB%E9%93%BE%EF%BC%8C%E4%B8%8D%E6%98%AF%E4%B8%80%E4%BB%BD%E7%BA%BF%E6%80%A7%E5%88%97%E8%A1%A8" tabindex="-1">顾问链是责任链，不是一份线性列表</h2><p><code>ChatClient.prompt(msg).call()</code>落地到具体代码，是<code>DefaultChatClient</code>构造出请求规格，调<code>buildAdvisorChain()</code>把顾问链攒出来，再触发链的执行。</p><p>链的执行方式是<code>Deque</code> + <code>pop()</code>，不是for循环遍历一份List。链的实现类每次弹出下一个Advisor，把「链的剩余部分」传给它，Advisor自己决定要不要继续往下走。这跟Servlet的<code>Filter</code>链、Spring MVC的<code>HandlerInterceptor</code>是同一种写法：每个Advisor都能同时拦截「请求进去」和「响应出来」两端，顺序由<code>Ordered</code>决定，数值越小越靠外层。</p><p>链的组装规则也值得记一下：用户手动加的Advisor（Memory、RAG、日志……）放前面，没手动指定就自动注册一个<code>ToolCallingAdvisor</code>；链的最底部固定压两个终结Advisor：非流式的<code>ChatModelCallAdvisor</code>和流式的<code>ChatModelStreamAdvisor</code>，它们的<code>getOrder()</code>都是最低优先级，永远排最后，负责真正调<code>ChatModel.call(prompt)</code>。</p><h2 id="tool-calling%E5%BE%AA%E7%8E%AF%EF%BC%9A2.0%E6%8A%8A%E5%AE%83%E4%BB%8Echatmodel%E6%90%AC%E5%88%B0%E4%BA%86advisor%E5%B1%82" tabindex="-1">Tool Calling循环：2.0把它从ChatModel搬到了Advisor层</h2><p>这是本文开头那个「断点只跳一次」的根源。循环主体在<code>ToolCallingAdvisor</code>里，是一个显式的<code>do...while(isToolCall)</code>，不是递归：</p><pre><code class="language-java">// ToolCallingAdvisor 核心循环（节选，改写自源码，去掉了部分细节）do {    processedChatClientRequest = this.doBeforeCall(processedChatClientRequest, callAdvisorChain);    chatClientResponse = callAdvisorChain.copy(this).nextCall(processedChatClientRequest); // 调下游 → ChatModelCallAdvisor → ChatModel    chatClientResponse = this.doAfterCall(chatClientResponse, callAdvisorChain);    isToolCall = this.toolExecutionEligibilityChecker.isToolCallResponse(chatResponse);    if (isToolCall) {        ToolExecutionResult toolExecutionResult =            this.toolCallingManager.executeToolCalls(processedChatClientRequest.prompt(), chatResponse);        if (toolExecutionResult.returnDirect()) break;   // 工具标了&quot;直接返回&quot;就跳出        instructions = this.doGetNextInstructionsForToolCall(...);  // 拼下一轮消息    }} while (isToolCall);</code></pre><p>具体某个<code>ChatModel</code>（比如<code>DeepSeekChatModel</code>）现在只干一件事：把工具定义塞进请求，发一次调用，原样把响应（哪怕里面带着还没处理的<code>tool_call</code>）交回去。从头到尾没有<code>while</code>，没有执行工具的逻辑。循环完全不是它的职责了。</p><p>流式版本没法用阻塞的<code>do-while</code>，改用递归：检测到<code>tool_call</code>就在内部方法里递归调用自己，工具执行本身是阻塞操作，会被扔到弹性调度线程池上跑，不占用响应式主线程。</p><h2 id="%E8%BF%99%E5%B0%B1%E6%98%AFreact%EF%BC%8C%E4%B8%8D%E6%98%AF%E3%80%8C%E5%83%8Freact%E3%80%8D" tabindex="-1">这就是ReAct，不是「像ReAct」</h2><p>Reason（模型决定动作）、Act（真正执行）、Observation（把结果喂回去），这三段循环是不是真的落到了Spring AI的代码里，而不是我强行往上套的概念？看下面这张图就知道了。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/spring-ai-diagram-2-react-loop.png" alt="ReAct循环怎么落到Spring AI代码上" /></p><p>关键证据在处理工具执行结果的那段代码里，逻辑压缩下来就两行：</p><pre><code class="language-java">messages.add(assistantMessage);      // Act：模型决定调哪个工具、传什么参数messages.add(toolResponseMessage);   // Observation：工具真实执行的结果</code></pre><p>这两条消息被塞回对话历史，变成下一轮Prompt的输入。模型在下一轮里，能看到自己刚才「决定调用工具」这个动作本身，以及这个动作换来的真实结果，然后接着推理，直到判定器认为不用再调工具为止。</p><p>循环载体从头到尾就是<code>ToolCallingAdvisor</code>这一个类，没有分散在别的地方。所以答案是肯定的：Spring AI的工具调用机制就是ReAct，只是把「循环」这件事封装成了一个可插拔的Advisor，而不是一个独立的Agent抽象。这也是它和LangGraph这类框架的根本区别：LangGraph把循环显式建模成图，节点加边；Spring AI把循环隐式塞进一个拦截器里，默认你看不见，但留了口子让你介入。</p><h2 id="%E6%8B%93%E6%89%91%E8%83%BD%E4%B8%8D%E8%83%BD%E8%87%AA%E5%AE%9A%E4%B9%89%EF%BC%9F%E8%83%BD%EF%BC%8C%E8%80%8C%E4%B8%94%E5%AE%98%E6%96%B9%E8%87%AA%E5%B7%B1%E5%B0%B1%E7%A4%BA%E8%8C%83%E8%BF%87" tabindex="-1">拓扑能不能自定义？能，而且官方自己就示范过</h2><h3 id="advisor%E9%A1%BA%E5%BA%8F%E4%BC%9A%E7%9C%9F%E5%AE%9E%E5%BD%B1%E5%93%8D%E8%A1%8C%E4%B8%BA" tabindex="-1">Advisor顺序会真实影响行为</h3><p><code>spring-ai-client-chat</code>自带内存管理、安全防护、日志记录几个Advisor；<code>spring-ai-rag</code>有做检索增强的Advisor；向量存储相关模块还有做问答和记忆管理的Advisor。这些都能自由插拔、自由排序。</p><p>顺序不是摆设。官方把记忆管理Advisor的默认优先级刻意排在<code>ToolCallingAdvisor</code>外面。记忆Advisor在更外层，只看得到「最终结果」，不会把工具调用循环里那几轮中间态的消息也存进聊天记忆。如果顺序反过来，聊天记忆里会混进一堆中间轮次的噪音，下次对话模型看到的上下文就是脏的。</p><h3 id="%E5%BE%AA%E7%8E%AF%E6%9C%AC%E8%BA%AB%E5%8F%AF%E4%BB%A5%E8%A2%AB%E5%AD%90%E7%B1%BB%E5%8C%96%E6%94%B9%E5%86%99" tabindex="-1">循环本身可以被子类化改写</h3><p><code>ToolCallingAdvisor</code>把循环的几个关键切入点设计成受保护的钩子方法，专门留给子类覆盖。官方自己就给了一个例子：一个继承出来的子类覆盖了其中两个钩子，每一轮调模型前先用关键词或向量检索，从整个工具库里挑出当下最相关的一小撮工具塞给模型，而不是无脑把全部工具定义都发过去。工具一多，光工具描述就能把上下文占满，模型选择也会变差。这个子类注册后会直接替换掉自动配置里默认的<code>ToolCallingAdvisor</code>。</p><p>拓扑到底能不能自定义，答案就在这：拓扑 = 你注册的Advisor集合 + 顺序 + 是否用自定义的循环子类，框架没有把这套东西封死。</p><h3 id="%E5%B7%A5%E5%85%B7%E6%9D%A5%E6%BA%90%E8%A2%AB%E7%BB%9F%E4%B8%80%E6%8A%BD%E8%B1%A1%EF%BC%8Cmcp%E5%92%8C%E6%9C%AC%E5%9C%B0%E6%96%B9%E6%B3%95%E6%B2%A1%E5%8C%BA%E5%88%AB" tabindex="-1">工具来源被统一抽象，MCP和本地方法没区别</h3><p>注解标记的方法、手写的工具回调，最终都被统一转成同一种工具抽象对象。MCP（Model Context Protocol）走另一条接入路径，但落地到同一个抽象：对<code>ToolCallingAdvisor</code>而言，MCP server上的远程工具和本地方法完全一样，都只是工具列表里的一项。区别在别处：MCP工具是跨进程调用，本地方法是进程内直接调用，延迟和失败模式不一样，出错处理要分开考虑。</p><h3 id="%E4%B8%80%E8%BD%AE%E5%A4%9A%E4%B8%AAtool_call%EF%BC%8C%E6%98%AF%E9%A1%BA%E5%BA%8F%E6%89%A7%E8%A1%8C%E4%B8%8D%E6%98%AF%E5%B9%B6%E5%8F%91" tabindex="-1">一轮多个tool_call，是顺序执行不是并发</h3><p>模型一轮确实可以同时吐出多个<code>tool_call</code>，这是主流模型API本身就支持的能力。但Spring AI这边是顺序跑完再拼一条汇总的响应消息，不是开线程池并发跑。如果工具之间互不依赖、又比较慢，这是个值得注意的性能点，目前得自己在工具实现里做并发，框架不替你做。</p><h2 id="%E6%B2%A1%E6%9C%89%E8%87%AA%E5%8A%A8%2F%E6%89%8B%E5%8A%A8%E4%B8%A4%E4%B8%AA%E5%BC%80%E5%85%B3%E4%BA%86%EF%BC%8C%E4%B9%9F%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%9C%80%E5%A4%A7%E8%BD%AE%E6%95%B0" tabindex="-1">没有自动/手动两个开关了，也没有内置的最大轮数</h2><p>1.x时代有个开关，控制「库自动帮你把循环跑完」还是「库只跑一轮、剩下的交还给你自己处理」。2.0里这个开关被彻底删掉了，升级说明里写明了移除原因。现在这两种模式变成了两条不同的调用路径，而不是一个配置项：默认路径走<code>ChatClient</code>加自动注册的<code>ToolCallingAdvisor</code>，循环全自动；手动路径绕开<code>ChatClient</code>，直接拿<code>ChatModel</code>调，框架不替你执行工具，得自己判断响应里有没有<code>tool_call</code>，自己拿工具调用管理器手写循环。</p><p>终止条件也不是「跑够N轮就停」，而是前面提到的那个判定器，默认逻辑是「响应不为空且带工具调用」。这个判定器可以自定义传入，官方升级说明里给的例子是叠加供应商的结束原因一起判断。</p><p>这里要特别提醒一句：如果模型一直吐<code>tool_call</code>，<code>ToolCallingAdvisor</code>的<code>do-while</code>理论上会一直转下去，没有内置的最大迭代次数保护。这不是框架的疏漏，是它把「什么时候该停」这个决定权完全交给了你自定义的判定器。生产环境这里必须自己加保护。自己实现一个判定器，内部包一个计数器，超过阈值就强制判定为不再调用工具，构造<code>ToolCallingAdvisor</code>（或者对应的自动配置）时传进去。不要假设框架默认值里有这个兜底。</p><h2 id="%E5%85%B3%E9%94%AE%E7%B1%BB%E9%80%9F%E6%9F%A5" tabindex="-1">关键类速查</h2><table><thead><tr><th>类</th><th>职责</th></tr></thead><tbody><tr><td><code>ChatClient</code> / <code>DefaultChatClient</code></td><td>fluent门面，<code>.prompt().call()/.stream()</code>入口</td></tr><tr><td><code>DefaultAroundAdvisorChain</code></td><td>责任链实现：<code>Deque.pop()</code>驱动，每步包一层可观测性埋点</td></tr><tr><td><code>ToolCallingAdvisor</code></td><td>ReAct循环本体，<code>do-while</code>驱动，非递归</td></tr><tr><td><code>ChatModelCallAdvisor</code> / <code>ChatModelStreamAdvisor</code></td><td>链的终结节点，真正调<code>ChatModel</code></td></tr><tr><td><code>ToolCallingManager</code></td><td>执行单次工具调用、拼装工具响应消息、判断是否直接返回</td></tr><tr><td><code>ChatModel</code>（如<code>DeepSeekChatModel</code>）</td><td>单次模型API调用，不含循环</td></tr><tr><td><code>ToolCallback</code> / <code>ToolCallbackProvider</code></td><td>工具统一抽象，本地方法和MCP工具都归一到这里</td></tr><tr><td><code>ToolExecutionEligibilityChecker</code></td><td>判定要不要继续循环，可自定义</td></tr></tbody></table><h2 id="%E5%86%99%E5%9C%A8%E6%9C%80%E5%90%8E" tabindex="-1">写在最后</h2><p>Spring AI没有把ReAct包装成一个显眼的Agent概念让你去学，它把循环压缩进了一个Advisor，默认情况下你甚至感觉不到自己在用Agent框架，只是在调一个会自动执行工具的聊天客户端。这种「藏起来」的设计对写业务代码的人友好，代价是调试的时候容易懵：断点打错层、以为循环丢了、不知道该去哪改最大轮数，都是这套设计换来的认知成本。搞清楚循环在哪、由谁驱动、什么时候停，这几个问题想明白了，Spring AI的工具调用机制就不再是一个黑盒。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[知识图谱与语义检索技术]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/zhi-shi-tu-pu-yu-yu-yi-jian-suo-ji-shu" />
                <id>tag:https://lymboy.com,2026-07-12:zhi-shi-tu-pu-yu-yu-yi-jian-suo-ji-shu</id>
                <published>2026-07-12T20:55:53+08:00</published>
                <updated>2026-07-12T20:55:53+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p01.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p02.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p03.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p04.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p05.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p06.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p07.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p08.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p09.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p10.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p11.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p12.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p13.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p14.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p15.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p16.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p17.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p18.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p19.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p20.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p21.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p22.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p23.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p24.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p25.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p26.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p27.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p28.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p29.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p30.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p31.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p32.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p33.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p34.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p35.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p36.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p37.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p38.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p39.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p40.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p41.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p42.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p43.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p44.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p45.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p46.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p47.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p48.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p49.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p50.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p51.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p52.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p53.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p54.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p55.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p56.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p57.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p58.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p59.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p60.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p61.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p62.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p63.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p64.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p65.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p66.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p67.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p68.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p69.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p70.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p71.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p72.jpg" alt="" /></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/p73.jpg" alt="" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[十二、Skywalking存储查询 DAO 实现]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/shi-er-skywalking-cun-chu-cha-xun-dao-shi-xian" />
                <id>tag:https://lymboy.com,2026-03-10:shi-er-skywalking-cun-chu-cha-xun-dao-shi-xian</id>
                <published>2026-03-10T22:11:35+08:00</published>
                <updated>2026-03-10T22:11:35+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E5%AD%98%E5%82%A8%E6%9F%A5%E8%AF%A2-dao-%E5%AE%9E%E7%8E%B0" tabindex="-1">存储查询 DAO 实现</h1><blockquote><p><strong>这一篇解决什么</strong><br />05-存储层 讲写入侧。这一篇讲查询侧：同一个&quot;读指标&quot;接口，BanyanDB / ES / JDBC 三个后端各用什么机制实现？BanyanDB 用 measure 倒排 + TopN 预聚合 + 原生 trace span 树；ES 用 multi-get 点查 + terms/avg 查询期聚合 + routing；JDBC 用主键 IN + SQL GROUP BY + tag 表 join。读完应该能回答：为什么 BanyanDB 查询最快、ES 次之、JDBC 最慢——以及这三套差别背后各自在押注什么。</p></blockquote><hr /><h2 id="%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%89%E7%A7%8D%E5%AD%98%E5%82%A8%E7%9A%84%E6%9F%A5%E8%AF%A2%E5%AE%9E%E7%8E%B0%E5%B7%AE%E5%88%AB%E8%BF%99%E4%B9%88%E5%A4%A7" tabindex="-1">为什么三种存储的查询实现差别这么大</h2><p>先把结论摆出来，再逐个看实现。</p><p>三个后端不是&quot;同一种查询的三种写法&quot;，而是<strong>三种存储哲学在三套查询语义上的投影</strong>。差别根源有三层：</p><ol><li><p><strong>数据是否原生支持时序预聚合</strong>。指标的本质是&quot;每个时间桶、每个实体一个值&quot;。BanyanDB 把这做成了一等公民（measure 模型，写入即按 <code>(time_bucket, entity_id)</code> 聚合），查询只是按 entity_id 倒排查一段时间的点。ES 和 JDBC 没有时序原生模型，靠把 <code>(time_bucket, entity_id)</code> 当作文档 ID / 主键来&quot;伪装&quot;成点查——查询时算出期望的几个桶 ID，直接按 ID 取回，不走聚合。这是三者指标查询性能接近、且都不在查询期聚合的原因。</p></li><li><p><strong>TopN 这类&quot;现算&quot;查询，谁来算、在哪算</strong>。BanyanDB 写入时就按 <code>bydb-topn.yaml</code> 规则把&quot;最忙的 N 个实体&quot;算好存起来，查询读预聚合结果；没有匹配规则时再退化到查询期 <code>meanBy + topN</code>。ES 没有预聚合，靠 <code>terms + avg</code> 聚合引擎现算。JDBC 也没有，还要按天分表，每张表各自取 TopN 再在 Java 端合并求平均——既慢又不精确。这一层拉开三档差距。</p></li><li><p><strong>trace 这种&quot;树形关联&quot;数据，存成什么样</strong>。一条 trace 由多个 span 组成，span 间有父子关系。BanyanDB 把 trace 当一等公民，span 嵌套存在 trace 记录里，一次查询出整棵树。ES 和 JDBC 没有 trace 原生模型，每个 segment（一段）是独立文档 / 行，靠 <code>trace_id</code> 字段关联，查一条 trace 要先捞所有 segment 再在应用层拼成树。这决定了 trace 查询的快慢和是否需要二次组装。</p></li></ol><p>一句话：<strong>BanyanDB 是为 SkyWalking 量身造的时序+链路库，押注&quot;贴近监控语义&quot;；ES 是租来的通用搜索引擎，押注&quot;倒排成熟、能全文搜&quot;；JDBC 是拿关系库硬扛，押注&quot;通用、好部署、不引新组件&quot;。</strong> 性能差距就是这三个押注的后果。</p><hr /><h2 id="%E5%87%A0%E4%B8%AA%E6%9C%AF%E8%AF%AD%EF%BC%8C%E5%85%88%E8%AF%B4%E6%B8%85%E6%A5%9A" tabindex="-1">几个术语，先说清楚</h2><p>后面反复用到，新人不熟的话先看这里。</p><ul><li><p><strong>倒排索引（inverted index）</strong>：正常查询是&quot;给一个文档，看它有哪些词&quot;；倒排是&quot;给一个词，看哪些文档含它&quot;。ES 和 BanyanDB 都给 <code>service_id</code>、<code>trace_id</code>、<code>tags</code> 这类可过滤字段建倒排表，值→文档列表。<code>term</code> 查询就是查这张表，复杂度 O(匹配数)，比扫全表快几个数量级。没有倒排的 JDBC 只能靠 B-tree 索引 + <code>WHERE</code> + join 模拟。</p></li><li><p><strong>multi-get（多点查）</strong>：一次请求按一组文档 ID / 主键取回多个文档。ES 的 <code>getClient().ids(...)</code>、JDBC 的 <code>where id in (?, ?, ...)</code> 都是 multi-get。适合&quot;我已经知道要哪几个 ID，直接拿&quot;的场景，不走聚合，代价 O(ID 数)。</p></li><li><p><strong>GROUP BY 聚合</strong>：SQL 的 <code>GROUP BY entity_id</code> 把同 entity 的多行折叠成一组，配合 <code>avg(value)</code> 求每组均值、<code>ORDER BY result DESC LIMIT N</code> 取 TopN。这是关系库的标准聚合手段，代价是扫表 + 排序。</p></li><li><p><strong>预聚合 vs 查询期聚合</strong>：预聚合是写入时就按规则算好结果存起来，查询直接读；要预先配规则，查询极快但不灵活。查询期聚合是查询来了才扫数据算，灵活但慢，数据量大时贵。</p></li><li><p><strong>routing（ES）</strong>：ES 写文档时可用一个 routing key 决定文档落哪个分片。SkyWalking 写 trace 用 traceId 作 routing，查询时也传 traceId，ES 就只查一个分片而不是全部分片广播——对按 traceId 点查，省掉 N-1 个分片的无效扫描。</p></li></ul><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/11-%E5%AD%98%E5%82%A8%E6%9F%A5%E8%AF%A2DAO%E5%AE%9E%E7%8E%B0.md_0.png" alt="图1" /></p><hr /><h2 id="%E6%9F%A5%E8%AF%A2%E5%B1%82-dao-%E6%8A%BD%E8%B1%A1%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" tabindex="-1">查询层 DAO 抽象解决什么问题</h2><p>查询层 DAO 抽象（<code>server-core</code> 里的 <code>I*QueryDAO</code> 接口）把&quot;读监控数据&quot;从具体存储引擎里剥出来。上游 GraphQL 查询服务（<code>QueryModule</code>）只认接口——“给我某指标在某时间段、按某实体的时序值”。至于这个值是靠 multi-get、ES terms 聚合、SQL GROUP BY 还是 BanyanDB measure 预聚合算出来，由各存储插件自己决定。这让 SkyWalking 不改查询业务逻辑，就能把同一套 UI 查询落到 ES / BanyanDB / 关系库上。</p><blockquote><p><strong>前置说明（修正任务清单两处与源码不符的表述）</strong></p><ul><li>接口名是 <strong><code>IRecordsQueryDAO</code></strong>（复数），源码中无 <code>IRecordQueryDAO</code>；<code>IMetricsQueryDAO</code> 里<strong>没有</strong> <code>readLabeledValues</code>，实际是 <code>readLabeledMetricsValues</code> / <code>readLabeledMetricsValuesWithoutEntity</code>。</li><li>ES 查询 DAO <strong>没有</strong>用 <code>date_histogram</code> 聚合。指标时序靠&quot;文档 ID 多点查&quot;实现，<code>terms</code>/<code>avg</code> 只用于 TopN 和拓扑。</li><li>PostgreSQL 特化 DAO <strong>没有</strong>用 PG 的 <code>jsonb</code>/数组类型（仓库内全局 grep <code>jsonb</code>/<code>array_agg</code>/<code>-&gt;&gt;</code>/<code>unnest</code> 零命中），只是 SQL 别名/子查询包装的方言适配。</li></ul></blockquote><hr /><h2 id="%E6%9F%A5%E8%AF%A2%E5%9E%8B-dao-%E6%8E%A5%E5%8F%A3%E6%80%BB%E8%A7%88%EF%BC%88server-core-%E5%AE%9A%E4%B9%89%EF%BC%89" tabindex="-1">查询型 DAO 接口总览（server-core 定义）</h2><p>接口目录：<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/storage/query/</code>。这些接口都继承 <code>DAO</code>（空标记接口）或 <code>Service</code>。每个接口带一组 <code>...Debuggable</code> 默认方法，包一层 <code>DebuggingTraceContext</code> 调试 span——查询层只关心它们最终委派的&quot;真&quot;方法。</p><h3 id="%E6%8E%A5%E5%8F%A3%E6%B8%85%E5%8D%95%E4%B8%8E%E6%96%B9%E6%B3%95%E7%AD%BE%E5%90%8D" tabindex="-1">接口清单与方法签名</h3><h4 id="imetricsquerydao-%E2%80%94-%E6%8C%87%E6%A0%87%E6%9F%A5%E8%AF%A2%EF%BC%88%E6%9C%80%E8%83%BD%E4%BD%93%E7%8E%B0%E4%B8%89%E5%90%8E%E7%AB%AF%E5%B7%AE%E5%BC%82%EF%BC%89" tabindex="-1">IMetricsQueryDAO — 指标查询（最能体现三后端差异）</h4><p><code>IMetricsQueryDAO.java:59</code>。查询侧最核心、也最能体现三后端差异的接口。</p><table><thead><tr><th>方法</th><th>行号</th><th>语义</th></tr></thead><tbody><tr><td><code>MetricsValues readMetricsValues(MetricsCondition, String valueColumnName, Duration)</code></td><td><code>:101</code></td><td>读单实体单指标的时序值（每个时间桶一个值）</td></tr><tr><td><code>List&lt;MetricsValues&gt; readLabeledMetricsValues(condition, valueColumnName, List&lt;KeyValue&gt; labels, Duration)</code></td><td><code>:105</code></td><td>读带标签的指标（一个指标多个 label 维度，值存在 <code>DataTable</code> 字符串列里）</td></tr><tr><td><code>List&lt;MetricsValues&gt; readLabeledMetricsValuesWithoutEntity(metricName, valueColumnName, labels, duration)</code></td><td><code>:113</code></td><td>不指定实体，用来取 label 元数据（限 10 条，常量 <code>METRICS_VALUES_WITHOUT_ENTITY_LIMIT=10</code> <code>:60</code>）</td></tr><tr><td><code>List&lt;String&gt; listEntityIdsInRange(metricName, valueColumnName, valueType, duration, limit)</code></td><td><code>:163</code></td><td>10.5.0 新增，列出某指标在时间区间内有过值的去重 <code>entity_id</code>，供 admin 的 <code>/inspect/entities</code> 用，按最近时间优先</td></tr><tr><td><code>HeatMap readHeatMap(condition, valueColumnName, duration)</code></td><td><code>:169</code></td><td>读热力图（直方图指标，值是 <code>HistogramMetrics.DATASET</code>）</td></tr></tbody></table><p>内嵌 <code>Util</code> 类（<code>:171</code>）做 label 拼装/排序/补默认值，是跨后端共享的纯计算逻辑——三个后端都调它，避免重复实现。</p><h4 id="iaggregationquerydao-%E2%80%94-topn-%E6%8E%92%E5%BA%8F%E6%9F%A5%E8%AF%A2" tabindex="-1">IAggregationQueryDAO — TopN 排序查询</h4><p><code>IAggregationQueryDAO.java:36</code>。问&quot;哪些实体最慢/最忙&quot;。</p><table><thead><tr><th>方法</th><th>行号</th><th>��义</th></tr></thead><tbody><tr><td><code>List&lt;SelectedRecord&gt; sortMetrics(TopNCondition, String valueColumnName, Duration, List&lt;KeyValue&gt; additionalConditions)</code></td><td><code>:56</code></td><td>按 value 列对实体排序取 TopN。接口注释明确：“基于存储侧聚合，大部分存储支持 groupby/aggregation”</td></tr></tbody></table><h4 id="itracequerydao-%2F-itracequeryv2dao-%E2%80%94-%E9%93%BE%E8%B7%AF%E8%BF%BD%E8%B8%AA%E6%9F%A5%E8%AF%A2" tabindex="-1">ITraceQueryDAO / ITraceQueryV2DAO — 链路追踪查询</h4><p><code>ITraceQueryDAO.java:35</code> / <code>ITraceQueryV2DAO.java:31</code>。<code>ITraceQueryV2DAO extends ITraceQueryDAO</code>（V2 是 BanyanDB 用的新版，V1 是 ES/JDBC 用的旧版）。</p><table><thead><tr><th>方法</th><th>行号</th><th>语义</th></tr></thead><tbody><tr><td><code>TraceBrief queryBasicTraces(duration, minDuration, maxDuration, serviceId, serviceInstanceId, endpointId, traceId, limit, from, TraceState, QueryOrder, List&lt;Tag&gt; tags)</code></td><td><code>ITraceQueryDAO.java:114</code></td><td>按 trace 列表查询</td></tr><tr><td><code>List&lt;SegmentRecord&gt; queryByTraceId(String traceId, @Nullable Duration)</code></td><td><code>:130</code></td><td>按 traceId 取该 trace 下所有 segment 记录。<code>duration</code> 注释明确&quot;nullable unless for BanyanDB query from cold stage&quot;</td></tr><tr><td><code>List&lt;SegmentRecord&gt; queryBySegmentIdList(List&lt;String&gt;, Duration)</code></td><td><code>:135</code></td><td></td></tr><tr><td><code>List&lt;SegmentRecord&gt; queryByTraceIdWithInstanceId(List&lt;String&gt;, List&lt;String&gt;, Duration)</code></td><td><code>:140</code></td><td></td></tr><tr><td><code>List&lt;Span&gt; doFlexibleTraceQuery(String traceId)</code></td><td><code>:145</code></td><td>给&quot;没有 segment 概念的第三方 trace&quot;用</td></tr><tr><td>V2：<code>List&lt;SpanWrapper&gt; queryByTraceIdV2(traceId, duration)</code></td><td><code>ITraceQueryV2DAO.java:54</code></td><td></td></tr><tr><td>V2：<code>TracesQueryResult queryTraces(TraceQueryCondition)</code></td><td><code>:56</code></td><td>条件对象化、返回带 <code>RetrievedTimeRange</code></td></tr></tbody></table><h4 id="itopologyquerydao-%E2%80%94-%E6%8B%93%E6%89%91%2F%E8%B0%83%E7%94%A8%E5%85%B3%E7%B3%BB%E6%9F%A5%E8%AF%A2" tabindex="-1">ITopologyQueryDAO — 拓扑/调用关系查询</h4><p><code>ITopologyQueryDAO.java:33</code>。服务、实例、端点、进程四层关系，分 client/server 两端检测。</p><table><thead><tr><th>方法</th><th>行号</th><th>语义</th></tr></thead><tbody><tr><td><code>loadServiceRelationsDetectedAtServerSide(Duration, List&lt;String&gt; serviceIds)</code> / <code>...ClientSide(...)</code></td><td><code>:190</code>/<code>:196</code></td><td></td></tr><tr><td>全局版（不带 serviceIds）</td><td><code>:202</code>/<code>:207</code></td><td></td></tr><tr><td><code>loadInstanceRelationDetectedAtServerSide(clientServiceId, serverServiceId, Duration)</code> / <code>...ClientSide(...)</code></td><td><code>:213</code>/<code>:221</code></td><td></td></tr><tr><td><code>loadEndpointRelation(Duration, destEndpointId)</code></td><td><code>:228</code></td><td></td></tr><tr><td><code>loadProcessRelationDetectedAtClientSide(serviceInstanceId, Duration)</code> / <code>...ServerSide(...)</code></td><td><code>:235</code>/<code>:242</code></td><td></td></tr></tbody></table><h4 id="ilogquerydao-%E2%80%94-%E6%97%A5%E5%BF%97%E6%9F%A5%E8%AF%A2" tabindex="-1">ILogQueryDAO — 日志查询</h4><p><code>ILogQueryDAO.java:40</code>。</p><table><thead><tr><th>方法</th><th>行号</th><th>语义</th></tr></thead><tbody><tr><td><code>boolean supportQueryLogsByKeywords()</code></td><td><code>:42</code></td><td>默认 <code>false</code>，只有 ES 翻成 <code>true</code>（<code>LogQueryEsDAO.java:59</code>），表示该后端支持按日志正文关键词检索</td></tr><tr><td><code>Logs queryLogs(serviceId, serviceInstanceId, endpointId, TraceScopeCondition relatedTrace, Order, from, limit, Duration, List&lt;Tag&gt; tags, List&lt;String&gt; keywordsOfContent, List&lt;String&gt; excludingKeywordsOfContent)</code></td><td><code>:87</code></td><td>日志可关联 trace、按 tag 过滤、按正文关键词（含/排除）检索</td></tr><tr><td>默认方法 <code>parserDataBinary(...)</code></td><td><code>:102</code>/<code>:106</code></td><td>把 base64 的 <code>LogTags</code> protobuf 反序列化成 <code>KeyValue</code> 列表——跨后端共享</td></tr></tbody></table><h4 id="%E5%85%B6%E4%BB%96%E6%9F%A5%E8%AF%A2%E6%8E%A5%E5%8F%A3%E9%80%9F%E8%A7%88" tabindex="-1">其他查询接口速览</h4><table><thead><tr><th>接口</th><th>行号</th><th>语义</th></tr></thead><tbody><tr><td><code>IAlarmQueryDAO</code></td><td><code>IAlarmQueryDAO.java:49</code></td><td>告警查询，<code>getAlarm</code>（旧）+ <code>queryAlarms</code>（11.0.0 新，支持 entity/layer/ruleName 过滤）</td></tr><tr><td><code>IBrowserLogQueryDAO</code></td><td><code>IBrowserLogQueryDAO.java:30</code></td><td>浏览器错误日志，<code>queryBrowserErrorLogs</code></td></tr><tr><td><code>IEventQueryDAO</code></td><td><code>IEventQueryDAO.java:27</code></td><td>事件查询，<code>queryEvents</code>，<code>MAX_SIZE = 100</code></td></tr><tr><td><code>ITagAutoCompleteQueryDAO</code></td><td><code>ITagAutoCompleteQueryDAO.java:27</code></td><td>标签自动补全，<code>queryTagAutocompleteKeys</code>/<code>Values</code></td></tr><tr><td><code>IHierarchyQueryDAO</code></td><td><code>IHierarchyQueryDAO.java:26</code></td><td>层级关系查询，<code>readAllServiceHierarchyRelations</code>/<code>readInstanceHierarchyRelations</code></td></tr><tr><td><code>IRecordsQueryDAO</code>（复数）</td><td><code>IRecordsQueryDAO.java:37</code></td><td>慢查询记录/TopN 采样记录，<code>readRecords</code></td></tr><tr><td><code>IZipkinQueryDAO</code></td><td><code>IZipkinQueryDAO.java:33</code></td><td>Zipkin 兼容查询，<code>getServiceNames</code>/<code>getTrace</code>/<code>getTraces</code></td></tr></tbody></table><h3 id="%E6%8E%A5%E5%8F%A3%E5%B1%82%E7%9A%84%E5%85%B1%E6%80%A7%E8%AE%BE%E8%AE%A1" tabindex="-1">接口层的共性设计</h3><blockquote><p><strong>三个共性</strong></p><ol><li><strong><code>...Debuggable</code> 默认方法模式</strong>：每个查询方法都有一个同名 <code>...Debuggable</code> 默认方法，先 <code>DebuggingTraceContext.TRACE_CONTEXT.get()</code>，开了调试就 <code>createSpan</code> 并把入参拼进 msg，再委派给真方法。调试埋点和业务实现解耦——子类只实现真方法，调试能力由接口默认方法统一注入。</li><li><strong><code>Duration</code> 贯穿所有接口</strong>：<code>Duration</code> 封装起止时间戳 + 步长（<code>DownSampling</code>：minute/hour/day）+ 冷数据标记（<code>isColdStage()</code>）。所有时间范围查询走 <code>Duration</code>，不直接传 long 时间戳，便于三后端统一处理冷热分层。</li><li><strong><code>@Nullable Duration</code> 的约定</strong>：trace 相关方法对 <code>duration</code> 标了 <code>@Nullable</code>，注释反复出现&quot;nullable unless for BanyanDB query from cold stage&quot;——只有 BanyanDB 冷阶段查询需要显式 duration，其余后端用默认最近 24h。</li></ol></blockquote><hr /><h2 id="banyandb-%E6%9F%A5%E8%AF%A2%E5%AE%9E%E7%8E%B0" tabindex="-1">BanyanDB 查询实现</h2><p>BanyanDB 是 SkyWalking 自研的时序数据库，查询 DAO 散落在 <code>storage-banyandb-plugin</code> 下按数据模型分包：<code>measure/</code>（指标类）、<code>stream/</code>（记录类：log/alarm/事件/profile）、<code>trace/</code>（原生 trace 模型，span 嵌套存储）、顶层 <code>BanyanDBAggregationQueryDAO</code>/<code>BanyanDBRecordsQueryDAO</code>。</p><h3 id="%E6%9F%A5%E8%AF%A2%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD-abstractbanyandbdao" tabindex="-1">查询基础设施 AbstractBanyanDBDAO</h3><p><code>oap-server/server-storage-plugin/storage-banyandb-plugin/.../stream/AbstractBanyanDBDAO.java:60</code>。所有 BanyanDB 查询 DAO 的父类，封装四种查询类型：</p><table><thead><tr><th>方法</th><th>查询类型</th><th>用途</th><th>行号</th></tr></thead><tbody><tr><td><code>query(...)</code> / <code>queryDebuggable(...)</code>（StreamQuery）</td><td><code>StreamQuery</code></td><td>stream 模型查询（log/alarm/record）</td><td><code>:73</code>/<code>:111</code></td></tr><tr><td><code>query(...)</code> / <code>queryDebuggable(...)</code>（MeasureQuery）</td><td><code>MeasureQuery</code></td><td>measure 模型查询（指标）</td><td><code>:271</code>/<code>:232</code></td></tr><tr><td><code>topNQuery(...)</code> / <code>topNQueryDebuggable(...)</code></td><td><code>TopNQuery</code></td><td>服务端预聚合 TopN</td><td><code>:196</code>/<code>:150</code></td></tr><tr><td><code>queryTrace(...)</code> / <code>queryTraceDebuggable(...)</code></td><td><code>TraceQuery</code></td><td>原生 trace 模型查询</td><td><code>:349</code>/<code>:310</code></td></tr></tbody></table><p>内嵌 <code>QueryBuilder&lt;T&gt;</code> 抽象类（<code>:399</code>）提供条件构造器：<code>eq</code>/<code>ne</code>/<code>gt</code>/<code>gte</code>/<code>lte</code>/<code>in</code>/<code>notIn</code>/<code>match</code>/<code>having</code>，以及 <code>and(List)</code>/<code>or(List)</code> 组合。其中 <strong><code>having(String name, List&lt;String&gt; value)</code></strong>（<code>:406</code>，底层 <code>StringArrayQueryCondition.having</code>）是 BanyanDB 专门用于&quot;在标签数组里包含某 key=value&quot;的原生算子——这是 BanyanDB 倒排索引查 tag 的方式，区别于 ES 的 <code>term</code> 查 <code>tags</code> 字段。</p><p><code>getTimestampRange(Duration)</code>（<code>:478</code>）：<code>duration==null</code> 默认取最近 24 小时。</p><p>冷数据支持：每个查询类型都有 <code>if (isColdStage) query.setStages(Set.of(&quot;cold&quot;))</code>（如 <code>:99</code>、<code>:298</code>、<code>:359</code>），把查询路由到 BanyanDB 冷阶段存储。</p><h3 id="banyandbmetricsquerydao%EF%BC%88measure-%E6%9F%A5%E8%AF%A2%EF%BC%89" tabindex="-1">BanyanDBMetricsQueryDAO（measure 查询）</h3><p><code>oap-server/server-storage-plugin/storage-banyandb-plugin/.../measure/BanyanDBMetricsQueryDAO.java:54</code>。实现 <code>IMetricsQueryDAO</code>。所有方法都用 <strong><code>MeasureQuery</code></strong>。</p><ul><li><strong><code>readMetricsValues</code></strong>（<code>:60</code>）：<ol><li><code>MetadataRegistry.INSTANCE.findMetricMetadata(modelName, step)</code> 拿到 measure schema（<code>:65</code>，注释提到对 foreign 指标会合成只读 schema）。</li><li><code>queryByEntityID(schema, valueColumnName, duration, entityID)</code>（<code>:261</code>）发起 <code>MeasureQuery</code>，条件是 <code>query.and(eq(Metrics.ENTITY_ID, entityID))</code>（<code>:267</code>）——<strong>按 entity_id 倒排过滤</strong>，project 出 <code>valueColumnName</code> 字段。</li><li>把返回的 <code>DataPoint</code> 按 <code>timeBucket</code> 映射，对 <code>duration.assembleDurationPoints()</code> 期望的每个时间桶补默认值（<code>:78-90</code>）。</li></ol></li></ul><blockquote><p><strong>measure 查询不聚合</strong><br />这里没有聚合。measure 在写入时已经按 <code>(time_bucket, entity_id)</code> 预聚合好，查询就是按 entity_id 倒排查一个时间段的点。BanyanDB 的 measure 本身就是&quot;时序指标&quot;语义，倒排索引让&quot;按 entity 过滤&quot;是 O(匹配数) 而非 O(全表)。</p></blockquote><ul><li><strong><code>readLabeledMetricsValues</code></strong>（<code>:108</code>）：同样 <code>queryByEntityID</code>，但取出 <code>DataTable</code> 字符串列（<code>:122</code>），再交给跨后端共享的 <code>Util.composeLabelValue</code>/<code>Util.sortValues</code> 做 label 拆装。</li><li><strong><code>listEntityIdsInRange</code></strong>（<code>:173</code>）：<code>MeasureQuery</code> project 出 <code>ENTITY_ID</code> tag + <code>valueColumnName</code>，<code>OrderBy DESC</code>（<code>:206</code>）+ <code>limit</code>，返回后用 <code>LinkedHashSet</code> 去重保序（<code>:217</code>）。foreign 指标走 <code>synthesizeForeignMetricSchema</code>（<code>:183</code>）。</li><li><strong><code>readHeatMap</code></strong>（<code>:225</code>）：取 <code>HistogramMetrics.DATASET</code> 字段，<code>heatMap.buildColumn</code> 解析。</li></ul><blockquote><p><strong>别混了 BanyanDBMetricsDAO 与 BanyanDBMetricsQueryDAO</strong><br /><code>BanyanDBMetricsDAO</code>（<code>measure/BanyanDBMetricsDAO.java:58</code>）实现的是 <code>IMetricsDAO</code>（<strong>写入/读缓存侧</strong>的 <code>multiGet</code>/<code>prepareBatchInsert</code>），不是查询侧的 <code>IMetricsQueryDAO</code>。两者名字相似但职责不同——一个给 OAL 流处理 worker 读缓存写指标用，一个给 GraphQL 查询读指标展示用。新手最容易混这点。</p></blockquote><h3 id="banyandbaggregationquerydao%EF%BC%88topn%EF%BC%9A%E9%A2%84%E8%81%9A%E5%90%88-%2B-%E6%9F%A5%E8%AF%A2%E6%9C%9F%E8%81%9A%E5%90%88%E5%8F%8C%E8%B7%AF%E5%BE%84%EF%BC%89" tabindex="-1">BanyanDBAggregationQueryDAO（TopN：预聚合 + 查询期聚合双路径）</h3><p><code>oap-server/server-storage-plugin/storage-banyandb-plugin/.../BanyanDBAggregationQueryDAO.java:51</code>。实现 <code>IAggregationQueryDAO</code>。<code>sortMetrics</code>（<code>:59</code>）有<strong>两条路径</strong>：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/11-%E5%AD%98%E5%82%A8%E6%9F%A5%E8%AF%A2DAO%E5%AE%9E%E7%8E%B0.md_1.png" alt="图2" /></p><p><strong>路径 A：服务端预聚合 TopN（<code>serverSideTopN</code> <code>:122</code>）</strong><br />schema 里定义了匹配当前查询 tag 组合的 <code>TopNAggregation</code>（<code>:91-92</code>，从 <code>bydb-topn.yaml</code> 配置，由 <code>MetadataRegistry.parseTopNSpecs</code> 解析，见 <code>MetadataRegistry.java:330</code>）时，走 <code>topNQueryDebuggable</code>（<code>:125</code>）。这是 BanyanDB 服务端预先聚合好的 TopN，查询只读结果，极快。<code>TopNQuery</code> 设 <code>Aggregation.Type.MEAN</code>（<code>AbstractBanyanDBDAO.java:208</code>）。</p><p><strong>路径 B：查询期聚合 TopN（<code>directMetricsTopN</code> <code>:143</code>）</strong><br />没有匹配的预聚合规则时，发 <code>MeasureQuery</code>：</p><pre><code class="language-">query.meanBy(valueColumnName, ImmutableSet.of(Metrics.ENTITY_ID));  // :154 按entity分组求均值query.topN(condition.getTopN(), valueColumnName);  // 或 bottomN，:156/158</code></pre><p>查询期在 measure 上做 group-by + mean + topN，由 BanyanDB 引擎现算。</p><blockquote><p><strong>TopN 预聚合 vs 查询期聚合</strong></p><ul><li><strong>预聚合 TopN</strong>：写入时数据库就按规则把&quot;最忙的 N 个实体&quot;算好存起来，查询时直接读结果，适合高频固定看板，但要预先在 <code>bydb-topn.yaml</code> 配规则。改规则不影响历史数据（历史按旧规则算好的已落盘）。</li><li><strong>查询期聚合</strong>：查询来了才扫数据算 group-by + 排序 + 取 N，灵活但慢，数据量大时贵。</li></ul><p>BanyanDB 两者都支持，先试预聚合、不行再查询期——这是它比 ES / JDBC 优越的核心点。代价是要维护 <code>bydb-topn.yaml</code>，规则没配就退化到查询期。</p></blockquote><h3 id="banyandbtopologyquerydao%EF%BC%88measure-group-by-tag%EF%BC%89" tabindex="-1">BanyanDBTopologyQueryDAO（measure group by tag）</h3><p><code>oap-server/server-storage-plugin/storage-banyandb-plugin/.../measure/BanyanDBTopologyQueryDAO.java:55</code>。实现 <code>ITopologyQueryDAO</code>。关系指标（service/instance/endpoint/process relation）也是 measure。</p><ul><li><code>loadServiceRelationsDetectedAtServerSide</code>（<code>:62</code>）→ <code>queryServiceRelation</code>（<code>:106</code>）：<code>MeasureQuery</code> project 出 <code>ENTITY_ID</code> + <code>COMPONENT_IDS</code>，条件 <code>query.or(in(SOURCE_SERVICE_ID, serviceIds)).or(in(DEST_SERVICE_ID, serviceIds))</code>（<code>:99-100</code>）+ <strong><code>query.groupBy({ENTITY_ID, COMPONENT_IDS})</code></strong>（<code>:101</code>）。</li><li>实例关系（<code>buildInstanceRelationsQuery</code> <code>:155</code>）：用 <code>and</code>/<code>or</code> 组合双向条件（source=client AND dest=server）OR（dest=client AND source=server），<code>query.criteria(or(...))</code>（<code>:175</code>）。</li><li>endpoint/进程关系类似，都靠 <code>groupBy</code> tag 去重。</li></ul><blockquote><p><strong>group by tag 去重</strong><br />BanyanDB 的 <code>groupBy(tag set)</code> 是原生 measure 操作，按 tag 维度折叠数据点。拓扑查询本质就是&quot;在这段时间内出现过哪些关系实体&quot;，group by entity_id 天然去重——同一关系实体出现多少次都被折成一行。这比 ES 的 <code>terms</code> 聚合去重更直接，因为 group by 是 measure 的原生命令。</p></blockquote><h3 id="banyandbtracequerydao%EF%BC%88%E5%8E%9F%E7%94%9F-trace-%E6%A8%A1%E5%9E%8B%E2%80%94%E2%80%94span-%E5%B5%8C%E5%A5%97%E5%AD%98%E5%82%A8%EF%BC%89" tabindex="-1">BanyanDBTraceQueryDAO（原生 trace 模型——span 嵌套存储）</h3><p><code>oap-server/server-storage-plugin/storage-banyandb-plugin/.../trace/BanyanDBTraceQueryDAO.java:59</code>。三后端里 trace 查询差异最大的一处。该类实现 <code>ITraceQueryV2DAO</code>（V2），但通过 <code>BanyanDBStorageProvider</code> 注册为 <code>ITraceQueryDAO.class</code>（<code>BanyanDBStorageProvider.java:173</code>，因为 V2 extends V1）。</p><ul><li><p><strong>旧 V1 方法直接抛 <code>UnsupportedOperationException</code></strong>：<code>queryBasicTraces</code>（<code>:71</code>，提示&quot;BanyanDB Trace Model changed, please use queryTraces&quot;）、<code>queryByTraceId</code>（<code>:76</code>，提示&quot;please use queryByTraceIdV2&quot;）。BanyanDB 升级了 trace 数据模型，不再支持按 segment 列表查。</p></li><li><p><strong><code>queryTraces(TraceQueryCondition)</code></strong>（<code>:148</code>）—— V2 主查询：用 <strong><code>TraceQuery</code></strong>（BanyanDB 原生 trace 查询 API）。条件用 <code>eq</code>/<code>gte</code>/<code>lte</code> 拼装（traceId、latency 区间、serviceId、instanceId、endpointId、isError），排序 <code>OrderBy(START_TIME/LATENCY, DESC)</code>，<strong>tag 查询用 <code>having(SegmentRecord.TAGS, tagsConditions)</code></strong>（<code>:211</code>）。返回 <code>TraceQueryResponse</code>，结构是 <strong><code>resp.getTraces()</code> 返回 trace 列表，每个 trace 的 <code>getSpansList()</code> 直接给出该 trace 下所有 span</strong>（<code>:222</code>、<code>:141</code>）。</p></li><li><p><strong><code>queryByTraceIdV2</code></strong>（<code>:123</code>）：<code>query.and(eq(TRACE_ID, traceId))</code>，返回单个 trace 的所有 <code>SpanWrapper</code>（<code>:140-145</code>，从 <code>resp.getTraces().get(0).getSpansList()</code> 解析）。返回超过 1 个 trace 抛异常（<code>:137</code>）。</p></li></ul><blockquote><p><strong>trace 的 span 关系查询</strong><br />一条 trace 由多个 span 组成，span 之间有父子关系。三后端存法不同：</p><ul><li><strong>BanyanDB</strong>：trace 是一等公民，span 嵌套存在 trace 记录里。<code>TraceQuery</code> 一次查出来就是&quot;trace→spans 列表&quot;的树状结构，<strong>不用二次拼装</strong>。这也是旧 V1（按 segment 列表查）被废弃的原因——segment 概念在 BanyanDB trace 模型里被 span 嵌套取代。</li><li><strong>ES / JDBC</strong>：trace 没有原生模型，每个 segment 是独立文档/行，靠 <code>trace_id</code> 字段关联。查一条 trace 要先按 <code>trace_id</code> 捞出所有 segment 文档，再在应用层拼成 span 树。</li></ul></blockquote><ul><li><strong><code>buildRecords</code></strong>（<code>:235</code>）：把 trace 查询结果转成 <code>SegmentRecord</code>（给 profile 等老接口用），从 <code>SpanWrapper</code> 解析 <code>SegmentObject</code> protobuf，重新算 serviceId/instanceId/endpointId/latency。注释（<code>:232</code>）说明&quot;only build the fields needed by ProfiledTraceSegments&quot;。</li></ul><h3 id="%E5%85%B6%E4%BB%96-banyandb-%E6%9F%A5%E8%AF%A2-dao" tabindex="-1">其他 BanyanDB 查询 DAO</h3><table><thead><tr><th>DAO</th><th>实现</th><th>数据模型</th><th>tag 查询</th></tr></thead><tbody><tr><td><code>BanyanDBLogQueryDAO</code></td><td><code>ILogQueryDAO</code></td><td>stream</td><td><code>having(LogRecord.TAGS, tagsConditions)</code>，<code>supportQueryLogsByKeywords</code> 默认 false（不支持正文关键词检索）</td></tr><tr><td><code>BanyanDBAlarmQueryDAO</code></td><td><code>IAlarmQueryDAO</code></td><td>stream</td><td><code>StreamQuery</code></td></tr><tr><td><code>BanyanDBRecordsQueryDAO</code></td><td><code>IRecordsQueryDAO</code></td><td>stream</td><td><code>StreamQuery</code> + <code>eq(ENTITY_ID)</code> + <code>OrderBy(valueColumnName)</code> + <code>limit</code></td></tr><tr><td><code>BanyanDBTagAutocompleteQueryDAO</code></td><td><code>ITagAutoCompleteQueryDAO</code></td><td>measure</td><td><code>query.groupBy({TAG_KEY})</code>/<code>groupBy({TAG_VALUE})</code> + <code>eq(TAG_TYPE)</code> 取去重 key/value</td></tr><tr><td><code>BanyanDBBrowserLogQueryDAO</code> / <code>BanyanDBEventQueryDAO</code> / <code>BanyanDBZipkinQueryDAO</code> / <code>BanyanDBHierarchyQueryDAO</code> / <code>BanyanDBMetadataQueryDAO</code></td><td>各自接口</td><td>按 stream/measure 选</td><td>同上</td></tr></tbody></table><h3 id="banyandb-%E6%9F%A5%E8%AF%A2%E7%89%B9%E7%82%B9%E5%B0%8F%E7%BB%93" tabindex="-1">BanyanDB 查询特点小结</h3><ol><li><strong>善用原生能力</strong>：measure 查询（预聚合时序点）、<code>TopNAggregation</code>（服务端预聚合 TopN）、<code>groupBy</code> tag（去重/折叠）、<code>having</code>（标签数组倒排查）、<code>TraceQuery</code>（原生 trace span 树）。</li><li><strong>冷热分层原生支持</strong>：所有查询带 <code>isColdStage</code> → <code>setStages(&quot;cold&quot;)</code>。</li><li><strong>V2 trace 模型</strong>：废弃 segment 列表查法，span 嵌套存储，一次查询出整棵 span 树。</li><li><strong>schema 驱动</strong>：所有查询先 <code>MetadataRegistry.findMetricMetadata/findRecordMetadata</code> 拿 schema，再按 schema 的 tag/field 构建 query——schema 不存在直接抛异常。foreign 指标有 <code>synthesizeForeignMetricSchema</code> 合成只读 schema 的能力。</li></ol><hr /><h2 id="elasticsearch-%E6%9F%A5%E8%AF%A2%E5%AE%9E%E7%8E%B0" tabindex="-1">Elasticsearch 查询实现</h2><p>查询 DAO 集中在 <code>oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/</code>，都继承 <code>EsDAO</code>。</p><h3 id="metricsqueryesdao%EF%BC%88%E6%8C%89%E6%96%87%E6%A1%A3-id-%E5%A4%9A%E7%82%B9%E6%9F%A5%E2%80%94%E2%80%94%E4%B8%8D%E5%81%9A%E8%81%9A%E5%90%88%EF%BC%89" tabindex="-1">MetricsQueryEsDAO（按文档 ID 多点查——不做聚合）</h3><p><code>oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/MetricsQueryEsDAO.java:59</code>。实现 <code>IMetricsQueryDAO</code>。理解 ES 指标查询的关键：ES 指标查询<strong>不用 ES 聚合</strong>，而是按文档 ID 多点查（multi-get）。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/11-%E5%AD%98%E5%82%A8%E6%9F%A5%E8%AF%A2DAO%E5%AE%9E%E7%8E%B0.md_2.png" alt="图3" /></p><ul><li><strong><code>readMetricsValues</code></strong>（<code>:66</code>）：<ol><li><code>duration.assembleDurationPoints()</code> 算出期望的时间桶列表（<code>:82</code>）。</li><li>对每个时间桶，用 <code>pointOfTime.id(entityId)</code> 拼出<strong>文档 ID</strong>（<code>:86</code>）。开了 merged table（多指标合并索引）时，用 <code>IndexController.generateDocId(metricName, id)</code> 给 ID 加指标名前缀（<code>:88</code>）。</li><li>按 <code>TimeSeriesUtils.queryIndexName</code> 把文档 ID 分组到对应时间分片索引（<code>:90-92</code>），得到 <code>indexIdsGroup: Map&lt;indexName, List&lt;docId&gt;&gt;</code>。</li><li><strong><code>idsDebuggable(indexIdsGroup)</code></strong>（<code>:97</code>）——调用 ES 的 <code>multi-get</code> API（<code>getClient().ids(...)</code>）按文档 ID 批量取文档。</li><li>取到的文档按 ID 取 <code>realValueColumn</code> 字段值，缺失补默认值，再 <code>Util.sortValues</code> 按时间桶顺序排好。</li></ol></li></ul><blockquote><p><strong>为什么 ES 不用 date_histogram</strong><br />SkyWalking 的指标在写入时已经按 <code>(time_bucket, entity_id)</code> 预聚合好，且把这个组合作为文档 ID。查询时只要算出期望的几个时间桶 ID，直接 multi-get 取回——这比 <code>date_histogram</code> 聚合便宜得多（O(桶数) 的点查 vs O(扫描全量) 的聚合）。<code>terms</code>/<code>avg</code> 聚合只用在 TopN 和拓扑。换句话说，ES 在指标查询上&quot;假装&quot;自己是 KV 存储，绕开了聚合引擎。</p></blockquote><ul><li><strong><code>readLabeledMetricsValues</code></strong>（<code>:127</code>）：同样 multi-get，取回 <code>DataTable</code> 字符串列。</li><li><strong><code>listEntityIdsInRange</code></strong>（<code>:219</code>）：这里<strong>用 <code>Search</code> + <code>range(TIME_BUCKET)</code> + <code>sort(TIME_BUCKET, DESC)</code> + <code>size(limit)</code></strong>（<code>:246-260</code>），扫最近 N 行再客户端 <code>LinkedHashSet</code> 去重 entity_id（<code>:267</code>）。foreign 指标走合并索引 <code>METRICS_LOGIC_TABLE_NAME</code> + <code>term(METRIC_TABLE_NAME, metricName)</code> 判别（<code>:240-258</code>）。</li><li><strong><code>readHeatMap</code></strong>（<code>:278</code>）：同样 multi-get 取 <code>dataset</code> 字段。</li><li><strong><code>buildQuery</code></strong>（<code>:322</code>，protected 辅助）：构造 <code>range(TIME_BUCKET)</code> + <code>terms(ENTITY_ID)</code> + 可选 <code>term(METRIC_TABLE_NAME)</code> 的 bool query，但 <code>size(0)</code>（<code>:361</code>）——只用于计数场景，不取文档。</li></ul><blockquote><p><strong>merged table 概念</strong><br />ES 默认把多个指标类型合并到一个逻辑索引（<code>METRICS_LOGIC_TABLE_NAME</code>），用 <code>metric_table</code> 字段区分；也支持 <code>logicSharding</code>（按指标 stream 类分索引）。<code>IndexController.LogicIndicesRegister.getPhysicalTableName/isMergedTable/getPhysicalColumnName</code> 处理这层映射。foreign 指标在 logicSharding 下无法解析物理索引（索引名来自本节点没有的 stream 类），所以 <code>:73</code>/<code>:135</code>/<code>:234</code> 直接拒绝。</p></blockquote><h3 id="tracequeryesdao%EF%BC%88%E5%80%92%E6%8E%92-%2B-%E6%97%B6%E9%97%B4%E8%8C%83%E5%9B%B4-%2B-routing%EF%BC%89" tabindex="-1">TraceQueryEsDAO（倒排 + 时间范围 + routing）</h3><p><code>oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/TraceQueryEsDAO.java:58</code>。实现 <code>ITraceQueryDAO</code>（V1，非 V2——ES 没有 V2 trace 模型）。注册为 <code>ITraceQueryDAO.class</code>（<code>StorageModuleElasticsearchProvider.java:236</code>）。</p><ul><li><p><strong><code>queryBasicTraces</code></strong>（<code>:68</code>）：构造 <code>BoolQueryBuilder</code>：</p><ul><li><code>term(RECORD_TABLE_NAME, INDEX_NAME)</code> 若 merged（<code>:88</code>）</li><li><code>range(TIME_BUCKET).gte().lte()</code> 时间范围（<code>:92</code>）</li><li><code>range(LATENCY).gte().lte()</code> 耗时区间（<code>:96-103</code>）</li><li><code>term(SERVICE_ID)</code>/<code>term(SERVICE_INSTANCE_ID)</code>/<code>term(ENDPOINT_ID)</code>/<code>term(TRACE_ID)</code>（<code>:106-116</code>）——<strong>全靠倒排索引 term 查询</strong></li><li><code>match(IS_ERROR, ...)</code> 状态（<code>:119-123</code>）</li><li><code>source(...)</code> 只取需要的字段（<code>:129-134</code>，减少回表）</li><li><code>sort(START_TIME/LATENCY, DESC)</code>（<code>:138-143</code>）</li><li><strong>tag 查询：<code>Query.term(SegmentRecord.TAGS, tag.toString())</code></strong>（<code>:146</code>）——<code>TAGS</code> 字段在 ES 里建了倒排，<code>term</code> 精确匹配 <code>key=value</code> 字符串</li><li><code>size(limit).from(from)</code> 分页（<code>:149</code>）</li><li>用 <code>TimeRangeIndexNameGenerator</code> 按时间范围选索引分片（<code>:152-156</code>）</li></ul></li><li><p><strong><code>queryByTraceId</code></strong>（<code>:182</code>）：<code>term(TRACE_ID, traceId)</code> + <code>size(segmentQueryMaxSize)</code>，<strong><code>RoutingUtils.addRoutingValueToSearchParam(searchParams, traceId)</code></strong>（<code>:192</code>）——把 traceId 作为 ES routing 值，让查询只落到存该 trace 的那个分片，避免广播查。</p></li></ul><blockquote><p><strong>倒排索引查询 + ES routing</strong></p><ul><li><strong>倒排索引</strong>：ES 对 <code>service_id</code>/<code>trace_id</code>/<code>tags</code> 这类字段建倒排表（值→文档列表）。<code>term</code> 查询就是查倒排表，O(匹配数)，比扫全表快几个数量级。SkyWalking 把所有可过滤字段都靠 <code>term</code>/<code>range</code> 走倒排。</li><li><strong>routing</strong>：ES 写入时可用一个 routing key 决定文档落哪个分片。SkyWalking 写 trace 时用 traceId 作 routing，查询时同样传 traceId 作 routing，ES 只查一个分片而不是全部分片——对按 traceId 查这种点查，性能提升巨大。代价是文档分布不均（热门 traceId 的分片会偏大），SkyWalking 接受这个权衡。</li></ul></blockquote><ul><li><code>queryBySegmentIdList</code>（<code>:200</code>）：<code>terms(SEGMENT_ID, list)</code>。</li><li><code>queryByTraceIdWithInstanceId</code>（<code>:216</code>）：<code>bool().must(terms(TRACE_ID, list)).must(terms(SERVICE_INSTANCE_ID, list))</code>。</li><li><strong>segment 在 ES 里是独立文档</strong>，靠 <code>trace_id</code> 关联，应用层 <code>buildRecords</code>（<code>:236</code>）逐文档还原 <code>SegmentRecord</code>。</li></ul><h3 id="aggregationqueryesdao%EF%BC%88%E6%9F%A5%E8%AF%A2%E6%9C%9F-terms-%2B-avg-%E8%81%9A%E5%90%88%EF%BC%89" tabindex="-1">AggregationQueryEsDAO（查询期 terms + avg 聚合）</h3><p><code>oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/AggregationQueryEsDAO.java:49</code>。实现 <code>IAggregationQueryDAO</code>。<code>sortMetrics</code>（<code>:56</code>）：</p><ol><li><code>range(TIME_BUCKET)</code> + 可选 <code>term(METRIC_TABLE_NAME)</code> + 可选 <code>term(attr)</code>/<code>mustNot(terms(attr))</code> + <code>terms(additionalCondition)</code> 过滤（<code>:61-107</code>）。</li><li><strong>核心聚合</strong>（<code>:109-117</code>）：<pre><code class="language-">Aggregation.terms(ENTITY_ID).field(ENTITY_ID)           .order(BucketOrder.aggregation(realValueColumn, asc))           .size(condition.getTopN())           .subAggregation(Aggregation.avg(realValueColumn).field(realValueColumn))           .executionHint(MAP).collectMode(BREADTH_FIRST)</code></pre>即 <strong><code>terms</code> 按 entity_id 分桶 + 子聚合 <code>avg</code> 求均值 + 按 avg 值排序 + 取 TopN</strong>。这是<strong>查询期 ES 聚合</strong>，与 BanyanDB 的预聚合 TopN 形成对比——没有预聚合可走，只能现算。</li><li>从 <code>response.getAggregations()</code> 解析 buckets（<code>:125-137</code>）。</li></ol><blockquote><p><strong>executionHint(MAP) / collectMode(BREADTH_FIRST)</strong><br />ES terms 聚合的调优参数。MAP 用内存哈希表收集（适合基数不高），BREADTH_FIRST 广度优先（适合桶多、深子聚合）。SkyWalking 对拓扑/TopN 都用这个组合，是经验值。基数很高（百万级 entity）时 MAP 会吃内存，这是 ES TopN 在大集群下的隐患。</p></blockquote><h3 id="topologyqueryesdao%EF%BC%88terms-%E8%81%9A%E5%90%88%E5%8E%BB%E9%87%8D%E5%85%B3%E7%B3%BB%E5%AE%9E%E4%BD%93%EF%BC%89" tabindex="-1">TopologyQueryEsDAO（terms 聚合去重关系实体）</h3><p><code>oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/TopologyQueryEsDAO.java:51</code>。实现 <code>ITopologyQueryDAO</code>。和 Aggregation 一样用 <code>terms</code> 聚合，但这里是为了<strong>去重拿到这段时间出现过哪些关系实体</strong>。</p><ul><li><code>loadServiceRelationsDetectedAtServerSide</code>（<code>:58</code>）→ <code>buildServiceRelation</code>（<code>:286</code>）：<code>terms(ENTITY_ID).size(1000)</code> + 子聚合 <code>terms(COMPONENT_IDS)</code>（<code>:289-299</code>），<code>BREADTH_FIRST</code>+<code>MAP</code>。从 buckets 还原 <code>Call.CallDetail</code>。</li><li>实例关系（<code>:121</code>）：单层 <code>terms(ENTITY_ID)</code>。</li><li>进程关系（<code>:221</code>）：<code>terms(ENTITY_ID)</code> + 子 <code>terms(COMPONENT_ID)</code>。</li><li>条件用 <code>term(SOURCE_SERVICE_ID)</code>/<code>term(DEST_SERVICE_ID)</code> + <code>should</code> 组合双向（<code>:174-175</code>）。</li><li><code>size(0)</code>（<code>:63</code>）——不取文档原文，只要聚合结果。</li></ul><h3 id="logqueryesdao%EF%BC%88%E5%80%92%E6%8E%92-%2B-matchphrase-%E5%85%B3%E9%94%AE%E8%AF%8D%EF%BC%89" tabindex="-1">LogQueryEsDAO（倒排 + matchPhrase 关键词）</h3><p><code>oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/LogQueryEsDAO.java:53</code>。实现 <code>ILogQueryDAO</code>，且 <strong><code>supportQueryLogsByKeywords()</code> 返回 <code>true</code></strong>（<code>:59</code>）——三后端里唯一支持正文关键词检索的。</p><ul><li><code>queryLogs</code>（<code>:64</code>）：<code>BoolQueryBuilder</code> + <code>term</code>/<code>range</code> 一套倒排过滤（service/instance/endpoint/traceId/segmentId/spanId/tags）。</li><li><strong>tag 查询：<code>Query.term(AbstractLogRecord.TAGS, tag.toString())</code></strong>（<code>:111</code>）——和 trace 一样靠 <code>TAGS</code> 倒排。</li><li><strong>关键词检索：<code>Query.matchPhrase(MatchCNameBuilder.build(CONTENT), content)</code></strong>（<code>:118</code>）——<code>matchPhrase</code> 短语匹配，走 ES 全文倒排。排除关键词用 <code>mustNot(matchPhrase(...))</code>（<code>:129-130</code>）。<code>MatchCNameBuilder</code> 处理列名映射（合并索引下正文列名带前缀）。</li><li><code>sort(TIMESTAMP, DESC/ASC)</code>（<code>:140</code>）+ <code>size(limit).from(from)</code> 分页（<code>:145</code>）。</li></ul><h3 id="es-%E6%9F%A5%E8%AF%A2%E7%89%B9%E7%82%B9%E5%B0%8F%E7%BB%93" tabindex="-1">ES 查询特点小结</h3><ol><li><strong>指标 multi-get，不用聚合</strong>：靠文档 ID（time_bucket+entity_id）点查，避免 <code>date_histogram</code>。</li><li><strong>TopN/拓扑用 <code>terms</code>+<code>avg</code> 查询期聚合</strong>：无预聚合，靠 ES 聚合引擎现算，调优 <code>MAP</code>+<code>BREADTH_FIRST</code>。</li><li><strong>trace/log 全倒排</strong>：<code>term</code>/<code>range</code>/<code>matchPhrase</code> 走倒排索引，<code>tags</code> 字段也建倒排。</li><li><strong>routing 优化点查</strong>：按 traceId 查带 routing，单分片命中。</li><li><strong>merged table 判别</strong>：合并索引下用 <code>metric_table</code>/<code>record_table_name</code> 字段 <code>term</code> 过滤区分指标/记录类型。</li><li><strong>无冷热分层原生支持</strong>（不像 BanyanDB 的 <code>setStages</code>），靠 <code>TimeRangeIndexNameGenerator</code> 按时间选索引分片。</li></ol><hr /><h2 id="jdbc-%E6%9F%A5%E8%AF%A2%E5%AE%9E%E7%8E%B0" tabindex="-1">JDBC 查询实现</h2><p>查询 DAO 在 <code>oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/</code>（通用，H2/MySQL/PG 共用），PostgreSQL 特化在 <code>postgresql/dao/</code>。</p><h3 id="jdbcmetricsquerydao%EF%BC%88%E6%8C%89%E4%B8%BB%E9%94%AE-in-%E6%9F%A5%E2%80%94%E2%80%94%E4%B8%8D%E5%81%9A%E8%81%9A%E5%90%88%EF%BC%89" tabindex="-1">JDBCMetricsQueryDAO（按主键 IN 查——不做聚合）</h3><p><code>oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCMetricsQueryDAO.java:45</code>。实现 <code>IMetricsQueryDAO</code>。和 ES 思路一样——<strong>指标按主键 ID 点查，不聚合</strong>。</p><ul><li><p><strong><code>readMetricsValues</code></strong>（<code>:56</code>）：</p><ol><li><code>tableHelper.getTablesForRead(metricName, startBucket, endBucket)</code> 拿到时间范围内要查的物理表（JDBC 按天分表，<code>:64</code>）。</li><li><code>duration.assembleDurationPoints()</code> 算期望时间桶，<code>TableHelper.generateId(metricName, pointOfTime.id(entityId))</code> 拼出<strong>主键 ID</strong>（<code>:72</code>）。</li><li>对每个表发 SQL：<strong><code>select id, valueColumnName from table where id in (?, ?, ...)</code></strong>（<code>:76-82</code>）——主键 IN 查询，等价于 ES 的 multi-get，走主键 B-tree 索引。</li><li>结果按 <code>Util.sortValues</code> 排序补默认值。</li></ol><ul><li>foreign 指标：try-catch 跳过不含该 value 列的表（<code>:97-103</code>）。</li></ul></li><li><p><strong><code>listEntityIdsInRange</code></strong>（<code>:217</code>）：这里<strong>用 SQL 聚合</strong>：<code>select entity_id, max(time_bucket) as latest_time_bucket ... where table_name=? and time_bucket&gt;=? and time_bucket&lt;=? group by entity_id order by latest_time_bucket desc limit ?</code>（<code>:250-259</code>）。注释（<code>:241-247</code>）解释为什么用 <code>GROUP BY</code>+<code>MAX</code> 而非 <code>SELECT DISTINCT</code>：<strong>PostgreSQL 拒绝 <code>ORDER BY</code> 一个不在 <code>DISTINCT</code> 列表里的列</strong>，所以用 GROUP BY 形式可移植。跨表结果在 Java 端按 <code>latest</code> 合并（<code>:270</code>）再全局排序取 limit。</p></li><li><p><strong><code>readHeatMap</code></strong>（<code>:285</code>）：同样 <code>id in (...)</code> 点查取 <code>dataset</code> 列。</p></li></ul><h3 id="jdbctracequerydao%EF%BC%88sql-where-%2B-tag-%E8%A1%A8-join%EF%BC%89" tabindex="-1">JDBCTraceQueryDAO（SQL WHERE + tag 表 JOIN）</h3><p><code>oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCTraceQueryDAO.java:62</code>。实现 <code>ITraceQueryDAO</code>（V1）。<strong>纯 SQL 字符串拼接</strong>，参数化防注入。</p><ul><li><strong><code>queryBasicTraces</code></strong>（<code>:81</code>）：<ol><li>先校验 tag 是否在 <code>SearchableTracesTags</code>（<code>:97</code>）——只有声明为可搜索的 tag 才能查，否则直接返回空 <code>TraceBrief</code>（避免全表扫）。</li><li>按 <code>getTablesForRead</code> / <code>getTablesWithinTTL</code> 遍历时间分表（<code>:113-115</code>）。</li><li>拼 SQL：<code>where table_name=? and time_bucket&gt;=? and time_bucket&lt;=? and latency&gt;=? and latency&lt;=? and service_id=? and ... and is_error=1</code>（<code>:138-185</code>），<code>order by start_time desc</code> / <code>order by latency desc</code>（<code>:188</code>/<code>:191</code>），<code>LIMIT ? OFFSET ?</code>（<code>:195</code>，<code>buildLimit</code> <code>:229</code>）。</li><li><strong>tag 查询靠 inner join 单独的 tag 表</strong>（<code>:132</code>、<code>:171-177</code>）：<code>inner join segment_tag t0 on main.id=t0.id and t0.tags=?</code>。每个 tag 一次 join。</li></ol></li></ul><blockquote><p><strong>JDBC 的 tag 存储模型</strong><br /><code>SegmentRecord</code> 用 <code>@SQLDatabase.AdditionalEntity(additionalTables = {ADDITIONAL_TAG_TABLE})</code>（<code>SegmentRecord.java:88</code>，<code>ADDITIONAL_TAG_TABLE = &quot;segment_tag&quot;</code> <code>:64</code>）把 tags 拆到独立表 <code>segment_tag(id, tags)</code>。主表不存 tags，查 tag 必须 join tag 表。这和 ES（tags 字段建倒排）、BanyanDB（tags 数组 + <code>having</code>）完全不同——<strong>JDBC 没有倒排索引，靠额外表 + B-tree 索引 + join 实现 tag 过滤</strong>。代价是每个 tag 一次 join，tag 多了 join 数线性增长。</p></blockquote><ul><li><p><strong><code>queryByTraceId</code></strong>（<code>:236</code>）：<strong>遍历 <code>getTablesWithinTTL</code> 所有 TTL 内的表</strong>（<code>:237</code>），每表发 <code>where table_name=? and trace_id=?</code>。注释里 <code>queryBasicTraces</code> 的 <code>TODO: sort</code>（<code>:226</code>）说明跨表结果排序未在 SQL 层做。<strong>没有 ES 那样的 routing</strong>——JDBC 无法按 traceId 路由到单表，只能全 TTL 表扫，这是 JDBC trace 查询最慢的根源。</p></li><li><p><code>queryBySegmentIdList</code>（<code>:257</code>）：<code>segment_id in (...)</code>。</p></li><li><p><code>queryByTraceIdWithInstanceId</code>（<code>:282</code>）：<code>trace_id in (...) and service_instance_id in (...)</code>。</p></li><li><p><code>doFlexibleTraceQuery</code>（<code>:309</code>）返回空。</p></li></ul><h3 id="jdbcaggregationquerydao%EF%BC%88sql-%E5%AD%90%E6%9F%A5%E8%AF%A2-group-by-%2B-java-%E7%AB%AF%E5%90%88%E5%B9%B6%EF%BC%89" tabindex="-1">JDBCAggregationQueryDAO（SQL 子查询 GROUP BY + Java 端合并）</h3><p><code>oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCAggregationQueryDAO.java:44</code>。实现 <code>IAggregationQueryDAO</code>。<code>sortMetrics</code>（<code>:50</code>）：</p><ol><li>遍历时间分表（<code>:55</code>）。</li><li><strong><code>buildSQL</code></strong>（<code>:94</code>）构造子查询：<pre><code class="language-">select result, entity_id from (  select avg(valueColumnName) as result, entity_id  from table where table_name=? and time_bucket&gt;=? and time_bucket&lt;=? [and attr=?]...  group by entity_id) as T order by result desc limit topN</code></pre>即 <strong><code>avg</code> + <code>GROUP BY entity_id</code> + <code>ORDER BY</code> + <code>LIMIT</code></strong>——SQL 查询期聚合，单表内取 TopN。</li><li><strong>跨表合并</strong>（<code>:73-91</code>）：因为按天分表，每个表各自取了 TopN，Java 端要 <code>groupingBy(SelectedRecord::getId)</code> 按实体合并（<code>:79</code>），<strong>对同一实体的多个表结果求平均</strong>（<code>:84</code>），再按 ASC/DESC 排序、<code>limit(topN)</code>（<code>:90</code>）。</li></ol><blockquote><p><strong>JDBC 分表是性能劣势根源</strong><br />对比：ES 是单索引一次 <code>terms</code> 聚合跨所有桶；JDBC 是分表各自聚合再 Java 端合并——<strong>分表是 JDBC 性能劣势的根源</strong>，跨表 TopN 不可能精确（每表各自的 TopN 合并不等于全局 TopN，除非每表都取足够多）。跨多天查询时，要么每表取的 N 够大（吞吐变差），要么接受近似结果。这是关系库做时序 TopN 的结构性短板。</p></blockquote><h3 id="jdbctopologyquerydao%EF%BC%88sql-group-by-%E5%8E%BB%E9%87%8D%EF%BC%89" tabindex="-1">JDBCTopologyQueryDAO（SQL GROUP BY 去重）</h3><p><code>oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCTopologyQueryDAO.java:47</code>。实现 <code>ITopologyQueryDAO</code>。<code>loadServiceCalls</code>（<code>:140</code>）：</p><pre><code class="language-">select entity_id, component_ids from tablewhere table_name=? and time_bucket&gt;=? and time_bucket&lt;=? [and (source=? or dest=?)]group by entity_id, component_ids</code></pre><p>（<code>:172-176</code>）——<code>GROUP BY</code> 去重拿到这段时间出现过的关系实体。实例/端点/进程关系同理（<code>:223-227</code>、<code>:258-263</code>、<code>:295-301</code>），条件用 <code>source=? and dest=?</code> 双向 OR。</p><h3 id="postgresql-%E7%89%B9%E5%8C%96%EF%BC%88%E4%BB%85-sql-%E6%96%B9%E8%A8%80%E9%80%82%E9%85%8D%EF%BC%8C%E6%97%A0-json%2F%E6%95%B0%E7%BB%84%EF%BC%89" tabindex="-1">PostgreSQL 特化（仅 SQL 方言适配，无 JSON/数组）</h3><p><code>oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../postgresql/dao/</code>。</p><ul><li><strong><code>PostgreSQLMetricsQueryDAO</code></strong>（<code>:26</code>）继承 <code>JDBCMetricsQueryDAO</code>，只重写 <code>buildMetricsValueSql</code>（<code>:33</code>）：给聚合列加 <code>as result</code> 别名（PG 对 <code>ORDER BY</code> 别名要求���格）。</li><li><strong><code>PostgreSQLAggregationQueryDAO</code></strong>（<code>:26</code>）继承 <code>JDBCAggregationQueryDAO</code>，重写 <code>buildMetricsValueSql</code>（<code>:32</code>）：用 <code>select * from (select avg(...) as result, entity_id from ...)</code> 子查询包装。</li></ul><blockquote><p><strong>PG 没用 JSON/数组</strong><br />对整个 JDBC 插件源码 grep <code>jsonb</code>/<code>array_agg</code>/<code>-&gt;&gt;</code>/<code>unnest</code>/<code>@&gt;</code> <strong>零命中</strong>。<code>PostgreSQLStorageProvider.prepare()</code>（<code>PostgreSQLStorageProvider.java:45</code>）<strong>只</strong>重注册 <code>IMetricsQueryDAO</code>（<code>:50</code>）和 <code>IAggregationQueryDAO</code>（<code>:53</code>）两个服务，其余 query DAO 全部继承通用 <code>JDBCStorageProvider</code> 实现。PG 特化纯粹是 SQL 语法适配（别名/子查询包装），<strong>没有用 PG 的 JSON/数组特性</strong>。哪怕 PG 有 <code>jsonb</code> 能存数组 tag，SkyWalking 也没用——保持和 H2/MySQL 同一套 <code>segment_tag</code> 表 + join 的实现。</p></blockquote><h3 id="jdbc-%E6%9F%A5%E8%AF%A2%E7%89%B9%E7%82%B9%E5%B0%8F%E7%BB%93" tabindex="-1">JDBC 查询特点小结</h3><ol><li><strong>指标按主键 IN 点查</strong>（同 ES multi-get 思路），不聚合。</li><li><strong>TopN/拓扑用 SQL <code>GROUP BY</code>+<code>avg</code>+<code>ORDER BY</code> 查询期聚合</strong>，跨天分表在 Java 端合并（不精确）。</li><li><strong>trace/log 全靠 SQL <code>WHERE</code></strong>，没有倒排索引——tag 查询靠独立 tag 表 + inner join + B-tree。</li><li><strong>按 traceId 查要扫全 TTL 表</strong>（无 routing 概念）。</li><li><strong>可搜索 tag 受配置限制</strong>：<code>SearchableTracesTags</code> 白名单外的 tag 直接拒绝查询。</li><li><strong>PG 特化仅 SQL 方言</strong>，无 JSON/数组。</li></ol><hr /><h2 id="%E4%B8%89%E5%90%8E%E7%AB%AF%E6%9F%A5%E8%AF%A2%E5%AE%9E%E7%8E%B0%E5%AF%B9%E6%AF%94" tabindex="-1">三后端查询实现对比</h2><h3 id="%E5%90%8C%E4%B8%80%E4%B8%AA-readmetricsvalues-%E7%9A%84%E4%B8%89%E7%A7%8D%E5%AE%9E%E7%8E%B0" tabindex="-1">同一个 readMetricsValues 的三种实现</h3><table><thead><tr><th>维度</th><th>BanyanDB</th><th>Elasticsearch</th><th>JDBC</th></tr></thead><tbody><tr><td>入口类</td><td><code>BanyanDBMetricsQueryDAO:60</code></td><td><code>MetricsQueryEsDAO:66</code></td><td><code>JDBCMetricsQueryDAO:56</code></td></tr><tr><td>查询机制</td><td><code>MeasureQuery</code> + <code>eq(ENTITY_ID)</code> 倒排过滤 + 时间范围</td><td>ES <code>multi-get</code>（<code>getClient().ids()</code>）按文档 ID 批量取</td><td><code>select ... where id in (...)</code> 主键 IN 查询</td></tr><tr><td>是否聚合</td><td>否（measure 写入已预聚合）</td><td>否（multi-get 点查）</td><td>否（主键点查）</td></tr><tr><td>时间桶对齐</td><td>客户端按 <code>assembleDurationPoints</code> 补默认值</td><td>同左</td><td>同左（<code>Util.sortValues</code>）</td></tr><tr><td>分片/分表</td><td>按 group/stage（冷热）</td><td><code>TimeSeriesUtils.queryIndexName</code> 按时间分索引</td><td><code>tableHelper.getTablesForRead</code> 按天分表</td></tr></tbody></table><blockquote><p><strong>三者都不在查询期聚合指标</strong><br />因为指标在写入时已按 <code>(time_bucket, entity_id)</code> 预聚合，且该组合就是主键/文档 ID。查询本质都是&quot;算出期望的时间桶 ID，按 ID 点查回填&quot;。差异只在存储引擎的 API：BanyanDB 用 measure 查询语言、ES 用 multi-get、JDBC 用 <code>WHERE id IN</code>。这一层三者性能接近，差距在 TopN 和 trace 才拉开。</p></blockquote><h3 id="%E8%B0%81%E7%94%A8%E5%8E%9F%E7%94%9F%E8%81%9A%E5%90%88%E3%80%81%E8%B0%81%E7%94%A8%E6%9F%A5%E8%AF%A2%E6%9C%9F%E8%AE%A1%E7%AE%97%EF%BC%88topn%EF%BC%89" tabindex="-1">谁用原生聚合、谁用查询期计算（TopN）</h3><table><thead><tr><th>维度</th><th>BanyanDB</th><th>Elasticsearch</th><th>JDBC</th></tr></thead><tbody><tr><td>入口类</td><td><code>BanyanDBAggregationQueryDAO:59</code></td><td><code>AggregationQueryEsDAO:56</code></td><td><code>JDBCAggregationQueryDAO:50</code></td></tr><tr><td><strong>预聚合 TopN</strong></td><td>✅ <code>TopNAggregation</code>（<code>bydb-topn.yaml</code>，服务端预聚合，<code>:99</code>）</td><td>❌ 无</td><td>❌ 无</td></tr><tr><td><strong>查询期聚合</strong></td><td>✅ <code>directMetricsTopN</code>：<code>meanBy</code>+<code>topN</code>（<code>:154-158</code>）</td><td>✅ <code>terms</code>+<code>avg</code>+<code>BucketOrder</code>（<code>:110-114</code>）</td><td>✅ <code>avg</code>+<code>GROUP BY</code>+<code>ORDER BY</code>+<code>LIMIT</code>（<code>:104-137</code>）</td></tr><tr><td>跨分片/分表</td><td>单 measure 内原生跨桶</td><td>单索引内 ES 聚合引擎</td><td><strong>分表各自聚合 + Java 端合并求平均</strong>（<code>:79-90</code>），不精确</td></tr></tbody></table><p><strong>性能差异根源</strong>：BanyanDB 有预聚合 TopN 路径，高频看板可走预聚合（O(预聚合结果大小)）；ES 和 JDBC 只能查询期现算。JDBC 还因为按天分表，跨表 TopN 要 Java 端合并，数据量大或跨多天时既慢又不精确。ES 单索引聚合引擎相对最优（无分表），但仍比 BanyanDB 预聚合慢。一句话：<strong>BanyanDB 用空间换时间（预存结果），ES 用算力换灵活（现算），JDBC 受限于分表结构两头不沾</strong>。</p><h3 id="trace-%E6%9F%A5%E8%AF%A2%EF%BC%9A%E8%B0%81%E7%94%A8%E5%80%92%E6%8E%92%E3%80%81%E8%B0%81%E7%94%A8-sql-where" tabindex="-1">trace 查询：谁用倒排、谁用 SQL where</h3><table><thead><tr><th>维度</th><th>BanyanDB</th><th>Elasticsearch</th><th>JDBC</th></tr></thead><tbody><tr><td>入口类</td><td><code>BanyanDBTraceQueryDAO:59</code>（<strong>V2</strong>）</td><td><code>TraceQueryEsDAO:58</code>（V1）</td><td><code>JDBCTraceQueryDAO:62</code>（V1）</td></tr><tr><td>查询机制</td><td><strong>原生 <code>TraceQuery</code></strong>（trace→spans 嵌套，一次出 span 树）</td><td><code>BoolQueryBuilder</code> + <code>term</code>/<code>range</code>/<code>match</code> 倒排 + <code>sort</code> + 分页</td><td>SQL <code>WHERE</code> 字符串拼接 + <code>ORDER BY</code> + <code>LIMIT/OFFSET</code></td></tr><tr><td>trace 列表查询</td><td><code>queryTraces(TraceQueryCondition)</code>（<code>:148</code>），返回 <code>TracesQueryResult</code> 含 span 树</td><td><code>queryBasicTraces</code>（<code>:68</code>），返回 <code>TraceBrief</code>（segment 列表）</td><td><code>queryBasicTraces</code>（<code>:81</code>），返回 <code>TraceBrief</code></td></tr><tr><td>按 traceId 取详情</td><td><code>queryByTraceIdV2</code>（<code>:123</code>），一次取整棵 span 树</td><td><code>queryByTraceId</code>（<code>:182</code>），<code>term(TRACE_ID)</code> + <strong>routing 优化</strong></td><td><code>queryByTraceId</code>（<code>:236</code>），<strong>扫全 TTL 表</strong> <code>where trace_id=?</code></td></tr><tr><td>tag 查询</td><td><strong><code>having(TAGS, [&quot;k=v&quot;])</code></strong>（<code>:211</code>，原生数组倒排查）</td><td><strong><code>term(TAGS, &quot;k=v&quot;)</code></strong>（<code>:146</code>，倒排字段精确匹配）</td><td><strong><code>inner join segment_tag on id=id and tags=?</code></strong>（<code>:132</code>，独立表 + B-tree）</td></tr><tr><td>关键词/正文</td><td>不支持</td><td>✅ <code>matchPhrase(CONTENT)</code>（<code>LogQueryEsDAO:118</code>）</td><td>不支持</td></tr><tr><td>冷数据</td><td>✅ <code>setStages(&quot;cold&quot;)</code></td><td>❌（靠时间索引分片）</td><td>❌（靠 TTL 分表）</td></tr><tr><td>span 关系组装</td><td><strong>无需组装</strong>（span 嵌套存储）</td><td>应用层从 segment 文档拼</td><td>应用层从 segment 行拼</td></tr></tbody></table><p><strong>性能差异根源</strong>：</p><ul><li>BanyanDB trace 是原生模型，span 嵌套，一次查询出整棵树，且 <code>having</code> 走原生倒排——最快。</li><li>ES 靠倒排 + routing，按 traceId 查能单分片命中，很快；但 span 树要应用层拼。</li><li>JDBC 没有倒排也没有 routing，按 traceId 查要扫所有 TTL 表（最慢）；tag 查询要 join tag 表；且 tag 必须在 <code>SearchableTracesTags</code> 白名单内。</li></ul><h3 id="%E6%80%A7%E8%83%BD%E5%B7%AE%E5%BC%82%E6%A0%B9%E6%BA%90%E6%80%BB%E7%BB%93" tabindex="-1">性能差异根源总结</h3><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/m11.png" alt="三后端查询机制对照" /></p><ol><li><strong>指标查询</strong>：三者都靠&quot;预聚合 + 主键点查&quot;，性能接近。差异主要在冷热分层（BanyanDB 原生支持）和分片策略。</li><li><strong>TopN 聚合</strong>：BanyanDB 有预聚合 TopN（最快）；ES 查询期聚合（次之，单索引）；JDBC 查询期聚合 + 分表 Java 合并（最慢且不精确）。</li><li><strong>trace 查询</strong>：BanyanDB 原生 span 树（最快，无需二次组装）；ES 倒排 + routing（快，需组装）；JDBC 全表扫 + join（最慢，需组装）。</li><li><strong>tag 查询</strong>：BanyanDB <code>having</code>（原生数组倒排）；ES <code>term</code>（倒排字段）；JDBC join tag 表（无倒排，B-tree）。</li><li><strong>关键词检索</strong>：仅 ES <code>matchPhrase</code> 支持正文检索；BanyanDB/JDBC 不支持。</li><li><strong>冷数据</strong>：仅 BanyanDB 原生 <code>setStages</code> 支持；ES/JDBC 靠时间分片/分表间接实现。</li></ol><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键类</th><th>路径</th></tr></thead><tbody><tr><td>查询接口</td><td>各 <code>I*QueryDAO</code></td><td><code>oap-server/server-core/.../storage/query/*.java</code></td></tr><tr><td>BanyanDB 查询</td><td>AbstractBanyanDBDAO</td><td><code>oap-server/server-storage-plugin/storage-banyandb-plugin/.../stream/AbstractBanyanDBDAO.java</code></td></tr><tr><td></td><td>BanyanDBMetricsQueryDAO</td><td>同目录 <code>measure/BanyanDBMetricsQueryDAO.java</code></td></tr><tr><td></td><td>BanyanDBAggregationQueryDAO</td><td><code>.../banyandb/BanyanDBAggregationQueryDAO.java</code></td></tr><tr><td></td><td>BanyanDBTraceQueryDAO</td><td><code>.../banyandb/trace/BanyanDBTraceQueryDAO.java</code></td></tr><tr><td>ES 查询</td><td>MetricsQueryEsDAO</td><td><code>oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../query/MetricsQueryEsDAO.java</code></td></tr><tr><td></td><td>TraceQueryEsDAO</td><td>同目录 <code>TraceQueryEsDAO.java</code></td></tr><tr><td></td><td>AggregationQueryEsDAO</td><td>同目录 <code>AggregationQueryEsDAO.java</code></td></tr><tr><td></td><td>LogQueryEsDAO</td><td>同目录 <code>LogQueryEsDAO.java</code></td></tr><tr><td>JDBC 查询</td><td>JDBCMetricsQueryDAO</td><td><code>oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/dao/JDBCMetricsQueryDAO.java</code></td></tr><tr><td></td><td>JDBCTraceQueryDAO</td><td>同目录 <code>JDBCTraceQueryDAO.java</code></td></tr><tr><td></td><td>JDBCAggregationQueryDAO</td><td>同目录 <code>JDBCAggregationQueryDAO.java</code></td></tr><tr><td></td><td>PostgreSQL 特化</td><td><code>.../postgresql/dao/PostgreSQLMetricsQueryDAO.java</code>、<code>PostgreSQLAggregationQueryDAO.java</code></td></tr></tbody></table><blockquote><p><strong>未确认事项</strong></p><ul><li>PostgreSQL 的 <code>PostgreSQLTableInstaller</code> DDL 是否用 PG 特殊类型（jsonb/数组）建表，本次未读 DDL 源码，标注未确认。但<strong>查询 DAO 层确定没用</strong> PG JSON/数组。</li><li>ES 是否在其它查询 DAO（如 <code>MetadataQueryEsDAO</code>、profiling 相关）用 <code>date_histogram</code>，本次未全量检索；但本篇重点的 5 个 DAO（Metrics/Trace/Aggregation/Log/Topology）<strong>确认未用</strong> <code>date_histogram</code>。</li><li>BanyanDB <code>TraceQuery</code> 服务端具体如何存 span 嵌套（protobuf 结构细节），本次只看到客户端 <code>TraceQueryResponse.getTraces().getSpansList()</code>，服务端存储格式未深查。</li></ul></blockquote><hr /><h2 id="%E5%85%A8%E7%B3%BB%E5%88%97%E5%9B%9E%E9%A1%BE" tabindex="-1">全系列回顾</h2><p>到这里，SkyWalking OAP 的核心子系统、数据模型、协议层、Profiling、查询 DAO 都拆解完了。回到 README 看整体地图，或回看任意一篇：</p><ul><li>00-整体架构与端到端数据流 / 01-模块系统与启动流程 / 02-数据采集层-Receiver</li><li>03-分析引擎与四大DSL / 04-流处理引擎-Worker链 / 05-存储层</li><li>06-查询层与告警 / 07-集群协调与动态配置</li><li>08-数据模型与Scope体系 / 09-协议层与Agent采集</li><li>10-Profiling体系</li></ul><blockquote><p><strong>心智模型</strong><br />把查询 DAO 想成&quot;展厅的三种导览系统&quot;：BanyanDB 是为 SkyWalking 量身定制的智能导览——measure 货架按 ID 直取、TopN 区有预分拣榜单、trace 区有原生族谱树、标签有数组倒排；ES 是租来的通用搜索引擎——指标靠 ID 点查、TopN 靠现场聚合、trace 靠倒排+routing、还能全文搜日志；JDBC 是拿关系库当仓库——指标靠主键 IN、TopN 靠 GROUP BY+跨表合并、trace 靠全表扫+join tag 表。同一份&quot;查询菜单&quot;（接口），三种仓库各有各的取法，性能差距源自谁更贴近时序+链路的原生语义。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[十一、Skywalking Profiling 体系]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/shi-yi-skywalkingprofiling-ti-xi" />
                <id>tag:https://lymboy.com,2026-02-24:shi-yi-skywalkingprofiling-ti-xi</id>
                <published>2026-02-24T22:08:52+08:00</published>
                <updated>2026-02-24T22:08:52+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="profiling-%E4%BD%93%E7%B3%BB" tabindex="-1">Profiling 体系</h1><blockquote><p><strong>这一篇解决什么</strong><br />链路追踪能告诉你请求慢在哪一段，说不清那一段代码为什么慢。Profiling 补上这一刀：在普通监控之外对运行中进程做 CPU、内存、锁的栈采样，生成火焰图，把根因从&quot;端点级&quot;推到&quot;代码行级&quot;。SkyWalking OAP 有四套独立的 profiling 子系统——Thread Profiling、eBPF Profiling、pprof、async-profiler。这篇把四者逐个拆透并对比。</p></blockquote><hr /><h2 id="%E6%A6%82%E5%BF%B5%E9%80%9F%E6%9F%A5%EF%BC%88%E5%AF%B9%E6%96%B0%E4%BA%BA%E4%B8%8D%E5%8F%8B%E5%A5%BD%E7%9A%84%E8%AF%8D%E5%85%88%E8%A7%A3%E9%87%8A%EF%BC%89" tabindex="-1">概念速查（对新人不友好的词先解释）</h2><p>先认人，再认戏。这几个词后面反复出现，先一次性讲清。</p><table><thead><tr><th>词</th><th>解释</th></tr></thead><tbody><tr><td><strong>Profiling（性能剖析）</strong></td><td>周期性&quot;拍快照&quot;记录程序当前正在执行的调用栈。成千上万次快照叠起来，某个函数出现得越多，它占的 CPU、内存或锁时间就越多</td></tr><tr><td><strong>火焰图（Flame Graph）</strong></td><td>把成千上万条调用栈按&quot;根→子&quot;层级堆叠画出来的图。y 轴是调用深度，x 轴是某函数在所有栈里出现的频次。宽度越宽越&quot;热&quot;，热点函数一眼就能看出</td></tr><tr><td><strong>on-CPU / off-CPU</strong></td><td>on-CPU 采样&quot;线程正在 CPU 上跑什么&quot;，找 CPU 热点；off-CPU 采样&quot;线程被挂起在等什么&quot;——等锁、等 IO、等网络，找阻塞点。两者互补，一个找&quot;忙&quot;，一个找&quot;等&quot;</td></tr><tr><td><strong>eBPF</strong></td><td>Linux 内核里的&quot;可编程沙箱&quot;。能在内核态挂钩子抓任何进程的系统调用、网络包、调用栈，不改业务代码、不重启业务进程</td></tr><tr><td><strong>pprof</strong></td><td>Go 自带的性能采样格式（<code>runtime/pprof</code>、<code>net/http/pprof</code>），产出 protobuf 二进制 <code>.pb.gz</code>，含 CPU、堆、goroutine、锁、线程创建等多种事件</td></tr><tr><td><strong>JFR（Java Flight Recorder）</strong></td><td>JVM 自带的低开销事件记录格式。async-profiler 是社区工具，能把 JVM 内的 CPU、堆、锁采样导出成 JFR</td></tr><tr><td><strong>Rover</strong></td><td>SkyWalking 官方的 eBPF Agent，独立进程，跑在被监控节点上。用 eBPF 采数据再 gRPC 上报给 OAP</td></tr></tbody></table><p>OAP 把 Profiling 拆成四套独立子系统。它们在&quot;谁采样、采什么、存哪、怎么画图&quot;上各有取舍——这才是后面要讲的重点。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/10-Profiling%E4%BD%93%E7%B3%BB.md_0.png" alt="图1" /></p><p>为什么要拆四套，而不是一套通吃？因为采样这件事天然有几组矛盾取舍：要不要侵入业务进程、能不能改代码、采 on-CPU 还是 off-CPU、要不要绑 trace、采样的开销能压多低。四套子系统各自站在不同的取舍点上，谁也替代不了谁。后面每一节会讲清它站在哪、为什么这么站。</p><hr /><h2 id="thread-profiling%EF%BC%88%E7%BA%BF%E7%A8%8B%E7%BA%A7-cpu-profiling%EF%BC%8Cjava-agent-%E4%B8%8A%E6%8A%A5%EF%BC%89" tabindex="-1">Thread Profiling（线程级 CPU profiling，Java agent 上报）</h2><h3 id="%E8%B0%81%E4%BA%A7%E7%94%9F%E6%95%B0%E6%8D%AE-%2F-%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE" tabindex="-1">谁产生数据 / 传输协议</h3><p>数据由 <strong>SkyWalking Java Agent</strong> 产生。Java Agent 集成了 <code>ThreadProfiler</code>。它收到 OAP 下发的&quot;对某 endpoint 做 N 分钟线程栈采样&quot;任务后，在业务线程被该 endpoint 命中且耗时超过阈值时，按 <code>dumpPeriod</code> 周期性地 <code>Thread.dumpStack()</code> 抓栈，把多条 <code>ThreadSnapshot</code> 用 <strong>gRPC 流</strong>上报。</p><p>协议定义在 <code>apm-protocol/apm-network/src/main/proto/profile/Profile.proto:30-93</code>，<code>service ProfileTask</code> 共 4 个 RPC：</p><table><thead><tr><th>RPC</th><th>方向</th><th>作用</th></tr></thead><tbody><tr><td><code>getProfileTaskCommands</code></td><td>agent → OAP</td><td>agent 拉取需要执行的采样任务命令</td></tr><tr><td><code>collectSnapshot</code> (stream)</td><td>agent → OAP</td><td>上报线程栈快照（Java）</td></tr><tr><td><code>goProfileReport</code> (stream)</td><td>go agent → OAP</td><td>上报 Go pprof 数据（复用本通道）</td></tr><tr><td><code>reportTaskFinish</code></td><td>agent → OAP</td><td>通知任务结束</td></tr></tbody></table><p><code>ThreadSnapshot</code> 消息（<code>Profile.proto:58-69</code>）含 <code>taskId / traceSegmentId / time / sequence / ThreadStack{codeSignatures[]}</code>——栈以&quot;代码签名列表&quot;形式存。</p><blockquote><p><strong>为什么栈存成&quot;代码签名列表&quot;而不是完整字节流</strong><br />Java 的 <code>Thread.dumpStack()</code> 拿到的是 <code>StackTraceElement[]</code>，每帧就是 <code>类名.方法名(文件:行号)</code>。直接存字符串列表，反序列化几乎零成本，也不用引入额外的二进制协议。Go 那条路走的是 pprof 二进制，所以另存 <code>stackBinary</code>——同一条通道，两种存储格式，靠 <code>language</code> 字段区分。</p></blockquote><h3 id="%E4%BB%BB%E5%8A%A1%E5%A6%82%E4%BD%95%E4%B8%8B%E5%8F%91%E5%88%B0-agent%EF%BC%88%E5%91%BD%E4%BB%A4%E4%B8%8B%E5%8F%91%E9%93%BE%E8%B7%AF%EF%BC%89" tabindex="-1">任务如何下发到 agent（命令下发链路）</h3><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/10-Profiling%E4%BD%93%E7%B3%BB.md_1.png" alt="图2" /></p><p>用户在 UI 创建任务，走 <code>ProfileTaskMutationService.createTask(...)</code>（<code>oap-server/server-core/.../profiling/trace/ProfileTaskMutationService.java:64</code>）。它先算任务执行时间窗，做合法性校验（<code>checkDataSuccess</code>，<code>:103</code>——duration 上下限、dumpPeriod 下限、maxSamplingCount 上限、同 service 同时段不可重叠），再构造 <code>ProfileTaskRecord</code> 并 <code>NoneStreamProcessor.getInstance().in(task)</code> 落库（<code>:98</code>）。任务表是 <strong>NoneStream</strong>——不参与聚合，只存原始任务记录。</p><p><code>ProfileTaskRecord</code>（<code>ProfileTaskRecord.java:46-84</code>）字段：<code>taskId / serviceId / endpointName / startTime / duration / minDurationThreshold / dumpPeriod / createTime / maxSamplingCount</code>，索引名 <code>profile_task</code>，scope <code>PROFILE_TASK</code>。</p><p>下发靠<strong>命令轮询 + 缓存</strong>。agent 周期性调 <code>getProfileTaskCommands</code>，handler 是 <code>ProfileTaskServiceHandler.getProfileTaskCommands</code>（<code>skywalking-profile-receiver-plugin/.../handler/ProfileTaskServiceHandler.java:67</code>）。它先从 <code>ProfileTaskCache</code>（<code>server-core/.../cache/ProfileTaskCache.java:42</code>，Guava Cache，写后 1 分钟过期，<code>:62</code>）按 <code>serviceId</code> 取任务列表，跳过 <code>createTime &lt;= lastCommandTime</code> 的（agent 已领过，<code>:84</code>），再调 <code>commandService.newProfileTaskCommand(task)</code> 把任务序列化成 <code>Commands</code> 下发，同时写一条 <code>ProfileTaskLogRecord{operationType=NOTIFIED}</code> 日志（<code>:89</code>）。</p><blockquote><p><strong>为什么用命令轮询而不是长连接推送</strong><br />Agent 通常部署在业务进程里，网络环境复杂，未必能被 OAP 反向连上。轮询把主动权交给 agent——它什么时候有空什么时候来领任务，OAP 只管把任务放进缓存。缓存 1 分钟过期是为了让任务列表能被定期刷新（新任务进得来、老任务出得去），又不至于每次轮询都打 DB。</p></blockquote><h3 id="%E5%BF%AB%E7%85%A7%E5%A6%82%E4%BD%95%E4%B8%8A%E6%8A%A5%E4%B8%8E%E5%AD%98%E5%82%A8" tabindex="-1">快照如何上报与存储</h3><p><code>ProfileTaskServiceHandler.collectSnapshot</code>（<code>ProfileTaskServiceHandler.java:100</code>）是 gRPC <code>StreamObserver&lt;ThreadSnapshot&gt;</code>。每收到一条 <code>ThreadSnapshot</code> 就构造成 <code>ProfileThreadSnapshotRecord</code> 并 <code>RecordStreamProcessor.getInstance().in(record)</code>（<code>:119</code>，<strong>Record 流</strong>，时序数据）。</p><p><code>ProfileThreadSnapshotRecord</code>（<code>ProfileThreadSnapshotRecord.java:47-74</code>）字段：<code>taskId / segmentId / dumpTime / sequence / stackBinary(byte[]) / language</code>，索引名 <code>profile_task_segment_snapshot</code>，scope <code>PROFILE_TASK_SEGMENT_SNAPSHOT</code>。<code>stackBinary</code> 存序列化的 <code>ThreadStack</code>；<code>id()</code> 由 <code>taskId+segmentId+sequence</code> 组成（<code>:77-82</code>），保证同一栈序列不重复。<code>language</code> 字段（<code>ProfileLanguageType</code>：<code>JAVA(0)/GO(1)</code>，<code>ProfileLanguageType.java:23-25</code>）支持 Java 与 Go 共存——这是后加的，让 Go 也能复用这套存储与 UI。</p><p>任务完成由 <code>reportTaskFinish</code>（<code>ProfileTaskServiceHandler.java:229</code>）记录 <code>EXECUTION_FINISHED</code> 日志（<code>:237</code>）。</p><h3 id="go-pprof-%E5%A4%8D%E7%94%A8%E9%80%9A%E9%81%93" tabindex="-1">Go pprof 复用通道</h3><p><code>goProfileReport</code>（<code>ProfileTaskServiceHandler.java:143</code>）接收 Go agent 分块上报的 pprof 字节流，攒齐后（<code>isLast=true</code> 时，<code>:178</code>）用 <code>library-pprof-parser</code> 的 <code>PprofParser.parseProfile</code>（自动嗅探 gzip 魔数 <code>0x1F 0x8B</code>）+ <code>PprofSegmentParser.parseSegments</code> 解析（<code>:181-182</code>），按 <code>segment_id</code>/<code>trace_id</code> label 拆成多个 segment。每个 segment 过滤出属于自己的 sample 后再存成 <code>ProfileThreadSnapshotRecord{language=GO, stackBinary=过滤后的pprof}</code>（<code>storeGoProfileSegment</code>，<code>ProfileTaskServiceHandler.java:265-307</code>）。Go 的 <code>sequence</code> 固定为 0（每个 segment 只一条记录，<code>:288</code>）。</p><blockquote><p><strong>为什么 Go 要复用 Thread Profiling 通道而不是走独立的 pprof receiver</strong><br />Thread Profiling 绑 <code>traceSegmentId</code>，能和 trace 联动看&quot;这条慢链路里 Go 那一段 CPU 烧在哪&quot;。独立的 pprof receiver（见后文）是 service/instance 级、不绑 trace。两套各有适用场景，所以 Go 同时支持两条路：要 trace 联动走这里，要纯服务级 profiling 走 pprof receiver。</p></blockquote><h3 id="profilemodule-%E7%9A%84-service" tabindex="-1">ProfileModule 的 Service</h3><p><code>ProfileModule</code>（<code>skywalking-profile-receiver-plugin/.../module/ProfileModule.java:26</code>）<code>services()</code> 返回空数组——它本身不暴露 Service，只通过 <code>ProfileModuleProvider.start()</code>（<code>ProfileModuleProvider.java:56</code>）把 <code>ProfileTaskServiceHandler</code> 注册到共享 gRPC server。真正的业务逻辑 Service 在 <strong>CoreModule</strong> 里注册：</p><ul><li><code>ProfileTaskMutationService</code>（创建任务）——<code>CoreModuleProvider.java:341</code></li><li><code>ProfileTaskQueryService</code>（查询/分析）——<code>CoreModuleProvider.java:343</code></li></ul><h3 id="analyzer-%E4%BE%A7%EF%BC%88%E7%94%9F%E6%88%90%E7%81%AB%E7%84%B0%E5%9B%BE%EF%BC%89" tabindex="-1">Analyzer 侧（生成火焰图）</h3><p><code>ProfileAnalyzer</code>（<code>server-core/.../profiling/trace/analyze/ProfileAnalyzer.java:47</code>）做栈聚合。先用时间窗算出每个 segment 的 <code>minSequence..maxSequence</code>（<code>getAllSequenceRange</code>，<code>:183</code>），分批（<code>threadSnapshotAnalyzeBatchSize</code>）并行从 DAO 拉记录（<code>:81</code>），反序列化成 <code>ProfileStack</code>（<code>ProfileStack.deserialize</code>，<code>ProfileStack.java:40</code>，Java 走 <code>ThreadStack.codeSignatures</code>，Go 走 pprof FrameTree），按栈顶函数 <code>groupingBy</code> 聚合成 <code>ProfileStackTree</code>（火焰图树，<code>analyzeByStack</code>，<code>:218</code>）。</p><p>若 Java 时间窗内没数据，回退到 Go 的全量范围分析（<code>ProfileAnalyzer.java:135-160</code>，忽略时间窗、按 <code>language=GO</code> 全量拉，交给 <code>GoProfileAnalyzer.analyzeRecords</code>）。这个回退逻辑是新加的——Java 和 Go 共用一张 snapshot 表，但 Go 的采样语义不同（一个 segment 一条记录，没有时间窗内的多 sequence），所以不能套同一套分析逻辑。</p><h3 id="ui-%E6%9F%A5%E8%AF%A2%EF%BC%88graphql-resolver%EF%BC%89" tabindex="-1">UI 查询（GraphQL resolver）</h3><p><code>ProfileQuery</code>（<code>server-query-plugin/query-graphql-plugin/.../resolver/ProfileQuery.java:39</code>）暴露 4 个查询：</p><ul><li><code>getProfileTaskList(serviceId, endpointName)</code> → 任务列表（<code>:57</code>）</li><li><code>getProfileTaskLogs(taskID)</code> → 任务下发/完成日志（<code>:61</code>）</li><li><code>getProfileTaskSegments(taskId)</code> → 被采样的 trace segment（含跨进程父子段合并，<code>ProfileTaskQueryService.java:205</code>）</li><li><code>getSegmentsProfileAnalyze(queries)</code> → 调 <code>ProfileAnalyzer.analyze</code> 出火焰图树（<code>:69</code>）</li></ul><h3 id="%E7%94%A8%E9%80%94" tabindex="-1">用途</h3><p>定位 Java 服务<strong>单个慢端点的 on-CPU 热点函数</strong>。和 trace 联动是它的杀手锏：先看 trace 发现某 segment 慢，再对该 segment 做 thread profiling 画出火焰图，锁定是哪段代码吃 CPU。代价是必须装 Java/Go agent、只覆盖 on-CPU、必须命中特定 endpoint 才采样。</p><hr /><h2 id="ebpf-profiling%EF%BC%88%E7%BD%91%E7%BB%9C-%2B-%E8%BF%9B%E7%A8%8B%E7%BA%A7%EF%BC%8Crover-%E4%B8%8A%E6%8A%A5%EF%BC%89" tabindex="-1">eBPF Profiling（网络 + 进程级，Rover 上报）</h2><h3 id="%E8%B0%81%E4%BA%A7%E7%94%9F%E6%95%B0%E6%8D%AE-%2F-%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE-1" tabindex="-1">谁产生数据 / 传输协议</h3><p>数据由 <strong>SkyWalking Rover</strong>（eBPF Agent，独立部署在被监控节点）产生，全部 <strong>gRPC</strong>，对应 proto 在 <code>apm-protocol/apm-network/src/main/proto/ebpf/</code>：</p><ul><li>进程上报：<code>ebpf/profiling/Process.proto:31</code> → <code>service EBPFProcessService</code>（<code>reportProcesses</code> / <code>keepAlive</code>）</li><li>profiling 数据：<code>ebpf/profiling/Profile.proto:30</code> → <code>service EBPFProfilingService</code>（<code>queryTasks</code> / <code>collectProfilingData</code> stream）</li><li>持续 profiling 策略：<code>ebpf/profiling/Continuous.proto:29</code> → <code>service ContinuousProfilingService</code>（<code>queryPolicies</code> / <code>reportProfilingTask</code>）</li><li>网络访问日志：<code>ebpf/accesslog.proto:29</code> → <code>service EBPFAccessLogService</code>（<code>collect</code> stream）</li></ul><h3 id="%E5%85%B3%E9%94%AE-handler" tabindex="-1">关键 Handler</h3><p>四个 handler 都在 <code>skywalking-ebpf-receiver-plugin/.../provider/handler/</code>，由 <code>EBPFReceiverProvider.start()</code>（<code>EBPFReceiverProvider.java:79</code>）注册——<code>addHandler</code> 四个 handler（<code>:123-126</code>），同时加载 <code>EBPFOALDefine</code>（<code>oal/ebpf.oal</code>）做 K8s 网络指标的 OAL 聚合（<code>:84</code>）。</p><p><strong>① <code>EBPFProcessServiceHandler</code>（<code>EBPFProcessServiceHandler.java:59</code>）</strong> — 进程注册与保活。</p><ul><li><code>reportProcesses</code>（<code>:72</code>）：Rover 发现的进程（<code>hostProcess</code> 虚机 / <code>k8sProcess</code> 容器）转成 core 的 <code>Process</code> source（<code>ProcessDetectType.VM</code>/<code>KUBERNETES</code>）经 <code>SourceReceiver</code> 落库，并把生成的 <code>processId</code> 下行回 Rover。属性里若带 <code>support_ebpf_profiling=true</code> 则标记 <code>ProfilingSupportStatus.SUPPORT_EBPF_PROFILING</code>（<code>getProfilingSupportStatus</code>，<code>EBPFProcessServiceHandler.java:233-237</code>）——<strong>只有支持 eBPF 的进程才能被采样</strong>。这个门槛必须卡：eBPF 采栈要靠进程符号表解析，进程信息不全就采不出有意义的栈。</li><li><code>keepAlive</code>（<code>:103</code>）：心跳，同时刷新 <code>Process</code>/<code>ServiceInstanceUpdate</code>/<code>ServiceMeta</code>/<code>ServiceLabel</code>。</li></ul><p><strong>② <code>EBPFProfilingServiceHandler</code>（<code>EBPFProfilingServiceHandler.java:72</code>）</strong> — profiling 任务下发与数据收集。</p><ul><li><code>queryTasks</code>（<code>:94</code>）：按 Rover 最近 5 分钟内上报的进程（<code>QUERY_TASK_PROCESSES_RANGE_MINUTES=5</code>，<code>:79</code>），反查这些 service 下的 <code>EBPFProfilingTaskRecord</code>（<code>FIXED_TIME</code> 触发，<code>:112</code>），构造 <code>EBPFProfilingTaskCommand</code> 下发，按 serviceId + <code>SUPPORT_EBPF_PROFILING</code> + 进程 label 匹配（<code>buildProfilingCommands</code>，<code>:128</code>）。</li><li><code>collectProfilingData</code>（stream，<code>:158</code>）：首帧带 <code>EBPFProfilingTaskMetadata{taskId, processId, profilingStartTime, currentTime}</code>，handler 据此构造 <code>EBPFProcessProfilingSchedule</code>（一次调度的 schedule 记录）经 <code>SourceReceiver</code> 落库（<code>:170-175</code>）；后续帧按 <code>oneof profiling{onCPU, offCPU}</code>（<code>Profile.proto:52-54</code>）分别 <code>processOnCPUProfiling</code> / <code>processOffCPUProfiling</code>，把栈按 <code>KERNEL_SPACE → USER_SPACE</code> 排序（<code>COMMON_STACK_TYPE_ORDER</code>，<code>EBPFProfilingServiceHandler.java:74-75</code>），组装 <code>EBPFProfilingData</code> source 经 <code>SourceReceiver</code> 落库（<code>:205</code>）。</li></ul><blockquote><p><strong>为什么栈要先排 KERNEL_SPACE 再排 USER_SPACE</strong><br />一次 on-CPU 采样，eBPF 能同时抓到内核态和用户态的栈。排序是为了让火焰图里&quot;内核做了什么 → 用户代码调了什么&quot;的因果关系稳定可读——内核栈在下、用户栈在上，函数谁调谁一目了然。<code>stackIdList</code> 把排序后的 stackId 用 <code>_</code> 拼起来当主键的一部分，保证同一组栈不会重复存。</p></blockquote><p><strong>③ <code>ContinuousProfilingServiceHandler</code>（<code>ContinuousProfilingServiceHandler.java:72</code>）</strong> — 持续 profiling（按阈值自动触发）。</p><ul><li><code>queryPolicies</code>（<code>:88</code>）：Rover 上报当前各 service 的 policy uuid，OAP 与 DB 中 <code>ContinuousProfilingPolicy</code> 比对（带 Guava 缓存 <code>continuousPolicyCacheTimeout</code>），uuid 不一致就下发 <code>ContinuousProfilingPolicyCommand</code>。</li><li><code>reportProfilingTask</code>（<code>:174</code>）：当 Rover 监控指标（CPU/线程数/系统负载/HTTP 错误率/HTTP 平均响应时间，见 <code>ContinuousProfilingTriggeredMonitorType</code>，<code>Continuous.proto:82</code>）超过阈值，Rover 主动上报触发原因，handler 构造一条 <code>EBPFProfilingTaskRecord{triggerType=CONTINUOUS_PROFILING}</code>（<code>:186</code>）落库，调 <code>generateLogicalId()</code>（<code>:198</code>），并回 <code>logicalId</code>（<code>newContinuousProfilingReportCommand</code>，<code>:204</code>），Rover 再用它走 <code>collectProfilingData</code> 采样。</li></ul><p><strong>④ <code>AccessLogServiceHandler</code>（<code>AccessLogServiceHandler.java:97</code>）</strong> — eBPF 抓的&quot;网络访问日志&quot;（L7 协议 + L4 内核操作）。</p><ul><li><code>collect</code>（stream，<code>:137</code>）：每条 <code>EBPFAccessLogMessage</code> 含 <code>node</code>（节点信息/网卡/启动时间）、<code>connection</code>（本地/远端地址、角色 client/server、TLS、协议）、<code>kernelLogs[]</code>（���核 read/write/connect/accept/close 各层 L2/L3/L4 时延指标）和可选 <code>protocolLog</code>（HTTP 等应用层协议）。</li><li>handler 把内核日志转成 <code>K8SMetrics</code>（L2 网卡、L3 IP/MAC/NetFilter、L4 包数/重传/时延），把协议日志转成 service/instance/relation/endpoint 指标；若连接经过 ztunnel（Istio Ambient），还生成 mesh 指标经 <code>TelemetryDataDispatcher</code> 喂给 service-mesh 分析器。</li><li>这部分<strong>不是火焰图 profiling</strong>，而是 eBPF 网络性能剖析 + 服务拓扑/网格监控。放在这个 receiver 是因为都靠 eBPF 采，但语义上是另一回事。</li></ul><h3 id="%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E5%AD%98%E5%82%A8" tabindex="-1">数据如何存储</h3><table><thead><tr><th>Record</th><th>位置</th><th>类型</th><th>scope</th></tr></thead><tbody><tr><td><code>EBPFProfilingTaskRecord</code> (<code>EBPFProfilingTaskRecord.java:47</code>)</td><td><code>ebpf_profiling_task</code></td><td>NoneStream</td><td><code>EBPF_PROFILING_TASK</code></td></tr><tr><td><code>EBPFProfilingDataRecord</code> (<code>EBPFProfilingDataRecord.java:44</code>)</td><td><code>ebpf_profiling_data</code></td><td>Record</td><td><code>EBPF_PROFILING_DATA</code></td></tr><tr><td><code>EBPFProcessProfilingSchedule</code> (<code>source/EBPFProcessProfilingSchedule.java</code>)</td><td>schedule</td><td>Source</td><td>—</td></tr><tr><td><code>ContinuousProfilingPolicy</code> (<code>ContinuousProfilingPolicy.java</code>)</td><td>policy 表</td><td>NoneStream</td><td>—</td></tr><tr><td><code>Process</code>/<code>ProcessTraffic</code></td><td>进程表</td><td>Metrics</td><td>—</td></tr></tbody></table><p><code>EBPFProfilingTaskRecord</code> 关键字段：<code>logicalId</code>（sha256 of serviceId+labels+startTime，<code>generateLogicalId</code>，<code>EBPFProfilingTaskRecord.java:107</code>）、<code>triggerType</code>（<code>UNKNOWN/FIXED_TIME/CONTINUOUS_PROFILING</code>，<code>EBPFProfilingTriggerType.java:31-43</code>）、<code>targetType</code>（<code>UNKNOWN/ON_CPU/OFF_CPU/NETWORK</code>，<code>EBPFProfilingTargetType.java:33-41</code>）、<code>processLabelsJson</code>、<code>extensionConfigJson</code>（网络采样规则）、<code>continuousProfilingJson</code>（触发原因）。注意 <code>id()</code> 是 sha256(logicalId+createTime)（<code>:95-101</code>），而 <code>logicalId</code> 本身又是 sha256(serviceId+labels+startTime)——两层 hash：logicalId 用来跨实例去重同一逻辑任务，id 用来区分同一逻辑任务的不同创建批次。</p><p><code>EBPFProfilingDataRecord</code>（<code>EBPFProfilingDataRecord.java:44-77</code>）字段：<code>scheduleId / taskId / stackIdList / targetType / dataBinary(storageOnly) / uploadTime</code>，<code>id()</code> 由 <code>scheduleId+stackIdList+uploadTime</code> sha256（<code>:69-76</code>）。<code>dataBinary</code> 存重新组装的 <code>EBPFOnCPUProfiling</code>/<code>EBPFOffCPUProfiling</code> protobuf。它先经 <code>EBPFProfilingData</code> source（<code>source/EBPFProfilingData.java:31</code>，scope <code>EBPF_PROFILING_DATA</code>）由 <code>SourceReceiver</code> 接收，再由 <code>EBPFProcessProfilingDataDispatcher</code>（<code>EBPFProcessProfilingDataDispatcher.java:26</code>）转成 Record 落库。</p><h3 id="analyzer-%E4%BE%A7" tabindex="-1">Analyzer 侧</h3><p><code>EBPFProfilingAnalyzer</code>（<code>profiling/ebpf/analyze/EBPFProfilingAnalyzer.java:51</code>）把时间窗切成 10 秒一段（<code>FETCH_DATA_DURATION</code>，<code>:54</code>）并行拉数据，线程池名 <code>EBPFProfiling-&lt;n&gt;</code>（<code>:67</code>，大小=<code>maxThreadCountOfQueryEBPFProfilingData</code>），反序列化成 <code>EBPFProfilingStack</code>，按栈顶 <code>groupingBy</code> 聚合成 <code>EBPFProfilingTree</code>（火焰图，<code>generateTrees</code>，<code>:103</code>）。On-CPU 用 <code>dumpCount</code> 权重，Off-CPU 用 <code>switchCount</code>/<code>duration</code> 权重——on-CPU 的&quot;热&quot;是出现次数多，off-CPU 的&quot;热&quot;是等待时间长，权重得分别给，否则 off-CPU 会被高频低耗时的切换淹没。</p><h3 id="ui-%E6%9F%A5%E8%AF%A2%EF%BC%88graphql-resolver%EF%BC%89-1" tabindex="-1">UI 查询（GraphQL resolver）</h3><ul><li><code>EBPFProcessProfilingMutation</code>（<code>EBPFProcessProfilingMutation.java:32</code>）：<code>createEBPFProfilingFixedTimeTask</code>（on/off CPU，<code>:50</code>）、<code>createEBPFNetworkProfiling</code>（网络，<code>:54</code>）、<code>keepEBPFNetworkProfiling</code>（续期网络任务，<code>:58</code>，默认 10 分钟）。</li><li><code>EBPFProcessProfilingQuery</code>（<code>EBPFProcessProfilingQuery.java:41</code>）：<code>queryPrepareCreateEBPFProfilingTaskData</code>（<code>:59</code>）、<code>queryEBPFProfilingTasks</code>（<code>:66</code>）、<code>queryEBPFProfilingSchedules</code>（<code>:80</code>）、<code>analysisEBPFProfilingResult</code>（出火焰图，<code>:84</code>）。</li><li>持续 profiling 策略由 <code>ContinuousProfilingMutationService</code>/<code>ContinuousProfilingQueryService</code>（<code>CoreModuleProvider.java:365-367</code>）+ 对应 resolver 维护。</li></ul><h3 id="%E7%94%A8%E9%80%94%E4%B8%8E%E5%92%8C-thread-profiling-%E7%9A%84%E5%8C%BA%E5%88%AB" tabindex="-1">用途与和 thread profiling 的区别</h3><p>eBPF Profiling <strong>不侵入业务进程</strong>。Rover 在内核态采样任意进程（Java/Go/Python/Node…）的 on-CPU、off-CPU 栈，外加网络级 L4/L7 剖析和 mesh 监控。适合无法注入 agent 的语言、容器/多语言混合环境。Thread profiling 必须装 Java/Go agent，只覆盖 Java/Go、只做 on-CPU、必须命中特定 endpoint 才采样。eBPF 的 off-CPU 与 network profiling 是 thread profiling 没有的能力——这也是它最难被替代的地方。</p><blockquote><p><strong>什么时候该选 eBPF</strong><br />语言五花八门、容器密集、不能改业务代码、需要看网络阻塞或 mesh——这几条命中任意一条，eBPF 基本上是唯一选项。代价是 Rover 要独立部署、需要较高内核版本、符号解析依赖进程信息，运维门槛比装 agent 高。</p></blockquote><hr /><h2 id="pprof%EF%BC%88go-pprof%EF%BC%89" tabindex="-1">pprof（Go pprof）</h2><h3 id="%E8%B0%81%E4%BA%A7%E7%94%9F%E6%95%B0%E6%8D%AE-%2F-%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE-2" tabindex="-1">谁产生数据 / 传输协议</h3><p>数据由 <strong>Go Agent</strong> 用 Go 自带 <code>runtime/pprof</code> 采集后上报，<strong>gRPC</strong>。协议 <code>apm-protocol/apm-network/src/main/proto/pprof/Pprof.proto:29</code>，<code>service PprofTask</code> 两个 RPC：</p><ul><li><code>getPprofTaskCommands</code> → 拉 pprof 任务（<code>:33</code>）</li><li><code>collect</code> (双向 stream <code>PprofData</code>/<code>PprofCollectionResponse</code>) → 上传 pprof 二进制（<code>:31</code>）</li></ul><p><code>PprofData</code> 第一帧带 <code>PprofMetaData{service, serviceInstance, taskId, type(状态), contentSize}</code>（<code>Pprof.proto:59-68</code>），后续帧 <code>content</code> 是 pprof 字节；状态枚举 <code>PprofProfilingStatus</code>（<code>PROFILING_SUCCESS / EXECUTION_TASK_ERROR / TERMINATED_BY_OVERSIZE</code>，<code>:46-53</code>）。</p><h3 id="%E5%85%B3%E9%94%AE-handler-%E4%B8%8E%E9%85%8D%E7%BD%AE" tabindex="-1">关键 handler 与配置</h3><p><code>PprofServiceHandler</code>（<code>skywalking-pprof-receiver-plugin/.../handler/PprofServiceHandler.java:53</code>）实现 <code>PprofTaskGrpc.PprofTaskImplBase</code>，构造时注入两个关键参数（来自 <code>PprofModuleConfig</code>，<code>PprofModuleConfig.java:27</code>）：</p><ul><li><strong><code>pprofMaxSize</code></strong>（默认 30MB，<code>PprofModuleConfig.java:32</code>）：单文件大小上限，超限返回 <code>PPROF_TERMINATED_BY_OVERSIZE</code> 并记 <code>PPROF_UPLOAD_FILE_TOO_LARGE_ERROR</code> 日志。</li><li><strong><code>memoryParserEnabled</code></strong>（默认 true，<code>PprofModuleConfig.java:45</code>）：决定用哪种接收 observer。</li></ul><p><code>collect()</code>（<code>PprofServiceHandler.java:72</code>）根据该开关返回 <code>PprofByteBufCollectionObserver</code>（内存解析）或 <code>PprofFileCollectionObserver</code>（落临时文件再解析）——前者省去磁盘挂载，后者省内存。<code>PprofByteBufCollectionObserver</code>（<code>PprofByteBufCollectionObserver.java:43</code>）用 <code>ByteBuffer</code> 攒齐字节，<code>onCompleted</code> 时调 <code>PprofParser.dumpTree(buf)</code> 解析成 <code>FrameTree</code>，包成 <code>PprofProfilingData</code> source（<code>parsePprofAndStorage</code>，<code>PprofByteBufCollectionObserver.java:145-155</code>）。File 版用 <code>Files.createTempFile</code> + <code>FileOutputStream</code>。</p><blockquote><p><strong>memoryParserEnabled 的取舍</strong><br />默认 true（内存解析）能避免 OAP 没挂卷导致落盘失败，但大 pprof 文件会吃内存。改成 false 省内存却要求容器挂了可写卷——没挂卷会直接报错。这是个&quot;内存 vs 磁盘&quot;的二选一，没有免费午餐。</p></blockquote><h3 id="library-pprof-parser-%E5%A6%82%E4%BD%95%E8%A7%A3%E6%9E%90" tabindex="-1">library-pprof-parser 如何解析</h3><p><code>PprofParser</code>（<code>library-pprof-parser/.../pprof/parser/PprofParser.java:34</code>）核心方法：</p><ul><li><code>parseProfile(byte[])</code>（<code>:74</code>）：自动嗅探 gzip 魔数 <code>0x1F 0x8B</code>（<code>:79</code>）决定是否 <code>GZIPInputStream</code> 解压，再 <code>ProfileProto.Profile.parseFrom</code> 解析 Google pprof protobuf。Go agent 走 <code>goProfileReport</code> 时用的就是它。</li><li><code>dumpTree(ByteBuffer/String)</code>（<code>:36</code> / <code>:46</code>）：直接 <code>GZIPInputStream</code> 解压 + <code>ProfileProto.Profile.parseFrom</code>，再交给 <code>FrameTreeBuilder(profile).build()</code> 构造成 <code>FrameTree</code>（调用栈树，供火焰图）。注意 <code>dumpTree</code> 不做 gzip 嗅探，<strong>默认输入就是 gzip 压缩的 pprof</strong>——pprof receiver 这条路假设 Go agent 上传的就是标准 gzip pprof。</li><li><code>resolveSignature</code>（<code>:62</code>）：把 location id 解析成 <code>functionName:line</code>，内联帧用 <code>;</code> 拼。</li></ul><p>支持的事件类型 <code>PprofEventType</code>（<code>server-core/.../query/type/PprofEventType.java:26</code>）：<code>CPU / HEAP / BLOCK / MUTEX / GOROUTINE / THREADCREATE / ALLOCS</code>——覆盖 Go 的 CPU、堆、阻塞、锁、goroutine、线程创建、分配采样。</p><blockquote><p><strong>什么是 FrameTree</strong><br />FrameTree 是 pprof 解析后内部的&quot;调用栈树&quot;结构：根节点是栈底，子节点是栈顶上层调用者，每个节点带函数签名和累计/自身采样值。火焰图就是把 FrameTree 拍平画出来。pprof 和 async-profiler 都���它当中间表示——存库存 FrameTree 的 JSON，查询时直接合并，省去每次查询都重新解析原始二进制。</p></blockquote><h3 id="%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8" tabindex="-1">数据存储</h3><ul><li>任务表 <code>PprofTaskRecord</code>（<code>PprofTaskRecord.java:48</code>）：<code>pprof_task</code>，NoneStream，scope <code>PPROF_TASK</code>，字段 <code>taskId / serviceId / serviceInstanceIds(JSON) / createTime / events / duration / dumpPeriod</code>。</li><li>数据表 <code>PprofProfilingDataRecord</code>（<code>PprofProfilingDataRecord.java:41</code>）：<code>pprof_profiling_data</code>，Record，scope <code>PPROF_PROFILING_DATA</code>，<code>dataBinary</code> 存 <strong><code>FrameTree</code> 的 JSON</strong>（不是原始 pprof！<code>PprofProfilingDataDispatcher.java:35</code> 用 <code>GSON.toJson(source.getFrameTree()).getBytes()</code> 转换；查询时 <code>GSON.fromJson</code> 反序列化，<code>PprofQueryService.java:96</code>）。先经 <code>PprofProfilingData</code> source（scope <code>PPROF_PROFILING_DATA</code>，含 <code>eventType</code> + <code>frameTree</code>）由 <code>PprofProfilingDataDispatcher</code> 落库。</li><li>日志表 <code>PprofTaskLogRecord</code>：任务下发/执行/错误日志。</li></ul><p>任务下发与 thread profiling 一样走命令 + 缓存：<code>PprofTaskCache</code>（<code>PprofTaskCache.java:32</code>，写后 1 分钟过期，<code>:43</code>）。<code>getPprofTaskCommands</code>（<code>PprofServiceHandler.java:79</code>）还会按 <code>serviceInstanceIds</code> 过滤——pprof 任务可以限定只发给某些实例（<code>:97-100</code>）。</p><h3 id="ui-%E6%9F%A5%E8%AF%A2" tabindex="-1">UI 查询</h3><p><code>PprofQuery</code>（<code>PprofQuery.java:40</code>）+ <code>PprofMutation</code>：<code>queryPprofTaskList</code>、<code>queryPprofAnalyze</code>（按 taskId+instanceIds 查数据，<code>PprofQueryService.queryPprofData</code> 把多个 instance 的 <code>FrameTree</code> 用 <code>PprofMergeBuilder</code> 合并成一棵，<code>PprofQueryService.java:92</code>）、<code>queryPprofTaskProgress</code>（成功/失败实例列表）。</p><h3 id="%E7%94%A8%E9%80%94-1" tabindex="-1">用途</h3><p>Go 服务的 <strong>CPU/内存/阻塞/锁/goroutine 火焰图</strong>。比 thread profiling 的 Go 复用通道更&quot;原生&quot;：thread profiling 把 Go pprof 拆进 segment 维度（与 trace 联动），而本节 pprof receiver 是独立的、按 service+instance 维度的通用 Go profiling，不绑定 trace。要 trace 联动选前者，要服务级全量 profiling 选后者。</p><hr /><h2 id="async-profiler%EF%BC%88java-jfr%EF%BC%89" tabindex="-1">async-profiler（Java JFR）</h2><h3 id="%E8%B0%81%E4%BA%A7%E7%94%9F%E6%95%B0%E6%8D%AE-%2F-%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE-3" tabindex="-1">谁产生数据 / 传输协议</h3><p>数据由 <strong>Java Agent 调用 async-profiler 工具</strong>采集 JFR 后上报，<strong>gRPC</strong>。协议 <code>apm-protocol/apm-network/src/main/proto/asyncprofiler/AsyncProfiler.proto:29</code>，<code>service AsyncProfilerTask</code>：<code>getAsyncProfilerTaskCommands</code> + <code>collect</code>（双向 stream <code>AsyncProfilerData</code>/<code>AsyncProfilerCollectionResponse</code>）。结构与 pprof proto 几乎对称：第一帧 <code>AsyncProfilerMetaData{type, contentSize}</code>（<code>AsyncProfiler.proto:60-69</code>），后续 <code>content</code> 是 JFR 二进制；状态枚举 <code>AsyncProfilingStatus</code>（<code>PROFILING_SUCCESS / EXECUTION_TASK_ERROR / TERMINATED_BY_OVERSIZE</code>，<code>:51-58</code>）。</p><h3 id="%E5%85%B3%E9%94%AE-handler-%E4%B8%8E%E9%85%8D%E7%BD%AE-1" tabindex="-1">关键 handler 与配置</h3><p><code>AsyncProfilerServiceHandler</code>（<code>skywalking-async-profiler-receiver-plugin/.../handler/AsyncProfilerServiceHandler.java:54</code>）实现 <code>AsyncProfilerTaskGrpc.AsyncProfilerTaskImplBase</code>，构造时注入（来自 <code>AsyncProfilerModuleConfig</code>，<code>AsyncProfilerModuleConfig.java:28</code>）：</p><ul><li><strong><code>jfrMaxSize</code></strong>（默认 30MB，<code>AsyncProfilerModuleConfig.java:33</code>）：JFR 文件大小上限，超限返回 <code>TERMINATED_BY_OVERSIZE</code> + <code>JFR_UPLOAD_FILE_TOO_LARGE_ERROR</code> 日志。</li><li><strong><code>memoryParserEnabled</code></strong>（默认 true，<code>AsyncProfilerModuleConfig.java:46</code>）：选 <code>AsyncProfilerByteBufCollectionObserver</code>（内存）或 <code>AsyncProfilerFileCollectionObserver</code>（临时文件）。</li></ul><p>任务下发同样走缓存：<code>AsyncProfilerTaskCache</code>（写后 1 分钟过期），按 <code>serviceId</code> 取，且按 <code>serviceInstanceIds</code> 过滤（<code>AsyncProfilerServiceHandler.java:80-87</code>）。</p><h3 id="library-async-profiler-jfr-parser-%E5%A6%82%E4%BD%95%E8%A7%A3%E6%9E%90-jfr" tabindex="-1">library-async-profiler-jfr-parser 如何解析 JFR</h3><p><code>JFRParser</code>（<code>library-async-profiler-jfr-parser/.../jfr/parser/JFRParser.java:30</code>）核心：</p><ul><li><code>dumpTree(ByteBuffer buf, Arguments args)</code>（<code>:40</code>）：用 <code>one.jfr.JfrReader</code>（第三方 jfr-converter 库）读 JFR 字节流，交给 <code>JFRToFrameTree</code> 转换器把事件转成 <code>FrameTree</code>，返回 <code>Map&lt;JFREventType, FrameTree&gt;</code>——一个 JFR 文件可同时含多种事件类型的树。</li><li>支持的事件类型 <code>JFREventType</code>（<code>library-async-profiler-jfr-parser/.../jfr/type/JFREventType.java:26</code>）：<code>EXECUTION_SAMPLE</code>(CPU 采样)、<code>JAVA_MONITOR_ENTER</code>/<code>THREAD_PARK</code>(锁竞争，合并为 <code>LOCK</code>)、<code>OBJECT_ALLOCATION_IN_NEW_TLAB</code>/<code>OBJECT_ALLOCATION_OUTSIDE_TLAB</code>(堆分配)、<code>PROFILER_LIVE_OBJECT</code>(存活对象)。</li></ul><blockquote><p><strong>async-profiler 和 thread profiling 都服务 Java，区别在哪</strong><br />async-profiler 靠 <code>async-profiler</code> 这个独立工具用 <code>perf_events</code> 采 CPU 样本、用字节码插桩采内存/锁，开销低、不依赖 JVMTI 的重量级 instrumentation，能同时出 CPU+内存+锁多类火焰图，是整服务/实例级持续采样。thread profiling 走的是 <code>Thread.dumpStack()</code>，端点级、只在命中慢端点时短时采样、只出 on-CPU。一个像&quot;给 Java 进程做 Holter 监护&quot;，一个像&quot;对某个慢工位贴身跟拍&quot;。</p></blockquote><h3 id="%E6%95%B0%E6%8D%AE%E5%AD%98%E5%82%A8-1" tabindex="-1">数据存储</h3><ul><li>任务表 <code>AsyncProfilerTaskRecord</code>（<code>AsyncProfilerTaskRecord.java:46</code>）：<code>async_profiler_task</code>，NoneStream，scope <code>ASYNC_PROFILER_TASK</code>，字段 <code>taskId / serviceId / serviceInstanceIds / createTime / duration / events(JSON List) / execArgs</code>。<code>execArgs</code> 是传给 async-profiler 的命令行参数，如 <code>event=cpu,interval=10ms</code>——这是 async-profiler 的原生用法，agent 把参数透传给工具进程。</li><li>数据表 <code>JFRProfilingDataRecord</code>（<code>JFRProfilingDataRecord.java:44</code>）：<code>jfr_profiling_data</code>，Record，scope <code>JFR_PROFILING_DATA</code>，字段 <code>taskId / instanceId / eventType / uploadTime / dataBinary</code>。多了 <strong><code>eventType</code></strong> 维度（一个任务一次上传可分多种事件），<code>id()</code> 含 <code>eventType</code>（<code>:76-84</code>）。<code>dataBinary</code> 同样存 <strong><code>FrameTree</code> 的 JSON</strong>（<code>JFRProfilingDataDispatcher.java:36</code> 用 <code>GSON.toJson</code>，查询时 <code>GSON.fromJson</code>，<code>AsyncProfilerQueryService.java:96</code>）。先经 source → <code>JFRProfilingDataDispatcher</code> 落库。</li><li>日志表 <code>AsyncProfilerTaskLogRecord</code>。</li></ul><h3 id="ui-%E6%9F%A5%E8%AF%A2-1" tabindex="-1">UI 查询</h3><p><code>AsyncProfilerQuery</code>（<code>AsyncProfilerQuery.java:42</code>）+ <code>AsyncProfilerMutation</code>：<code>queryAsyncProfilerTaskList</code>（<code>:60</code>）、<code>queryAsyncProfilerAnalyze</code>（按 taskId+instanceIds+<code>eventType</code> 查，<code>:67</code>，<code>AsyncProfilerQueryService.queryJFRData</code> 用 <code>JFRMergeBuilder</code> 合并多实例的 <code>FrameTree</code>，<code>AsyncProfilerQueryService.java:93</code>）、<code>queryAsyncProfilerTaskProgress</code>（<code>:74</code>）。</p><h3 id="%E7%94%A8%E9%80%94-2" tabindex="-1">用途</h3><p>Java 服务的 <strong>CPU、内存分配、锁竞争</strong> 火焰图，基于 async-profiler 的低开销采样。和 thread profiling 都服务 Java，但定位不同：async-profiler 看&quot;整个服务/实例的 CPU/内存/锁长什么样&quot;，thread profiling 看&quot;某条慢链路里 CPU 烧在哪&quot;。</p><hr /><h2 id="%E5%9B%9B%E8%80%85%E5%AF%B9%E6%AF%94%E8%A1%A8" tabindex="-1">四者对比表</h2><table><thead><tr><th>维度</th><th>Thread Profiling</th><th>eBPF Profiling</th><th>pprof</th><th>async-profiler</th></tr></thead><tbody><tr><td><strong>数据生产者</strong></td><td>SkyWalking Java/Go Agent</td><td>SkyWalking Rover（eBPF Agent）</td><td>Go Agent（<code>runtime/pprof</code>）</td><td>Java Agent + async-profiler</td></tr><tr><td><strong>是否侵入业务进程</strong></td><td>是，需装 agent</td><td><strong>否</strong>，内核态旁路</td><td>是，需装 agent</td><td>是，需装 agent + async-profiler 二进制</td></tr><tr><td><strong>传输协议</strong></td><td>gRPC（<code>ProfileTask</code> service）</td><td>gRPC（4 个 service）</td><td>gRPC（<code>PprofTask</code> service）</td><td>gRPC（<code>AsyncProfilerTask</code> service）</td></tr><tr><td><strong>关键 receiver handler</strong></td><td><code>ProfileTaskServiceHandler</code></td><td><code>EBPFProcessServiceHandler</code> / <code>EBPFProfilingServiceHandler</code> / <code>ContinuousProfilingServiceHandler</code> / <code>AccessLogServiceHandler</code></td><td><code>PprofServiceHandler</code> + <code>PprofByteBuf/FileCollectionObserver</code></td><td><code>AsyncProfilerServiceHandler</code> + <code>AsyncProfilerByteBuf/FileCollectionObserver</code></td></tr><tr><td><strong>任务下���机制</strong></td><td>命令轮询 + <code>ProfileTaskCache</code></td><td>命令轮询（按 Rover 进程反查 task） + Rover 持续 profiling 上报</td><td>命令轮询 + <code>PprofTaskCache</code></td><td>命令轮询 + <code>AsyncProfilerTaskCache</code></td></tr><tr><td><strong>触发方式</strong></td><td>用户 UI 手动，端点级</td><td>UI 手动(FIXED_TIME) / 指标超阈值自动(CONTINUOUS)</td><td>UI 手动</td><td>UI 手动</td></tr><tr><td><strong>采集粒度</strong></td><td>端点 + trace segment 维度</td><td>进程 + network 维度</td><td>service + instance 维度</td><td>service + instance + event 维度</td></tr><tr><td><strong>能采的样本类型</strong></td><td>on-CPU（Java 栈）；Go pprof 复用通道</td><td><strong>on-CPU / off-CPU / network(L4+L7)</strong></td><td>CPU/HEAP/BLOCK/MUTEX/GOROUTINE/THREADCREATE/ALLOCS</td><td>CPU(EXECUTION_SAMPLE)/堆分配/锁(LOCK)/存活对象</td></tr><tr><td><strong>栈存储格式</strong></td><td><code>ThreadStack</code> protobuf / 过滤后 pprof，存 <code>stackBinary</code></td><td>重组后的 on/off CPU protobuf，存 <code>dataBinary</code></td><td><strong>FrameTree JSON</strong>，存 <code>dataBinary</code></td><td><strong>FrameTree JSON</strong>，存 <code>dataBinary</code></td></tr><tr><td><strong>存储结构</strong></td><td><code>ProfileTaskRecord</code>(NoneStream) + <code>ProfileThreadSnapshotRecord</code>(Record) + 日志</td><td><code>EBPFProfilingTaskRecord</code> + <code>EBPFProfilingDataRecord</code>(Record) + schedule + <code>ContinuousProfilingPolicy</code> + Process</td><td><code>PprofTaskRecord</code>(NoneStream) + <code>PprofProfilingDataRecord</code>(Record) + 日志</td><td><code>AsyncProfilerTaskRecord</code>(NoneStream) + <code>JFRProfilingDataRecord</code>(Record, 带 eventType) + 日志</td></tr><tr><td><strong>走 Record 还是专门结构</strong></td><td>Record（snapshot）+ NoneStream（task）</td><td>Record（data）+ NoneStream（task/policy），额外有 source <code>EBPFProfilingData</code></td><td>Record（data）+ NoneStream（task）</td><td>Record（data）+ NoneStream（task）</td></tr><tr><td><strong>解析库</strong></td><td><code>library-pprof-parser</code>（仅 Go 路径）</td><td>内置 <code>EBPFProfilingStack.deserialize</code></td><td><code>library-pprof-parser</code>（<code>PprofParser</code>/<code>FrameTreeBuilder</code>）</td><td><code>library-async-profiler-jfr-parser</code>（<code>JFRParser</code>/<code>one.jfr.JfrReader</code>/<code>JFRToFrameTree</code>）</td></tr><tr><td><strong>大小限制配置</strong></td><td>无单文件限制，靠 <code>maxSamplingCount</code>/<code>dumpPeriod</code></td><td>无单文件限制</td><td><code>pprofMaxSize</code>(30MB)</td><td><code>jfrMaxSize</code>(30MB)</td></tr><tr><td><strong>解析模式开关</strong></td><td>—</td><td>—</td><td><code>memoryParserEnabled</code>(内存/临时文件)</td><td><code>memoryParserEnabled</code>(内存/临时文件)</td></tr><tr><td><strong>Analyzer/火焰图</strong></td><td><code>ProfileAnalyzer</code>（时间窗 + Go 回退）</td><td><code>EBPFProfilingAnalyzer</code>（10s 分片并行）</td><td><code>PprofMergeBuilder</code> 合并多实例 FrameTree</td><td><code>JFRMergeBuilder</code> 合并多实例 FrameTree</td></tr><tr><td><strong>GraphQL resolver</strong></td><td><code>ProfileQuery</code> (+ ProfileMutation)</td><td><code>EBPFProcessProfilingQuery</code> / <code>EBPFProcessProfilingMutation</code> (+ Continuous resolvers)</td><td><code>PprofQuery</code> / <code>PprofMutation</code></td><td><code>AsyncProfilerQuery</code> / <code>AsyncProfilerMutation</code></td></tr><tr><td><strong>UI 火焰图类型</strong></td><td>on-CPU 调用栈</td><td>on-CPU / off-CPU / network</td><td>CPU/heap/block/mutex/goroutine/…</td><td>CPU/heap/lock/分配/存活对象</td></tr><tr><td><strong>典型适用场景</strong></td><td>定位 Java 慢端点的 CPU 热点函数（与 trace 联动）</td><td>多语言/容器环境的 CPU、阻塞、网络剖析 + mesh，无需改业务代码</td><td>Go 服务全方位 profiling</td><td>Java 服务的 CPU+内存+锁剖析，开销低</td></tr><tr><td><strong>语言支持</strong></td><td>Java、Go</td><td><strong>任意语言</strong>（内核采栈，符号需进程信息）</td><td>Go</td><td>Java</td></tr></tbody></table><h3 id="%E5%85%B1%E6%80%A7%E4%B8%8E%E5%B7%AE%E5%BC%82%E8%A6%81%E7%82%B9" tabindex="-1">共性与差异要点</h3><blockquote><p><strong>四者共性</strong><br />四者都是&quot;任务下发 → agent 采集 → gRPC 上报 → 落 Record/NoneStream → 查询时反序列化聚合成火焰图树&quot;。任务下发都走<strong>命令轮询 + Guava Cache</strong>（<code>ProfileTaskCache</code>/<code>PprofTaskCache</code>/<code>AsyncProfilerTaskCache</code>，均写后 1 分钟过期；eBPF 按 process 反查），都受 <code>CoreModuleProvider</code> 集中注册的 Mutation/Query Service 驱动（<code>CoreModuleProvider.java:341-367</code>）。任务表统一用 <code>NoneStreamProcessor</code>，采样数据表统一用 <code>RecordStreamProcessor</code>。</p></blockquote><p>核心差异六条：</p><ol><li><strong>侵入性</strong> — eBPF 是唯一不需要在业务进程内装探针的，适合无法改代码或语言五花八门的环境。</li><li><strong>off-CPU / 阻塞剖析</strong> — 只有 eBPF 有原生 off-CPU；async-profiler 通过锁事件(<code>LOCK</code>)间接覆盖；thread/pprof 的 block/mutex 是 Go/Java agent 内采的。</li><li><strong>网络剖析</strong> — 只有 eBPF 有 L4/L7 + mesh��<code>AccessLogServiceHandler</code>），其它三种纯函数栈。</li><li><strong>数据存储格式</strong> — thread/eBPF 存原始 protobuf 字节（<code>stackBinary</code>/<code>dataBinary</code>），查询时反序列化；pprof/async-profiler 在接收时<strong>就解析成 FrameTree JSON 存库</strong>，查询时直接 <code>GSON.fromJson</code> 合并——用空间换 CPU，查询快但存储大。</li><li><strong>持续/自动触发</strong> — 只有 eBPF 有 <code>ContinuousProfiling</code>（监控指标超阈值自动拉起 profiling 任务）。</li><li><strong>与 trace 联动</strong> — thread profiling 独有：采样绑定 <code>traceSegmentId</code>，能和 trace 一起看&quot;这条慢链路里 CPU 烧在哪&quot;。</li></ol><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键类</th><th>路径</th></tr></thead><tbody><tr><td>Thread Profiling</td><td>ProfileTaskServiceHandler</td><td><code>oap-server/server-receiver-plugin/skywalking-profile-receiver-plugin/.../handler/ProfileTaskServiceHandler.java</code></td></tr><tr><td></td><td>ProfileTaskMutationService / ProfileTaskQueryService</td><td><code>oap-server/server-core/.../profiling/trace/</code></td></tr><tr><td></td><td>ProfileAnalyzer / ProfileStack / GoProfileAnalyzer</td><td><code>oap-server/server-core/.../profiling/trace/analyze/</code></td></tr><tr><td></td><td>Profile.proto</td><td><code>apm-protocol/apm-network/src/main/proto/profile/Profile.proto</code></td></tr><tr><td>eBPF</td><td>EBPFReceiverProvider</td><td><code>oap-server/server-receiver-plugin/skywalking-ebpf-receiver-plugin/.../EBPFReceiverProvider.java</code></td></tr><tr><td></td><td>EBPFProcessServiceHandler / EBPFProfilingServiceHandler / ContinuousProfilingServiceHandler / AccessLogServiceHandler</td><td>同目录 <code>.../provider/handler/</code></td></tr><tr><td></td><td>EBPFProfilingAnalyzer</td><td><code>oap-server/server-core/.../profiling/ebpf/analyze/EBPFProfilingAnalyzer.java</code></td></tr><tr><td></td><td>ebpf proto</td><td><code>apm-protocol/apm-network/src/main/proto/ebpf/</code></td></tr><tr><td>pprof</td><td>PprofServiceHandler</td><td><code>oap-server/server-receiver-plugin/skywalking-pprof-receiver-plugin/.../handler/PprofServiceHandler.java</code></td></tr><tr><td></td><td>PprofParser / FrameTreeBuilder</td><td><code>oap-server/server-library/library-pprof-parser/.../pprof/parser/PprofParser.java</code></td></tr><tr><td></td><td>Pprof.proto</td><td><code>apm-protocol/apm-network/src/main/proto/pprof/Pprof.proto</code></td></tr><tr><td>async-profiler</td><td>AsyncProfilerServiceHandler</td><td><code>oap-server/server-receiver-plugin/skywalking-async-profiler-receiver-plugin/.../handler/AsyncProfilerServiceHandler.java</code></td></tr><tr><td></td><td>JFRParser / JFRToFrameTree</td><td><code>oap-server/server-library/library-async-profiler-jfr-parser/.../jfr/parser/JFRParser.java</code></td></tr><tr><td></td><td>JFREventType</td><td><code>oap-server/server-library/library-async-profiler-jfr-parser/.../jfr/type/JFREventType.java</code></td></tr><tr><td></td><td>AsyncProfiler.proto</td><td><code>apm-protocol/apm-network/src/main/proto/asyncprofiler/AsyncProfiler.proto</code></td></tr></tbody></table><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5" tabindex="-1">接下来</h2><p>Profiling 数据存进去了，查询时三个存储后端怎么读？详见 11-存储查询DAO实现。</p><blockquote><p><strong>心智模型</strong><br />把 Profiling 想成&quot;工厂的高级体检设备&quot;。普通监控（trace/metrics）是流水线仪表盘——看哪里慢；Profiling 是给工人拍 X 光——看为什么慢。Thread Profiling 是贴身跟拍，agent 装在工人身上，只盯某个慢工位；eBPF 是厂房监控摄像头，内核态旁路，谁都能拍，还能拍网络和阻塞；pprof 是 Go 工人的自带体检报告；async-profiler 是 Java 工人的低损耗 Holter。四种都能生成火焰图，区别在于谁能拍、拍什么、要不要侵入。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[十、Skywalking协议层与 Agent 采集]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/shi-skywalking-xie-yi-ceng-yu-agent-cai-ji" />
                <id>tag:https://lymboy.com,2026-02-10:shi-skywalking-xie-yi-ceng-yu-agent-cai-ji</id>
                <published>2026-02-10T22:04:51+08:00</published>
                <updated>2026-02-10T22:04:51+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E5%8D%8F%E8%AE%AE%E5%B1%82%E4%B8%8E-agent-%E9%87%87%E9%9B%86" tabindex="-1">协议层与 Agent 采集</h1><blockquote><p><strong>这一篇解决什么</strong><br />前面的文档讲了 OAP receiver 怎么接收数据,但&quot;线上跑的到底是什么数据结构&quot;没展开。这一篇把协议本身挖到底:<code>apm-protocol</code> 全部 26 个 proto 文件、SegmentObject/SpanObject 逐字段、Command 反向下发机制、OAP 端采样策略(基于 <code>traceId.hashCode() % 10000</code> 的确定性桶)、Compat 兼容协议为什么能让旧 Agent 不断链。读完这篇,Agent ↔ OAP 的&quot;语法&quot;就通了。</p></blockquote><hr /><h2 id="apm-protocol-%E6%A8%A1%E5%9D%97%E7%BB%93%E6%9E%84" tabindex="-1">apm-protocol 模块结构</h2><p><code>apm-protocol</code> 模块只有一个子模块 <code>apm-network</code>,把所有 proto 定义集中托管,通过 protoc 生成各语言桩代码。proto 文件按&quot;数据域&quot;分目录,每个目录对应一类采集能力。</p><h3 id="proto-%E9%80%9A%E7%94%A8%E7%BA%A6%E5%AE%9A(%E6%96%B0%E6%89%8B%E5%BF%85%E8%AF%BB)" tabindex="-1">proto 通用约定(新手必读)</h3><p>所有 proto 共享下面几条约定,看懂这几条就能读懂任何一份:</p><ul><li><strong>package</strong>:绝大多数是 <code>skywalking.v3</code>;只有 <code>asyncprofiler/AsyncProfiler.proto</code> 和 <code>pprof/Pprof.proto</code> 用 <code>skywalking.v10</code>(较新协议,独立版本号)。</li><li><strong>java_package</strong>:生成代码落到 <code>org.apache.skywalking.apm.network.&lt;域&gt;.v3</code>,OAP 按 import 区分。</li><li><strong>option java_multiple_files = true</strong>:每个 message 生成独立 Java 类,所以你在 OAP 源码里能直接 <code>import ...SegmentObject</code>。</li><li><strong>import “common/Common.proto” / “common/Command.proto”</strong>:被引用后,<code>KeyStringValuePair</code>、<code>CPU</code>、<code>Commands</code> 等可直接用。这也解释了为什么&quot;几乎所有 service 的返回值都是 <code>Commands</code>&quot;——它们都 import 了 <code>Command.proto</code>。</li><li><strong>service 定义</strong>:proto 里的 <code>service X { rpc ... }</code> 就是 gRPC 服务契约,Agent 是 client,OAP 是 server。</li><li><strong>stream 关键字</strong>:<code>rpc collect (stream SegmentObject)</code> 表示客户端流式上传——Agent 在一条 gRPC 连接里连续发多条,<code>returns (Commands)</code> 是普通单条响应;双向流(<code>stream ... returns (stream ...)</code>)只在 async-profiler/pprof 出现,用来让 OAP 中途控制 Agent 停止。</li></ul><blockquote><p><strong>什么是 protobuf 和 gRPC streaming</strong></p><ul><li><strong>protobuf</strong>(Protocol Buffers):Google 的二进制序列化格式。你写一份 <code>.proto</code>,描述数据的字段和类型,protoc 工具帮你生成各语言类。相比 JSON,它更小、更快、有强类型检查,代价是人不方便直接读。</li><li><strong>gRPC streaming</strong>:普通 RPC 是&quot;一问一答&quot;,gRPC 的 <code>stream</code> 关键字让一次调用可以连续发多条消息。<code>stream</code> 在参数侧叫客户端流(Agent 一直发,OAP 收),在返回值侧叫服务端流,两边都加 <code>stream</code> 是双向流。SkyWalking 的 trace 上报就是客户端流——一个请求的几十个 span 不一个个发,而是打包成一条流连续推上去。</li></ul></blockquote><h3 id="proto-%E6%96%87%E4%BB%B6%E5%85%A8%E9%87%8F%E6%B8%85%E5%8D%95(26-%E4%B8%AA)" tabindex="-1">proto 文件全量清单(26 个)</h3><table><thead><tr><th>目录</th><th>文件</th><th>作用域</th></tr></thead><tbody><tr><td><code>common/</code></td><td><code>Common.proto</code></td><td>公共基础类型(KeyStringValuePair、CPU、DetectPoint、Instant)</td></tr><tr><td><code>common/</code></td><td><code>Command.proto</code></td><td>OAP 反向下发指令的 Command/Commands 结构</td></tr><tr><td><code>language-agent/</code></td><td><code>Tracing.proto</code></td><td>trace 段上报(SegmentObject/SpanObject)</td></tr><tr><td><code>language-agent/</code></td><td><code>JVMMetric.proto</code></td><td>JVM 指标上报</td></tr><tr><td><code>language-agent/</code></td><td><code>Meter.proto</code></td><td>自定义 Meter 上报</td></tr><tr><td><code>language-agent/</code></td><td><code>CLRMetric.proto</code></td><td>.NET CLR 指标上报</td></tr><tr><td><code>language-agent/</code></td><td><code>ConfigurationDiscoveryService.proto</code></td><td>配置动态发现(CDS)</td></tr><tr><td><code>language-agent/</code></td><td><code>TracingCompat.proto</code> / <code>JVMMetricCompat.proto</code> / <code>MeterCompat.proto</code> / <code>CLRMetricCompat.proto</code></td><td>旧版包名兼容服务定义</td></tr><tr><td><code>logging/</code></td><td><code>Logging.proto</code></td><td>日志上报</td></tr><tr><td><code>event/</code></td><td><code>Event.proto</code></td><td>事件上报</td></tr><tr><td><code>profile/</code></td><td><code>Profile.proto</code></td><td>线程级 Profile 任务下发与快照上报</td></tr><tr><td><code>profile/</code></td><td><code>ProfileCompat.proto</code></td><td>Profile 兼容服务</td></tr><tr><td><code>management/</code></td><td><code>Management.proto</code></td><td>实例注册/保活/属性上报</td></tr><tr><td><code>management/</code></td><td><code>ManagementCompat.proto</code></td><td>管理兼容服务</td></tr><tr><td><code>browser/</code></td><td><code>BrowserPerf.proto</code></td><td>浏览器性能/错误日志/Web Vitals 上报</td></tr><tr><td><code>browser/</code></td><td><code>BrowserPerfCompat.proto</code></td><td>浏览器兼容服务</td></tr><tr><td><code>service-mesh-probe/</code></td><td><code>service-mesh.proto</code></td><td>service mesh(Envoy sidecar)指标上报</td></tr><tr><td><code>ebpf/</code></td><td><code>accesslog.proto</code></td><td>eBPF(Rover)访问日志/网络内核指标</td></tr><tr><td><code>ebpf/profiling/</code></td><td><code>Process.proto</code></td><td>eBPF 进程发现与保活</td></tr><tr><td><code>ebpf/profiling/</code></td><td><code>Profile.proto</code></td><td>eBPF on/off CPU profiling 数据</td></tr><tr><td><code>ebpf/profiling/</code></td><td><code>Continuous.proto</code></td><td>eBPF 连续 profiling 策略</td></tr><tr><td><code>asyncprofiler/</code></td><td><code>AsyncProfiler.proto</code></td><td>async-profiler(JFR)任务(package skywalking.v10)</td></tr><tr><td><code>pprof/</code></td><td><code>Pprof.proto</code></td><td>Go pprof 任务(package skywalking.v10)</td></tr></tbody></table><h3 id="%E5%90%84-proto-%E5%AE%9A%E4%B9%89%E7%9A%84%E6%A0%B8%E5%BF%83-service-%E4%B8%8E-message-%E9%80%9F%E8%A7%88" tabindex="-1">各 proto 定义的核心 service 与 message 速览</h3><table><thead><tr><th>proto 文件</th><th>service</th><th>核心 message</th></tr></thead><tbody><tr><td><code>language-agent/Tracing.proto:33</code></td><td><code>TraceSegmentReportService</code>(collect 流式 / collectInSync 单条)</td><td><code>SegmentObject</code>、<code>SpanObject</code>、<code>SegmentReference</code>、<code>Log</code>、<code>SpanAttachedEvent</code>、<code>SegmentCollection</code></td></tr><tr><td><code>language-agent/Tracing.proto:235</code></td><td><code>SpanAttachedEventReportService</code>(v3.1,eBPF 附加事件)</td><td><code>SpanAttachedEvent</code></td></tr><tr><td><code>language-agent/JVMMetric.proto:32</code></td><td><code>JVMMetricReportService</code></td><td><code>JVMMetricCollection</code>、<code>JVMMetric</code>、<code>Memory</code>、<code>MemoryPool</code>、<code>GC</code>、<code>Thread</code>、<code>Class</code></td></tr><tr><td><code>language-agent/Meter.proto:29</code></td><td><code>MeterReportService</code>(collect / collectBatch)</td><td><code>MeterData</code>、<code>MeterSingleValue</code>、<code>MeterHistogram</code>、<code>MeterBucketValue</code>、<code>Label</code>、<code>MeterDataCollection</code></td></tr><tr><td><code>language-agent/CLRMetric.proto:31</code></td><td><code>CLRMetricReportService</code></td><td><code>CLRMetricCollection</code>、<code>CLRMetric</code>、<code>ClrGC</code>、<code>ClrThread</code></td></tr><tr><td><code>language-agent/ConfigurationDiscoveryService.proto:31</code></td><td><code>ConfigurationDiscoveryService</code></td><td><code>ConfigurationSyncRequest</code></td></tr><tr><td><code>logging/Logging.proto:32</code></td><td><code>LogReportService</code></td><td><code>LogData</code>、<code>LogDataBody</code>、<code>TextLog/JSONLog/YAMLLog</code>、<code>TraceContext</code>、<code>LogTags</code></td></tr><tr><td><code>event/Event.proto:30</code></td><td><code>EventService</code></td><td><code>Event</code>、<code>Source</code>、<code>Type</code></td></tr><tr><td><code>profile/Profile.proto:30</code></td><td><code>ProfileTask</code></td><td><code>ProfileTaskCommandQuery</code>、<code>ThreadSnapshot</code>、<code>ThreadStack</code>、<code>ProfileTaskFinishReport</code>、<code>GoProfileData</code></td></tr><tr><td><code>management/Management.proto:32</code></td><td><code>ManagementService</code></td><td><code>InstanceProperties</code>、<code>InstancePingPkg</code></td></tr><tr><td><code>browser/BrowserPerf.proto:31</code></td><td><code>BrowserPerfService</code></td><td><code>BrowserPerfData</code>、<code>BrowserErrorLog</code>、<code>BrowserWebVitalsPerfData</code>、<code>BrowserResourcePerfData</code>、<code>BrowserWebInteractionsPerfData</code></td></tr><tr><td><code>service-mesh-probe/service-mesh.proto:31</code></td><td><code>ServiceMeshMetricService</code></td><td><code>ServiceMeshMetrics</code>、<code>HTTPServiceMeshMetric</code>、<code>TCPServiceMeshMetric</code>、<code>MeshProbeDownstream</code></td></tr><tr><td><code>ebpf/accesslog.proto:29</code></td><td><code>EBPFAccessLogService</code></td><td><code>EBPFAccessLogMessage</code>、<code>AccessLogConnection</code>、<code>AccessLogKernelLog</code>、<code>AccessLogHTTPProtocol</code> 等</td></tr><tr><td><code>ebpf/profiling/Process.proto:31</code></td><td><code>EBPFProcessService</code></td><td><code>EBPFProcessReportList</code>、<code>EBPFProcessProperties</code>、<code>EBPFProcessEntityMetadata</code></td></tr><tr><td><code>ebpf/profiling/Profile.proto:30</code></td><td><code>EBPFProfilingService</code></td><td><code>EBPFProfilingTaskQuery</code>、<code>EBPFProfilingData</code>、<code>EBPFOnCPUProfiling</code>、<code>EBPFOffCPUProfiling</code></td></tr><tr><td><code>ebpf/profiling/Continuous.proto:29</code></td><td><code>ContinuousProfilingService</code></td><td><code>ContinuousProfilingPolicyQuery</code>、<code>ContinuousProfilingReport</code>、<code>ContinuousProfilingCause</code></td></tr><tr><td><code>asyncprofiler/AsyncProfiler.proto:29</code></td><td><code>AsyncProfilerTask</code></td><td><code>AsyncProfilerData</code>、<code>AsyncProfilerMetaData</code>、<code>AsyncProfilerTaskCommandQuery</code>、<code>AsyncProfilingStatus</code></td></tr><tr><td><code>pprof/Pprof.proto:29</code></td><td><code>PprofTask</code></td><td><code>PprofData</code>、<code>PprofMetaData</code>、<code>PprofTaskCommandQuery</code>、<code>PprofProfilingStatus</code></td></tr></tbody></table><blockquote><p><strong>common 不定义 service</strong><br /><code>common/Common.proto</code> 只定义公共 message;<code>common/Command.proto</code> 只定义 <code>Command</code>/<code>Commands</code>,都不定义 service。</p></blockquote><hr /><h2 id="segmentobject-%2F-spanobject-%E7%BB%93%E6%9E%84%E8%AF%A6%E8%A7%A3(trace-%E6%A0%B8%E5%BF%83)" tabindex="-1">SegmentObject / SpanObject 结构详解(trace 核心)</h2><blockquote><p><strong>Segment vs Span</strong><br />Zipkin 这类链路追踪系统,一条 trace 就是一堆平铺的 span。SkyWalking 不一样:它要求&quot;同一个进程(典型如 Java 的同一个线程)里的所有 span 必须先打包成一个 Segment,再整体上报&quot;。</p><p>这样设计有两个好处:一是减少上报次数——一个请求一个 Segment,而不是几十个 span 各报一次;二是 Segment 本身就是一个分析单位。一条完整的链路通常由多个 Segment 跨进程拼接而成,每个进程报自己的 Segment。</p></blockquote><h3 id="segmentobject-%E5%AD%97%E6%AE%B5(trace-%E6%AE%B5%E5%AF%B9%E8%B1%A1)" tabindex="-1">SegmentObject 字段(trace 段对象)</h3><p>定义见 <code>apm-protocol/apm-network/src/main/proto/language-agent/Tracing.proto:54</code>。</p><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>traceId</code></td><td>string</td><td>全局 trace 唯一 ID,跨所有 Segment 共享</td></tr><tr><td><code>traceSegmentId</code></td><td>string</td><td>本 Segment 的唯一 ID,其他 Segment 通过它引用本段作为父段</td></tr><tr><td><code>spans</code></td><td>repeated SpanObject</td><td>本段包含的所有 span</td></tr><tr><td><code>service</code></td><td>string</td><td>服务逻辑名(一组同行为的工作负载),拓扑图上的节点</td></tr><tr><td><code>serviceInstance</code></td><td>string</td><td>服务实例名(典型是一个 OS 进程,如一个 pod)</td></tr><tr><td><code>isSizeLimited</code></td><td>bool</td><td>本 Segment 是否因 span 太多被截断(Agent 可能为了性能丢弃部分 span)</td></tr></tbody></table><p>注释(<code>Tracing.proto:48-53</code>)明确:在 Java 里&quot;一个 Segment = 一个请求上下文在同一个线程里的所有 span&quot;;在没有线程概念的语言(如 Go)里,则表示&quot;一个请求上下文跨 goroutine 的所有 span&quot;。</p><h3 id="spanobject-%E5%AD%97%E6%AE%B5(%E6%89%A7%E8%A1%8C%E5%8D%95%E5%85%83)" tabindex="-1">SpanObject 字段(执行单元)</h3><p>定义见 <code>apm-protocol/apm-network/src/main/proto/language-agent/Tracing.proto:113</code>。</p><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>spanId</code></td><td>int32</td><td>本段内 span 编号,从 0 开始,段内唯一</td></tr><tr><td><code>parentSpanId</code></td><td>int32</td><td>本段内父 span 编号;<code>-1</code> 表示这是根 span(segment 的第一个 span)</td></tr><tr><td><code>startTime</code> / <code>endTime</code></td><td>int64</td><td>毫秒时间戳(1970-01-01 UTC 起算)</td></tr><tr><td><code>refs</code></td><td>repeated SegmentReference</td><td>跨线程/跨进程的父段引用;通常只有一个,但 MQ 批量消费场景可能有多个</td></tr><tr><td><code>operationName</code></td><td>string</td><td>span 的逻辑名(如 HTTP URI、gRPC 方法签名);强烈建议不含参数,参数放 tags</td></tr><tr><td><code>peer</code></td><td>string</td><td>对端地址(ip/host:port);<strong>Exit span 必填</strong>,用于构建拓扑(STAM 方法)</td></tr><tr><td><code>spanType</code></td><td>SpanType</td><td>Entry/Exit/Local</td></tr><tr><td><code>spanLayer</code></td><td>SpanLayer</td><td>技术栈层级(DB/RPC/HTTP/MQ/Cache 等)</td></tr><tr><td><code>componentId</code></td><td>int32</td><td>组件 ID,对应 <code>component-libraries.yml</code> 里预定义的框架(如 Tomcat=1)</td></tr><tr><td><code>isError</code></td><td>bool</td><td>是否异常;决定后端成功率统计</td></tr><tr><td><code>tags</code></td><td>repeated KeyStringValuePair</td><td>字符串键值对,承载参数/额外信息</td></tr><tr><td><code>logs</code></td><td>repeated Log</td><td>span 执行期间发生的事件(带时间戳的键值对)</td></tr><tr><td><code>skipAnalysis</code></td><td>bool</td><td>为 true 时让后端跳过分析(Agent 比后端更了解服务角色时用)</td></tr></tbody></table><h3 id="%E5%85%B3%E9%94%AE%E6%9E%9A%E4%B8%BE" tabindex="-1">关键枚举</h3><p><strong>SpanType</strong>(<code>Tracing.proto:183-190</code>):</p><table><thead><tr><th>值</th><th>含义</th><th>典型场景</th></tr></thead><tbody><tr><td><code>Entry = 0</code></td><td>服务端入口</td><td>RPC 的服务端、MQ 的消费端</td></tr><tr><td><code>Exit = 1</code></td><td>客户端出口</td><td>RPC 的客户端、MQ 的生产端</td></tr><tr><td><code>Local = 2</code></td><td>本地代码</td><td>本地普通代码执行</td></tr></tbody></table><p><strong>SpanLayer</strong>(<code>Tracing.proto:207-224</code>):标识 span 的技术栈层级,用于分层统计:</p><table><thead><tr><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>Unknown = 0</code></td><td></td></tr><tr><td><code>Database = 1</code></td><td>数据库客户端</td></tr><tr><td><code>RPCFramework = 2</code></td><td>RPC 客户端/服务端</td></tr><tr><td><code>Http = 3</code></td><td>HTTP,是更具体的 RPCFramework</td></tr><tr><td><code>MQ = 4</code></td><td>MQ 生产/消费</td></tr><tr><td><code>Cache = 5</code></td><td>缓存客户端</td></tr><tr><td><code>FAAS = 6</code></td><td>函数即服务</td></tr><tr><td><code>GenAI = 7</code></td><td>生成式 AI 服务,较新</td></tr></tbody></table><blockquote><p><strong>SpanType 与 SpanLayer 正交</strong><br />一个 HTTP 服务端 span 是 <code>spanType=Entry, spanLayer=Http</code>;一个 MySQL 查询 span 是 <code>spanType=Exit, spanLayer=Database</code>。Entry/Exit 决定拓扑角色,SpanLayer 决定分层统计。</p></blockquote><p><strong>RefType</strong>(<code>Tracing.proto:198-204</code>):</p><table><thead><tr><th>值</th><th>含义</th></tr></thead><tbody><tr><td><code>CrossProcess = 0</code></td><td>引用指向另一个 OS 进程的 Segment(跨进程 RPC/MQ)</td></tr><tr><td><code>CrossThread = 1</code></td><td>引用指向同一进程内跨线程的 Segment(仅在有线程概念的语言用)</td></tr></tbody></table><h3 id="segmentreference-%E5%AD%97%E6%AE%B5(%E8%B7%A8%E6%AE%B5%E5%BC%95%E7%94%A8)" tabindex="-1">SegmentReference 字段(跨段引用)</h3><p>定义见 <code>Tracing.proto:80</code>。这是把多个 Segment 串成一条链路的&quot;胶水&quot;。</p><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>refType</code></td><td>RefType</td><td>CrossProcess / CrossThread</td></tr><tr><td><code>traceId</code></td><td>string</td><td>全局 traceId(与子段相同)</td></tr><tr><td><code>parentTraceSegmentId</code></td><td>string</td><td>父 Segment 的 ID</td></tr><tr><td><code>parentSpanId</code></td><td>int32</td><td>父段中具体的父 span 编号</td></tr><tr><td><code>parentService</code></td><td>string</td><td>父段所属服务逻辑名</td></tr><tr><td><code>parentServiceInstance</code></td><td>string</td><td>父段所属实例名</td></tr><tr><td><code>parentEndpoint</code></td><td>string</td><td>父段的 endpoint 名(父段第一个 Entry span 的名)</td></tr><tr><td><code>networkAddressUsedAtPeer</code></td><td>string</td><td>父端 Exit span 使用的网络地址(如 <code>127.0.0.1:913</code>),是 STAM 拓扑分析的关键</td></tr></tbody></table><h3 id="traceid-%2F-segmentid-%2F-parentspanid-%E7%9A%84%E5%85%B3%E7%B3%BB(%E9%93%BE%E8%B7%AF%E6%8B%BC%E6%8E%A5%E5%8E%9F%E7%90%86)" tabindex="-1">traceId / segmentId / parentSpanId 的关系(链路拼接原理)</h3><p>这是新手最容易混乱的地方,用一个例子讲清楚:</p><pre><code class="language-">服务A 收到 HTTP 请求 → 调用 服务B</code></pre><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/09-%E5%8D%8F%E8%AE%AE%E5%B1%82%E4%B8%8EAgent%E9%87%87%E9%9B%86.md_0.png" alt="图1" /></p><p>要点:</p><ul><li><code>traceId</code> 全链路唯一,所有 Segment 共享。</li><li><code>traceSegmentId</code> 段内唯一,是&quot;被引用&quot;的句柄。</li><li><code>parentSpanId</code> 有两层含义:段内(<code>SpanObject.parentSpanId</code>,指向本段内的父 span)和跨段(<code>SegmentReference.parentSpanId</code>,指向父段里的某个 span)。</li></ul><h3 id="tag-%E4%B8%8E-log-%E5%AD%97%E6%AE%B5" tabindex="-1">tag 与 log 字段</h3><ul><li><strong>tag</strong>(<code>SpanObject.tags</code>,<code>Tracing.proto:163</code>):<code>repeated KeyStringValuePair</code>,纯字符串键值对。某些特殊 tag 会被 OAP 用于高级特性(如 Java 插件开发指南里的 special span tags)。通俗说,tag 是 span 的&quot;贴标签&quot;,比如 <code>db.statement=&quot;SELECT * FROM users&quot;</code>、<code>http.method=GET</code>。</li><li><strong>log</strong>(<code>SpanObject.logs</code>,<code>Tracing.proto:166</code> → <code>Log</code> 定义于 <code>Tracing.proto:174</code>):<code>repeated Log</code>,每个 Log 是 <code>{time: int64 毫秒时间戳, data: repeated KeyStringValuePair}</code>。用于记录 span 执行期间的事件,典型用法是发生异常时把异常类名、消息、堆栈作为 key-value 存进去。</li></ul><h3 id="spanattachedevent(v3.1%2Cebpf-%E9%99%84%E5%8A%A0%E4%BA%8B%E4%BB%B6)" tabindex="-1">SpanAttachedEvent(v3.1,eBPF 附加事件)</h3><p>定义见 <code>Tracing.proto:246</code>,由 <code>SpanAttachedEventReportService</code>(<code>Tracing.proto:235</code>)流式上报。</p><blockquote><p><strong>这是 Rover 的功能</strong><br />当一个 RPC 正被进程内语言 Agent trace 时,SkyWalking Rover(eBPF Agent)通过 trace header(<code>sw8</code>)感知到这次 RPC,然后从 OS 内核层面(syscall 级)采集额外的网络诊断信息,&quot;附加&quot;到这个 span 上。</p></blockquote><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>startTime</code> / <code>endTime</code></td><td>Instant</td><td><strong>纳秒</strong>时间戳(注意:SkyWalking 多数时间戳是毫秒,这里因 syscall 极快而用纳秒)</td></tr><tr><td><code>event</code></td><td>string</td><td>事件名(如 syscall 栈里的方法签名)</td></tr><tr><td><code>tags</code></td><td>repeated KeyStringValuePair</td><td>OS 级信息(如 net_device、L7 协议)</td></tr><tr><td><code>summary</code></td><td>repeated KeyIntValuePair</td><td>统计汇总(name → int64)</td></tr><tr><td><code>traceContext</code></td><td>SpanReference</td><td>从 <code>sw8</code> header 解码出的 trace 上下文(traceId/traceSegmentId/spanId)</td></tr></tbody></table><p><code>Instant</code>(<code>common/Common.proto:58</code>)是纳秒级时间点:<code>seconds</code>(epoch 秒)+ <code>nanos</code>(秒内纳秒,0-999999999)。</p><p><code>SpanReference.SpanReferenceType</code>(<code>Tracing.proto:284</code>)支持 <code>SKYWALKING = 0</code> 和 <code>ZIPKIN = 1</code>,即 Rover 既能附加到 SkyWalking trace,也能附加到 Zipkin trace。</p><hr /><h2 id="command-%E4%B8%8B%E5%8F%91%E6%9C%BA%E5%88%B6" tabindex="-1">Command 下发机制</h2><blockquote><p><strong>为什么几乎所有 rpc 都返回 Commands</strong><br />SkyWalking 的 gRPC 是双向的。Agent 上报数据的同时,OAP 借助每个 rpc 的返回值 <code>Commands</code> 反向给 Agent 下发指令——采样率调整、Profile 任务、配置更新、eBPF profiling 任务等。Agent 不需要额外的轮询通道。</p><p>复用 Agent 已经在建立的 gRPC 连接做反向控��。Agent 每次上报(trace/JVM/meter/log…),OAP 都可以顺带把&quot;接下来你要做什么&quot;塞进响应里,免去 Agent 单独长轮询配置/任务的开销。这也是为什么这些 proto 全都 <code>import &quot;common/Command.proto&quot;</code>。</p></blockquote><blockquote><p><strong>什么是 Command 下发</strong><br />“Command 下发&quot;就是 OAP 在 Agent 上报数据的回程里,夹带一条指令给 Agent 执��。比如 OAP 想 profile 某个慢接口,不用等 Agent 来问,而是趁 Agent 下一次上报 trace 时,在返回值里塞一句&quot;去采集这个 endpoint 的线程快照”。Agent 收到后照做,下次上报时再把结果带回来。本质是把&quot;控制流&quot;搭在&quot;数据流&quot;的回程上,省一条独立通道。</p></blockquote><h3 id="command-%2F-commands-%E7%BB%93%E6%9E%84" tabindex="-1">Command / Commands 结构</h3><p>定义见 <code>apm-protocol/apm-network/src/main/proto/common/Command.proto</code>。</p><p><code>Command</code>(<code>Command.proto:99</code>):</p><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>command</code></td><td>string</td><td>指令名,区分不同指令类型(如 <code>ProfileTaskQuery</code>、<code>ConfigurationDiscoveryCommand</code>)</td></tr><tr><td><code>args</code></td><td>repeated KeyStringValuePair</td><td>指令参数,键值对;基本类型转字符串、列表用逗号分隔、复杂结构用 JSON 序列化</td></tr></tbody></table><p><code>Commands</code>(<code>Command.proto:112</code>):<code>repeated Command commands</code>,一次可下发多条指令。</p><blockquote><p><strong>通用信封设计</strong><br />Command 是&quot;无 schema 的通用信封&quot;——<code>command</code> 名字 + <code>args</code> 键值对。具体每条指令有哪些 arg,由注释文档约定(见 <code>Command.proto:32-98</code>),不强制在 proto 里枚举。新增指令类型不用改 proto,只需新增一个指令名和约定 args。</p></blockquote><h3 id="oap-%E4%BE%A7%E7%9A%84-command-%E7%B1%BB%E5%9E%8B%E5%AE%9E%E7%8E%B0" tabindex="-1">OAP 侧的 Command 类型实现</h3><p>OAP 在 <code>apm-protocol/apm-network/src/main/java/org/apache/skywalking/oap/server/network/trace/component/command/</code> 下为每种指令写了 Java 封装类,统一继承 <code>BaseCommand</code>,实现 <code>Serializable</code>(OAP→Agent 方向序列化)和 <code>Deserializable</code>(Agent→OAP 方向反序列化,用于去重/确认)。</p><p><code>BaseCommand</code>(<code>BaseCommand.java:24</code>):所有指令都带 <code>command</code> 名和 <code>SerialNumber</code>(序列号,Agent 用于指令去重,避免重复执行同一指令)。它内部维护一个 <code>Command.Builder</code>,构造时先把 <code>SerialNumber</code> 塞进去(<code>BaseCommand.java:36</code>)。</p><p>已实现的指令类型:</p><table><thead><tr><th>Java 类</th><th>指令名(NAME 常量)</th><th>说明</th></tr></thead><tbody><tr><td><code>ProfileTaskCommand.java:28</code></td><td><code>ProfileTaskQuery</code></td><td>给 Agent 下发线程级 Profile 任务(taskId、endpoint、duration、dumpPeriod 等)</td></tr><tr><td><code>ConfigurationDiscoveryCommand.java:29</code></td><td><code>ConfigurationDiscoveryCommand</code></td><td>CDS 配置发现,含 <code>UUID</code>(配置指纹)和 config 键值对</td></tr><tr><td><code>EBPFProfilingTaskCommand.java</code></td><td><code>EBPFProfilingTaskQuery</code></td><td>给 Rover 下发 eBPF profiling 任务</td></tr><tr><td><code>ContinuousProfilingPolicyCommand.java</code></td><td><code>ContinuousProfilingPolicyQuery</code></td><td>下发连续 profiling 策略(阈值监控)</td></tr><tr><td><code>ContinuousProfilingReportCommand.java</code></td><td><code>ContinuousProfilingReportTask</code></td><td>处理 Agent 上报的连续 profiling 触发任务</td></tr><tr><td><code>AsyncProfilerTaskCommand.java</code></td><td>(async-profiler 任务)</td><td>给 Java Agent 下发 async-profiler(JFR)任务</td></tr><tr><td><code>PprofTaskCommand.java</code></td><td>(pprof 任务)</td><td>给 Go Agent 下发 pprof 任务</td></tr><tr><td><code>TraceIgnoreCommand.java</code></td><td>(trace 忽略规则)</td><td>下发 trace 忽略规则</td></tr></tbody></table><h3 id="profiletaskcommand-%E8%AF%A6%E8%A7%A3(%E6%9C%80%E5%85%B8%E5%9E%8B%E7%9A%84%E6%8C%87%E4%BB%A4)" tabindex="-1">ProfileTaskCommand 详解(最典型的指令)</h3><p><code>ProfileTaskCommand.java:26</code> 实现的指令 <code>ProfileTaskQuery</code>(对应 <code>Command.proto:43-53</code> 的约定)字段:</p><ul><li><code>TaskId</code>:任务唯一 ID</li><li><code>EndpointName</code>:要 profile 的 endpoint 名</li><li><code>Duration</code>:profile 持续时长(秒)</li><li><code>MinDurationThreshold</code>:最小耗时门槛,只有慢于这个值的请求才 profile</li><li><code>DumpPeriod</code>:线程快照 dump 间隔(毫秒)</li><li><code>MaxSamplingCount</code>:最大采样快照数</li><li><code>StartTime</code> / <code>CreateTime</code>:任务起始/创建时间戳</li></ul><p>序列化时(<code>ProfileTaskCommand.java:92</code>)把这些字段逐个转成字符串塞进 <code>args</code>;反序列化时(<code>ProfileTaskCommand.java:54</code>)遍历 <code>args</code> 按 key 还原。这正是&quot;通用信封 + 约定 args&quot;的落地方式。</p><h3 id="%E9%85%8D%E7%BD%AE%E5%8F%91%E7%8E%B0%E7%9A%84-uuid-%E4%BC%98%E5%8C%96" tabindex="-1">配置发现的 UUID 优化</h3><p><code>ConfigurationDiscoveryService.proto:40</code> 的 <code>fetchConfigurations</code> rpc 返回 <code>Commands</code>,期望指令是 <code>ConfigurationDiscoveryCommand</code>。注释(<code>ConfigurationDiscoveryService.proto:33-39</code>)说明:用一个保留 key <code>UUID</code> 作为配置指纹——Agent 缓存上次的 UUID,下次请求带上它(<code>ConfigurationSyncRequest.uuid</code>,<code>ConfigurationDiscoveryService.proto:53</code>),如果配置没变 OAP 返回空 <code>Commands</code>,减少流量。<code>ConfigurationDiscoveryCommand.java:31</code> 也定义了 <code>UUID_CONST_NAME = &quot;UUID&quot;</code>。</p><hr /><h2 id="%E9%87%87%E6%A0%B7%E5%8D%8F%E8%AE%AE%E4%B8%8E%E7%AD%96%E7%95%A5" tabindex="-1">采样协议与策略</h2><blockquote><p><strong>采样在 OAP 后端做,不在 proto 里</strong><br />SkyWalking 的 trace 采样主要在 <strong>OAP 后端</strong>完成,而不是 Agent 端全量决定。Agent 默认全量上报,OAP 用一个基于 <code>traceId.hashCode()</code> 的 0-9999 概率桶 + 慢/错误 trace 优先的策略,决定哪些 trace 真正落库。采样率可以按 service 维度配置,且支持动态下发。</p><p>采样策略本身不在 proto 协议里显式定义字段。<code>SegmentObject</code> 没有专门的&quot;采样率&quot;字段。采样是 OAP 收到 Segment 后的&quot;丢弃/保留&quot;决策,不改变 Agent 上报的格式。</p></blockquote><blockquote><p><strong>什么是采样限流</strong><br />线上流量一大,每秒几十万个 trace 全存下来,存储扛不住、查询也慢。“采样&quot;就是只保留一部分 trace 落库——比如只留 30%,剩下 70% 收到即丢。关键是怎么决定&quot;留哪些”:随机丢可能正好把出问题的慢请求丢了,所以 SkyWalking 的策略是&quot;确定性桶 + 慢 trace 优先&quot;——同一个 traceId 永远得到同一个采样号(要么全留要么全丢,保证链路完整),而且只要这个请求够慢(超过阈值),不管采样号是多少都留。</p></blockquote><h3 id="%E5%8D%8F%E8%AE%AE%E5%B1%82%E4%B8%8E%E9%87%87%E6%A0%B7%E7%9A%84%E5%94%AF%E4%B8%80%E8%81%94%E7%B3%BB" tabindex="-1">协议层与采样的唯一联系</h3><ul><li><code>SpanObject.isError</code> 和 <code>startTime/endTime</code>(算 duration)是 OAP 采样决策的输入。</li><li>采样率配置<strong>不是</strong>通过 Command 下发给 Agent,而是 <strong>OAP 侧自己加载 <code>trace-sampling-policy-settings.yml</code></strong>(静态文件 + 动态配置),由 <code>traceSamplingPolicy</code> 这个 dynamic config key 驱动。</li></ul><blockquote><p><strong>新手常见误解</strong><br />“采样率是 OAP 通过 Command 下发给 Agent 的”。当前实现里<strong>不是</strong>——采样是 OAP 端的后置过滤。Command 下发的是 Profile 任务、配置发现等,不是 trace 采样率。</p></blockquote><h3 id="oap-%E4%BE%A7%E9%87%87%E6%A0%B7%E5%AE%9E%E7%8E%B0" tabindex="-1">OAP 侧采样实现</h3><blockquote><p><strong>路径修正</strong><br />原文把 <code>TraceSegmentSampler</code> 和 <code>TraceSamplingPolicyWatcher</code> 都归到 <code>trace/sampling/</code> 目录,实际不是。<code>TraceSegmentSampler.java</code> 在 <code>trace/parser/listener/</code>,<code>TraceSamplingPolicyWatcher.java</code> 在 <code>trace/</code>(sampling 子目录只放 <code>SamplingPolicy</code>、<code>SamplingPolicySettings</code>、<code>SamplingPolicySettingsReader</code> 三个纯数据类)。</p></blockquote><p>核心类分散在两个目录:</p><p><strong><code>TraceSegmentSampler</code></strong>(<code>oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/parser/listener/TraceSegmentSampler.java:30</code>):</p><pre><code class="language-java">// TraceSegmentSampler.java:33public boolean shouldSample(SegmentObject segmentObject, int duration) {    int sample = Math.abs(segmentObject.getTraceId().hashCode()) % 10000;    String serviceName = segmentObject.getService();    return traceSamplingPolicyWatcher.shouldSample(serviceName, sample, duration);}</code></pre><p>逻辑很简单:用 <code>traceId.hashCode()</code> 取模 10000 得到 <code>sample</code>(0-9999 的确定性桶),再交给 <code>TraceSamplingPolicyWatcher.shouldSample(service, sample, duration)</code> 判定。关键是<strong>同一条 trace 的所有 Segment 因为 traceId 相同,hash 结果相同,要么全采要么全丢——保证了链路完整性</strong>。如果用随机数,同一个请求的 A 段采了、B 段丢了,链路就断了。</p><p><strong><code>SamplingPolicy</code></strong>(<code>oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/sampling/SamplingPolicy.java:31</code>):极简值对象,两个字段 <code>rate</code>(采样率,0-10000)和 <code>duration</code>(慢 trace 阈值,毫秒)。</p><p><strong><code>SamplingPolicySettings</code></strong>(<code>SamplingPolicySettings.java:30</code>):</p><ul><li><code>defaultPolicy</code>:全局默认策略,构造时为 <code>new SamplingPolicy(10000, -1)</code>,即 10000/10000=<strong>100% 采样</strong>,慢阈值 -1 表示不启用慢 trace 优先(<code>SamplingPolicySettings.java:39</code>)。</li><li><code>services</code>:<code>Map&lt;String, SamplingPolicy&gt;</code>(<code>SamplingPolicySettings.java:31</code>),按 service 名覆盖默认策略。</li></ul><blockquote><p><strong>采样精度</strong><br />采样率精度是 1/10000,10000 表示 100%;慢 trace 阈值默认 -1(不采样慢 trace);单位毫秒。</p></blockquote><p><strong><code>TraceSamplingPolicyWatcher</code></strong>(<code>oap-server/analyzer/agent-analyzer/src/main/java/org/apache/skywalking/oap/server/analyzer/provider/trace/TraceSamplingPolicyWatcher.java:37</code>):继承 <code>ConfigChangeWatcher</code>,key 为 <code>traceSamplingPolicy</code>(<code>:44</code>)。判定逻辑(<code>:73-113</code>):</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/09-%E5%8D%8F%E8%AE%AE%E5%B1%82%E4%B8%8EAgent%E9%87%87%E9%9B%86.md_1.png" alt="图2" /></p><ol><li>取该 service 的 <code>SamplingPolicy</code>;没有则用默认(<code>TraceSamplingPolicyWatcher.java:73-79</code>)。</li><li>优先级:<strong>慢阈值 &gt; 采样率</strong>(注释 <code>:98-100</code>)。意思是只要 duration 超了慢阈值就采,不管采样号。</li><li><code>shouldSampleByDefault(sample, duration)</code>(<code>:89</code>):<code>duration &gt;= 默认慢阈值</code> <strong>或</strong> <code>sample &lt; 默认采样率</code> 则采样。</li><li><code>shouldSampleService(...)</code>(<code>:107-113</code>):服务级慢阈值或服务级采样率命中则采样;服务级某项为 null 则回退到全局对应项。</li></ol><p><code>withinRateRange</code> 的实现就是 <code>currentSample &lt; policySample</code>(<code>:127-129</code>)——<code>sample</code> 是 0-9999 的桶号,只要桶号小于采样率阈值就采。所以 <code>rate=3000</code> 意味着桶号 0-2999 采(30%),3000-9999 丢。</p><h3 id="%E5%85%A8%E9%87%8F-vs-%E6%85%A2%2F%E9%94%99%E8%AF%AF-trace-%E7%9A%84%E5%8C%BA%E5%88%AB" tabindex="-1">全量 vs 慢/错误 trace 的区别</h3><table><thead><tr><th>模式</th><th>配置</th><th>行为</th></tr></thead><tbody><tr><td>全量(默认)</td><td><code>rate=10000</code></td><td>所有 trace 都采(<code>sample &lt; 10000</code> 恒真)</td></tr><tr><td>采样模式</td><td>如 <code>rate=3000</code></td><td>30% 概率采(<code>sample &lt; 3000</code> 才采),其余丢弃省存储</td></tr><tr><td>慢 trace 优先</td><td><code>duration=500</code>(ms)</td><td>无论是否命中采样率,只要耗时 ≥500ms 就采——保证慢请求的链路不丢</td></tr></tbody></table><blockquote><p><strong>错误 trace</strong><br /><code>SpanObject.isError=true</code> 不会被采样器直接豁免(采样器只看 duration 和 sample),但 OAP 在 <code>isSizeLimited</code> 等场景对错误 span 有保留逻辑。<strong>未确认</strong>错误 trace 是否有独立采样豁免分支。</p></blockquote><h3 id="%E9%85%8D%E7%BD%AE%E6%9D%A5%E6%BA%90%E4%B8%8E%E5%8A%A8%E6%80%81%E6%9B%B4%E6%96%B0" tabindex="-1">配置来源与动态更新</h3><ul><li>静态文件:<code>trace-sampling-policy-settings.yml</code>,由 <code>SamplingPolicySettingsReader</code>(<code>SamplingPolicySettingsReader.java:35</code>)用 SnakeYAML 解析。结构(<code>:61-86</code>):顶层 <code>default: {rate, duration}</code> + <code>services: [{name, rate, duration}, ...]</code>。</li><li>动态更新:<code>TraceSamplingPolicyWatcher</code> 注册为 <code>traceSamplingPolicy</code> 配置的 watcher(<code>:44</code>),当动态配置(如 ZooKeeper/Apollo/配置文件热更)下发新 YAML 字符串时,<code>notify()</code>(<code>:50-58</code>) → <code>activeSetting()</code>(<code>:136-141</code>) → <code>parseFromYml()</code>(<code>:164-175</code>)重新解析并替换 <code>samplingPolicySettings</code>(<code>:143-151</code>)。解析失败则保留上一份配置,不会回退到默认。</li></ul><hr /><h2 id="compat-%E5%8D%8F%E8%AE%AE" tabindex="-1">Compat 协议</h2><blockquote><p><strong>Compat 是什么</strong><br /><code>*Compat.proto</code> 是&quot;旧版 proto 包名&quot;的兼容服务定义,让使用旧版 <code>skywalking.v3</code> 包名(无独立 package 声明、java_package 不同)的 Agent 仍能连上新版 OAP。OAP 侧用 <code>*HandlerCompat</code> 类把这些旧包名请求委托给新版 handler 处理,从而实现 proto 字段演进时旧 Agent 不断链。</p></blockquote><h3 id="%E5%90%84-compat-%E6%96%87%E4%BB%B6%E7%9A%84%E4%BD%9C%E7%94%A8" tabindex="-1">各 Compat 文件的作用</h3><table><thead><tr><th>Compat proto</th><th>对应主 proto</th><th>java_package</th></tr></thead><tbody><tr><td><code>language-agent/TracingCompat.proto</code></td><td>Tracing.proto</td><td><code>...language.agent.v3.compat</code></td></tr><tr><td><code>language-agent/JVMMetricCompat.proto</code></td><td>JVMMetric.proto</td><td><code>...language.agent.v3.compat</code></td></tr><tr><td><code>language-agent/MeterCompat.proto</code></td><td>Meter.proto</td><td><code>...language.agent.v3.compat</code></td></tr><tr><td><code>language-agent/CLRMetricCompat.proto</code></td><td>CLRMetric.proto</td><td><code>...language.agent.v3.compat</code></td></tr><tr><td><code>management/ManagementCompat.proto</code></td><td>Management.proto</td><td><code>...management.v3.compat</code></td></tr><tr><td><code>profile/ProfileCompat.proto</code></td><td>Profile.proto</td><td><code>...language.profile.v3.compat</code></td></tr><tr><td><code>browser/BrowserPerfCompat.proto</code></td><td>BrowserPerf.proto</td><td><code>...language.agent.v3.compat</code></td></tr></tbody></table><h3 id="%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-compat" tabindex="-1">为什么需要 Compat</h3><blockquote><p><strong>gRPC 服务发现基于全限定方法名</strong><br />proto 的字段编号一旦发布就不能改(改了就破坏线上的 Agent)。但有时需要&quot;演进服务定义&quot;——比如调整 java_package、调整 service 方法集合。问题在于:<strong>gRPC 的服务发现基于全限定方法名</strong>(包含 package)。如果直接改主 proto 的 <code>package</code>,旧版 Agent(已编译进老包名)发出的请求方法名对不上新版 OAP 注册的方法名,直接断链。</p><p>Compat 的解法:保留一份&quot;旧包名、旧 service 名&quot;的 proto(<code>option deprecated = true</code> 标记废弃),让 OAP 同时注册<strong>新旧两套 gRPC 服务</strong>。旧 Agent 连旧服务名、新 Agent 连新服务名,OAP 内部都用同一套 handler 处理。</p></blockquote><h3 id="compat-proto-%E7%9A%84%E5%86%99%E6%B3%95%E7%89%B9%E5%BE%81" tabindex="-1">Compat proto 的写法特征</h3><p>观察 <code>TracingCompat.proto</code>:</p><ul><li>没有 <code>package skywalking.v3;</code> 声明(默认包,模拟旧版无 package 的状态)。</li><li><code>option java_package = &quot;...language.agent.v3.compat&quot;</code>(<code>TracingCompat.proto:22</code>),生成的 gRPC stub 类落到 <code>compat</code> 子包。</li><li><code>option deprecated = true</code>(<code>TracingCompat.proto:25</code>):标记这是废弃的兼容层。</li><li><code>import &quot;language-agent/Tracing.proto&quot;</code>(<code>TracingCompat.proto:28</code>):复用新版 message 定义(<code>skywalking.v3.SegmentObject</code> 等),只是 service 用旧包名包一层。</li><li>service 体里直接引用 <code>skywalking.v3.SegmentObject</code> / <code>skywalking.v3.Commands</code>(见 <code>TracingCompat.proto:36-43</code>),即 message 是共享的,只是 service 全限定名不同。</li></ul><h3 id="*handlercompat-%E5%A6%82%E4%BD%95%E6%8A%8A%E6%97%A7%E8%AF%B7%E6%B1%82%E8%BD%AC%E6%96%B0%E5%A4%84%E7%90%86" tabindex="-1">*HandlerCompat 如何把旧请求转新处理</h3><p>每个 receiver 插件都有一个 <code>*HandlerCompat</code> 类,它继承<strong>旧包名生成的 gRPC 基类</strong>,但内部直接委托给<strong>新版 handler</strong>。以 <code>TraceSegmentReportServiceHandlerCompat</code>(<code>oap-server/server-receiver-plugin/skywalking-trace-receiver-plugin/src/main/java/org/apache/skywalking/oap/server/receiver/trace/provider/handler/v8/grpc/TraceSegmentReportServiceHandlerCompat.java:30</code>)为例:</p><pre><code class="language-java">// TraceSegmentReportServiceHandlerCompat.java:30public class TraceSegmentReportServiceHandlerCompat    extends TraceSegmentReportServiceGrpc.TraceSegmentReportServiceImplBase  // 旧包名 stub    implements GRPCHandler {    private final TraceSegmentReportServiceHandler delegate;  // 新版 handler    public StreamObserver&lt;SegmentObject&gt; collect(StreamObserver&lt;Commands&gt; responseObserver) {        return delegate.collect(responseObserver);  // 直接转发    }    public void collectInSync(SegmentCollection request, StreamObserver&lt;Commands&gt; responseObserver) {        delegate.collectInSync(request, responseObserver);    }}</code></pre><p>因为 Compat proto 复用了 <code>skywalking.v3.SegmentObject</code>(同一个生成类),新旧 handler 收到的 <code>SegmentObject</code> 是同一个 Java 类型,所以<strong>零成本转发</strong>——不需要任何字段转换。</p><blockquote><p><strong>Compat 不是协议转换器</strong><br />Compat 不是&quot;旧字段转新字段&quot;的协议转换器,而是&quot;旧 gRPC 服务名 → 新 handler&quot;的桥接器。真正的字段演进兼容靠 proto3 的&quot;新增字段不破坏旧 consumer&quot;特性保证,Compat 只解决 service 全限定名变化的问题。</p></blockquote><hr /><h2 id="jvm%2Fclr%2Fmeter%2Flog%2Fevent-%E7%9A%84-message-%E7%BB%93%E6%9E%84" tabindex="-1">JVM/CLR/Meter/Log/Event 的 message 结构</h2><h3 id="jvmmetriccollection-%2F-jvmmetric(jvm-%E6%8C%87%E6%A0%87)" tabindex="-1">JVMMetricCollection / JVMMetric(JVM 指标)</h3><p>定义见 <code>apm-protocol/apm-network/src/main/proto/language-agent/JVMMetric.proto</code>。service 是 <code>JVMMetricReportService.collect(JVMMetricCollection) returns (Commands)</code>(<code>:32</code>)。</p><p><code>JVMMetricCollection</code>(<code>:37</code>):<code>repeated JVMMetric metrics</code> + <code>service</code> + <code>serviceInstance</code>。一次上报一个实例的多个时间点指标。</p><p><code>JVMMetric</code>(<code>:43</code>)字段:</p><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>time</code></td><td>int64</td><td>指标时间戳(毫秒)</td></tr><tr><td><code>cpu</code></td><td>CPU</td><td>CPU 使用率(<code>CPU.usagePercent: double</code>,见 <code>Common.proto:40</code>)</td></tr><tr><td><code>memory</code></td><td>repeated Memory</td><td>内存(堆/非堆)</td></tr><tr><td><code>memoryPool</code></td><td>repeated MemoryPool</td><td>内存池</td></tr><tr><td><code>gc</code></td><td>repeated GC</td><td>GC 统计</td></tr><tr><td><code>thread</code></td><td>Thread</td><td>线程统计</td></tr><tr><td><code>clazz</code></td><td>Class</td><td>类加载统计</td></tr></tbody></table><p><code>Memory</code>(<code>:53</code>):<code>isHeap</code>(是否堆)、<code>init</code>、<code>max</code>、<code>used</code>、<code>committed</code>(int64,字节)。</p><p><code>MemoryPool</code>(<code>:61</code>):<code>type</code>(<code>PoolType</code> 枚举)+ init/max/used/committed。<code>PoolType</code>(<code>:69</code>)枚举了 CODE_CACHE/NEWGEN/OLDGEN/SURVIVOR/PERMGEN/METASPACE/ZHEAP 等 11 种 JVM 内存池类型。</p><p><code>GC</code>(<code>:83</code>):<code>phase</code>(<code>GCPhase</code>:NEW/OLD/NORMAL,NORMAL 用于 ZGC 这类无新旧代之分的 GC)+ <code>count</code> + <code>time</code>。</p><p><code>Thread</code>(<code>:96</code>):liveCount、daemonCount、peakCount,以及各状态线程数(runnableStateThreadCount、blockedStateThreadCount、waitingStateThreadCount、timedWaitingStateThreadCount)。</p><p><code>Class</code>(<code>:107</code>):loadedClassCount、totalUnloadedClassCount、totalLoadedClassCount。</p><h3 id="clrmetric(.net-clr-%E6%8C%87%E6%A0%87)" tabindex="-1">CLRMetric(.NET CLR 指标)</h3><p>定义见 <code>CLRMetric.proto</code>,结构对称于 JVM。</p><p><code>CLRMetricCollection</code>(<code>:36</code>)与 <code>CLRMetric</code>(<code>:42</code>):<code>time</code> + <code>cpu</code> + <code>gc</code>(ClrGC) + <code>thread</code>(ClrThread)。</p><p><code>ClrGC</code>(<code>:49</code>):Gen0/Gen1/Gen2 回收次数 + <code>HeapMemory</code>。</p><p><code>ClrThread</code>(<code>:56</code>):可用/最大 CompletionPort 线程数、可用/最大 Worker 线程数。</p><h3 id="meterdata(%E8%87%AA%E5%AE%9A%E4%B9%89-meter)" tabindex="-1">MeterData(自定义 Meter)</h3><p>定义见 <code>Meter.proto</code>。service <code>MeterReportService</code> 有两个 rpc:</p><ul><li><code>collect(stream MeterData)</code>(<code>:32</code>):每个 stream 作为 MAL 引擎的一个完整输入数据集,client 应每周期 <code>onComplete</code> 一次流。</li><li><code>collectBatch(stream MeterDataCollection)</code>(<code>:38</code>):批量模式,流里每个 <code>MeterDataCollection</code> 都被当作 MAL 的一个完整输入。</li></ul><p><code>MeterData</code>(<code>:80</code>)字段:</p><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>metric</code></td><td>oneof</td><td><code>singleValue</code>(MeterSingleValue)或 <code>histogram</code>(MeterHistogram)</td></tr><tr><td><code>service</code></td><td>string</td><td>服务名(流首元素设置)</td></tr><tr><td><code>serviceInstance</code></td><td>string</td><td>实例名(流首元素设置)</td></tr><tr><td><code>timestamp</code></td><td>int64</td><td>上报时间(流首元素设置)</td></tr></tbody></table><p><code>Label</code>(<code>:43</code>):<code>name</code> + <code>value</code>,metric 的标签维度。</p><p><code>MeterSingleValue</code>(<code>:60</code>):<code>name</code> + <code>repeated Label labels</code> + <code>value: double</code>,单一数值型指标。</p><p><code>MeterHistogram</code>(<code>:70</code>):<code>name</code> + <code>labels</code> + <code>repeated MeterBucketValue values</code>,直方图。</p><p><code>MeterBucketValue</code>(<code>:49</code>):<code>bucket</code>(桶下界,double)+ <code>count</code>(桶内计数,int64)+ <code>isNegativeInfinity</code>(是否负无穷,为 true 时 bucket 值无效)。桶上界由&quot;下一个 MeterBucketValue 的 bucket&quot;决定,最后一个桶上界为正无穷。</p><blockquote><p><strong>当前 Meter 只有两种类型</strong><br />当前版本的 <code>Meter.proto</code> 中 <code>MeterData.metric</code> 的 oneof(<code>Meter.proto:82-84</code>)<strong>只有 <code>singleValue</code> 和 <code>histogram</code> 两种</strong>,无 <code>mMultiIntValue</code> 字段。<code>MultiIntValuesHolder</code> 是 OAP <code>server-core</code> 里 metrics 存储层的概念(用于多 int 值 metrics 如 percentile),与 Meter 上报协议无关。</p></blockquote><p><code>MeterDataCollection</code>(<code>:94</code>):<code>repeated MeterData meterData</code>,用于 collectBatch。</p><h3 id="logdata(%E6%97%A5%E5%BF%97)" tabindex="-1">LogData(日志)</h3><p>定义见 <code>logging/Logging.proto</code>。service <code>LogReportService.collect(stream LogData) returns (Commands)</code>(<code>:32</code>)。</p><p><code>LogData</code>(<code>:42</code>)字段:</p><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>timestamp</code></td><td>int64</td><td>日志时间(毫秒,可选;不设则 OAP 用接收时间)</td></tr><tr><td><code>service</code></td><td>string</td><td>服务名(必填;流式上报中非首元素可复用前一个非空值)</td></tr><tr><td><code>serviceInstance</code></td><td>string</td><td>实例名(可选)</td></tr><tr><td><code>endpoint</code></td><td>string</td><td>endpoint(可选)</td></tr><tr><td><code>body</code></td><td>LogDataBody</td><td>日志内容(必填)</td></tr><tr><td><code>traceContext</code></td><td>TraceContext</td><td>关联的 trace 上下文(可选)</td></tr><tr><td><code>tags</code></td><td>LogTags</td><td>标签(可选,供 OAP 搜索/分析)</td></tr><tr><td><code>layer</code></td><td>string</td><td>服务/实例所属 layer(9.0.0+,缺省则 OAP 设为 <code>general</code>)</td></tr></tbody></table><p><code>LogDataBody</code>(<code>:75</code>):<code>type</code>(字符串,匹配 OAP 侧 analyzer)+ <code>oneof content</code>:<code>TextLog.text</code> / <code>JSONLog.json</code> / <code>YAMLLog.yaml</code>。</p><p><code>TraceContext</code>(<code>:103</code>):<code>traceId</code> + <code>traceSegmentId</code> + <code>spanId</code>——当 Agent 把 trace ID 注入日志文本时,用这个把日志和链路关联起来。</p><p><code>LogTags</code>(<code>:113</code>):<code>repeated KeyStringValuePair data</code>。</p><h3 id="event(%E4%BA%8B%E4%BB%B6)" tabindex="-1">Event(事件)</h3><p>定义见 <code>event/Event.proto</code>。service <code>EventService.collect(stream Event) returns (Commands)</code>(<code>:30</code>)。注释(<code>:31-34</code>):一个事件通常调两次 collect——一次开始一次结束,用同一个 UUID 关联;若起止时间都已知(如从第三方系统导入)则只调一次。</p><p><code>Event</code>(<code>:38</code>)字段:</p><table><thead><tr><th>字段</th><th>类型</th><th>含义</th></tr></thead><tbody><tr><td><code>uuid</code></td><td>string</td><td>事件唯一 ID,关联开始/结束</td></tr><tr><td><code>source</code></td><td>Source</td><td>事件发生源</td></tr><tr><td><code>name</code></td><td>string</td><td>事件名(如 <code>Reboot</code>、<code>Upgrade</code>)</td></tr><tr><td><code>type</code></td><td>Type</td><td><code>Normal=0</code> / <code>Error=1</code>,影响 UI 颜色</td></tr><tr><td><code>message</code></td><td>string</td><td>一行简述(不建议塞详细日志/堆栈)</td></tr><tr><td><code>parameters</code></td><td>map&lt;string,string&gt;</td><td>message 里的参数</td></tr><tr><td><code>startTime</code></td><td>int64</td><td>起始时间(毫秒,必填)</td></tr><tr><td><code>endTime</code></td><td>int64</td><td>结束时间(毫秒,可空表示未结束)</td></tr><tr><td><code>layer</code></td><td>string</td><td>所属 layer(9.0.0+ 必填)</td></tr></tbody></table><p><code>Source</code>(<code>:82</code>):<code>service</code> + <code>serviceInstance</code> + <code>endpoint</code>,按粒度选择性必填(注释 <code>:79-81</code>:仅 service 事件只需 service;instance 事件需 service+serviceInstance;endpoint 事件需 service+endpoint)。</p><hr /><h2 id="%E5%85%B6%E4%BB%96%E5%9F%9F-message-%E9%80%9F%E8%A7%88" tabindex="-1">其他域 message 速览</h2><ul><li><strong>Management</strong>(<code>Management.proto</code>):<code>InstanceProperties</code>(service+serviceInstance+properties+layer,上报实例自定义属性)、<code>InstancePingPkg</code>(service+serviceInstance+layer,保活心跳)。</li><li><strong>BrowserPerf</strong>(<code>BrowserPerf.proto</code>):<code>BrowserPerfData</code>(页面级性能:redirectTime/dnsTime/ttfbTime/tcpTime/transTime/domAnalysisTime/fptTime/domReadyTime/loadPageTime/resTime/sslTime/ttlTime/firstPackTime/fmpTime 等毫秒级字段);<code>BrowserErrorLog</code>(错误日志,含 category 枚举 ajax/resource/vue/promise/js/unknown、grade、message、line、col、stack、errorUrl、firstReportedError);<code>BrowserWebVitalsPerfData</code>(FMP、CLS、LCP);<code>BrowserResourcePerfData</code>(资源加载);<code>BrowserWebInteractionsPerfData</code>(INP)。浏览器场景里 <code>serviceVersion</code> 对应后端的 Instance 概念、<code>pagePath</code> 对应 Endpoint 概念(注释 <code>:56,61</code>)。</li><li><strong>Service Mesh</strong>(<code>service-mesh.proto</code>):<code>ServiceMeshMetrics</code> oneof <code>httpMetrics</code>/<code>tcpMetrics</code>;<code>HTTPServiceMeshMetric</code> 含 source/dest 服务名+实例、endpoint、latency、responseCode、status、protocol、detectPoint、tlsMode、internalErrorCode、Envoy 内部请求/响应延迟(纳秒);<code>TCPServiceMeshMetric</code> 额外有 receivedBytes/sentBytes。返回值是空 <code>MeshProbeDownstream</code>(<code>:133</code>)而非 Commands,是少数例外。</li><li><strong>eBPF accesslog</strong>(<code>ebpf/accesslog.proto</code>):<code>EBPFAccessLogMessage</code>(node 信息 + connection + kernelLogs + protocolLog),含 L2/L3/L4 网络栈纳秒级延迟指标、HTTP 协议解析、trace 信息;返回空 <code>EBPFAccessLogDownstream</code>。</li><li><strong>eBPF profiling</strong>(<code>ebpf/profiling/</code>):<code>EBPFProfilingData</code> oneof <code>onCPU</code>/<code>offCPU</code>(<code>Profile.proto:48</code>),栈元数据区分 <code>PROCESS_KERNEL_SPACE</code>/<code>PROCESS_USER_SPACE</code>;<code>ContinuousProfilingReport</code>(<code>Continuous.proto:53</code>)含触发原因 <code>ContinuousProfilingCause</code>(CPU/线程数/系统负载/HTTP 错误率/HTTP 平均响应时间阈值),目标 task oneof onCPU/offCPU/network。</li><li><strong>eBPF process</strong>(<code>ebpf/profiling/Process.proto</code>):<code>EBPFProcessProperties</code> oneof hostProcess/k8sProcess,含 <code>EBPFProcessEntityMetadata</code>(layer+serviceName+instanceName+processName+labels)。</li><li><strong>async-profiler</strong>(<code>AsyncProfiler.proto</code>,package <code>skywalking.v10</code>):<code>AsyncProfilerData</code>(metaData + oneof errorMessage/content(JFR 二进制));<code>AsyncProfilingStatus</code> 枚举 PROFILING_SUCCESS/EXECUTION_TASK_ERROR/TERMINATED_BY_OVERSIZE;<code>collect</code> 是双向流,OAP 可中途用响应控制停止(如文件超限)。</li><li><strong>pprof</strong>(<code>Pprof.proto</code>,package <code>skywalking.v10</code>):结构与 async-profiler 对称,<code>PprofData</code>(metadata + errorMessage/content pprof 二进制),同样双向流。</li></ul><hr /><h2 id="%E9%99%84%E5%BD%95%3Acomponentid-%E4%B8%8E-component-libraries.yml" tabindex="-1">附录:componentId 与 component-libraries.yml</h2><p><code>SpanObject.componentId</code>(<code>Tracing.proto:153</code>)是一个预定义数字 ID,代表本次 span 用的框架/技术栈。所有 ID 在 <code>oap-server/server-starter/src/main/resources/component-libraries.yml</code> 集中定义(<code>component-libraries.yml:43</code> 起,<code>Unknown: id=0</code>),如 <code>Tomcat: id=1</code>、<code>HttpClient: id=2</code>、<code>Dubbo: id=3</code>、<code>H2: id=4</code>、<code>Mysql: id=5</code> 等。</p><blockquote><p><strong>ID 一旦发布不可更改</strong><br />注释(<code>component-libraries.yml:17</code>)强调:<strong>ID 一旦发布不可更改,只能追加且必须唯一</strong>,废弃的 ID 永久保留,否则会导致可视化和聚合错误。每个���件还声明 <code>languages</code>(哪些语言用)和 <code>priority</code>(0-100,越接近业务代码优先级越高,默认 50,用于决定同一 span 多组件叠加时哪个胜出)。Agent 上报时只传数字 ID,OAP 据此反查组件名,省去传输字符串开销。</p></blockquote><hr /><h2 id="%E5%85%B3%E9%94%AE%E7%BB%93%E8%AE%BA%E6%B1%87%E6%80%BB" tabindex="-1">关键结论汇总</h2><ol><li><strong>apm-protocol 是纯协议层</strong>:只定义 proto + 少量 Command 序列化工��类,不含任何接收/分析逻辑;接收在 receiver 插件,分析在 analyzer 模块。</li><li><strong>trace 模型是 Segment 优先</strong>:不是&quot;一堆 span 平铺&quot;,而是&quot;每进程一个 Segment 包含若干 span&quot;,跨进程靠 <code>SegmentReference</code> 拼接,这是 SkyWalking 区别于 Zipkin 的核心设计。</li><li><strong>Command 是反向控制的通用信封</strong>:<code>command</code> 名 + <code>args</code> 键值对,复用所有 rpc 的 <code>returns (Commands)</code> 通道,新增指令不改 proto。</li><li><strong>采样在 OAP 后端做,不在 proto 里</strong>:基于 <code>traceId.hashCode() % 10000</code> 的确定性桶 + 慢 trace 优先,按 service 配置,走 OAP 动态配置体系而非 gRPC Command 下发。</li><li><strong>Compat 解决的是 gRPC 服务全限定名变化</strong>,不是字段转换:靠&quot;旧包名 service + 共享 message + HandlerCompat 委托新版 handler&quot;实现旧 Agent 不断链。</li><li><strong>当前 Meter 只有 singleValue 和 histogram</strong>(<code>Meter.proto:82-84</code>),无 <code>mMultiIntValue</code> 字段。</li></ol><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键文件</th><th>路径</th></tr></thead><tbody><tr><td>trace 协议</td><td>Tracing.proto</td><td><code>apm-protocol/apm-network/src/main/proto/language-agent/Tracing.proto</code></td></tr><tr><td>Command</td><td>Command.proto</td><td><code>apm-protocol/apm-network/src/main/proto/common/Command.proto</code></td></tr><tr><td>公共类型</td><td>Common.proto</td><td><code>apm-protocol/apm-network/src/main/proto/common/Common.proto</code></td></tr><tr><td>JVM</td><td>JVMMetric.proto</td><td><code>apm-protocol/apm-network/src/main/proto/language-agent/JVMMetric.proto</code></td></tr><tr><td>Meter</td><td>Meter.proto</td><td><code>apm-protocol/apm-network/src/main/proto/language-agent/Meter.proto</code></td></tr><tr><td>Log</td><td>Logging.proto</td><td><code>apm-protocol/apm-network/src/main/proto/logging/Logging.proto</code></td></tr><tr><td>Event</td><td>Event.proto</td><td><code>apm-protocol/apm-network/src/main/proto/event/Event.proto</code></td></tr><tr><td>eBPF</td><td>accesslog.proto + profiling/*.proto</td><td><code>apm-protocol/apm-network/src/main/proto/ebpf/</code></td></tr><tr><td>async-profiler</td><td>AsyncProfiler.proto</td><td><code>apm-protocol/apm-network/src/main/proto/asyncprofiler/AsyncProfiler.proto</code></td></tr><tr><td>pprof</td><td>Pprof.proto</td><td><code>apm-protocol/apm-network/src/main/proto/pprof/Pprof.proto</code></td></tr><tr><td>Compat</td><td>各 *Compat.proto</td><td><code>apm-protocol/apm-network/src/main/proto/{language-agent,management,profile,browser}/*Compat.proto</code></td></tr><tr><td>Command Java 封装</td><td>BaseCommand + 各 *Command</td><td><code>apm-protocol/apm-network/src/main/java/.../network/trace/component/command/</code></td></tr><tr><td>采样入口</td><td>TraceSegmentSampler</td><td><code>oap-server/analyzer/agent-analyzer/.../provider/trace/parser/listener/TraceSegmentSampler.java</code></td></tr><tr><td>采样策略 watcher</td><td>TraceSamplingPolicyWatcher</td><td><code>oap-server/analyzer/agent-analyzer/.../provider/trace/TraceSamplingPolicyWatcher.java</code></td></tr><tr><td>采样策略数据类</td><td>SamplingPolicy / SamplingPolicySettings / SamplingPolicySettingsReader</td><td><code>oap-server/analyzer/agent-analyzer/.../provider/trace/sampling/</code></td></tr><tr><td>componentId</td><td>component-libraries.yml</td><td><code>oap-server/server-starter/src/main/resources/component-libraries.yml</code></td></tr></tbody></table><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5" tabindex="-1">接下来</h2><p>协议清楚了,OAP 怎么对运行中进程做更深的性能剖析?四种 profiling 体系怎么对比?详见 10-Profiling体系。</p><blockquote><p><strong>心智模型</strong><br />把 apm-protocol 想成&quot;Agent 和 OAP 之间的合同&quot;:proto 文件是合同条款(每个数据域一份),Segment 是&quot;每进程打包上报的集装箱&quot;(里面装 span),Command 是&quot;OAP 塞在回程包裹里的指令条&quot;(复用上报通道反向下发),采样是&quot;OAP 收货后决定哪些集装箱入库的筛选规则&quot;(基于 traceId 的确定性桶,慢件优先),Compat 是&quot;旧版合同的复印件&quot;(让还在用旧合同的 Agent 也能签收,OAP 内部统一处理)。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[九、Skywalking数据模型与 Scope 体系]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/jiu-skywalking-shu-ju-mo-xing-yu-scope-ti-xi" />
                <id>tag:https://lymboy.com,2026-01-07:jiu-skywalking-shu-ju-mo-xing-yu-scope-ti-xi</id>
                <published>2026-01-07T21:55:21+08:00</published>
                <updated>2026-01-07T21:55:21+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B%E4%B8%8E-scope-%E4%BD%93%E7%B3%BB" tabindex="-1">数据模型与 Scope 体系</h1><blockquote><p><strong>这一篇解决什么</strong><br />前面把 Source/Metrics/Record 当抽象概念用,这一篇挖到底:Source 全家族 56 个子类逐个列字段、Scope 全表 98 个 scopeId 常量、Metrics 全家族 18 个聚合函数的 combine/calculate、DataTable 多值指标的哈希存储格式。读完这篇,OAL 里 <code>from(Service.latency).longAvg()</code> 这行字背后的每个零件都落到实处。</p></blockquote><hr /><h2 id="%E4%B8%89%E5%B1%82%E7%BB%93%E6%9E%84%E6%80%BB%E8%A7%88" tabindex="-1">三层结构总览</h2><p>数据模型分三层,靠两套注解驱动——<code>@ScopeDeclaration</code> 标 Source,<code>@MetricsFunction</code> 标 Metrics——<code>AnnotationScan</code> 启动时扫包注册。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/08-%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B%E4%B8%8EScope%E4%BD%93%E7%B3%BB.md_0.png" alt="图1" /></p><p>每个概念的真实职责:</p><ul><li><strong>Source</strong> 是从 Receiver 进入 OAP 的原始事实——一次请求的延迟、一次 GC 的耗时。还没被统计过。</li><li><strong>Scope</strong> 是给每个 Source 分配的整数 ID + 名字。OAL 里 <code>from(Service).longAvg()</code>,编译器靠 Scope 名找到对应的 Source 类。它本质是 Source 的类型标签,但用整数 ID 而不是类名,是为了在 OAL 编译产物和运行时分发里走最快的等值匹配路径。</li><li><strong>Catalog</strong> 比 Scope 粗。告警和 UI 渲染需要知道&quot;这个指标挂在哪个拓扑节点上&quot;——Service?Endpoint?Instance?Catalog 就是这个归属标记,一个 Catalog 下挂多个 Scope。</li><li><strong>Metrics</strong> 是 Source 聚合后的时间窗口统计值。每个子类的契约就两个方法:<code>combine()</code> 累加、<code>calculate()</code> 终算。</li><li><strong>DataTable</strong> 是多值指标容器。普通 Metrics 一个时间窗口只存一个数;DataTable 用一个 <code>HashMap&lt;String,Long&gt;</code> 同时存多个值,典型场景是一次算出 P50/P75/P90/P95/P99 五个分位数。</li></ul><hr /><h2 id="source-%E5%85%A8%E5%AE%B6%E6%97%8F" tabindex="-1">Source 全家族</h2><blockquote><p><strong>Source 是什么</strong><br />Source 是 OAP 分析管线的事实之源,每个子类对应一类可观测数据(一个服务、一个实例、一次 JVM GC、一次 DB 访问……),通过 <code>@ScopeDeclaration</code> 注册后被 OAL 引用。</p></blockquote><h3 id="%E4%B8%BA%E4%BB%80%E4%B9%88-source-%E8%A6%81%E5%88%86%E8%BF%99%E4%B9%88%E5%A4%9A%E5%AD%90%E7%B1%BB" tabindex="-1">为什么 Source 要分这么多子类</h3><p>56 个 Source 子类不是过度设计,而是可观测域本身的形状决定的。一条请求进来,你想看服务级延迟(Service)、实例级延迟(ServiceInstance)、端点级延迟(Endpoint)、服务间调用关系(ServiceRelation)、JVM 内部状态、数据库访问、缓存访问、消息队列、eBPF 网络层、K8s 网格层……这些数据的<strong>字段集互不重叠</strong>:JVM GC 有 <code>phase/time/count</code>,数据库访问有 <code>databaseTypeId/statement</code>,缓存访问有 <code>cacheTypeId/operation</code>。硬塞一个 God Source 既丢类型安全,又让 OAL 编译器无法按字段名精准映射。</p><p>拆成 56 个子类的代价是注册成本,但被 <code>AnnotationScan</code> 启动期一次性扫描消化,运行期零开销。收益是 OAL 脚本里 <code>from(Service.latency)</code> 这类写法能被静态校验——<code>latency</code> 字段确实存在于 <code>Service</code> 类上,编译期就锁定。这就是 Source 多子类的根本理由:<strong>字段集的差异是天然的分类边界,用类型系统固化它</strong>。</p><h3 id="isource-%E6%8E%A5%E5%8F%A3%E7%9A%84%E4%B8%89%E4%B8%AA%E9%92%A9%E5%AD%90" tabindex="-1">ISource 接口的三个钩子</h3><p>定义在 <code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/ISource.java</code>:</p><table><thead><tr><th>钩子</th><th>作用</th><th>行号</th></tr></thead><tbody><tr><td><code>int scope()</code></td><td>返回 scopeId</td><td><code>:31</code>。编译期 OAL 用它匹配;运行期分发器用它找正确的 <code>SourceDispatcher</code></td></tr><tr><td><code>long getTimeBucket() / setTimeBucket()</code></td><td>时间桶</td><td><code>:33-35</code>。所有 Source 都带时间维度,用 <code>yyyMMddHHmm</code> 形式的长整型表示一个时间窗口</td></tr><tr><td><code>String getEntityId()</code></td><td>实体唯一标识</td><td><code>:37</code>。决定这条 Source 聚合到哪一行;同一 entityId 的 Source 合并到同一个 Metrics 行</td></tr><tr><td><code>default void prepare()</code></td><td>分发前预处理</td><td><code>:42</code>。默认空实现,子类用来&quot;用名字反算 ID&quot;</td></tr><tr><td><code>String toJson()</code></td><td>手写 JSON 调试负载</td><td><code>:55</code>。<strong>手写</strong>而非反射序列化,用于 dsl-debug 捕获</td></tr></tbody></table><h3 id="prepare()-%E7%9A%84%E5%85%B8%E5%9E%8B%E7%94%A8%E6%B3%95" tabindex="-1">prepare() 的典型用法</h3><p><code>prepare()</code> 在 <code>SourceDispatcher.dispatch()</code> 之前调用(<code>ISource.java:40</code> 的注释明确点出这个时序),核心作用是把&quot;好读的 name&quot;转成&quot;好关联的 ID&quot;:</p><ul><li><code>Endpoint.prepare()</code>(<code>Endpoint.java:131</code>):<code>serviceId = IDManager.ServiceID.buildId(serviceName, serviceLayer.isNormal())</code></li><li><code>ServiceRelation.prepare()</code>(<code>ServiceRelation.java:110</code>):同时算 <code>sourceServiceId</code> 和 <code>destServiceId</code></li><li><code>GenAIModelAccess.prepare()</code>(<code>GenAIModelAccess.java:68</code>):算 <code>serviceId</code>(用 <code>VIRTUAL_GENAI</code> 层)+ <code>entityId</code></li></ul><p><code>Service</code>、<code>DatabaseAccess</code>、<code>MQAccess</code> 这类&quot;自己就是顶层实体&quot;的 Source 没有 <code>prepare()</code>,而是把 ID 计算放在 <code>getEntityId()</code> 里做懒加载。区别在于:prepare 是分发前一次性补字段(适合一条 Source 关联多个实体的场景,如 ServiceRelation 要算两端),getEntityId 是用到才算(适合只算自身 ID 的场景)。</p><h3 id="getentityid()-%E7%9A%84%E4%B8%89%E7%A7%8D%E9%A3%8E%E6%A0%BC" tabindex="-1">getEntityId() 的三种风格</h3><table><thead><tr><th>风格</th><th>代表</th><th>实现</th></tr></thead><tbody><tr><td>懒加载 + 缓存</td><td><code>Service</code>、<code>ServiceInstance</code>、<code>ServiceRelation</code></td><td><code>if (entityId == null) entityId = IDManager.xxx.buildId(...)</code></td></tr><tr><td>直接返回</td><td><code>ServiceInstanceJVMGC</code> 等 JVM/CLR 类</td><td><code>return String.valueOf(id)</code>,用 Agent 上报的 instance id</td></tr><tr><td>固定空串</td><td><code>DatabaseSlowStatement</code></td><td><code>return Const.EMPTY_STRING</code>,每条慢 SQL 都是独立行,不按实体聚合</td></tr></tbody></table><p>第三种是刻意为之:慢 SQL 不该被聚合,每条都该能在 UI 单独查到,所以 entityId 给空串,让每条 Source 落成独立行。</p><h3 id="source-%E5%AD%90%E7%B1%BB%E5%85%A8%E8%A1%A8(%E6%8C%89%E7%B1%BB%E5%88%AB%E5%88%86%E7%BB%84)" tabindex="-1">Source 子类全表(按类别分组)</h3><p>下面覆盖 <code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/</code> 下全部带 <code>@ScopeDeclaration</code> 的类。scopeId 常量来自 <code>DefaultScopeDefine.java</code>。</p><h4 id="a.-%E6%8B%93%E6%89%91%E6%A0%B8%E5%BF%83%E7%B1%BB(service-%2F-instance-%2F-endpoint-%E5%8F%8A%E5%85%B6-relation)" tabindex="-1">A. 拓扑核心类(Service / Instance / Endpoint 及其 Relation)</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>catalog</th><th>关键字段</th></tr></thead><tbody><tr><td><code>Service</code></td><td>SERVICE</td><td>1</td><td>SERVICE</td><td>name, layer, latency, status, httpResponseStatusCode, rpcStatusCode, type, tags, attr0~attr5</td></tr><tr><td><code>ServiceInstance</code></td><td>SERVICE_INSTANCE</td><td>2</td><td>SERVICE_INSTANCE</td><td>serviceId, name, serviceName, serviceLayer, latency, status, sideCar</td></tr><tr><td><code>Endpoint</code></td><td>ENDPOINT</td><td>3</td><td>ENDPOINT</td><td>name, serviceId, serviceName, serviceLayer, latency, status, attr0~attr5</td></tr><tr><td><code>ServiceRelation</code></td><td>SERVICE_RELATION</td><td>4</td><td>SERVICE_RELATION</td><td>sourceServiceId/Name/Layer, destServiceId/Name/Layer, endpoint, componentId, latency, detectPoint</td></tr><tr><td><code>ServiceInstanceRelation</code></td><td>SERVICE_INSTANCE_RELATION</td><td>5</td><td>SERVICE_INSTANCE_RELATION</td><td>source/dest 的 serviceId+instanceId+name+layer, endpoint, componentId, latency, detectPoint, tlsMode</td></tr><tr><td><code>EndpointRelation</code></td><td>ENDPOINT_RELATION</td><td>6</td><td>ENDPOINT_RELATION</td><td>endpoint, serviceId/Name, childEndpoint, childServiceId/Name, componentId, rpcLatency, detectPoint</td></tr></tbody></table><blockquote><p><strong>拓扑三件套 × 两层</strong><br />这 6 个是 SkyWalking 最经典的&quot;拓扑三件套 × 两层&quot;:每个维度(Service/Instance/Endpoint)既有自身指标,也有 Relation 描述调用关系。<code>@ScopeDefaultColumn.VirtualColumnDefinition(fieldName=&quot;entityId&quot;, columnName=&quot;entity_id&quot;, isID=true)</code> 是这 6 个的统一主键声明——一个 entityId 列既是聚合 key 也是存储主键,Source 的多态主键设计在这里收口。</p></blockquote><h4 id="b.-jvm-%E7%9B%91%E6%8E%A7%E7%B1%BB(%E5%85%A8%E9%83%A8-catalog-%3D-service_instance)" tabindex="-1">B. JVM 监控类(全部 catalog = SERVICE_INSTANCE)</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>关键字段</th><th>位置</th></tr></thead><tbody><tr><td><code>ServiceInstanceJVMCPU</code></td><td>SERVICE_INSTANCE_JVM_CPU</td><td>8</td><td>id, name, serviceName, serviceId, usePercent(double)</td><td><code>ServiceInstanceJVMCPU.java:28</code></td></tr><tr><td><code>ServiceInstanceJVMMemory</code></td><td>SERVICE_INSTANCE_JVM_MEMORY</td><td>9</td><td>id, name, serviceId, heapStatus, init, max, used, committed</td><td><code>ServiceInstanceJVMMemory.java:28</code></td></tr><tr><td><code>ServiceInstanceJVMMemoryPool</code></td><td>SERVICE_INSTANCE_JVM_MEMORY_POOL</td><td>10</td><td>id, name, serviceId, poolType(MemoryPoolType), init, max, used, committed</td><td><code>ServiceInstanceJVMMemoryPool.java:28</code></td></tr><tr><td><code>ServiceInstanceJVMGC</code></td><td>SERVICE_INSTANCE_JVM_GC</td><td>11</td><td>id, name, serviceId, phase(GCPhase), time, count</td><td><code>ServiceInstanceJVMGC.java:28</code></td></tr><tr><td><code>ServiceInstanceJVMThread</code></td><td>SERVICE_INSTANCE_JVM_THREAD</td><td>33</td><td>id, name, serviceId, liveCount, daemonCount, peakCount, runnable/blocked/waiting/timedWaiting 各状态线程数</td><td><code>ServiceInstanceJVMThread.java:28</code></td></tr><tr><td><code>ServiceInstanceJVMClass</code></td><td>SERVICE_INSTANCE_JVM_CLASS</td><td>44</td><td>已加载/未加载类数</td><td><code>ServiceInstanceJVMClass.java:28</code></td></tr></tbody></table><blockquote><p><strong>JVM 类的共同模式</strong><br /><code>getEntityId()</code> 直接返回 Agent 上报的 <code>String.valueOf(id)</code>,<code>prepare()</code> 不存在或仅设 serviceId,字段都是长整型数值。注意 scopeId 跳过了 7。</p></blockquote><h4 id="c.-clr(.net)%E7%9B%91%E6%8E%A7%E7%B1%BB(%E5%85%A8%E9%83%A8-catalog-%3D-service_instance)" tabindex="-1">C. CLR(.NET)监控类(全部 catalog = SERVICE_INSTANCE)</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>关键字段</th><th>位置</th></tr></thead><tbody><tr><td><code>ServiceInstanceCLRCPU</code></td><td>SERVICE_INSTANCE_CLR_CPU</td><td>19</td><td>usePercent</td><td><code>ServiceInstanceCLRCPU.java:31</code></td></tr><tr><td><code>ServiceInstanceCLRGC</code></td><td>SERVICE_INSTANCE_CLR_GC</td><td>20</td><td>gen0CollectCount, gen1CollectCount, gen2CollectCount, heapMemory</td><td><code>ServiceInstanceCLRGC.java:31</code></td></tr><tr><td><code>ServiceInstanceCLRThread</code></td><td>SERVICE_INSTANCE_CLR_THREAD</td><td>21</td><td>线程相关统计</td><td><code>ServiceInstanceCLRThread.java:31</code></td></tr></tbody></table><h4 id="d.-%E6%95%B0%E6%8D%AE%E5%BA%93-%2F-%E7%BC%93%E5%AD%98-%2F-%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97-%E8%AE%BF%E9%97%AE%E7%B1%BB" tabindex="-1">D. 数据库 / 缓存 / 消息队列 访问类</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>catalog</th><th>关键字段</th></tr></thead><tbody><tr><td><code>DatabaseAccess</code></td><td>DATABASE_ACCESS</td><td>17</td><td>SERVICE</td><td>name, databaseTypeId, latency, status</td></tr><tr><td><code>DatabaseSlowStatement</code></td><td>DATABASE_SLOW_STATEMENT</td><td>18</td><td>SERVICE</td><td>id, databaseServiceId, statement, latency, traceId, timestamp;<code>getEntityId()</code> 返回空串</td></tr><tr><td><code>ServiceDatabaseSlowStatement</code></td><td>SERVICE_DATABASE_SLOW_STATEMENT</td><td>91</td><td>SERVICE</td><td>服务维度的慢 SQL</td></tr><tr><td><code>CacheAccess</code></td><td>CACHE_ACCESS</td><td>55</td><td>SERVICE</td><td>name, cacheTypeId, latency, status, operation(VirtualCacheOperation)</td></tr><tr><td><code>CacheSlowAccess</code></td><td>CACHE_SLOW_ACCESS</td><td>56</td><td>SERVICE</td><td>慢缓存访问</td></tr><tr><td><code>MQAccess</code></td><td>MESSAGE_QUEUE_ACCESS</td><td>63</td><td>SERVICE</td><td>name, typeId, transmissionLatency, status, operation(MQOperation)</td></tr><tr><td><code>MQEndpointAccess</code></td><td>MESSAGE_QUEUE_ENDPOINT_ACCESS</td><td>64</td><td>ENDPOINT</td><td>MQ 的 endpoint 级访问</td></tr></tbody></table><blockquote><p><strong>catalog 归属</strong><br /><code>DatabaseAccess</code>/<code>CacheAccess</code>/<code>MQAccess</code> 的 catalog 都是 <code>SERVICE</code>,在告警/UI 里归到&quot;服务&quot;维度;<code>MQEndpointAccess</code> 归到 <code>ENDPOINT</code> 维度。这决定了 UI 拓扑图上它们挂哪个节点。</p></blockquote><h4 id="e.-genai-%E7%B1%BB(ai-%E6%A8%A1%E5%9E%8B%E8%A7%82%E6%B5%8B%2C10.x-%E5%BC%95%E5%85%A5)" tabindex="-1">E. GenAI 类(AI 模型观测,10.x 引入)</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>catalog</th><th>关键字段</th></tr></thead><tbody><tr><td><code>GenAIProviderAccess</code></td><td>GEN_AI_PROVIDER_ACCESS</td><td>96</td><td>SERVICE</td><td>name, inputTokens, outputTokens, totalEstimatedCost, latency, status</td></tr><tr><td><code>GenAIModelAccess</code></td><td>GEN_AI_MODEL_ACCESS</td><td>97</td><td>SERVICE_INSTANCE</td><td>serviceName, serviceId, modelName, inputTokens, outputTokens, totalEstimatedCost, timeToFirstToken, latency, status</td></tr></tbody></table><blockquote><p><strong>GenAI cost 单位</strong><br /><code>GenAIMetrics.java:24</code> 是普通 POJO(<code>@Data</code>,无 <code>@ScopeDeclaration</code>),不是 Source,用作中间数据载体。注释说明 cost 单位是 1e-6 货币(micro-USD)——避免浮点累加误差,所有 cost 用 long 存,展示时再除以 1e6。</p></blockquote><h4 id="f.-ebpf-%2F-cilium-%2F-envoy-%E7%B1%BB" tabindex="-1">F. eBPF / Cilium / Envoy 类</h4><p>Cilium 类全部继承抽象基类 <code>CiliumMetrics</code>(<code>CiliumMetrics.java:24</code>),后者预定义了 <code>verdict</code>(forwarded/dropped)、<code>type</code>(tcp/http/dns/kafka)、<code>direction</code>(ingress/egress)、<code>dropReason</code> 以及内嵌的 <code>HTTPMetrics</code>/<code>KafkaMetrics</code>/<code>DNSMetrics</code> 子结构。抽象基类把 6 个 Cilium Source 的公共字段抽出来,避免 6 份重复声明。</p><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>catalog</th><th>位置</th></tr></thead><tbody><tr><td><code>CiliumService</code></td><td>CILIUM_SERVICE</td><td>78</td><td>SERVICE</td><td><code>CiliumService.java:30</code></td></tr><tr><td><code>CiliumServiceInstance</code></td><td>CILIUM_SERVICE_INSTANCE</td><td>79</td><td>SERVICE_INSTANCE</td><td><code>CiliumServiceInstance.java:30</code></td></tr><tr><td><code>CiliumServiceRelation</code></td><td>CILIUM_SERVICE_RELATION</td><td>80</td><td>SERVICE_RELATION</td><td><code>CiliumServiceRelation.java:30</code></td></tr><tr><td><code>CiliumServiceInstanceRelation</code></td><td>CILIUM_SERVICE_INSTANCE_RELATION</td><td>81</td><td>SERVICE_INSTANCE_RELATION</td><td><code>CiliumServiceInstanceRelation.java:30</code></td></tr><tr><td><code>CiliumEndpoint</code></td><td>CILIUM_ENDPOINT</td><td>82</td><td>ENDPOINT</td><td><code>CiliumEndpoint.java:30</code></td></tr><tr><td><code>CiliumEndpointRelation</code></td><td>CILIUM_ENDPOINT_REALATION</td><td>83</td><td>ENDPOINT_RELATION</td><td><code>CiliumEndpointRelation.java:30</code>(注:常量名源码即拼成 <code>REALATION</code>)</td></tr><tr><td><code>EnvoyInstanceMetric</code></td><td>ENVOY_INSTANCE_METRIC</td><td>22</td><td>SERVICE_INSTANCE</td><td><code>EnvoyInstanceMetric.java:33</code></td></tr></tbody></table><p>eBPF Profiling 数据类(非观测指标,是 profiling 明细):</p><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>位置</th></tr></thead><tbody><tr><td><code>EBPFProfilingData</code></td><td>EBPF_PROFILING_DATA</td><td>48</td><td><code>EBPFProfilingData.java:29</code></td></tr><tr><td><code>EBPFProcessProfilingSchedule</code></td><td>EBPF_PROFILING_SCHEDULE</td><td>47</td><td><code>EBPFProcessProfilingSchedule.java:31</code></td></tr><tr><td><code>JFRProfilingData</code></td><td>JFR_PROFILING_DATA</td><td>84</td><td><code>JFRProfilingData.java:30</code></td></tr><tr><td><code>PprofProfilingData</code></td><td>PPROF_PROFILING_DATA</td><td>93</td><td><code>PprofProfilingData.java:28</code></td></tr></tbody></table><h4 id="g.-k8s-%E7%B1%BB(rover-%E9%87%87%E9%9B%86%E7%9A%84-mesh%2F%E7%BD%91%E7%BB%9C%E5%B1%82%E6%95%B0%E6%8D%AE)" tabindex="-1">G. K8s 类(rover 采集的 mesh/网络层数据)</h4><p>K8s 类全部继承抽象基类 <code>K8SMetrics</code>(<code>K8SMetrics.java:24</code>),后者预定义了 connect/accept/close/write/read 五种网络操作,每种再分 L2/L3/L4 三层 + Protocol(HTTP) 嵌套结构。</p><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>catalog</th><th>位置</th></tr></thead><tbody><tr><td><code>K8SService</code></td><td>K8S_SERVICE</td><td>72</td><td>SERVICE</td><td><code>K8SService.java:34</code></td></tr><tr><td><code>K8SServiceInstance</code></td><td>K8S_SERVICE_INSTANCE</td><td>73</td><td>SERVICE_INSTANCE</td><td><code>K8SServiceInstance.java:30</code></td></tr><tr><td><code>K8SServiceRelation</code></td><td>K8S_SERVICE_RELATION</td><td>74</td><td>SERVICE_RELATION</td><td><code>K8SServiceRelation.java:34</code></td></tr><tr><td><code>K8SServiceInstanceRelation</code></td><td>K8S_SERVICE_INSTANCE_RELATION</td><td>75</td><td>SERVICE_INSTANCE_RELATION</td><td><code>K8SServiceInstanceRelation.java:30</code></td></tr><tr><td><code>K8SEndpoint</code></td><td>K8S_ENDPOINT</td><td>76</td><td>ENDPOINT</td><td><code>K8SEndpoint.java:30</code></td></tr></tbody></table><blockquote><p><strong>K8s 和 Cilium 复用拓扑 catalog</strong><br />K8s 和 Cilium 复用了同一套拓扑 catalog(SERVICE/SERVICE_INSTANCE/…),能在 UI 上和普通 Service 拓扑混排。两者都带 <code>attr0~attr5</code> 透传属性。复用 catalog 是刻意的——网络层和 mesh 层最终都要在服务拓扑图上呈现,共用 catalog 让 UI 不必为每层单独写渲染逻辑。</p></blockquote><h4 id="h.-process-%E7%B1%BB(%E8%BF%9B%E7%A8%8B%E7%BA%A7%2Cebpf-%E4%B9%8B%E4%B8%8A)" tabindex="-1">H. Process 类(进程级,eBPF 之上)</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>catalog</th><th>关键字段</th><th>位置</th></tr></thead><tbody><tr><td><code>Process</code></td><td>PROCESS</td><td>45</td><td>PROCESS</td><td>instanceId, serviceId, name, serviceName, instanceName, agentId, detectType, labels, profilingSupportStatus</td><td><code>Process.java:33</code></td></tr><tr><td><code>ProcessRelation</code></td><td>PROCESS_RELATION</td><td>54</td><td>PROCESS_RELATION</td><td>进程间调用关系</td><td><code>ProcessRelation.java:30</code></td></tr></tbody></table><h4 id="i.-log-%2F-event-%2F-segment-%E7%AD%89%E6%98%8E%E7%BB%86%E7%B1%BB" tabindex="-1">I. Log / Event / Segment 等明细类</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>说明</th><th>位置</th></tr></thead><tbody><tr><td><code>Segment</code></td><td>SEGMENT</td><td>12</td><td>链路分段原始数据:segmentId, traceId, serviceId, serviceInstanceId, endpointId, startTime, latency, isError, dataBinary, tags</td><td><code>Segment.java:32</code></td></tr><tr><td><code>Log</code></td><td>LOG</td><td>41</td><td>继承 <code>AbstractLog</code>:uniqueId, timestamp, serviceId, serviceInstanceId, endpointId, traceId, traceSegmentId, spanId, contentType, content, error</td><td><code>Log.java:23</code></td></tr></tbody></table><blockquote><p><strong>这些是明细 Record</strong><br /><code>Segment</code>、<code>Log</code>、<code>DatabaseSlowStatement</code> 是明细/Record 性质的 Source,<code>@ScopeDeclaration</code> 没有 catalog(告警不基于它们),它们走 LAL 脚本或硬编码 dispatcher 转成 Record 落库。</p></blockquote><h4 id="j.-%E5%85%83%E6%95%B0%E6%8D%AE-%2F-%E9%85%8D%E7%BD%AE%E7%B1%BB(%E6%97%A0-catalog%2C%E4%B8%8D%E5%8F%82%E4%B8%8E%E6%8C%87%E6%A0%87%E5%91%8A%E8%AD%A6)" tabindex="-1">J. 元数据 / 配置类(无 catalog,不参与指标告警)</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>作用</th></tr></thead><tbody><tr><td><code>ServiceMeta</code></td><td>SERVICE_META</td><td>29</td><td>只含 name + layer,用于服务元数据更新</td></tr><tr><td><code>ServiceInstanceUpdate</code></td><td>SERVICE_INSTANCE_UPDATE</td><td>30</td><td>实例元数据更新</td></tr><tr><td><code>EndpointMeta</code></td><td>ENDPOINT_META</td><td>42</td><td>端点元数据</td></tr><tr><td><code>ServiceLabel</code></td><td>SERVICE_LABEL</td><td>49</td><td>给服务打标签:serviceId, serviceName, label</td></tr><tr><td><code>TagAutocomplete</code></td><td>TAG_AUTOCOMPLETE</td><td>50</td><td>tag 自动补全索引</td></tr></tbody></table><h4 id="k.-tcp-%2F-%E9%9D%9E%E6%A0%87%E6%8B%93%E6%89%91%E7%B1%BB" tabindex="-1">K. TCP / 非标拓扑类</h4><table><thead><tr><th>类名</th><th>scopeId 常量</th><th>值</th><th>catalog</th></tr></thead><tbody><tr><td><code>TCPService</code></td><td>TCP_SERVICE</td><td>57</td><td>SERVICE</td></tr><tr><td><code>TCPServiceInstance</code></td><td>TCP_SERVICE_INSTANCE</td><td>58</td><td>SERVICE_INSTANCE</td></tr><tr><td><code>TCPServiceRelation</code></td><td>TCP_SERVICE_RELATION</td><td>59</td><td>SERVICE_RELATION</td></tr><tr><td><code>TCPServiceInstanceRelation</code></td><td>TCP_SERVICE_INSTANCE_RELATION</td><td>60</td><td>SERVICE_INSTANCE_RELATION</td></tr><tr><td><code>TCPServiceInstanceUpdate</code></td><td>TCP_SERVICE_INSTANCE_UPDATE</td><td>61</td><td>(无)</td></tr></tbody></table><blockquote><p><strong>范围说明</strong><br />上面 A-K 覆盖 <code>server-core/source/</code> 下全部 56 个带 <code>@ScopeDeclaration</code> 的类。其余 scopeId(BROWSER_<em>、ZIPKIN_</em>、PROFILE_<em>、UI_</em>、ALARM、EVENT、SPAN_ATTACHED_EVENT、CONTINUOUS_PROFILING_POLICY、RUNTIME_RULE、ALARM_RECOVERY、PPROF_TASK、ASYNC_PROFILER_* 等)的 Source 类位于 <code>server-core</code> 其它子包(manual/generic/browser/zipkin/profile 等),不在 <code>source/</code> 目录内,注册机制完全相同(都靠 <code>AnnotationScan</code> 扫 <code>@ScopeDeclaration</code>)。</p></blockquote><h3 id="sourcedecorator-%E6%9C%BA%E5%88%B6" tabindex="-1">SourceDecorator 机制</h3><blockquote><p><strong>Decorator 是什么</strong><br />Decorator 是 OAL 在 Source 进入流处理前补字段的扩展点,典型用途是把 <code>Layer</code> 名写到 <code>attr0</code>,让生成的指标带一个可查询的属性列。</p></blockquote><p>接口 <code>oap-server/server-core/.../analysis/ISourceDecorator.java:27</code>:</p><pre><code class="language-java">public interface ISourceDecorator&lt;SOURCE extends ISource&gt; {    int getSourceScope();      // 声明装饰哪个 scope 的 Source    void decorate(SOURCE source);  // 补字段,如 set attr0}</code></pre><p><code>SourceDecoratorManager</code>(<code>analysis/SourceDecoratorManager.java:31</code>)维护静态 <code>DECORATOR_MAP</code>,key 是 decorator 类的 <code>getSimpleName()</code>。<code>addIfAsSourceDecorator()</code> 通过反射拿到泛型参数 <code>&lt;SOURCE&gt;</code>,校验它是 <code>ISource</code> 子类后实例化注册。</p><p>全仓库目前只有 3 个 <code>ISourceDecorator</code> 实现:</p><table><thead><tr><th>Decorator</th><th>装饰的 Source</th><th>行为</th><th>位置</th></tr></thead><tbody><tr><td><code>ServiceDecorator</code></td><td>Service</td><td><code>setAttr0(layer.name())</code></td><td><code>source/ServiceDecorator.java:23</code></td></tr><tr><td><code>EndpointDecorator</code></td><td>Endpoint</td><td><code>setAttr0(serviceLayer.name())</code></td><td><code>source/EndpointDecorator.java:23</code></td></tr><tr><td><code>K8SServiceDecorator</code></td><td>K8SService</td><td>设置 attr0(layer 相关)</td><td><code>source/K8SServiceDecorator.java</code></td></tr></tbody></table><p>调用方式是 Source 上的 <code>decorate(String decoratorName)</code> 方法(见 <code>Service.java:124</code>、<code>Endpoint.java:125</code>),按类名从 <code>DECORATOR_MAP</code> 取出对应 decorator 执行。</p><p><code>ScopeDefaultColumn.DefinedByField</code> 注解的 <code>isAttribute=true</code>(<code>ScopeDefaultColumn.java:77</code>,since 10.2.0)正是为这些 attr 字段标记&quot;这是 decorator 写的属性列&quot;,用于查询条件。</p><hr /><h2 id="scope-%E4%BD%93%E7%B3%BB%E5%AE%8C%E6%95%B4" tabindex="-1">Scope 体系完整</h2><h3 id="%E4%B8%BA%E4%BB%80%E4%B9%88-scope-%E7%94%A8%E6%95%B4%E6%95%B0-id-%E5%88%86%E5%8C%BA" tabindex="-1">为什么 Scope 用整数 ID 分区</h3><p>scopeId 是整数而不是字符串,不是省内存的小技巧,是 OAP 性能链路上的一个关键决策。Source 进入流处理后,<code>SourceDispatcher</code> 要按 scope 把 Source 路由到正确的处理器——这是一个每条数据都要走的热路径。用整数做 <code>Map&lt;Integer,?&gt;</code> 的 key,hash 和等值比较都是 O(1) 的原生操作;若用字符串 scope 名,每次路由都要 <code>String.equals</code> 走字符比较,在高 TPS 下不可忽略。</p><p>更关键的是 OAL 编译产物:<code>from(Service).longAvg()</code> 编译后,<code>Service</code> 这个名字被换成它的 scopeId 常量 <code>1</code>,后续所有匹配都是 int 等值。这让 OAL 的运行时和 Source 分发器共享同一套整数契约,字符串只在启动期注册阶段出现一次。</p><p><code>DefaultScopeDefine.java:36-40</code> 的注释把分区规则写死了:</p><blockquote><p>All metrics IDs in [0, 10,000) are reserved in Apache SkyWalking. If you want to extend the scope, recommend to start with 10,000.</p></blockquote><p>0~9999 是官方保留区,第三方扩展从 10000 开始。整数空间足够大,官方用前 100 个,留 9900 个给扩展,且 <code>addNewScope()</code> 会做冲突校验(<code>DefaultScopeDefine.java:217-231</code>):id 重复抛 <code>UnexpectedException</code>,id 为负抛异常,name 重复抛异常。这套校验保证注册表的唯一性,不靠人工纪律。</p><h3 id="defaultscopedefine-%E5%85%A8%E9%83%A8-scopeid-%E5%B8%B8%E9%87%8F" tabindex="-1">DefaultScopeDefine 全部 scopeId 常量</h3><p>文件:<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java</code>。下表按文件出现顺序列出(<code>@since</code>/<code>@Deprecated</code> 直接来自源码):</p><table><thead><tr><th>常量名</th><th>值</th><th>备注</th><th>行号</th></tr></thead><tbody><tr><td><code>UNKNOWN</code></td><td>0</td><td>since 9.0.0</td><td><code>:45</code></td></tr><tr><td><code>ALL</code></td><td>0</td><td>@Deprecated from 9.0.0</td><td><code>:50</code></td></tr><tr><td><code>SERVICE</code></td><td>1</td><td></td><td><code>:51</code></td></tr><tr><td><code>SERVICE_INSTANCE</code></td><td>2</td><td></td><td><code>:52</code></td></tr><tr><td><code>ENDPOINT</code></td><td>3</td><td></td><td><code>:53</code></td></tr><tr><td><code>SERVICE_RELATION</code></td><td>4</td><td></td><td><code>:54</code></td></tr><tr><td><code>SERVICE_INSTANCE_RELATION</code></td><td>5</td><td></td><td><code>:55</code></td></tr><tr><td><code>ENDPOINT_RELATION</code></td><td>6</td><td></td><td><code>:56</code></td></tr><tr><td><code>SERVICE_INSTANCE_JVM_CPU</code></td><td>8</td><td>注意:7 跳过</td><td><code>:57</code></td></tr><tr><td><code>SERVICE_INSTANCE_JVM_MEMORY</code></td><td>9</td><td></td><td><code>:58</code></td></tr><tr><td><code>SERVICE_INSTANCE_JVM_MEMORY_POOL</code></td><td>10</td><td></td><td><code>:59</code></td></tr><tr><td><code>SERVICE_INSTANCE_JVM_GC</code></td><td>11</td><td></td><td><code>:60</code></td></tr><tr><td><code>SEGMENT</code></td><td>12</td><td></td><td><code>:61</code></td></tr><tr><td><code>ALARM</code></td><td>13</td><td></td><td><code>:62</code></td></tr><tr><td><code>DATABASE_ACCESS</code></td><td>17</td><td></td><td><code>:63</code></td></tr><tr><td><code>DATABASE_SLOW_STATEMENT</code></td><td>18</td><td></td><td><code>:64</code></td></tr><tr><td><code>SERVICE_INSTANCE_CLR_CPU</code></td><td>19</td><td></td><td><code>:65</code></td></tr><tr><td><code>SERVICE_INSTANCE_CLR_GC</code></td><td>20</td><td></td><td><code>:66</code></td></tr><tr><td><code>SERVICE_INSTANCE_CLR_THREAD</code></td><td>21</td><td></td><td><code>:67</code></td></tr><tr><td><code>ENVOY_INSTANCE_METRIC</code></td><td>22</td><td></td><td><code>:68</code></td></tr><tr><td><code>ZIPKIN_SPAN</code></td><td>23</td><td></td><td><code>:69</code></td></tr><tr><td><code>JAEGER_SPAN</code></td><td>24</td><td>@Deprecated</td><td><code>:71</code></td></tr><tr><td><code>HTTP_ACCESS_LOG</code></td><td>25</td><td>@Deprecated</td><td><code>:73</code></td></tr><tr><td><code>PROFILE_TASK</code></td><td>26</td><td></td><td><code>:74</code></td></tr><tr><td><code>PROFILE_TASK_LOG</code></td><td>27</td><td></td><td><code>:75</code></td></tr><tr><td><code>PROFILE_TASK_SEGMENT_SNAPSHOT</code></td><td>28</td><td></td><td><code>:76</code></td></tr><tr><td><code>SERVICE_META</code></td><td>29</td><td></td><td><code>:77</code></td></tr><tr><td><code>SERVICE_INSTANCE_UPDATE</code></td><td>30</td><td></td><td><code>:78</code></td></tr><tr><td><code>NETWORK_ADDRESS_ALIAS</code></td><td>31</td><td></td><td><code>:79</code></td></tr><tr><td><code>UI_TEMPLATE</code></td><td>32</td><td></td><td><code>:80</code></td></tr><tr><td><code>SERVICE_INSTANCE_JVM_THREAD</code></td><td>33</td><td></td><td><code>:81</code></td></tr><tr><td><code>BROWSER_ERROR_LOG</code></td><td>34</td><td>browser 段开始</td><td><code>:84</code></td></tr><tr><td><code>BROWSER_APP_PERF</code></td><td>35</td><td></td><td><code>:85</code></td></tr><tr><td><code>BROWSER_APP_PAGE_PERF</code></td><td>36</td><td></td><td><code>:86</code></td></tr><tr><td><code>BROWSER_APP_SINGLE_VERSION_PERF</code></td><td>37</td><td></td><td><code>:87</code></td></tr><tr><td><code>BROWSER_APP_TRAFFIC</code></td><td>38</td><td></td><td><code>:88</code></td></tr><tr><td><code>BROWSER_APP_SINGLE_VERSION_TRAFFIC</code></td><td>39</td><td></td><td><code>:89</code></td></tr><tr><td><code>BROWSER_APP_PAGE_TRAFFIC</code></td><td>40</td><td></td><td><code>:90</code></td></tr><tr><td><code>LOG</code></td><td>41</td><td></td><td><code>:92</code></td></tr><tr><td><code>ENDPOINT_META</code></td><td>42</td><td></td><td><code>:93</code></td></tr><tr><td><code>EVENT</code></td><td>43</td><td></td><td><code>:95</code></td></tr><tr><td><code>SERVICE_INSTANCE_JVM_CLASS</code></td><td>44</td><td></td><td><code>:97</code></td></tr><tr><td><code>PROCESS</code></td><td>45</td><td></td><td><code>:99</code></td></tr><tr><td><code>EBPF_PROFILING_TASK</code></td><td>46</td><td></td><td><code>:100</code></td></tr><tr><td><code>EBPF_PROFILING_SCHEDULE</code></td><td>47</td><td></td><td><code>:101</code></td></tr><tr><td><code>EBPF_PROFILING_DATA</code></td><td>48</td><td></td><td><code>:102</code></td></tr><tr><td><code>SERVICE_LABEL</code></td><td>49</td><td></td><td><code>:103</code></td></tr><tr><td><code>TAG_AUTOCOMPLETE</code></td><td>50</td><td></td><td><code>:104</code></td></tr><tr><td><code>ZIPKIN_SERVICE</code></td><td>51</td><td></td><td><code>:105</code></td></tr><tr><td><code>ZIPKIN_SERVICE_SPAN</code></td><td>52</td><td></td><td><code>:106</code></td></tr><tr><td><code>ZIPKIN_SERVICE_RELATION</code></td><td>53</td><td></td><td><code>:107</code></td></tr><tr><td><code>PROCESS_RELATION</code></td><td>54</td><td></td><td><code>:108</code></td></tr><tr><td><code>CACHE_ACCESS</code></td><td>55</td><td></td><td><code>:109</code></td></tr><tr><td><code>CACHE_SLOW_ACCESS</code></td><td>56</td><td></td><td><code>:110</code></td></tr><tr><td><code>TCP_SERVICE</code></td><td>57</td><td></td><td><code>:112</code></td></tr><tr><td><code>TCP_SERVICE_INSTANCE</code></td><td>58</td><td></td><td><code>:113</code></td></tr><tr><td><code>TCP_SERVICE_RELATION</code></td><td>59</td><td></td><td><code>:114</code></td></tr><tr><td><code>TCP_SERVICE_INSTANCE_RELATION</code></td><td>60</td><td></td><td><code>:115</code></td></tr><tr><td><code>TCP_SERVICE_INSTANCE_UPDATE</code></td><td>61</td><td></td><td><code>:116</code></td></tr><tr><td><code>SAMPLED_SLOW_TRACE</code></td><td>62</td><td></td><td><code>:117</code></td></tr><tr><td><code>MESSAGE_QUEUE_ACCESS</code></td><td>63</td><td></td><td><code>:119</code></td></tr><tr><td><code>MESSAGE_QUEUE_ENDPOINT_ACCESS</code></td><td>64</td><td></td><td><code>:120</code></td></tr><tr><td><code>SPAN_ATTACHED_EVENT</code></td><td>65</td><td></td><td><code>:122</code></td></tr><tr><td><code>SAMPLED_STATUS_4XX_TRACE</code></td><td>66</td><td></td><td><code>:123</code></td></tr><tr><td><code>SAMPLED_STATUS_5XX_TRACE</code></td><td>67</td><td></td><td><code>:124</code></td></tr><tr><td><code>CONTINUOUS_PROFILING_POLICY</code></td><td>68</td><td></td><td><code>:126</code></td></tr><tr><td><code>UI_MENU</code></td><td>69</td><td></td><td><code>:128</code></td></tr><tr><td><code>SERVICE_HIERARCHY_RELATION</code></td><td>70</td><td>服务层级关系</td><td><code>:130</code></td></tr><tr><td><code>INSTANCE_HIERARCHY_RELATION</code></td><td>71</td><td>实例层级关系</td><td><code>:131</code></td></tr><tr><td><code>K8S_SERVICE</code></td><td>72</td><td></td><td><code>:133</code></td></tr><tr><td><code>K8S_SERVICE_INSTANCE</code></td><td>73</td><td></td><td><code>:134</code></td></tr><tr><td><code>K8S_SERVICE_RELATION</code></td><td>74</td><td></td><td><code>:135</code></td></tr><tr><td><code>K8S_SERVICE_INSTANCE_RELATION</code></td><td>75</td><td></td><td><code>:136</code></td></tr><tr><td><code>K8S_ENDPOINT</code></td><td>76</td><td></td><td><code>:137</code></td></tr><tr><td><code>CILIUM_SERVICE</code></td><td>78</td><td>注意:77 跳过</td><td><code>:139</code></td></tr><tr><td><code>CILIUM_SERVICE_INSTANCE</code></td><td>79</td><td></td><td><code>:140</code></td></tr><tr><td><code>CILIUM_SERVICE_RELATION</code></td><td>80</td><td></td><td><code>:141</code></td></tr><tr><td><code>CILIUM_SERVICE_INSTANCE_RELATION</code></td><td>81</td><td></td><td><code>:142</code></td></tr><tr><td><code>CILIUM_ENDPOINT</code></td><td>82</td><td></td><td><code>:143</code></td></tr><tr><td><code>CILIUM_ENDPOINT_REALATION</code></td><td>83</td><td>源码常量名即此拼写</td><td><code>:144</code></td></tr><tr><td><code>JFR_PROFILING_DATA</code></td><td>84</td><td></td><td><code>:146</code></td></tr><tr><td><code>ASYNC_PROFILER_TASK</code></td><td>85</td><td></td><td><code>:147</code></td></tr><tr><td><code>ASYNC_PROFILER_TASK_LOG</code></td><td>86</td><td></td><td><code>:148</code></td></tr><tr><td><code>BROWSER_APP_WEB_VITALS_PAGE_PERF</code></td><td>87</td><td></td><td><code>:150</code></td></tr><tr><td><code>BROWSER_APP_RESOURCE_PERF</code></td><td>88</td><td></td><td><code>:151</code></td></tr><tr><td><code>BROWSER_APP_WEB_INTERACTION_PAGE_PERF</code></td><td>89</td><td></td><td><code>:152</code></td></tr><tr><td><code>SW_SPAN_ATTACHED_EVENT</code></td><td>90</td><td></td><td><code>:153</code></td></tr><tr><td><code>SERVICE_DATABASE_SLOW_STATEMENT</code></td><td>91</td><td></td><td><code>:154</code></td></tr><tr><td><code>PPROF_TASK</code></td><td>92</td><td></td><td><code>:155</code></td></tr><tr><td><code>PPROF_PROFILING_DATA</code></td><td>93</td><td></td><td><code>:156</code></td></tr><tr><td><code>PPROF_TASK_LOG</code></td><td>94</td><td></td><td><code>:157</code></td></tr><tr><td><code>ALARM_RECOVERY</code></td><td>95</td><td></td><td><code>:158</code></td></tr><tr><td><code>GEN_AI_PROVIDER_ACCESS</code></td><td>96</td><td></td><td><code>:159</code></td></tr><tr><td><code>GEN_AI_MODEL_ACCESS</code></td><td>97</td><td></td><td><code>:160</code></td></tr><tr><td><code>RUNTIME_RULE</code></td><td>98</td><td></td><td><code>:161</code></td></tr></tbody></table><blockquote><p><strong>跳过的 ID</strong><br />7、14、15、16、77 在源码中无对应常量。这些是历史废弃或预留——<code>addNewScope()</code> 的冲突校验只挡重复,不挡空缺,所以跳号不会报错,只是 ID 空间留白。</p></blockquote><h3 id="scopedeclaration-%E6%B3%A8%E8%A7%A3" tabindex="-1">ScopeDeclaration 注解</h3><p>定义在 <code>source/ScopeDeclaration.java:43</code>:</p><pre><code class="language-java">@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface ScopeDeclaration {    int id();              // 对应 DefaultScopeDefine 里的常量值    String name();        // scope 名字,如 &quot;Service&quot;,OAL 脚本里用这个名字    String catalog() default &quot;&quot;;  // 顶层 catalog 名,空串表示不参与告警}</code></pre><p><code>catalog</code> 字段(<code>:53-55</code>)注释明确:当 scope 不是 metric,或其生成的 metrics 不支持告警时,留空。这就是 Segment/Log/元数据类没有 catalog 的注解层依据。</p><h3 id="catalog-%E6%9C%BA%E5%88%B6" tabindex="-1">Catalog 机制</h3><blockquote><p><strong>Catalog 是什么</strong><br />Catalog 是告警/UI 的维度归属标签。告警规则需要知道一个指标挂在哪个拓扑节点上才能在 UI 渲染——<code>inServiceCatalog(scopeId)</code> 返回 true 就说明这条指标能在服务拓扑图上显示。</p></blockquote><p><code>DefaultScopeDefine.java:166-182</code> 定义了 8 个 catalog 名字常量和 8 个对应的 <code>Map&lt;Integer,Boolean&gt;</code> catalog 表。注意是 8 张独立的 Map 而不是一张大 Map——查 catalog 时只需 <code>SERVICE_CATALOG.containsKey(scopeId)</code>,O(1),不必遍历。</p><table><thead><tr><th>catalog 名常量</th><th>字符串值</th><th>含义</th><th>典型成员 scope</th></tr></thead><tbody><tr><td><code>SERVICE_CATALOG_NAME</code></td><td>“SERVICE”</td><td>服务维度</td><td>Service(1), DatabaseAccess(17), CacheAccess(55), MQAccess(63), K8SService(72), CiliumService(78), TCPService(57), GenAIProviderAccess(96)</td></tr><tr><td><code>SERVICE_INSTANCE_CATALOG_NAME</code></td><td>“SERVICE_INSTANCE”</td><td>实例维度</td><td>ServiceInstance(2), 所有 JVM/CLR 类, Envoy(22), K8SServiceInstance(73), CiliumServiceInstance(79), GenAIModelAccess(97)</td></tr><tr><td><code>ENDPOINT_CATALOG_NAME</code></td><td>“ENDPOINT”</td><td>端点维度</td><td>Endpoint(3), MQEndpointAccess(64), K8SEndpoint(76), CiliumEndpoint(82)</td></tr><tr><td><code>SERVICE_RELATION_CATALOG_NAME</code></td><td>“SERVICE_RELATION”</td><td>服务关系</td><td>ServiceRelation(4), K8SServiceRelation(74), CiliumServiceRelation(80), TCPServiceRelation(59)</td></tr><tr><td><code>SERVICE_INSTANCE_RELATION_CATALOG_NAME</code></td><td>“SERVICE_INSTANCE_RELATION”</td><td>实例关系</td><td>ServiceInstanceRelation(5), K8s/Cilium/TCP 对应关系</td></tr><tr><td><code>ENDPOINT_RELATION_CATALOG_NAME</code></td><td>“ENDPOINT_RELATION”</td><td>端点关系</td><td>EndpointRelation(6), CiliumEndpointRelation(83)</td></tr><tr><td><code>PROCESS_CATALOG_NAME</code></td><td>“PROCESS”</td><td>进程</td><td>Process(45)</td></tr><tr><td><code>PROCESS_RELATION_CATALOG_NAME</code></td><td>“PROCESS_RELATION”</td><td>进程关系</td><td>ProcessRelation(54)</td></tr></tbody></table><p>查询 API:<code>catalogOf(int scope)</code>(<code>:422</code>)返回该 scope 所属 catalog 名,找不到返回 “ALL”;<code>inServiceCatalog/inServiceInstanceCatalog/inEndpointCatalog/...</code>(<code>:342-414</code>)判断 scopeId 是否属于某 catalog。</p><h3 id="annotationscan-%E5%A6%82%E4%BD%95%E6%89%AB%E6%8F%8F%E6%B3%A8%E5%86%8C" tabindex="-1">AnnotationScan 如何扫描注册</h3><p>文件:<code>oap-server/server-core/.../annotation/AnnotationScan.java</code>。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/08-%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B%E4%B8%8EScope%E4%BD%93%E7%B3%BB.md_1.png" alt="图2" /></p><p>机制(<code>AnnotationScan.java:53-69</code>):</p><ol><li><code>ClassPath.from(classLoader).getTopLevelClassesRecursive(&quot;org.apache.skywalking&quot;)</code>——递归扫描 <code>org.apache.skywalking</code> 包下所有顶层类(<code>:55</code>)。用 ClassPath API 而不是自写文件遍历,是因为 SkyWalking 可能打成 fat jar 或被 OSGi 容器加载,ClassPath API 能正确处理这些部署形态。</li><li>对每个类,遍历已注册的 <code>AnnotationListener</code>,若类上有该 listener 关心的注解则加入缓存(<code>:59-62</code>)。</li><li>全部扫完后,按类名排序,逐个回调 <code>listener.notify(aClass)</code>(<code>:66-68</code>,排序见 <code>:89</code>)。排序这一步不是洁癖——<code>DefaultScopeDefine</code> 的注册会写静态 Map,排序保证相同 scopeId 的冲突抛错信息在不同机器上可复现,便于排查。</li></ol><p><code>DefaultScopeDefine.Listener</code>(<code>:194-207</code>)就是这样一个 listener:<code>annotation()</code> 返回 <code>ScopeDeclaration.class</code>,<code>notify()</code> 调 <code>addNewScope(declaration, originalClass)</code>。</p><p><code>addNewScope()</code>(<code>:215-297</code>)做四件事:</p><ol><li>冲突校验 + 把 <code>id→name</code>、<code>name→id</code> 存入 <code>ID_2_NAME</code>/<code>NAME_2_ID</code>(<code>:233-234</code>),建立双向查找表。</li><li>扫描类上的 <code>@ScopeDefaultColumn.VirtualColumnDefinition</code>(类级注解,定义主键列)和字段上的 <code>@ScopeDefaultColumn.DefinedByField</code>,收集成 <code>ScopeDefaultColumn</code> 列表存入 <code>SCOPE_COLUMNS</code>(<code>:236-268</code>)。这一步把 Source 的&quot;默认列&quot;传递给后续生成的 Metrics 实体——Metrics 继承 Source 的列,这样查询时能按 Source 的字段过滤。</li><li>根据 <code>catalog()</code> 把 scopeId 归入对应 catalog 表(<code>:270-296</code>)。</li><li>字段上若标了 <code>@ScopeDefaultColumn.BanyanDB(shardingKeyIdx=N)</code>,记录分片键索引(<code>:252-254</code>),供 BanyanDB 存储层做分片。</li></ol><p><code>ScopeDefaultColumn</code>(<code>source/ScopeDefaultColumn.java:34</code>)承载这些列定义,其中 <code>requireDynamicActive=true</code> 表示只有当 <code>core/activeExtraModelColumns=true</code> 时才真正加到生成的指标列里(<code>:62-64</code>),用于按需开启冗余列以省存储。</p><hr /><h2 id="metrics-%E5%AD%90%E7%B1%BB%E5%AE%B6%E6%97%8F" tabindex="-1">Metrics 子类家族</h2><blockquote><p><strong>Metrics 是什么</strong><br />Metrics 是对 Source 聚合后的时间窗口统计值,每个子类封装一种聚合算法(求和/平均/分位数/直方图……),通过 <code>@MetricsFunction</code> 注解暴露给 OAL,核心契约是 <code>combine()</code> 累加 + <code>calculate()</code> 终算。</p></blockquote><h3 id="%E4%B8%BA%E4%BB%80%E4%B9%88-combine-%E5%92%8C-calculate-%E8%A6%81%E5%88%86%E4%B8%A4%E6%AD%A5" tabindex="-1">为什么 combine 和 calculate 要分两步</h3><p>这是 SkyWalking 流处理模型的核心设计,不是工程洁癖。一条 Source 进来时,你不知道它是不是这个时间窗口的最后一条——窗口可能还没结束,实例可能还在不断上报。所以 <code>combine()</code> 只做<strong>可合并的中间态累加</strong>:LongAvg 存 <code>summation</code> 和 <code>count</code> 而不是 <code>value</code>,因为 <code>summation</code> 和 <code>count</code> 在多实例合并时各自相加结果正确,而 <code>value = avg1 + avg2</code> 是错的。</p><p><code>calculate()</code> 在窗口结束或降采样时才调,这时所有中间态都齐了,做一次除法产出最终值。这个分工让<strong>跨实例合并</strong>变得自然:实例 A 的 <code>(sum=100, count=10)</code> 和实例 B 的 <code>(sum=200, count=20)</code> 合并成 <code>(sum=300, count=30)</code>,最后 <code>calculate</code> 得到 <code>value=10</code>。如果一开始就存 <code>value</code>,A 是 10、B 是 10,合并后还是 10,但真实平均是 10——这次碰巧对,但只要 count 不同就错。</p><p>不是所有 Metrics 都需要两步:<code>CountMetrics</code>、<code>SumMetrics</code>、<code>MaxLongMetrics</code> 的 <code>calculate()</code> 是空操作,因为它们的中间态就是终态。但契约统一保留两步,让流处理框架不用关心具体子类。</p><h3 id="metrics-%E5%9F%BA%E7%B1%BB%E5%A5%91%E7%BA%A6" tabindex="-1">Metrics 基类契约</h3><p>文件:<code>oap-server/server-core/.../analysis/metrics/Metrics.java</code>。<code>Metrics</code> 继承 <code>StreamData</code> 并实现 <code>StorageData, ToJson</code>(<code>:41</code>)。核心抽象方法:</p><table><thead><tr><th>方法</th><th>作用</th><th>行号</th></tr></thead><tbody><tr><td><code>boolean combine(Metrics metrics)</code></td><td>合并同类型 metrics;返回 true 表示继续处理,false 表示丢弃</td><td><code>:68</code></td></tr><tr><td><code>void calculate()</code></td><td>时间窗口结束时计算最终值</td><td><code>:73</code></td></tr><tr><td><code>Metrics toHour() / toDay()</code></td><td>降采样到小时/天</td><td><code>:80,87</code></td></tr><tr><td><code>StorageID id0()</code></td><td>生成存储唯一 ID(基类 <code>id()</code> 做缓存)</td><td><code>:167</code></td></tr></tbody></table><p>时间相关:</p><ul><li><code>timeBucket</code> 字段(<code>:49</code>)+ <code>lastUpdateTimestamp</code>(<code>:59</code>):用于 session cache 过期判断 <code>isExpired(timestamp, expiredThreshold)</code>(<code>:103</code>)。</li><li><code>toTimeBucketInHour()</code>(<code>:107</code>):分钟桶 <code>/100</code> 转小时桶;<code>toTimeBucketInDay()</code>(<code>:115</code>):分钟桶 <code>/10000</code> 或小时桶 <code>/100</code>。</li><li><code>getDurationInMinute()</code>(<code>:128</code>):分钟桶返回 1,小时桶 60,天桶 1440——这是 <code>CPMMetrics</code> 算&quot;每分钟次数&quot;的依据。</li></ul><h3 id="oal-%E6%B3%A8%E8%A7%A3%E4%BD%93%E7%B3%BB(metrics-%E5%A6%82%E4%BD%95%E8%A2%AB-oal-%E5%BC%95%E7%94%A8)" tabindex="-1">OAL 注解体系(Metrics 如何被 OAL 引用)</h3><p>每个 Metrics 子类用 <code>@MetricsFunction(functionName=&quot;xxx&quot;)</code> 声明 OAL 函数名(如 <code>count</code>/<code>longAvg</code>/<code>percentile2</code>)。<code>combine</code> 方法上还用一组参数注解告诉 OAL 编译器&quot;这个参数从哪来&quot;:</p><table><thead><tr><th>注解</th><th>含义</th></tr></thead><tbody><tr><td><code>@Entrance</code></td><td>标记这是 OAL 入口 combine 方法</td></tr><tr><td><code>@SourceFrom</code></td><td>参数来自 Source 的某字段</td></tr><tr><td><code>@ConstOne</code></td><td>参数是常量 1(每来一条 Source 计 1)</td></tr><tr><td><code>@Arg</code></td><td>参数是 OAL 调用者传的参数</td></tr><tr><td><code>@Expression</code></td><td>参数是 OAL 表达式(boolean)</td></tr><tr><td><code>@DefaultValue</code></td><td>给 @Arg 一个默认值</td></tr></tbody></table><p>例如 <code>LongAvgMetrics.combine(@SourceFrom long summation, @ConstOne long count)</code>(<code>LongAvgMetrics.java:57</code>)表示&quot;summation 取自 Source 的 latency,count 每次计 1&quot;。OAL 编译器读这些注解,把 <code>from(Service.latency).longAvg()</code> 翻译成&quot;取 Source.latency 字段传给 summation,count 固定传 1&quot;。</p><h3 id="%E5%85%A8%E9%83%A8-metrics-%E5%AD%90%E7%B1%BB(18-%E4%B8%AA%E5%87%BD%E6%95%B0)" tabindex="-1">全部 Metrics 子类(18 个函数)</h3><table><thead><tr><th>类名</th><th>OAL 函数名</th><th>聚合语义</th><th>combine 算法</th><th>calculate 算法</th><th>值类型</th><th>位置</th></tr></thead><tbody><tr><td><code>CountMetrics</code></td><td><code>count</code></td><td>计数</td><td><code>value += count</code>(@ConstOne)</td><td>空操作</td><td>long</td><td><code>CountMetrics.java:32</code></td></tr><tr><td><code>SumMetrics</code></td><td><code>sum</code></td><td>求和</td><td><code>value += count</code>(@SourceFrom)</td><td>空操作</td><td>long</td><td><code>SumMetrics.java:32</code></td></tr><tr><td><code>LongAvgMetrics</code></td><td><code>longAvg</code></td><td>长整型平均</td><td><code>summation += s; count += 1</code></td><td><code>value = summation / count</code></td><td>long</td><td><code>LongAvgMetrics.java:33</code></td></tr><tr><td><code>DoubleAvgMetrics</code></td><td><code>doubleAvg</code></td><td>浮点平均</td><td><code>summation += s; count += 1</code></td><td><code>value = summation / count</code></td><td>double</td><td><code>DoubleAvgMetrics.java:33</code></td></tr><tr><td><code>PercentMetrics</code></td><td><code>percent</code></td><td>百分比</td><td>match 命中则 <code>match++; total++</code></td><td><code>percentage = match*10000/total</code></td><td>int</td><td><code>PercentMetrics.java:32</code></td></tr><tr><td><code>RateMetrics</code></td><td><code>rate</code></td><td>比率</td><td>两个 @Expression boolean 分别累加 numerator/denominator</td><td><code>percentage = numerator*10000/denominator</code></td><td>int</td><td><code>RateMetrics.java:31</code></td></tr><tr><td><code>CPMMetrics</code></td><td><code>cpm</code></td><td>每分钟调用数</td><td><code>total += count</code></td><td><code>value = total / getDurationInMinute()</code></td><td>long</td><td><code>CPMMetrics.java:32</code></td></tr><tr><td><code>ApdexMetrics</code></td><td><code>apdex</code></td><td>Apdex 满意度</td><td>按 t(阈值)/4t 分档累加 sNum/tNum/totalNum</td><td><code>value = (sNum*10000 + tNum*10000/2) / totalNum</code></td><td>int</td><td><code>ApdexMetrics.java:41</code></td></tr><tr><td><code>MaxLongMetrics</code></td><td><code>max</code></td><td>长整型最大值</td><td><code>if (count &gt; value) value = count</code></td><td>空操作</td><td>long</td><td><code>MaxLongMetrics.java:32</code></td></tr><tr><td><code>MinLongMetrics</code></td><td><code>min</code></td><td>长整型最小值</td><td><code>if (count &lt; value) value = count</code></td><td>空操作</td><td>long</td><td><code>MinLongMetrics.java</code></td></tr><tr><td><code>MaxDoubleMetrics</code></td><td><code>maxDouble</code></td><td>浮点最大值</td><td>同 max</td><td>空操作</td><td>double</td><td><code>MaxDoubleMetrics.java</code></td></tr><tr><td><code>MinDoubleMetrics</code></td><td><code>minDouble</code></td><td>浮点最小值</td><td>同 min,初值 <code>Double.MAX_VALUE</code></td><td>空操作</td><td>double</td><td><code>MinDoubleMetrics.java:32</code></td></tr><tr><td><code>PercentileMetrics</code></td><td><code>percentile</code></td><td>多分位数(旧)</td><td><code>dataset.valueAccumulation(value/precision, 1)</code></td><td>排序后按 roof 取 P50/75/90/95/99</td><td>int[] (MultiIntValuesHolder)</td><td><code>PercentileMetrics.java:44</code>(@Deprecated)</td></tr><tr><td><code>PercentileMetrics2</code></td><td><code>percentile2</code></td><td>多分位数(新)</td><td>同上</td><td>同上,但结果存为 <code>{p=50},v1\\\|{p=75},v2...</code> 的 DataTable</td><td>DataTable (LabeledValueHolder)</td><td><code>PercentileMetrics2.java:43</code></td></tr><tr><td><code>HistogramMetrics</code></td><td><code>histogram</code></td><td>直方图/热力图</td><td><code>dataset.valueAccumulation(index*step, 1)</code></td><td>空操作(无单值)</td><td>无(用 dataset)</td><td><code>HistogramMetrics.java:39</code></td></tr><tr><td><code>LabelCountMetrics</code></td><td><code>labelCount</code></td><td>按标签计数</td><td><code>dataset.valueAccumulation(label, count, maxLabelCount)</code></td><td>把 dataset 转 <code>{n=label},count</code> 的 value DataTable</td><td>DataTable</td><td><code>LabelCountMetrics.java:33</code></td></tr><tr><td><code>LabelAvgMetrics</code></td><td><code>labelAvg</code></td><td>按标签平均</td><td><code>summation.valueAccumulation(label,count); count.valueAccumulation(label,1)</code></td><td><code>value = {n=label} : summation/count</code></td><td>DataTable</td><td><code>LabelAvgMetrics.java:34</code></td></tr></tbody></table><blockquote><p>另有辅助类型:<code>LongValueHolder</code>/<code>DoubleValueHolder</code>/<code>IntValueHolder</code>(单值接口)、<code>MultiIntValuesHolder</code>(多 int 值)、<code>LabeledValueHolder</code>(返回 DataTable)、<code>DataLabel</code>(标签 map)。</p></blockquote><h3 id="%E4%B8%89%E4%B8%AA%E5%85%B8%E5%9E%8B-combine%2Fcalculate-%E5%AE%9E%E7%8E%B0%E8%AF%A6%E8%A7%A3" tabindex="-1">三个典型 combine/calculate 实现详解</h3><p><strong>1) LongAvgMetrics —— 最经典的&quot;先存中间态、终算时做除法&quot;</strong></p><pre><code class="language-java">// LongAvgMetrics.java:57-72@Entrancepublic final void combine(@SourceFrom long summation, @ConstOne long count) {    this.summation += summation;   // 累加被求和字段    this.count += count;          // 累加计数}public final boolean combine(Metrics metrics) {  // 跨实例合并    LongAvgMetrics m = (LongAvgMetrics) metrics;    combine(m.summation, m.count);    return true;}public final void calculate() {  // 窗口终算    this.value = this.summation / this.count;}</code></pre><p><code>summation</code> 和 <code>count</code> 标了 <code>@Column(storageOnly=true)</code>(<code>:41,46</code>,不建索引只存值),<code>value</code> 标 <code>@Column(dataType=COMMON_VALUE)</code>(<code>:52</code>,可查询的最终值)。combine 阶段只累加中间态,calculate 阶段才做除法——这样多实例合并时先把 sum 和 count 各自加起来,最后再除,结果正确。</p><p><strong>2) PercentileMetrics2 —— 用 DataTable 算多分位数</strong></p><pre><code class="language-java">// PercentileMetrics2.java:81-128@Entrancepublic final void combine(@SourceFrom int value, @Arg int precision) {    String index = String.valueOf(value / precision);   // 把 latency 按 precision 分桶    dataset.valueAccumulation(index, 1L);                 // 该桶计数 +1}public boolean combine(Metrics metrics) {    this.dataset.append(percentileMetrics.dataset);       // DataTable 直接合并    return true;}public final void calculate() {    long total = dataset.sumOfValues();    int[] roofs = {Math.round(total*50/100), ...99};      // 每个 rank 的累计计数阈值    // 按 key(桶下标)升序累加,达到某 rank 的 roof 就记录该分位数值    percentileValues.put(label.toString(), Long.parseLong(key) * precision);}</code></pre><p><code>RANKS = {50,75,90,95,99}</code>(<code>:48</code>),一次 combine 同时算 5 个分位数。算法本质是<strong>桶计数 + 累加定位</strong>:每条 latency 除以 precision 落到某个桶,calculate 时把所有桶按 key 升序累加,累加值首次达到 <code>total * p/100</code> 的那个桶,其桶下标乘 precision 就是该分位数的近似值。precision 越小越精确,但桶数越多。</p><p>结果用 <code>DataLabel{p=50}</code> 作 key 存入 <code>percentileValues</code> 这个 DataTable,存储格式为 <code>{p=50},value1|{p=75},value2|...</code>(注释 <code>:37-39</code>)。</p><p><strong>3) ApdexMetrics —— 带&quot;档位&quot;的满意度评分</strong></p><pre><code class="language-java">// ApdexMetrics.java:75-100@Entrancepublic final void combine(@SourceFrom int value, @Arg String name, @Arg boolean status) {    int t = DICT.lookup(name).intValue();   // 从 ConfigurationDictionary 取阈值 T    int t4 = t * 4;    totalNum++;    if (!status || value &gt; t4) return;       // 失败或 &gt;4T 不计入满意    if (value &gt; t) tNum++;                   // (T, 4T] 是 Tolerating    else sNum++;                              // [0, T] 是 Satisfied}public void calculate() {    value = (int) ((sNum * 10000 + tNum * 10000 / 2) / totalNum);}</code></pre><p>Satisfied 权重 1.0,Tolerating 权重 0.5。结果乘 10000(万分级)避免浮点。<code>DICT</code> 是静态 <code>ConfigurationDictionary</code>,阈值通过 <code>@Arg name</code> 从配置查(<code>:75-76</code>)——不同服务可以有不同 T 阈值。</p><h3 id="%40metricsextension-%E6%B3%A8%E8%A7%A3" tabindex="-1">@MetricsExtension 注解</h3><p>文件:<code>oap-server/server-core/.../analysis/MetricsExtension.java:32</code>。</p><pre><code class="language-java">@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)public @interface MetricsExtension {    boolean supportDownSampling();   // 是否支持降采样(小时/天)    boolean supportUpdate();         // 是否支持更新(而非只追加)    boolean timeRelativeID() default false;  // ID 是否含时间戳}</code></pre><blockquote><p><strong>三个字段含义</strong></p><ul><li><strong>supportDownSampling</strong>:true 表示该 metrics 能被 <code>toHour()/toDay()</code> 降采样。某些指标(如 max)降采样有意义,某些(如 percentile 的中间 dataset)降采样需特殊处��。</li><li><strong>supportUpdate</strong>:true 表示可被更新(适合&quot;当前值类&quot;指标如 max/min),false 表示纯追加。</li><li><strong>timeRelativeID</strong>:true 则实体 ID 带 timeBucket 前缀(如 <code>202211081200-serviceId</code>),典型用于 metadata 级 metrics 如 <code>ServiceTraffic</code>。</li></ul></blockquote><hr /><h2 id="datatable-%E5%A4%9A%E5%80%BC%E6%8C%87%E6%A0%87" tabindex="-1">DataTable 多值指标</h2><blockquote><p><strong>DataTable 是什么</strong><br />DataTable 是一个 <code>HashMap&lt;String,Long&gt;</code> 的包装,让一个 Metrics 实体在一个时间窗口内同时存多个值,是 percentile(多分位数)、histogram(多 bucket)、labelAvg/labelCount(多标签)的多值载体。</p></blockquote><p>文件:<code>oap-server/server-core/.../analysis/metrics/DataTable.java:41</code>。</p><pre><code class="language-java">public class DataTable implements StorageDataComplexObject&lt;DataTable&gt; {    private HashMap&lt;String, Long&gt; data;}</code></pre><h3 id="datatable-vs-%E5%8D%95%E5%80%BC-metrics" tabindex="-1">DataTable vs 单值 Metrics</h3><table><thead><tr><th>维度</th><th>单值 Metrics(CountMetrics/LongAvgMetrics…)</th><th>多值 DataTable(Percentile2/Histogram/Label*)</th></tr></thead><tbody><tr><td>一个时间窗口存</td><td>1 个最终值(<code>value</code> 列)</td><td>N 个值(一个 HashMap)</td></tr><tr><td>combine 中间态</td><td>几个 long 字段(summation/count)</td><td>一个 DataTable(dataset)</td></tr><tr><td>calculate</td><td>做除法/比较,产出单值</td><td>遍历 dataset 产出另一个 DataTable</td></tr><tr><td>存储列标注</td><td><code>@Column(dataType=COMMON_VALUE)</code></td><td><code>@Column(dataType=LABELED_VALUE/HISTOGRAM, multiIntValues=true)</code></td></tr><tr><td>OAL 返回类型</td><td>LongValueHolder/IntValueHolder</td><td>LabeledValueHolder/MultiIntValuesHolder</td></tr></tbody></table><h3 id="%E5%A4%9A%E5%80%BC%E6%8C%87%E6%A0%87%E6%80%8E%E4%B9%88%E5%AD%98%3A%E5%93%88%E5%B8%8C%E7%BB%93%E6%9E%84-%E2%86%92-%E5%AD%97%E7%AC%A6%E4%B8%B2" tabindex="-1">多值指标怎么存:哈希结构 → 字符串</h3><p>DataTable 不是直接把 HashMap 存库,而是序列化成字符串。原因在于底层存储(BanyanDB/ElasticSearch)的列式模型不接受 HashMap 类型,只能存标量或字符串。<code>toStorageData()</code>(<code>DataTable.java:134</code>)把整个 HashMap 压成一个字符串:</p><pre><code class="language-">key1=value1,key2=value2,key3=value3</code></pre><p>其中 <code>,</code> 是 <code>Const.ARRAY_SPLIT</code>,<code>=</code> 是 <code>Const.KEY_VALUE_SPLIT</code>(<code>DataTable.java:140,142</code>)。反序列化 <code>toObject()</code>(<code>:148</code>)按 <code>,</code> 切分再按最后一个 <code>=</code> 切分 key/value。</p><p>当 key 本身是 <code>DataLabel</code>(如 <code>{p=50}</code>)时,key 里也带 <code>=</code>,所以反序列化按<strong>最后一个</strong> <code>=</code> 切,而不是第一个。存储形如 <code>{p=50}=120|{p=75}=200|...</code>(<code>|</code> 是 <code>Const.ARRAY_PARSER_SPLIT</code>)。这正是 <code>PercentileMetrics2</code> 注释(<code>:37-39</code>)描述的格式。</p><p>这个设计的取舍:用字符串换通用性——任何后端都能存字符串,不必为每种多值结构定制列类型。代价是查询时要做一次解析,但多值指标本来就不是高频过滤字段,可接受。</p><h3 id="combine-%E8%AF%AD%E4%B9%89" tabindex="-1">combine 语义</h3><p>DataTable 提供多种合并方式(<code>DataTable.java:163-215</code>):</p><table><thead><tr><th>方法</th><th>语义</th><th>行号</th></tr></thead><tbody><tr><td><code>append(DataTable, maxDataSize)</code></td><td>同 key 值相加;key 不存在则新增(受 maxDataSize 限制)</td><td><code>:171</code></td></tr><tr><td><code>copyFrom(source)</code></td><td>等价于 append</td><td><code>:163</code></td></tr><tr><td><code>valueAccumulation(key, value, maxDataSize)</code></td><td>单 key 累加;超容量则丢弃新 key</td><td><code>:79</code></td></tr><tr><td><code>setMaxValue(DataTable)</code></td><td>同 key 取较大</td><td><code>:187</code></td></tr><tr><td><code>setMinValue(DataTable)</code></td><td>同 key 取较小</td><td><code>:202</code></td></tr><tr><td><code>sumOfValues()</code></td><td>所有 value 求和(Percentile 算 total 用)</td><td><code>:95</code></td></tr><tr><td><code>sortedKeys/sortedValues(Comparator)</code></td><td>按 key 排序取 keys/values(Percentile 排序找分位用)</td><td><code>:106-115</code></td></tr></tbody></table><p><strong>典型 combine 流程</strong>(以 PercentileMetrics2 为例):</p><ol><li>每来一条 Source:<code>dataset.valueAccumulation(value/precision, 1L)</code>——把延迟按精度分桶,桶计数 +1;</li><li>跨实例合并:<code>this.dataset.append(other.dataset)</code>——两个 DataTable 同桶累加;</li><li>calculate:<code>sumOfValues()</code> 得总数 → 按 key 排序 → 累加到每个 rank 的 <code>roof = total*p/100</code> 时记录该分位值 → 写入 <code>percentileValues</code> 这个新 DataTable。</li></ol><p><code>maxDataSize</code> 参数用于 LabelCount/LabelAvg 这类&quot;标签可能爆炸&quot;的场景,限制最多保留多少个 label(默认 50,见 <code>@DefaultValue(&quot;50&quot;)</code>)。没有这个限制,一个高基数标签会把单条 Metrics 的 HashMap 撑爆内存。</p><h3 id="%E5%9C%A8-oal-%E9%87%8C%E7%9A%84%E4%BD%BF%E7%94%A8" tabindex="-1">在 OAL 里的使用</h3><table><thead><tr><th>OAL 函数</th><th>对应类</th><th>DataTable 角色</th></tr></thead><tbody><tr><td><code>percentile2</code></td><td><code>PercentileMetrics2</code></td><td><code>dataset</code>(中间桶计数)+ <code>percentileValues</code>(结果 5 个分位值,key=<code>{p=50}</code>等)</td></tr><tr><td><code>percentile</code>(旧)</td><td><code>PercentileMetrics</code></td><td>同上但结果用 <code>int[]</code>(MultiIntValuesHolder),已废弃</td></tr><tr><td><code>histogram</code></td><td><code>HistogramMetrics</code></td><td><code>dataset</code>(每个 bucket 一项,key=<code>0/100/200...</code>,value=计数),无单值</td></tr><tr><td><code>labelCount</code></td><td><code>LabelCountMetrics</code></td><td><code>dataset</code>(中间按 label 计数)+ <code>value</code>(结果 <code>{n=label},count</code>)</td></tr><tr><td><code>labelAvg</code></td><td><code>LabelAvgMetrics</code></td><td><code>summation</code>+<code>count</code> 两个 DataTable + <code>value</code> 结果 DataTable</td></tr></tbody></table><p><strong>DataLabel</strong>(<code>metrics/DataLabel.java:28</code>)是 DataTable 的 key 之一:它继承 <code>LinkedHashMap&lt;String,String&gt;</code>,<code>toString()</code> 生成 <code>{k1=v1,k2=v2}</code> 格式(<code>:72-91</code>)。预定义两个 label 名:</p><ul><li><code>GENERAL_LABEL_NAME = &quot;_&quot;</code>(<code>DataLabel.java:29</code>):无标签时的通用 key;</li><li><code>PERCENTILE_LABEL_NAME = &quot;p&quot;</code>(<code>:30</code>):分位数标签 key。</li></ul><p><code>buildLabelIndex()</code>(<code>DataTable.java:217</code>)会把 DataTable 的 key 拆成倒排索引(label→keys),供按标签查询使用。</p><h3 id="%E5%AD%98%E5%82%A8%E5%88%97%E7%B1%BB%E5%9E%8B%E6%A0%87%E6%B3%A8" tabindex="-1">存储列类型标注</h3><p>DataTable 字段在 <code>@Column</code> 上用特殊 <code>dataType</code>,告诉存储层这是多值结构:</p><ul><li><code>Column.ValueDataType.LABELED_VALUE</code>:带标签的多值(Percentile2、LabelCount、LabelAvg 的 value 列);</li><li><code>Column.ValueDataType.HISTOGRAM</code>:直方图(HistogramMetrics 的 dataset,<code>HistogramMetrics.java:45</code>);</li><li><code>Column.ValueDataType.COMMON_VALUE</code>:普通单值。</li></ul><p>PercentileMetrics 旧版还用 <code>multiIntValues=true</code>(<code>PercentileMetrics.java:59</code>)表示多 int 值,新版改用 LABELED_VALUE + DataTable。</p><hr /><h2 id="%E6%95%B0%E6%8D%AE%E6%B5%81%E6%80%BB%E8%A7%88(source-%E2%86%92-dispatcher-%E2%86%92-metrics-%E2%86%92-storage)" tabindex="-1">数据流总览(Source → Dispatcher → Metrics → Storage)</h2><p>把上面三节串起来的完整数据流:</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/08-%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B%E4%B8%8EScope%E4%BD%93%E7%B3%BB.md_2.png" alt="图3" /></p><ol><li>Receiver 收到上游数据 → 构造某个 Source 子类(如 <code>Service</code>)→ 调 <code>SourceReceiver.receive()</code>;</li><li><code>Source.prepare()</code> 执行(补 ID 字段);</li><li><code>SourceDecorator</code> 执行(补 attr0~attr5 属性列);</li><li><code>SourceDispatcher.dispatch(source)</code>(接口在 <code>analysis/SourceDispatcher.java:32</code>)——分发器有两种:硬编码的(在 manual 包内)和 OAL 运行时按模板生成的(注释 <code>:24-28</code>)。它把 Source 字段映射成 Metrics 实例并触发 <code>combine</code>;</li><li>Metrics 进入 <code>MetricsStreamProcessor</code> 的 session cache,同 entityId + timeBucket 的会调 <code>Metrics.combine()</code>;</li><li>时间窗口结束/降采样时调 <code>Metrics.calculate()</code>,产出最终 <code>value</code> 或 <code>DataTable</code>;</li><li>按 <code>@MetricsExtension(supportDownSampling/supportUpdate)</code> 决定是否降采样、是否 update;</li><li>通过 <code>Metrics.id0()</code> 生成 <code>StorageID</code>(注释 <code>Metrics.java:162-167</code>,BanyanDB 用 <code>@BanyanDB.SeriesID</code> 替代)落库。</li></ol><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键类</th><th>路径</th></tr></thead><tbody><tr><td>Source 基类</td><td>ISource / Source</td><td><code>oap-server/server-core/.../source/ISource.java</code>、<code>Source.java</code></td></tr><tr><td>Source 全家族</td><td>56 个子类</td><td><code>oap-server/server-core/.../source/*.java</code></td></tr><tr><td>Scope 注册</td><td>DefaultScopeDefine</td><td><code>oap-server/server-core/.../source/DefaultScopeDefine.java</code></td></tr><tr><td></td><td>ScopeDeclaration</td><td>同目录 <code>ScopeDeclaration.java</code></td></tr><tr><td></td><td>ScopeDefaultColumn</td><td>同目录 <code>ScopeDefaultColumn.java</code></td></tr><tr><td>注解扫描</td><td>AnnotationScan</td><td><code>oap-server/server-core/.../annotation/AnnotationScan.java</code></td></tr><tr><td>SourceDecorator</td><td>ISourceDecorator / SourceDecoratorManager</td><td><code>oap-server/server-core/.../analysis/ISourceDecorator.java</code>、<code>SourceDecoratorManager.java</code></td></tr><tr><td>Metrics 基类</td><td>Metrics</td><td><code>oap-server/server-core/.../analysis/metrics/Metrics.java</code></td></tr><tr><td>Metrics 子类</td><td>18 个函数类</td><td><code>oap-server/server-core/.../analysis/metrics/*.java</code></td></tr><tr><td></td><td>MetricsExtension</td><td><code>oap-server/server-core/.../analysis/MetricsExtension.java</code></td></tr><tr><td>DataTable</td><td>DataTable / DataLabel</td><td><code>oap-server/server-core/.../analysis/metrics/DataTable.java</code>、<code>DataLabel.java</code></td></tr><tr><td>Dispatcher</td><td>SourceDispatcher</td><td><code>oap-server/server-core/.../analysis/SourceDispatcher.java</code></td></tr></tbody></table><blockquote><p><strong>未���认事项</strong></p><ul><li>非 <code>source/</code> 目录的 Scope 类(BROWSER_<em>、ZIPKIN_</em>、PROFILE_*、ALARM、EVENT 等)的 <code>@ScopeDeclaration</code> 类位于 <code>server-core</code> 其它子包(manual/generic/browser/zipkin/profile 等),注册机制与 <code>source/</code> 下的类完全相同,本篇未逐一展开其字段。</li><li>scopeId 7/14/15/16/77 在 <code>DefaultScopeDefine</code> 中无对应常量(被跳过),推测为历史废弃保留,源码无注释说明,未确认具体用途。</li></ul></blockquote><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5" tabindex="-1">接下来</h2><p>数据模型清楚了,这些数据到底怎么从 Agent 传到 OAP?协议层每条消息长什么样?详见 09-协议层与Agent采集。</p><blockquote><p><strong>心智模型</strong><br />把数据模型想成工厂的数据字典:Source 是原材料规格书(56 种原料,每种贴一个 Scope 标签,归到 8 个 Catalog 大类),Metrics 是加工配方表(18 种配方,每种 combine 算中间态、calculate 出成品),DataTable 是多格包装盒(一个盒子能同时装 P50/P75/P90/P95/P99 五个值)。OAL 脚本就是工艺单,从某类原材料(Source)取某字段,用某配方(Metrics 函数)加工,产出成品(value 或 DataTable)。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[八、Skywalking集群协调与动态配置]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/ba-skywalking-ji-qun-xie-diao-yu-dong-tai-pei-zhi" />
                <id>tag:https://lymboy.com,2025-11-19:ba-skywalking-ji-qun-xie-diao-yu-dong-tai-pei-zhi</id>
                <published>2025-11-19T21:53:07+08:00</published>
                <updated>2025-11-19T21:53:07+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E9%9B%86%E7%BE%A4%E5%8D%8F%E8%B0%83%E4%B8%8E%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE" tabindex="-1">集群协调与动态配置</h1><blockquote><p><strong>这一篇解决什么</strong><br />OAP 多节点怎么互相发现、怎么不重启改配置、怎么监控自己、怎么把数据外发、AI 基线怎么接、管理面怎么用。这篇收尾所有外围支撑子系统:集群协调、动态配置、Telemetry 自监控、Exporter、AI Pipeline、server-admin(含运行时规则热更新)、health-checker、server-tools、server-testing。读完能回答:OAP 集群怎么搭、配置怎么热更新、规则怎么不停机改。</p></blockquote><hr /><h2 id="%E9%9B%86%E7%BE%A4%E5%8D%8F%E8%B0%83-%C2%B7-server-cluster-plugin" tabindex="-1">集群协调 · server-cluster-plugin</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" tabindex="-1">解决什么问题</h3><p>多个 OAP 节点要按 hash 分片转发数据,所以得&quot;互相发现 + 维护 gRPC 连接&quot;。集群协调插件就干这件事——底层可以是 zk/etcd/consul/nacos 注册中心,也可以是 k8s 直接查 pod。</p><p>什么是集群协调?一句话:让一组对等的 OAP 节点各自知道&quot;集群里还有谁、地址多少&quot;,这样数据才能跨节点转发。OAP 的流处理是 hash 分片的,同一份数据要稳定落到同一个节点处理,节点之间互相建 gRPC 长连接按 hash 转发。</p><h3 id="clustermodule-%E5%AE%9A%E4%B9%89%E4%BA%86%E4%BB%80%E4%B9%88-service" tabindex="-1">ClusterModule 定义了什么 Service</h3><p><code>ClusterModule</code>(<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/cluster/ClusterModule.java:23</code>)只暴露三个 Service,见 <code>services()</code>(<code>:32</code>):</p><table><thead><tr><th>Service</th><th>方法</th><th>职责</th></tr></thead><tbody><tr><td><code>ClusterRegister</code></td><td><code>registerRemote(RemoteInstance)</code></td><td>节点启动时把自己注册到集群</td></tr><tr><td><code>ClusterNodesQuery</code></td><td><code>queryRemoteNodes()</code></td><td>返回当前集群所有 <code>RemoteInstance</code> 列表</td></tr><tr><td><code>ClusterCoordinator</code>(<code>ClusterCoordinator.java:27</code>)</td><td>—</td><td>抽象类,<strong>同时实现</strong> <code>ClusterRegister + ClusterNodesQuery + ClusterWatcherRegister</code>,三合一基类,维护 <code>List&lt;ClusterWatcher&gt;</code>,节点列表变化时 <code>notifyWatchers</code> 回调(<code>:40</code>)</td></tr></tbody></table><p>为什么把三个接口揉成一个抽象类?因为任意一种注册中心实现都必然要同时回答这三件事:我要注册、我要查列表、我要通知别人列表变了。拆成三个独立 provider 没意义,合在一起让插件作者一次实现全部,减少样板代码。不这么做的话,每个插件要写三个 provider 类,且三者共享的 <code>List&lt;ClusterWatcher&gt;</code> 状态没地方放。</p><blockquote><p><strong>RemoteInstance 是什么</strong><br /><code>RemoteInstance</code>(<code>RemoteInstance.java:25</code>)本质是 <code>Address</code>(host+port+self 标志)的薄包装,<code>Comparable</code> 按 address 排序(<code>:39</code>)。<code>Address.isSelf()</code> 区分&quot;自己&quot;和&quot;其他 OAP 节点&quot;,决定建本地 <code>SelfRemoteClient</code> 还是远端 <code>GRPCRemoteClient</code>。为什么要排序?后面 <code>RemoteClientManager</code> 要靠稳定顺序做 hash 分片——顺序一变,数据落点就变,流处理会乱。</p></blockquote><h3 id="%E5%90%84%E6%8F%92%E4%BB%B6%E5%AE%9E%E7%8E%B0" tabindex="-1">各插件实现</h3><p>所有插件继承 <code>ClusterCoordinator</code>,注册一个 <code>XxxCoordinator</code> 同时作为三个 Service 的实现。各 Provider 在 <code>prepare()</code> 中 <code>registerServiceImplementation(ClusterRegister.class, coordinator)</code> 等。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/07-%E9%9B%86%E7%BE%A4%E5%8D%8F%E8%B0%83%E4%B8%8E%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE.md_0.png" alt="图1" /></p><ul><li><strong>standalone</strong>(<code>StandaloneManager.java:30</code>):纯内存,<code>registerRemote</code>(<code>:35</code>)只存自己,<code>queryRemoteNodes</code>(<code>:41</code>)只返回自己。单机模式,不连任何注册中心。</li><li><strong>zookeeper</strong>(<code>ZookeeperCoordinator.java:48</code>):用 Curator 的 <code>ServiceDiscovery&lt;RemoteInstance&gt;</code>,节点注册为 <code>remote</code> 路径下一个 ServiceInstance,payload 是 RemoteInstance(<code>registerRemote</code> <code>:68</code>)。通过 <code>ServiceCache</code> 监听节点变化,<code>cacheChanged</code>(<code>:149</code>)事件触发 <code>notifyWatchers</code>(<code>:152</code>)。</li><li><strong>etcd</strong>(<code>EtcdCoordinator.java:55</code>):节点信息以 JSON 写入 etcd,key 为 <code>serviceName/host_port</code>,带 30s lease + keepAlive 续约(<code>registerRemote</code> <code>:144</code> 起 lease,<code>:156</code> 起 keepAlive)。<code>queryRemoteNodes</code>(<code>:93</code>)用前缀查询读所有 key。Watch 客户端监听 PUT/DELETE 事件,<code>EtcdEventListener.onNext</code>(<code>:212</code>)触发 <code>notifyWatchers</code>(<code>:221</code>)。</li><li><strong>consul</strong>:用 <code>ImmutableRegistration</code> 注册服务,<code>ServiceHealthCache</code> 监听健康实例变化。</li><li><strong>nacos</strong>:通过 Nacos naming 服务注册 + 订阅。</li><li><strong>kubernetes</strong>(<code>KubernetesCoordinator.java:52</code>):<strong>不主动注册</strong>,而是通过 <code>KubernetesLabelSelectorEndpointGroup</code>(基于 label selector)从 Kubernetes API 列出 collector pod,构造 <code>RemoteInstance</code> 列表。<code>port</code> 取自 <code>ConfigService.getGRPCPort()</code>(<code>createEndpointGroup</code> <code>:73</code>)。这是&quot;无注册中心&quot;的发现方式——k8s 本身就是注册中心。</li></ul><p>etcd 为什么用 lease + keepAlive 而不是直接写 key?因为节点崩溃时要让注册信息自动失效。lease 30s 到期没人续约,etcd 自动删 key,其他节点下次查询就看不到这个死节点。不这么做的话,死掉的 OAP 会永久留在集群列表里,数据一直往一个不存在的地址转发。</p><p>每个 Coordinator 都通过 <code>TelemetryModule</code> 的 <code>MetricsCreator</code> 创建 <code>HealthCheckMetrics</code>,在查询/注册失败时标记 unHealth(如 <code>EtcdCoordinator.java:198</code> 用 <code>createHealthCheckerGauge(&quot;cluster_etcd&quot;, ...)</code>),并经 <code>OAPNodeChecker.isHealth()</code> 做节点健康校验(<code>EtcdCoordinator.java:114</code>)。这些 <code>health_check_*</code> 指标后面会被 health-checker 聚合成&quot;健康分&quot;。</p><h3 id="%E4%B8%8E-remoteclientmanager-%E7%9A%84%E5%85%B3%E7%B3%BB" tabindex="-1">与 RemoteClientManager 的关系</h3><blockquote><p><strong>不要直接 find ClusterModule</strong><br />业务代码不该直接 <code>moduleManager.find(ClusterModule.NAME)</code> 拿集群服务。应该走 <code>CoreModule</code> 的 <code>RemoteClientManager</code>——它才是稳定的对外门面,内部再去找 <code>ClusterModule</code>。这样换集群实现(zookeeper/etcd/k8s)对业务透明。</p></blockquote><p><code>RemoteClientManager</code>(<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/remote/client/RemoteClientManager.java:59</code>)实现 <code>Service</code> 和 <code>ClusterWatcher</code> 两个接口(<code>:59 implements Service, ClusterWatcher</code>):</p><ol><li><code>start()</code>(<code>:95</code>)起定时任务,每 10 秒 <code>refresh()</code>(<code>:99</code> 的 <code>scheduleWithFixedDelay</code>,参数 <code>1, 10, SECONDS</code>)。</li><li><code>refresh()</code>(<code>:105</code>)通过 <code>moduleDefineHolder.find(ClusterModule.NAME).provider().getService(ClusterNodesQuery.class)</code> 拿集群节点列表(<code>:107</code>)——这正是上面提示的关键:业务代码通过 CoreModule 拿 RemoteClientManager,RemoteClientManager 内部才 find ClusterModule。</li><li><code>refresh(List&lt;RemoteInstance&gt;)</code>(<code>:118</code>,synchronized)去重(<code>distinct</code> <code>:179</code>)、排序(<code>Collections.sort</code> <code>:134</code>)、与现有 client 比对(<code>compare</code> <code>:142</code>);若变化则 <code>reBuildRemoteClients</code>(<code>:146</code>)。</li><li><code>reBuildRemoteClients</code>(<code>:202</code>):对每个地址,self 用 <code>SelfRemoteClient</code>(<code>:239</code>),否则建 <code>GRPCRemoteClient</code> 并 <code>connect()</code>(<code>:243-244</code>);消失的 client 关闭(<code>:261</code>)。最后排序保证稳定顺序(<code>:252</code> 注释 <code>//for stable ordering for rolling selector</code>)。</li><li>同时实现 <code>ClusterWatcher.onClusterNodesChanged</code>(<code>:278</code>),所以支持两种刷新:定时轮询 + 事件推送(watcher 模式)。</li></ol><p>什么是 Watcher 机制?就是&quot;被观察者状态一变,主动通知所有观察者&quot;。<code>ClusterCoordinator</code> 维护一个 <code>List&lt;ClusterWatcher&gt;</code>(<code>:28</code>),节点列表变化时 <code>notifyWatchers</code> 遍历回调每个 watcher 的 <code>onClusterNodesChanged</code>(<code>:45</code>)。<code>RemoteClientManager</code> 注册自己为 watcher,所以注册中心一变,它立刻被通知重建连接,不用等 10 秒轮询。两条腿走路,事件快、轮询兜底。</p><p><code>refresh</code> 还会调 <code>ServerStatusService.rebalancedCluster()</code>(<code>:147-150</code>)通知集群拓扑变化,让 OAP 进入 rebalanced 状态(此期间某些写入行为会调整)。</p><p>详见 04-流处理引擎-Worker链 的 RemoteClientManager 节。</p><hr /><h2 id="%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE-%C2%B7-server-configuration" tabindex="-1">动态配置 · server-configuration</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-1" tabindex="-1">解决什么问题</h3><p>让 OAP 不重启就能从外部配置中心(Apollo/Nacos/ZK/etcd/Consul/K8s ConfigMap/gRPC)拉取并热更新某些配置项,比如采样率、告警阈值、log analyzer 规则开关。</p><p>什么是动态配置中心?一个独立于 OAP 的存储(Apollo/Nacos 等),运维改了里面的值,OAP 运行时感知到并生效,不用重启。传统配置写在 <code>application.yml</code>,改完要重启进程;动态配置把&quot;易变的运营参数&quot;抽出来,让 OAP 边跑边读。</p><h3 id="%E6%8A%BD%E8%B1%A1" tabindex="-1">抽象</h3><ul><li><code>ConfigurationModule</code>(<code>oap-server/server-configuration/configuration-api/src/main/java/org/apache/skywalking/oap/server/configuration/api/ConfigurationModule.java:30</code>)只暴露一个 Service:<code>DynamicConfigurationService</code>(<code>:39</code>)。</li><li><code>DynamicConfigurationService</code>(<code>DynamicConfigurationService.java:24</code>)只有一个方法 <code>registerConfigChangeWatcher(ConfigChangeWatcher)</code>——模块启动时把自己的 watcher 注册进去。</li><li><code>AbstractConfigurationProvider</code>(<code>:25</code> 起)是所有 provider 基类。<code>prepare()</code> 调子类 <code>initConfigReader()</code> 得到一个 <code>ConfigWatcherRegister</code>(<code>:40</code>),注册为 <code>DynamicConfigurationService</code> 实现(<code>:41</code>);<code>notifyAfterCompleted()</code> 调 <code>configWatcherRegister.start()</code>(<code>:53</code>)。</li></ul><h3 id="configchangewatcher-%2F-configsync-%E6%9C%BA%E5%88%B6" tabindex="-1">ConfigChangeWatcher / ConfigSync 机制</h3><p><code>ConfigChangeWatcher</code>(<code>ConfigChangeWatcher.java:30</code>)抽象类,字段 <code>module / provider / itemName / watchType(SINGLE|GROUP)</code>(<code>:31-34</code>),抽象方法 <code>notify(ConfigChangeEvent)</code>(<code>:48</code>)和 <code>value()</code>(<code>:53</code>)。事件类型 <code>ADD/MODIFY/DELETE</code>(<code>:72</code>)。<code>GroupConfigChangeWatcher</code> 用于一组相关配置项(如多条告警规则)。</p><p><code>ConfigWatcherRegister</code>(<code>ConfigWatcherRegister.java:31</code>)抽象基类,<code>notifySingleValue</code>(<code>:35</code>)是核心逻辑:比较新值与 <code>watcher.value()</code>,仅在变化时回调 <code>notify</code>,DELETE 时传 null(<code>:41</code>)。<code>WatcherHolder</code> 的 key 格式为 <code>module.provider.itemName</code>(<code>:125-128</code>)——这就是配置项命名约定,各模块按这个三元组注册自己的 watcher。</p><p>为什么要&quot;比较再回调&quot;?因为配置中心的 watch 事件可能重复触发(同一个值也会回调),直接通知 watcher 会让他反复重载配置,代价大且可能闪断。比一下 <code>value()</code>,没变就跳过。</p><p>两种同步风格:</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/07-%E9%9B%86%E7%BE%A4%E5%8D%8F%E8%B0%83%E4%B8%8E%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE.md_1.png" alt="图2" /></p><ul><li><strong><code>FetchingConfigWatcherRegister</code></strong>(<code>FetchingConfigWatcherRegister.java:36</code>):定时轮询。<code>start()</code>(<code>:79</code>)用虚拟线程定时器每 <code>syncPeriod</code>(默认 60s,构造器 <code>:46</code>)调 <code>configSync()</code>(<code>:99</code>),子类实现 <code>readConfig(keys)</code>(<code>:155</code>)/<code>readGroupConfig(keys)</code>(<code>:157</code>)拉配置,再逐项 <code>notifySingleValue</code>(<code>:120</code>)。<code>isStarted</code> 标志(<code>:42</code>)保证启动后不能再注册新 watcher(<code>:55-56</code> 抛 <code>IllegalStateException</code>)——启动后再加 watcher 会被轮询漏掉,行为不确定,干脆禁止。</li><li><strong><code>ListeningConfigWatcherRegister</code></strong>(<code>ListeningConfigWatcherRegister.java:27</code>):事件监听。<code>registerConfigChangeWatcher</code>(<code>:30</code>)直接调子类 <code>startListening(holder, callback)</code>(<code>:31</code>),配置中心变化时回调 <code>onSingleValueChanged</code>(<code>:33</code>)/<code>onGroupValuesChanged</code>(<code>:38</code>)。<code>start()</code> 是空的(<code>:46</code>)——没有定时器要起。</li></ul><h3 id="%E5%90%84-provider-%E5%AE%9E%E7%8E%B0" tabindex="-1">各 provider 实现</h3><table><thead><tr><th>Provider</th><th>基类</th><th>机制</th></tr></thead><tbody><tr><td>apollo</td><td>ConfigWatcherRegister</td><td>Apollo 监听</td></tr><tr><td>consul</td><td>—</td><td>Consul KV watch</td></tr><tr><td>etcd</td><td>—</td><td>etcd KV watch</td></tr><tr><td>nacos</td><td>—</td><td>Nacos config listener</td></tr><tr><td>zookeeper</td><td>—</td><td>ZK node watch</td></tr><tr><td>k8s-configmap(<code>ConfigurationConfigmapInformer</code>)</td><td>Fetching</td><td>fabric8 informer 列 ConfigMap(按 labelSelector 过滤),定时读 <code>configMapData()</code></td></tr><tr><td>grpc(<code>GRPCConfigWatcherRegister</code>)</td><td>Fetching</td><td>见下</td></tr></tbody></table><h3 id="grpc-configuration-sync-%C2%B7-oap-%E4%BD%9C%E4%B8%BA%E9%85%8D%E7%BD%AE%E5%AE%A2%E6%88%B7%E7%AB%AF" tabindex="-1">grpc-configuration-sync · OAP 作为配置客户端</h3><blockquote><p><strong>重要澄清</strong><br /><code>grpc-configuration-sync</code> 模块<strong>只是客户端</strong>(OAP 作为 gRPC client 去拉配置),不是服务端。</p><p><code>GRPCConfigWatcherRegister.java:35</code> 继承 <code>FetchingConfigWatcherRegister</code>,构造器(<code>:41</code>)用 <code>ConfigurationServiceGrpc.newBlockingStub</code> 建到远端的 stub。<code>readConfig</code>(<code>:52</code>)调远端 <code>call()</code>(<code>:60</code>),用 UUID 做变更检测:若返回 UUID 与本地一致则返回 <code>Optional.empty()</code> 表示无变化(<code>:62-65</code>)。UUID 一致就跳过,省掉反序列化和逐项比对的代价。</p><p>proto 定义在 <code>oap-server/server-configuration/grpc-configuration-sync/src/main/proto/configuration-service.proto</code>,定义了 <code>ConfigurationService.call / callGroup</code>。</p><p><strong>服务端实现不在本仓库</strong>。全仓搜 <code>ConfigurationServiceImplBase</code> 只命中测试代码 <code>oap-server/server-configuration/grpc-configuration-sync/src/test/java/org/apache/skywalking/oap/server/configuration/grpc/MockGRPCConfigService.java</code>,生产代码无命中。因此 <strong>gRPC 配置服务端需要单独部署</strong>(可能是 SkyWalking 社区的独立 config-server 项目)。<strong>未确认</strong>该服务端是否在 SkyWalking 其它仓库——本仓库源码无法证实。</p></blockquote><hr /><h2 id="telemetry-%C2%B7-oap-%E8%87%AA%E7%9B%91%E6%8E%A7" tabindex="-1">Telemetry · OAP 自监控</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-2" tabindex="-1">解决什么问题</h3><p>让 OAP 把自己的运行时指标(内存、线程、各模块处理量、集群健康)以 Prometheus 格式暴露,供外部 Prometheus 抓取,实现&quot;监控监控系统本身&quot;。</p><h3 id="%E6%8A%BD%E8%B1%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0" tabindex="-1">抽象与实现</h3><ul><li><code>TelemetryModule</code>(<code>oap-server/server-telemetry/telemetry-api/src/main/java/org/apache/skywalking/oap/server/telemetry/TelemetryModule.java:28</code>)暴露 <code>MetricsCreator</code> 和 <code>MetricsCollector</code> 两个 Service(<code>:37-39</code>)。</li><li><code>MetricsCreator</code> 是工厂接口,创建 <code>createCounter</code>/<code>createGauge</code>/<code>createHistogramMetric</code>/<code>createHealthCheckerGauge</code>。<code>HEALTH_METRIC_PREFIX = &quot;health_check_&quot;</code>(<code>MetricsCreator.java:31</code>),健康检查指标名都加这个前缀。<code>createHealthCheckerGauge</code>(<code>:53</code>)默认实现就是把名字拼上前缀再建 gauge(<code>:55</code>),<code>isHealthCheckerMetrics</code>(<code>:65</code>)用前缀判断指标是否属于健康检查,<code>extractModuleName</code>(<code>:73</code>)去前缀还原模块名。</li><li>有 <code>none</code> 实现(<code>NoneTelemetryProvider</code>/<code>MetricsCreatorNoop</code>)做无操作占位——不想要自监控时配 <code>none</code>,所有 create 方法返回空实现,业务代码不用改。</li><li><code>PrometheusTelemetryProvider</code>(<code>oap-server/server-telemetry/telemetry-prometheus/src/main/java/org/apache/skywalking/oap/server/telemetry/prometheus/PrometheusTelemetryProvider.java:34</code>):<code>prepare()</code>(<code>:63</code>)注册 <code>PrometheusMetricsCreator</code>/<code>PrometheusMetricsCollector</code>(<code>:64-65</code>),启动 <code>HttpServer</code>(<code>:67</code>),<code>DefaultExports.initialize()</code>(<code>:72</code>)暴露 JVM hotspot 指标。</li><li>HTTP 暴露:<code>HttpServerHandler</code>(<code>oap-server/server-telemetry/telemetry-prometheus/src/main/java/org/apache/skywalking/oap/server/telemetry/prometheus/httpserver/HttpServerHandler.java:51</code> 持有 <code>CollectorRegistry.defaultRegistry</code>)基于 Netty,请求到来时用 <code>io.prometheus.client.exporter.common.TextFormat.write004</code>(<code>:68</code>)把 <code>CollectorRegistry.defaultRegistry</code> 的所有指标序列化成 Prometheus text format 返回。</li></ul><blockquote><p><strong>集群插件的 HealthCheckMetrics</strong><br />集群插件里的 <code>HealthCheckMetrics</code>(如 <code>cluster_zookeeper</code>)也通过 Telemetry 的 <code>MetricsCreator.createHealthCheckerGauge</code> 创建(见 <code>ZookeeperCoordinator.java:132</code>),这些 <code>health_check_*</code> 指标会被 health-checker 子系统消费。</p></blockquote><hr /><h2 id="exporter-%C2%B7-%E6%95%B0%E6%8D%AE%E5%A4%96%E5%8F%91" tabindex="-1">Exporter · 数据外发</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-3" tabindex="-1">解决什么问题</h3><p>把 OAP 内部计算/聚合后的指标和 trace/log,按订阅关系重新导出到外部系统(如 OTel collector、Kafka),让 SkyWalking 数据流出到第三方链路。</p><blockquote><p><strong>Exporter vs Telemetry</strong><br />Telemetry 暴露的是&quot;OAP 自己有多忙&quot;(自监控,面向 Prometheus),Exporter 暴露的是&quot;OAP 算出来的业务指标/trace&quot;(业务数据外发,面向 OTel collector/Kafka)。前者自监控,后者数据外发。且指标导出是<strong>订阅驱动</strong>——外部系统告诉 OAP&quot;我要 metric X 的增量&quot;,OAP 才发。</p></blockquote><h3 id="%E6%A8%A1%E5%9D%97%E4%B8%8E%E5%AE%9E%E7%8E%B0" tabindex="-1">模块与实现</h3><ul><li><code>ExporterModule</code>(<code>oap-server/server-core/.../exporter/ExporterModule.java:20</code>)暴露三个 Service:<code>MetricValuesExportService</code>、<code>TraceExportService</code>、<code>LogExportService</code>。这是&quot;消费方契约定义在 core、实现在 exporter 模块&quot;的典型模式——core 定接口,业务代码调接口,具体发到 gRPC 还是 Kafka 由 provider 决定。</li><li><code>ExporterProvider</code>(<code>oap-server/exporter/src/main/java/org/apache/skywalking/oap/server/exporter/provider/ExporterProvider.java:34</code>):<code>prepare()</code>(<code>:66</code>)注册三个导出器:<code>GRPCMetricsExporter</code>(实现 <code>MetricValuesExportService</code>,<code>:70</code>)、<code>KafkaTraceExporter</code>(实现 <code>TraceExportService</code>,<code>:71</code>)、<code>KafkaLogExporter</code>(实现 <code>LogExportService</code>,<code>:72</code>)。<code>start()</code>(<code>:76</code>)按开关分别 <code>start()</code>,<code>notifyAfterCompleted()</code>(<code>:89</code>)先拉一次订阅列表。</li></ul><h3 id="grpcmetricsexporter-%C2%B7-%E8%AE%A2%E9%98%85%E6%A8%A1%E5%BC%8F" tabindex="-1">GRPCMetricsExporter · 订阅模式</h3><p><code>GRPCMetricsExporter</code>(<code>oap-server/exporter/src/main/java/org/apache/skywalking/oap/server/exporter/provider/grpc/GRPCMetricsExporter.java:60</code> 左右):</p><ul><li>连接远端 <code>MetricExportService</code> gRPC 服务,本地起 <code>BatchQueue</code> 缓冲(<code>:82</code> 起 <code>BatchQueueManager.create</code>)。</li><li><strong>订阅模式</strong>:导出不是无脑全发。<code>export(event)</code>(<code>:99</code>)里,订阅列表为空且事件是 INCREMENT 时兜底全发(<code>:103</code>);否则只发匹配订阅的指标(<code>:106-111</code> 比对 <code>metricName</code> + <code>eventType</code>)。<code>fetchSubscriptionList()</code>(<code>:126</code>)硬编码 30s 周期(<code>FETCH_SUBSCRIPTION_PERIOD = 30_000</code>,<code>:65</code>)拉订阅——用 <code>lastFetchTimestamp</code> + <code>ReentrantLock</code> 做节流,避免每次 export 都打 RPC。</li><li>Kafka trace/log 导出器走 Kafka producer 把 trace/log 推到 Kafka topic。</li></ul><p>为什么订阅驱动?如果 OAP 把所有指标无脑推给外部,外部不想要的就全是噪声流量。订阅让外部系统声明&quot;只要这些&quot;,OAP 只发这些,带宽和外部处理压力都降下来。</p><hr /><h2 id="ai-pipeline-%C2%B7-ai-%E8%83%BD%E5%8A%9B%E6%8E%A5%E5%85%A5%E5%B1%82" tabindex="-1">AI Pipeline · AI 能力接入层</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-4" tabindex="-1">解决什么问题</h3><p>把 OAP 与外部 AI 服务对接,提供&quot;指标预测基线(用于异常检测告警)&quot;和&quot;HTTP URI 模式归一化识别&quot;两类 AI 能力的接入层。</p><h3 id="%E6%A8%A1%E5%9D%97%E5%AE%9A%E4%B9%89" tabindex="-1">模块定义</h3><ul><li><code>AIPipelineModule</code>(<code>oap-server/ai-pipeline/src/main/java/org/apache/skywalking/oap/server/ai/pipeline/AIPipelineModule.java:24</code>)只暴露一个 Service:<code>BaselineQueryService</code>(<code>:33</code>)。</li><li><code>AIPipelineProvider</code>(<code>AIPipelineProvider.java:32</code>):<code>prepare()</code>(<code>:61</code>)注册 <code>BaselineQueryServiceImpl</code>(连接外部 baseline server);<code>start()</code>(<code>:69</code>)构造 <code>HttpUriRecognitionService</code> 并注入 <code>EndpointNameGroupService.startHttpUriRecognitionSvr</code>(<code>:74-76</code>)。</li><li><code>requiredModules()</code>(<code>:84</code>)只依赖 <code>CoreModule</code>。</li></ul><h3 id="baselinequeryservice-%C2%B7-%E9%A2%84%E6%B5%8B%E5%9F%BA%E7%BA%BF" tabindex="-1">BaselineQueryService · 预测基线</h3><ul><li><code>BaselineQueryService</code>(<code>.../services/BaselineQueryService.java:18</code>)接口方法:<code>querySupportedMetrics</code>、<code>queryPredictMetrics(serviceMetrics, start, end)</code>、<code>queryPredictMetricsFromCache(serviceName, timeBucketHour)</code>。</li><li><code>BaselineQueryServiceImpl</code>(<code>BaselineQueryServiceImpl.java:51</code>)通过 gRPC stub <code>AlarmBaselineServiceGrpc</code>(<code>:52</code>)调用外部 baseline 服务(包名 <code>org.apache.skywalking.apm.baseline.v3</code>)。构造器(<code>:55</code>)用 Guava Cache 缓存预测结果,1 小时过期(<code>expireAfterAccess(1, HOURS)</code>,<code>:57</code>)。未配置地址则 stub 为 null(<code>:59-61</code>),方法返回空并告警(<code>:70-72</code> 等多处 <code>stub == null</code> 判空)。</li><li><code>PredictServiceMetrics</code> 预测值结构,含 <code>PredictSingleValue(value, upperValue, lowerValue)</code>(<code>BaselineQueryServiceImpl.java:173-177</code>):预测基线值 + 上下界区间,用于判断实际值是否越界(异常检测)。</li><li><code>queryPredictMetricsFromCache</code>(<code>:95</code>)的逻辑:缓存没有就按&quot;过去 24h ~ 未来 24h&quot;窗口(<code>:114-117</code>)向外部服务批量查预测,塞回缓存(<code>:135-141</code>),再按 <code>(timeBucket, serviceName)</code> 取(<code>:144</code>)。批量预取而不是逐次查,降低对外部 AI 服务的 QPS。</li></ul><h3 id="httpurirecognitionservice-%C2%B7-uri-%E6%A8%A1%E5%BC%8F%E8%AF%86%E5%88%AB" tabindex="-1">HttpUriRecognitionService · URI 模式识别</h3><p><code>HttpUriRecognitionService</code>(<code>HttpUriRecognitionService.java:37</code> 实现 <code>HttpUriRecognition</code>)通过 gRPC 调用外部 URI 识别服务,<code>fetchAllPatterns(service)</code>(<code>:57</code>)拉取归一化后的 URI 模式。这是把 <code>/api/user/12345</code> 和 <code>/api/user/99999</code> 归并成 <code>/api/user/:id</code> 的能力,由外部 AI 模型完成,OAP 调用并把结果用于 endpoint 分组。</p><p>为什么要归一化 URI?不归一化的话,每个动态 ID 都是一个独立 endpoint,指标维度爆炸,没法做有意义的聚合分析。归一化后同一类接口合并成一条曲线,异常检测才有意义。SkyWalking 自己不做归一化(需要训练模型),外包给 AI 服务。</p><h3 id="%E4%B8%8E-mqe-baseline-%E6%93%8D%E4%BD%9C%E7%9A%84%E5%85%B3%E7%B3%BB" tabindex="-1">与 MQE baseline 操作的关系</h3><p>已从源码确认的关键链路:</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/07-%E9%9B%86%E7%BE%A4%E5%8D%8F%E8%B0%83%E4%B8%8E%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE.md_2.png" alt="图3" /></p><ul><li><code>MQEVisitorBase.java:42-43</code> import 了 <code>AIPipelineModule</code> 和 <code>BaselineQueryService</code>。</li><li><code>MQEVisitorBase.java:78-91</code> 持有 <code>BaselineQueryService</code>,通过 <code>moduleManager.find(AIPipelineModule.NAME).provider().getService(BaselineQueryService.class)</code> 懒加载(<code>getBaselineQueryService</code> <code>:87</code>)。懒加载是因为 MQE 模块不一定依赖 AI Pipeline,没装 AI Pipeline 时不能在构造期就 find,否则启动报错。</li><li><code>MQEVisitorBase.java:502</code> <code>visitBaselineOP</code> —— MQE 表达式中的 <code>baseline()</code> 操作符调用此方法,进而调 <code>queryBaseline</code>(<code>:557</code>)/<code>queryLabeledBaseline</code>(<code>:582</code>),从 <code>BaselineQueryService.queryPredictMetricsFromCache</code> 拿预测基线。</li></ul><blockquote><p><strong>baseline 与异常检测</strong><br />MQE 支持在 UI 查询里写 <code>metric + baseline</code> 之类运算。<code>baseline()</code> 操作符向 AI Pipeline 背后的外部预测服务请求&quot;这个指标在此时此刻应该是什么值、正常波动区间多大&quot;,然后与实际值比较——超出上下界就可能是异常。AI Pipeline 模块本身不做预测,只做&quot;OAP ↔ 外部 AI 服务&quot;的 gRPC 桥接和缓存。</p></blockquote><blockquote><p><strong>未确认</strong><br />baseline server 和 URI recognition server 的具体实现(AI 模型)不在本仓库,本仓库只含客户端。仓内 <code>ai-pipeline/src/test/java/BaselineQueryServer.java</code> 是测试用的 mock server。</p></blockquote><hr /><h2 id="server-admin-%C2%B7-%E7%AE%A1%E7%90%86%E9%9D%A2" tabindex="-1">server-admin · 管理面</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-5" tabindex="-1">解决什么问题</h3><p>提供一组默认关闭、需手动开启的管理 REST/gRPC 端口(默认 HTTP 17128 / gRPC 17129,区别于面向 agent 的 11800),承载 DSL 调试、运行时规则热更新、状态检查、UI 模版管理等运维操作。</p><h3 id="admin-server-%C2%B7-%E5%85%B1%E4%BA%AB%E7%AE%A1%E7%90%86%E4%B8%BB%E6%9C%BA" tabindex="-1">admin-server · 共享管理主机</h3><p><code>AdminServerModule</code>(<code>oap-server/server-admin/admin-server/src/main/java/org/apache/skywalking/oap/server/admin/server/module/AdminServerModule.java:40</code>)通过 <code>SW_ADMIN_SERVER=default</code> 开启。<code>services()</code>(<code>:48</code>)暴露 <code>HTTPHandlerRegister</code>、<code>GRPCHandlerRegister</code>、<code>AdminClusterChannelManager</code>(admin-internal gRPC 基础设施,17129 端口)。注释(<code>:51-55</code>)明确把 admin-internal gRPC 与 agent/cluster gRPC 11800 分开,避免特权管理 RPC 与 agent 遥测共享爆炸半径——一个 agent 打爆 gRPC 端口不会拖垮 runtime-rule 的热更新通道。</p><p><code>AdminServerModuleProvider</code> 是<strong>宿主</strong>,本身不提供业务端点,各 feature 模块在 <code>start()</code> 阶段 <code>find(AdminServerModule.NAME).provider().getService(HTTPHandlerRegister.class).addHandler(...)</code> 注册自己的 handler。</p><blockquote><p><strong>安全提示</strong><br />admin 端口<strong>无内置鉴权</strong>(源码注释 <code>:34-38</code> 反复强调),运维必须用 IP 白名单 + 鉴权反代保护,<strong>绝不暴露公网</strong>。注释直接指向 <code>docs/en/setup/backend/admin-api/readme.md</code> 的安全说明。</p></blockquote><h3 id="dsl-debugging-%C2%B7-dsl-%E8%B0%83%E8%AF%95" tabindex="-1">dsl-debugging · DSL 调试</h3><p>在线调试 MAL/LAL/OAL DSL 的 REST 面。<code>RuntimeOalRestHandler</code>(OAL)和 <code>DSLDebuggingRestHandler</code>(MAL/LAL 通用):提交一段 DSL 脚本,OAP 编译并试跑,返回结果/错误,便于规则上线前验证。</p><h3 id="runtime-rule-%C2%B7-%E8%BF%90%E8%A1%8C%E6%97%B6%E8%A7%84%E5%88%99%E7%83%AD%E6%9B%B4%E6%96%B0" tabindex="-1">runtime-rule · 运行时规则热更新</h3><p>这是 server-admin 中<strong>最重</strong>的子模块。模块 <code>RuntimeRuleModule</code>(<code>oap-server/server-admin/runtime-rule/.../RuntimeRuleModule.java:30</code>,NAME=<code>receiver-runtime-rule</code>,<code>:31</code>),默认关闭,需 <code>SW_RECEIVER_RUNTIME_RULE=default</code> 且 admin-server 开启才加载。</p><p>REST 路由(<code>RuntimeRuleRestHandler</code>):<code>/runtime/rule/addOrUpdate</code>、<code>/inactivate</code>、<code>/delete</code>、<code>/list</code>、<code>/status</code>、<code>/dump</code>,以及按 DSL 分的 <code>/runtime/mal/otel/*</code>、<code>/runtime/mal/log/*</code>、<code>/runtime/lal/*</code>。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/07-%E9%9B%86%E7%BE%A4%E5%8D%8F%E8%B0%83%E4%B8%8E%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE.md_3.png" alt="图4" /></p><p>三层架构(<code>RuntimeRuleModuleProvider</code> 注释详述):</p><ul><li><strong>Scheduler</strong>(<code>DSLManager</code> + REST handler):DSL 无关。负责锁获取、集群 Suspend/Resume RPC 扇出、DAO 持久化、跨文件所有权校验��tick 调度(默认 30s + 启动同步 tick)、自愈、classloader 回收、alarm-reset 分发。</li><li><strong>Orchestrators</strong>:<code>DSLRuntimeApply</code>(apply 流程:NEW/FILTER_ONLY/STRUCTURAL 分类 → 编译 → fireSchemaChanges → verify → commit/rollback);<code>DSLRuntimeUnregister</code>(下线流程)。</li><li><strong>Engines</strong>(<code>RuleEngine</code> SPI):<code>MalRuleEngine</code>、<code>LalRuleEngine</code>(未来 <code>OalRuleEngine</code>)。每个引擎实现 classify/compile/fireSchemaChanges/verify/commit/rollback/unregister,拥有 DSL 相关的 Javassist 类生成、DDL 探测、classloader 退役等。<strong>新增 DSL 只需实现 SPI + <code>RuleEngineRegistry.register</code>,不动 scheduler/orchestrator</strong>——这是 SPI 分层的意义:把&quot;DSL 怎么编译&quot;这种易变的逻辑隔离在引擎里,scheduler 只管调度和一致性,两者解耦。</li></ul><p>集群协调:<code>RuntimeRuleClusterServiceImpl</code>(集群 gRPC 总线,收 Suspend/Resume/Forward RPC)+ <code>RuntimeRuleClusterClient</code>(广播/转发到 main 节点)+ <code>MainRouter</code>(main/peer 路由)。状态机:<code>DSLRuntimeState</code>、<code>ApplyStatus</code>、<code>ApplyPhase</code>、<code>SchemaApplyCoordinator</code>、<code>AppliedRuleScriptLock</code>。<code>reconcile</code> 包下 <code>RuleSync</code>、<code>SuspendResumeCoordinator</code>、<code>StructuralCommitCoordinator</code>、<code>PendingApplyCommit</code> 做最终一致收敛。<code>layer</code> 包做运行时分层与冲突检测。<code>extension/DbOverrideRuntimeRuleResolver</code> 让 DB 覆盖规则。</p><blockquote><p><strong>热更新规则</strong><br />MAL/LAL 是 SkyWalking 用 DSL 写的&quot;指标加工规则/日志分析规则&quot;。传统方式改规则要重启 OAP。runtime-rule 让你通过 admin REST 提交新规则文件,OAP 在线编译、生成新的处理类、平滑切换数据流、必要时挂起旧流再恢复新流,并跨集群节点同步状态,做到不停机改规则。复杂度集中在&quot;如何在不丢数据、不破坏 schema 的前提下切换运行中的处理流水线&quot;,所以有锁、状态机、两阶段提交、集群扇出这一大套机制。</p><p>为什么不直接热替换?因为运行中的流处理正在消费 Kafka/接收数据,直接切类会丢在途数据。挂起旧流(停止拉新数据但处理完在途的)→ 切换 → 恢复新流,这套 Suspend/Resume 才不丢。两阶段提交保证集群所有节点要么都切成功、要么都回滚,不会出现一半节点跑新规则一半跑旧规则的不一致。</p></blockquote><h3 id="inspect-%C2%B7-%E6%95%B0%E6%8D%AE%E6%A3%80%E8%A7%86" tabindex="-1">inspect · 数据检视</h3><p>admin-only。<code>InspectRestHandler</code> 暴露 <code>GET /inspect/metrics</code>(指标目录)和 <code>GET /inspect/entities</code>(某指标在时间范围内的实体列表,解码成 MQE-ready 形式)。entity 枚举扫描会与用户查询竞争存储资源,故放在私有 admin 端口。</p><h3 id="status-%C2%B7-%E7%8A%B6%E6%80%81%E6%9F%A5%E8%AF%A2" tabindex="-1">status · 状态查询</h3><p>替代旧 status-query-plugin。暴露 <code>AlarmStatusQueryService</code>。<code>StatusModuleProvider</code> 注册:<code>DebuggingHTTPHandler</code>(per-query 调试 trace)、<code>TTLConfigQueryHandler</code>(<code>/status/config/ttl</code>,也挂公共 REST 端口供生态工具发现 TTL)、<code>ClusterStatusQueryHandler</code>、<code>AlarmStatusQueryHandler</code>。</p><h3 id="ui-management-%C2%B7-ui-%E6%A8%A1%E6%9D%BF%E7%AE%A1%E7%90%86" tabindex="-1">ui-management · UI 模板管理</h3><p>admin-only。<code>UIManagementRestHandler</code> 提供 dashboard 模板 CRUD:<code>GET/POST/PUT /ui-management/templates</code>、<code>POST /ui-management/templates/{id}/disable</code>。转发到 CoreModule 的 <code>UITemplateManagementService</code>。注释明确:11.0.0+ 起 sidebar 菜单不再由 OAP 提供,改由 Horizon UI 自带 bundle。</p><hr /><h2 id="health-checker-%C2%B7-%E5%81%A5%E5%BA%B7%E8%81%9A%E5%90%88" tabindex="-1">health-checker · 健康聚合</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-6" tabindex="-1">解决什么问题</h3><p>把 Telemetry 暴露的 <code>health_check_*</code> 指标聚合成一个&quot;健康分数&quot;并通过 HTTP 暴露,供 LB/外部探针判断 OAP 是否该接流量。</p><h3 id="%E5%AE%9E%E7%8E%B0" tabindex="-1">实现</h3><ul><li><code>HealthCheckerModule</code>(<code>oap-server/server-health-checker/src/main/java/org/apache/skywalking/oap/server/health/checker/module/HealthCheckerModule.java</code>)暴露 <code>HealthQueryService</code>。</li><li><code>HealthCheckerProvider.java:50</code>:<code>prepare()</code>(<code>:82</code>)把 <code>score</code> 初始化为 -1(<code>:83</code>,表示还没算过),建虚拟线程定时器(<code>:84</code>)。<code>notifyAfterCompleted()</code>(<code>:106</code>)起定时任务(<code>scheduleAtFixedRate</code>,初始延迟 2s,周期 <code>config.getCheckIntervalSeconds()</code>,<code>:127</code>):从 <code>MetricsCollector.collect()</code>(<code>:110</code>)收集所有样本,<code>metricsCreator.isHealthCheckerMetrics</code>(<code>:112</code>)过滤出 <code>health_check_*</code> 指标,值 &lt;1 视为不健康(<code>:114</code>),累加计数并拼出不健康模块名列表(<code>:115</code>),写入 <code>score</code>(不健康模块数,0 表示全健康,-1 初始态,<code>:120-124</code>)和 <code>details</code>(<code>:125</code>)。</li><li><code>start()</code>(<code>:95</code>)把 <code>HealthCheckerHttpService</code> 挂到 CoreModule 的 <code>HTTPHandlerRegister</code>(<code>:100-103</code>,HEAD + GET)。<code>requiredModules()</code>(<code>:130</code>)依赖 <code>TelemetryModule</code>。</li></ul><p>为什么是值 &lt;1 而不是 ==0?因为 <code>HealthCheckMetrics</code> 是 gauge,健康时上报 1,不健康上报 0,中间态或上报失败可能给个介于 0 和 1 的值。用 &lt;1 当不健康门槛,留出余量,避免边界抖动。</p><hr /><h2 id="server-tools-%C2%B7-profile-exporter" tabindex="-1">server-tools · profile-exporter</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-7" tabindex="-1">解决什么问题</h3><p>一个独立的 CLI 工具,从已落库的 profile 数据里把&quot;线程快照 + segment 基础信息&quot;导出成文件,用于离线调试 Continuous Profiling。</p><h3 id="%E5%AE%9E%E7%8E%B0-1" tabindex="-1">实现</h3><ul><li><code>ProfileSnapshotExporterBootstrap</code>(<code>oap-server/server-tools/profile-exporter/tool-profile-snapshot-bootstrap/.../ProfileSnapshotExporterBootstrap.java:42</code>):<code>export(args)</code> 解析 <code>ExporterConfig</code>、用 <code>ApplicationConfigLoader</code> 加载 OAP 配置、初始化 <code>ModuleManager</code>(<strong>复用 OAP 启动框架,但用 mock 模块</strong>如 <code>MockCoreModuleProvider</code>、<code>MockRemoteClientManager</code> 避免起真实服务)、查 profiled segment + span、写基础信息文件、<code>ProfileSnapshotDumper.dump</code> 查并写快照文件,最后 <code>System.exit</code>。</li><li><code>tool-profile-snapshot-server-mock</code> 提供 mock 实现(<code>MockSourceReceiver</code>、<code>MockGRPCHandlerRegister</code> 等),让导出器在&quot;只读存储 + 不开端口&quot;的精简模式下跑起来。</li><li>这是工具而非常驻服务,按需手动运行。</li></ul><blockquote><p><strong>MockCoreModuleProvider 的注意事项</strong><br /><a href="http://CLAUDE.md" target="_blank">CLAUDE.md</a> 提到:给 <code>CoreModule.services()</code> 加 Service 时,要更新<strong>所有</strong> <code>CoreModuleProvider</code> 实现,包括 <code>server-tools/profile-exporter/</code> 里的 <code>MockCoreModuleProvider</code>,否则 profile exporter e2e 测试启动会失败。</p></blockquote><hr /><h2 id="server-testing-%C2%B7-%E6%B5%8B%E8%AF%95%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD" tabindex="-1">server-testing · 测试基础设施</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-8" tabindex="-1">解决什么问题</h3><p>给 OAP 其它模块的单元/集成测试提供 mock 框架和 DSL 测试工具。</p><h3 id="%E5%AE%9E%E7%8E%B0-2" tabindex="-1">实现</h3><ul><li><code>MockModuleManager</code>、<code>MockModuleProvider</code>、<code>ModuleManagerTesting</code>:提供 mock 的 <code>ModuleManager</code>/<code>ModuleProvider</code>,便于不启动完整 OAP 测单个模块。</li><li><code>dsl/</code> 下 <code>HierarchyRuleLoader</code>、<code>DslClassOutput</code> 等支持 DSL 测试。</li><li>该目录还有 <code>org/junit/rules/TestRule.java</code>、<code>org/junit/runners/model/Statement.java</code>,用于在受 JPMS/类加载限制时提供 JUnit 兼容桩。这是纯测试支撑 jar,不参与生产运行。</li></ul><hr /><h2 id="%E5%A4%96%E5%9B%B4%E5%AD%90%E7%B3%BB%E7%BB%9F%E6%80%BB%E8%A7%88" tabindex="-1">外围子系统总览</h2><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/07-%E9%9B%86%E7%BE%A4%E5%8D%8F%E8%B0%83%E4%B8%8E%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE.md_4.png" alt="图5" /></p><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%A6%82%E5%BF%B5%E6%96%B0%E6%89%8B%E7%AD%94%E7%96%91%E5%B0%8F%E7%BB%93" tabindex="-1">关键概念新手答疑小结</h2><ul><li><strong>集群协调</strong>:多 OAP 节点要按 hash 分片转发数据,因此需要&quot;互相发现 + 维护 gRPC 连接&quot;。<code>ClusterModule</code>(<code>ClusterModule.java:23</code>)抽象注册/查询接口,插件对接不同注册中心,<code>RemoteClientManager</code>(<code>RemoteClientManager.java:59</code>)消费节点列表建连。业务代码应通过 <code>CoreModule</code> 的 <code>RemoteClientManager</code> 间接使用,不直接 find <code>ClusterModule</code>。</li><li><strong>动态配置 watch</strong>:OAP 不重启改配置。各模块注册 <code>ConfigChangeWatcher</code>(<code>ConfigChangeWatcher.java:30</code>,带 <code>module.provider.item</code> key),配置中心 provider 轮询或监听外部,值变化回调 <code>watcher.notify</code>(<code>ConfigWatcherRegister.java:35</code> 比较后再回调)。分 Fetching(轮询,<code>FetchingConfigWatcherRegister.java:36</code>)和 Listening(事件,<code>ListeningConfigWatcherRegister.java:27</code>)两派。</li><li><strong>自监控 vs 数据导出</strong>:Telemetry 是&quot;OAP 自己有多忙&quot;(Prometheus /metrics),Exporter 是&quot;OAP 算出的业务数据外发&quot;(gRPC/Kafka,订阅驱动)。两者都用 prometheus client 但服务对象不同。</li><li><strong>AI baseline</strong>:MQE 的 <code>baseline()</code> 运算符经 AI Pipeline 模块向外部预测服务取基线 + 上下界(<code>BaselineQueryServiceImpl.java:51</code>),用于异常检测,不是 OAP 自己算的。</li><li><strong>热更新规则</strong>:runtime-rule 通过 admin REST 在线编译/切换 MAL/LAL 规则,配锁、状态机、集群扇出做最终一致,不停机改处理流水线。所有管理面端点默认关闭且无鉴权,必须网关保护。</li></ul><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键类</th><th>路径</th></tr></thead><tbody><tr><td>集群</td><td>ClusterModule</td><td><code>oap-server/server-core/.../cluster/ClusterModule.java</code></td></tr><tr><td></td><td>RemoteClientManager</td><td><code>oap-server/server-core/.../remote/client/RemoteClientManager.java</code></td></tr><tr><td></td><td>ZookeeperCoordinator</td><td><code>oap-server/server-cluster-plugin/cluster-zookeeper-plugin/.../ZookeeperCoordinator.java</code></td></tr><tr><td></td><td>KubernetesCoordinator</td><td><code>oap-server/server-cluster-plugin/cluster-kubernetes-plugin/.../KubernetesCoordinator.java</code></td></tr><tr><td>动态配置</td><td>ConfigurationModule</td><td><code>oap-server/server-configuration/configuration-api/.../ConfigurationModule.java</code></td></tr><tr><td></td><td>ConfigChangeWatcher</td><td>同目录 <code>api/ConfigChangeWatcher.java</code></td></tr><tr><td></td><td>FetchingConfigWatcherRegister</td><td><code>oap-server/server-configuration/configuration-api/.../FetchingConfigWatcherRegister.java</code></td></tr><tr><td></td><td>GRPCConfigWatcherRegister</td><td><code>oap-server/server-configuration/grpc-configuration-sync/.../GRPCConfigWatcherRegister.java</code></td></tr><tr><td>Telemetry</td><td>TelemetryModule / PrometheusTelemetryProvider</td><td><code>oap-server/server-telemetry/telemetry-api/...</code>、<code>telemetry-prometheus/.../PrometheusTelemetryProvider.java</code></td></tr><tr><td>Exporter</td><td>ExporterModule / ExporterProvider / GRPCMetricsExporter</td><td><code>oap-server/exporter/...</code></td></tr><tr><td>AI Pipeline</td><td>AIPipelineModule / BaselineQueryServiceImpl</td><td><code>oap-server/ai-pipeline/...</code></td></tr><tr><td>server-admin</td><td>AdminServerModuleProvider</td><td><code>oap-server/server-admin/admin-server/...</code></td></tr><tr><td></td><td>runtime-rule 三层</td><td><code>oap-server/server-admin/runtime-rule/...</code></td></tr><tr><td>health-checker</td><td>HealthCheckerProvider</td><td><code>oap-server/server-health-checker/.../HealthCheckerProvider.java</code></td></tr><tr><td>server-tools</td><td>ProfileSnapshotExporterBootstrap</td><td><code>oap-server/server-tools/profile-exporter/tool-profile-snapshot-bootstrap/.../ProfileSnapshotExporterBootstrap.java</code></td></tr><tr><td>server-testing</td><td>MockModuleManager</td><td><code>oap-server/server-testing/.../mock/MockModuleManager.java</code></td></tr></tbody></table><hr /><h2 id="%E5%85%A8%E7%B3%BB%E5%88%97%E5%9B%9E%E9%A1%BE" tabindex="-1">全系列回顾</h2><p>到这里,SkyWalking OAP 的所有核心子系统都拆解完了。回到 README 看整体地图,或回看任意一篇:</p><ul><li>00-整体架构与端到端数据流</li><li>01-模块系统与启动流程</li><li>02-数据采集层-Receiver</li><li>03-分析引擎与四大DSL</li><li>04-流处理引擎-Worker链</li><li>05-存储层</li><li>06-查询层与告警</li></ul><blockquote><p><strong>心智模型</strong><br />把外围子系统想成 OAP 工厂的&quot;后勤部&quot;:集群协调是&quot;分厂通讯录&quot;(谁在哪、怎么联系),动态配置是&quot;工艺参数远程调整&quot;(不停线改设置),Telemetry 是&quot;工厂自身的仪表盘&quot;(机器转得快不快),Exporter 是&quot;对外供货窗口&quot;(按订单把成品发给外部),AI Pipeline 是&quot;聘请的外部顾问&quot;(预测基线、归一化 URI 都外包给它),server-admin 是&quot;厂长办公室&quot;(调试、改工艺单、查状态,默认锁门要授权进),health-checker 是&quot;门卫&quot;(看工厂健康分决定要不要放行流量)。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[七、Skywalking查询层与告警]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/skywalking-cha-xun-ceng-yu-gao-jing" />
                <id>tag:https://lymboy.com,2025-11-10:skywalking-cha-xun-ceng-yu-gao-jing</id>
                <published>2025-11-10T21:51:54+08:00</published>
                <updated>2025-11-10T21:51:54+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E6%9F%A5%E8%AF%A2%E5%B1%82%E4%B8%8E%E5%91%8A%E8%AD%A6" tabindex="-1">查询层与告警</h1><blockquote><p><strong>这一篇解决什么</strong><br />数据存好了，怎么取出来给 UI 看？指标异常了怎么告警？这篇讲查询层（默认 GraphQL，兼容 PromQL/LogQL/TraceQL/Zipkin 四套协议让 Grafana 等直连）和告警系统（规则用 MQE 表达式、滑窗求值、状态机控制触发/静默/恢复、九种 hook 输出）。读完应该能回答：UI 怎么查到数据、Grafana 怎么接 OAP、告警规则怎么写、触发后怎么发钉钉。</p></blockquote><hr /><h2 id="%E6%9F%A5%E8%AF%A2%E5%B1%82%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" tabindex="-1">查询层解决什么问题</h2><p>把存储层里已经聚合好的指标/Trace/Log/告警/拓扑等数据，按统一接口暴露给 UI（SkyWalking 自带 Booster UI）和外部可视化工具。默认用 GraphQL，因为它能让前端按需取字段、一次请求组合多种资源、自带 schema 自文档。兼容 PromQL/LogQL/TraceQL/Zipkin 是为了让 Grafana、Tempo、已有 Zipkin 生态不用改接发直连 OAP。</p><hr /><h2 id="graphql-%C2%B7-%E4%B8%BB%E6%9F%A5%E8%AF%A2%E5%85%A5%E5%8F%A3" tabindex="-1">GraphQL · 主查询入口</h2><h3 id="%E6%A8%A1%E5%9D%97%E7%BB%93%E6%9E%84" tabindex="-1">模块结构</h3><p><code>QueryModule</code>（<code>oap-server/server-core/.../query/QueryModule.java:26</code>）：根查询模块，名字 <code>&quot;query&quot;</code>，<code>services()</code> 返回空数组。它不暴露服务接口，只是一个挂载点，让各种查询协议插件以 <code>ModuleProvider</code> 形式注册进来。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/06-%E6%9F%A5%E8%AF%A2%E5%B1%82%E4%B8%8E%E5%91%8A%E8%AD%A6.md_0.png" alt="图1" /></p><blockquote><p><strong>Module/Provider 插槽比喻</strong><br />把 <code>QueryModule</code> 想成&quot;插槽&quot;，各 provider 想成&quot;插进去的卡&quot;。同一 Module 可以有多个 Provider，但运行时只激活一个（配置 selector 决定）。不过查询层特殊——<code>QueryModule</code> 实际上可以同时启用多个 provider（GraphQL + PromQL + LogQL 各占一个端口），因为它们不冲突。</p></blockquote><h3 id="graphql-schema-%E4%B8%8E-resolver" tabindex="-1">GraphQL schema 与 resolver</h3><p><code>GraphQLQueryProvider</code>（<code>oap-server/server-query-plugin/query-graphql-plugin/.../GraphQLQueryProvider.java:69</code>），<code>name()</code> 返回 <code>&quot;graphql&quot;</code>（<code>:75</code>），<code>module()</code> 返回 <code>QueryModule.class</code>（<code>:79</code>）。</p><blockquote><p><strong>GraphQL schema / resolver 速成</strong><br /><code>.graphqls</code> 文件用 GraphQL SDL（Schema Definition Language）声明&quot;有哪些类型、有哪些查询字段、字段参数是什么&quot;——这是契约。<code>resolver</code> 是 Java 类，同名方法负责&quot;字段被请求时实际去哪取数据&quot;。</p><p><code>extend type Query { ... }</code> 是给根 Query 类型追加字段，多个 <code>.graphqls</code> 各自 <code>extend type Query</code>，最终合并成完整的 Query 类型。这就是为什么几十个查询类型能分散在不同文件里却合成一个端点。</p></blockquote><p><code>prepare()</code>（<code>GraphQLQueryProvider.java:99-166</code>）用 <code>graphql-kickstart-tools</code> 的 <code>SchemaParser</code> 把一堆 <code>.graphqls</code> 和对应 resolver 注册进去。每条 <code>.file(...).resolvers(...)</code> 就是&quot;把某个 schema 文件挂到某个 Java resolver 类上&quot;。主要查询类型映射：</p><table><thead><tr><th>查询类型</th><th>resolver</th><th>schema 文件</th></tr></thead><tbody><tr><td>trace</td><td><code>TraceQuery</code> / <code>TraceQueryV2</code></td><td><code>trace.graphqls</code> / <code>trace-v2.graphqls</code></td></tr><tr><td>metrics (v3, MQE)</td><td><code>MetricsExpressionQuery</code></td><td><code>metrics-v3.graphqls</code></td></tr><tr><td>topN / aggregation</td><td><code>AggregationQuery</code> / <code>TopNRecordsQuery</code></td><td><code>aggregation.graphqls</code> / <code>top-n-records.graphqls</code></td></tr><tr><td>log</td><td><code>LogQuery</code> / <code>LogTestQuery</code> / <code>OndemandLogQuery</code></td><td><code>log.graphqls</code> / <code>ondemand-pod-log.graphqls</code></td></tr><tr><td>alarm</td><td><code>AlarmQuery</code></td><td><code>alarm.graphqls</code></td></tr><tr><td>event</td><td><code>EventQuery</code></td><td><code>event.graphqls</code></td></tr><tr><td>topology</td><td><code>TopologyQuery</code></td><td><code>topology.graphqls</code></td></tr><tr><td>hierarchy</td><td><code>HierarchyQuery</code></td><td><code>hierarchy.graphqls</code></td></tr><tr><td>profile</td><td><code>ProfileQuery</code> / <code>ProfileMutation</code> 等</td><td><code>profile.graphqls</code> / <code>ebpf-profiling.graphqls</code> 等</td></tr><tr><td>metadata</td><td><code>MetadataQuery</code> / <code>MetadataQueryV2</code></td><td><code>metadata.graphqls</code> / <code>metadata-v2.graphqls</code></td></tr></tbody></table><h3 id="graphql-%E6%89%A7%E8%A1%8C%E9%93%BE%E8%B7%AF" tabindex="-1">GraphQL 执行链路</h3><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/06-%E6%9F%A5%E8%AF%A2%E5%B1%82%E4%B8%8E%E5%91%8A%E8%AD%A6.md_1.png" alt="图2" /></p><p><code>GraphQLQueryHandler</code>（<code>oap-server/server-query-plugin/query-graphql-plugin/.../GraphQLQueryHandler.java:41</code>）：</p><ul><li>构造时用 Armeria 的 <code>GraphqlService.builder().schema(schema).instrumentation(new MaxQueryComplexityInstrumentation(allowedComplexity, ...)).build()</code>（<code>:74-85</code>）。<code>MaxQueryComplexityInstrumentation</code> 防止恶意/超重查询——每个查询有复杂度上限，超过就中止并记日志。</li><li><code>@Post(&quot;/graphql&quot;)</code> + <code>@Blocking</code>（<code>:88-89</code>）：<code>@Blocking</code> 让 Armeria 把请求放到阻塞线程池（GraphQL 查询涉及存储 IO，不能占事件循环线程）。</li><li>方法体包了直方图计时 + 错误计数器（OAP 自身的 self-telemetry）。</li></ul><h3 id="resolver-%E4%B8%8E-coremodule-queryservice-%E7%9A%84%E5%AF%B9%E6%8E%A5" tabindex="-1">resolver 与 CoreModule QueryService 的对接</h3><p>每个 resolver 持有 <code>ModuleManager</code>，懒加载从 <code>CoreModule</code> 拿对应服务。典型模式（<code>TraceQuery.java:59-64</code>）：</p><pre><code class="language-java">private TraceQueryService getQueryService() {    if (queryService == null) {        this.queryService = moduleManager.find(CoreModule.NAME).provider().getService(TraceQueryService.class);    }    return queryService;}</code></pre><p>各 resolver 与 CoreModule 服务对接：</p><ul><li><code>TraceQuery</code> → <code>TraceQueryService</code>、<code>TagAutoCompleteQueryService</code></li><li><code>MetricsQuery</code> → <code>MetricsQueryService</code>、<code>AggregationQueryService</code>、<code>RecordQueryService</code>、<code>MetricsMetadataQueryService</code></li><li><code>AlarmQuery</code> → <code>AlarmQueryService</code>、<code>EventQueryService</code>、<code>TagAutoCompleteQueryService</code></li><li><code>HierarchyQuery</code> → <code>HierarchyQueryService</code></li></ul><p>所有 resolver 方法返回 <code>CompletableFuture</code>，通过 <code>AsyncQueryUtils.queryAsync</code> 异步执行；每个方法接受 <code>boolean debug</code>，开启后构造 <code>DebuggingTraceContext</code> 把 OAP 内部查询执行 trace 回填到结果里。</p><blockquote><p><strong>GraphQL 按需取字段的优化</strong><br /><code>AlarmQuery.getAlarm</code>（<code>AlarmQuery.java:124-128</code>）用 <code>DataFetchingEnvironment.getSelectionSet().contains(&quot;**/events/**&quot;)</code> 判断前端有没有在查询里选了 <code>events</code> 子字段，只有选了才去 <code>EventQueryService</code> 关联事件——这是 GraphQL 按需取字段带来的优化机会，避免每次查告警都连带查事件。</p></blockquote><hr /><h2 id="mqe-%E5%9C%A8%E6%9F%A5%E8%AF%A2%E4%B8%AD%E7%9A%84%E6%8E%A5%E5%85%A5" tabindex="-1">MQE 在查询中的接入</h2><p><code>MetricsExpressionQuery</code>（<code>.../resolver/MetricsExpressionQuery.java:44</code>）暴露 <code>execExpression(expression, entity, duration, debug, dumpStorageRsp)</code>，对应 <code>metrics-v3.graphqls:124</code> 的 <code>execExpression</code>。</p><p>执行流程（<code>MetricsExpressionQuery.java:58-103</code>）：</p><ol><li>构造 <code>DebuggingTraceContext</code>，<code>MQEVisitor</code> 实例化（持有 moduleManager/entity/duration）。</li><li><code>MQELexer</code> + <code>MQEParser</code> 解析表达式得到 <code>ParseTree</code>；解析失败返回 <code>ExpressionResultType.UNKNOWN</code> + 错误信息。</li><li><code>visitor.visit(tree)</code> 求值，得到 <code>ExpressionResult</code>。</li><li>对结果数值用 <code>DecimalFormat</code> 格式化（<code>setGroupingUsed(false)</code>，不加分隔符）。</li></ol><p><code>ExpressionResult</code> 类型有 4 种（<code>metrics-v3.graphqls:45-58</code>）：<code>SINGLE_VALUE</code>、<code>TIME_SERIES_VALUES</code>、<code>SORTED_LIST</code>、<code>RECORD_LIST</code>，外加 <code>UNKNOWN</code>。</p><p><code>MQEExecutor</code>（<code>.../mqe/rt/MQEExecutor.java:52</code>）是从 <code>MetricsExpressionQuery</code> 抽出来的同步求值器，给 admin inspect 路径用，复用同一套 MQE 引擎但支持叠加&quot;外部指标&quot;（foreign metrics）元数据。详见 03-分析引擎与四大DSL 的 MQE 节。</p><hr /><h2 id="%E5%85%B6%E4%BB%96%E6%9F%A5%E8%AF%A2%E5%8D%8F%E8%AE%AE" tabindex="-1">其他查询协议</h2><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/06-%E6%9F%A5%E8%AF%A2%E5%B1%82%E4%B8%8E%E5%91%8A%E8%AD%A6.md_2.png" alt="图3" /></p><blockquote><p><strong>���什么要四套兼容协议</strong><br />GraphQL 是 SkyWalking 原生 UI 用的，但运维团队往往已有 Grafana（看 PromQL 指标 + Loki 日志 + Tempo Trace）或 Zipkin 体系。这四个插件让 OAP 不必改造既有可视化栈就能接入，降低迁移成本。</p></blockquote><h3 id="promql-%C2%B7-grafana-%E6%8C%87%E6%A0%87%E7%9B%B4%E8%BF%9E" tabindex="-1">PromQL · Grafana 指标直连</h3><ul><li><code>PromQLModule</code> + <code>PromQLProvider</code>（<code>.../promql/PromQLProvider.java:34</code>），<code>requiredModules</code> 含 <code>TelemetryModule</code> + <code>CoreModule</code>。</li><li>走<strong>独立 HTTP 端口</strong>：<code>start()</code>（<code>:70-89</code>）自己 new <code>HTTPServer</code>，<code>addHandler(new PromQLApiHandler)</code>。</li><li>端点完全兼容 Prometheus HTTP API（<code>PromQLApiHandler.java:183-584</code>）：<code>/api/v1/metadata</code>、<code>/api/v1/labels</code>、<code>/api/v1/label/{name}/values</code>、<code>/api/v1/series</code>、<code>/api/v1/query</code>（瞬时）、<code>/api/v1/query_range</code>（区间）、<code>/api/v1/format_query</code>、<code>/api/v1/status/buildinfo</code>。</li><li>解析侧：<code>PromQLLexer</code>/<code>PromQLParser</code>（ANTLR）+ <code>PromQLExprQueryVisitor</code>。底层调 <code>MetricsQueryService</code>/<code>AggregationQueryService</code>/<code>RecordQueryService</code>/<code>MetricsMetadataQueryService</code>。</li></ul><h3 id="logql-%C2%B7-grafana-loki-%E6%97%A5%E5%BF%97" tabindex="-1">LogQL · Grafana Loki 日志</h3><ul><li><code>LogQLModule</code> + <code>LogQLProvider</code>（<code>.../logql/LogQLProvider.java:33</code>），独立 HTTP 端口，<code>requiredModules</code> 仅 <code>CoreModule</code>。</li><li><code>LogQLApiHandler</code>（<code>.../handler/LogQLApiHandler.java:71</code>）暴露 Loki 兼容路径：<code>/loki/api/v1/labels</code>（<code>:86-88</code>）委托 <code>TagAutoCompleteQueryService.queryTagAutocompleteKeys(TagType.LOG, duration)</code>，还有 label values、log range query 等。底层 <code>LogQueryService</code>，解析用 <code>LogQLLexer</code>/<code>LogQLParser</code> + <code>LogQLExprVisitor</code>。</li></ul><h3 id="traceql-%C2%B7-grafana-tempo-trace" tabindex="-1">TraceQL · Grafana Tempo trace</h3><ul><li><code>TraceQLModule</code> + <code>TraceQLProvider</code>（<code>.../traceql/TraceQLProvider.java:34</code>），独立 HTTP server。</li><li>根据配置开关注册两个 handler（<code>:85-101</code>）：<ul><li><code>config.isEnableDatasourceZipkin()</code> → <code>ZipkinTraceQLApiHandler</code> 到 <code>/zipkin</code> 前缀（Tempo 的 Zipkin 兼容数据源 API）。</li><li><code>config.isEnableDatasourceSkywalking()</code> → <code>SkyWalkingTraceQLApiHandler</code> 到 <code>/skywalking</code> 前缀。</li></ul></li><li>解析侧：<code>TraceQLQueryParser</code> + <code>TraceQLQueryVisitor</code>，有 <code>OTLPConversion</code>/<code>SkyWalkingOTLPConversion</code>/<code>ZipkinOTLPConversion</code> 做 OTLP trace 格式与各后端格式互转。</li></ul><blockquote><p><strong>TraceQL 是什么</strong><br />TraceQL 是 Grafana Tempo 的 trace 查询语法。OAP 实现它让 Grafana Tempo 数据源能直连 OAP 查 trace。</p></blockquote><h3 id="zipkin-%C2%B7-zipkin-api-%E5%85%BC%E5%AE%B9" tabindex="-1">Zipkin · Zipkin API 兼容</h3><ul><li><code>ZipkinQueryModule</code> + <code>ZipkinQueryProvider</code>（<code>.../zipkin/ZipkinQueryProvider.java:34</code>），独立 HTTP 端口，<code>requiredModules</code> 含 <code>TelemetryModule</code> + <code>CoreModule</code>。</li><li><code>ZipkinQueryHandler</code> 暴露 Zipkin v2 API（<code>GET</code>）：<code>/config.json</code>、<code>/api/v2/services</code>、<code>/api/v2/remoteServices</code>、<code>/api/v2/spans</code>、<code>/api/v2/trace/{traceId}</code>、<code>/api/v2/traces</code>、<code>/api/v2/traceMany</code>、<code>/api/v2/autocompleteKeys</code>、<code>/api/v2/autocompleteValues</code>。底层 <code>ZipkinQueryService</code>。</li></ul><hr /><h2 id="%E5%91%8A%E8%AD%A6%E7%B3%BB%E7%BB%9F" tabindex="-1">告警系统</h2><h3 id="%E6%A8%A1%E5%9D%97%E7%BB%93%E6%9E%84-1" tabindex="-1">模块结构</h3><p><code>AlarmModule</code>（<code>oap-server/server-core/.../alarm/AlarmModule.java:28</code>），名字 <code>&quot;alarm&quot;</code>，暴露 4 个 Service（<code>:36-43</code>）：</p><table><thead><tr><th>Service</th><th>职责</th></tr></thead><tbody><tr><td><code>MetricsNotify</code></td><td>流处理链调用，把指标喂给告警引擎</td></tr><tr><td><code>AlarmRulesWatcherService</code></td><td>规则热更新接口</td></tr><tr><td><code>AlarmStatusWatcherService</code></td><td>告警状态观察</td></tr><tr><td><code>AlarmKernelService</code></td><td>运行时规则重置</td></tr></tbody></table><p><code>AlarmModuleProvider</code>（<code>oap-server/server-alarm-plugin/.../AlarmModuleProvider.java:39</code>），<code>requiredModules</code> 是 <code>CoreModule</code> + <code>ConfigurationModule</code>（告警规则支持配置中心动态更新）。</p><p>启动流程：</p><ul><li><code>prepare()</code>（<code>:61-69</code>）：创建 <code>AlarmRulesWatcher</code>（持空 <code>Rules</code>）、<code>NotifyHandler</code>，注册 4 个服务实现。</li><li><code>start()</code>（<code>:72-78</code>）：从 <code>ConfigurationModule</code> 拿 <code>DynamicConfigurationService</code>，把 <code>AlarmRulesWatcher</code> 注册为配置变更监听器（告警规则可热更新）。</li><li><code>notifyAfterCompleted()</code>（<code>:81-92</code>）：从 classpath 读 <code>alarm-settings.yml</code>，用 <code>RulesReader</code> 解析成 <code>Rules</code>，<code>alarmRulesWatcher.initConfig(rules)</code> 注入；然后 <code>notifyHandler.init(new AlarmStandardPersistence(getManager()))</code>。</li></ul><h3 id="%E5%91%8A%E8%AD%A6%E8%A7%84%E5%88%99%E6%A0%BC%E5%BC%8F" tabindex="-1">告警规则格式</h3><p>真实示例（<code>oap-server/server-alarm-plugin/src/test/resources/alarm-settings.yml</code>）：</p><pre><code class="language-yaml">rules:  endpoint_percent_rule:    expression: sum(endpoint_percent &lt; 75) &gt;= 3    period: 10    silence-period: 10    message: Successful rate of endpoint {name} is lower than 75%  service_percent_rule:    expression: sum(service_percent &lt; 85) &gt;= 4    include-names:      - service_a      - service_b    exclude-names:      - service_c    period: 10  comp1_rule:    # 复合规则：成功率在 (60,75) 之间    expression: sum((endpoint_percent &gt; 60) * (endpoint_percent &lt; 75)) &gt;= 3    period: 10    silence-period: 10    message: xxxxxhooks:  webhook:    default:      is-default: true      urls:        - http://127.0.0.1/notify/  slack:    default:      is-default: true      text-template: &#39;{&quot;type&quot;:&quot;section&quot;,&quot;text&quot;:{&quot;type&quot;:&quot;mrkdwn&quot;,&quot;text&quot;:&quot;:alarm_clock: *Apache Skywalking Alarm* \\ **%s**.&quot;}}&#39;      webhooks:        - https://hooks.slack.com/services/x/y/zssss  dingtalk:    default:      is-default: true      text-template: &#39;{&quot;msgtype&quot;:&quot;text&quot;,&quot;text&quot;:{&quot;content&quot;:&quot;Apache SkyWalking Alarm: \\ %s.&quot;}}&#39;      webhooks:        - url: https://oapi.dingtalk.com/robot/send?access_token=dummy_token          secret: dummysecret</code></pre><p>字段含义（<code>AlarmRule.java</code> + <code>RulesReader.java:88-132</code>）：</p><table><thead><tr><th>字段</th><th>含义</th><th>默认值</th></tr></thead><tbody><tr><td><code>expression</code></td><td>MQE 表达式，必须是布尔单值结果（根运算必须是比较）</td><td>必填</td></tr><tr><td><code>period</code></td><td>评估窗口长度（分钟），滑窗保留多少个桶</td><td>1</td></tr><tr><td><code>silence-period</code></td><td>触发后静默多少次检查不再重复告警</td><td>= period</td></tr><tr><td><code>recovery-observation-period</code></td><td>恢复观察期（分钟）</td><td>0</td></tr><tr><td><code>message</code></td><td>告警文案模板，<code>{name}</code> 会被实体名替换</td><td>“Alarm caused by Rule {ruleName}”</td></tr><tr><td><code>include-names</code> / <code>exclude-names</code></td><td>实体名白/黑名单</td><td>空</td></tr><tr><td><code>include-names-regex</code> / <code>exclude-names-regex</code></td><td>实体名正则白/黑名单</td><td>空</td></tr><tr><td><code>tags</code></td><td>告警附加标签</td><td>空</td></tr><tr><td><code>hooks</code></td><td>指定使用哪些 hook（格式 <code>{hookType}.{hookName}</code>，如 <code>slack.default</code>）</td><td>空 → 用所有 <code>is-default: true</code> 的</td></tr></tbody></table><blockquote><p><strong>规则名必须以 <code>_rule</code> 结尾</strong><br /><code>RulesReader.java:95</code> 的 <code>((String) k).endsWith(&quot;_rule&quot;)</code> 判断，规则名不以此结尾会被忽略。加载的文件名是 <code>alarm-settings.yml</code>（<code>AlarmModuleProvider.java:84</code> 的 <code>ResourceUtils.read(&quot;alarm-settings.yml&quot;)</code>）。</p></blockquote><h3 id="%E8%A7%84%E5%88%99%E5%8A%A0%E8%BD%BD%E4%B8%8E%E7%BC%96%E8%AF%91" tabindex="-1">规则加载与编译</h3><p><code>RulesReader</code>（<code>RulesReader.java:53</code>）用 SnakeYAML 解析，先读 <code>hooks</code> 再读 <code>rules</code>（顺序重要，因为规则的 <code>hooks</code> 字段要校验是否引用了已声明的 hook，<code>checkSpecificHooks</code> 在 <code>:450-455</code>）。</p><p><code>AlarmRule.setExpression</code>（<code>AlarmRule.java:75-109</code>）不是简单赋值，而是<strong>编译 + 校验</strong>：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/06-%E6%9F%A5%E8%AF%A2%E5%B1%82%E4%B8%8E%E5%91%8A%E8%AD%A6.md_3.png" alt="图4" /></p><ol><li>ANTLR 解析 MQE 表达式。</li><li><code>AlarmMQEVerifyVisitor</code> 遍历，提取 <code>includeMetrics</code>（表达式引用的所有指标名）和 <code>maxTrendRange</code>。</li><li>校验三件事：解析无错（<code>:90</code>）、<code>parseResult.isBoolResult()</code> 为真（<code>:93</code>，否则抛 <code>IllegalExpressionException(&quot;root operation is not a Compare Operation&quot;)</code>）、结果类型 <code>SINGLE_VALUE</code>（<code>:97</code>）。</li><li><code>verifyIncludeMetrics</code>（<code>:111-120</code>）校验所有指标必须属于<strong>同一 scope</strong>。</li></ol><p><code>AlarmRulesWatcher</code>（<code>AlarmRulesWatcher.java:54</code>）继承 <code>ConfigChangeWatcher</code>，监听配置 key <code>&quot;alarm-settings&quot;</code>（<code>:67</code>）。维护三个核心 map：<code>runningContext</code>（表达式 → List<RunningRule>）、<code>alarmRuleRunningRuleMap</code>（AlarmRule → RunningRule，热更新时复用保留历史窗口）、<code>exprMetricsMap</code>（表达式 → Set&lt;指标名&gt;，快速查找某指标命中哪些规则）。</p><blockquote><p><strong>热更新策略</strong><br /><code>notify(Rules)</code>（<code>:112-139</code>）：如果新规则和旧规则&quot;相等&quot;（<code>AlarmRule</code> 用 <code>@EqualsAndHashCode</code>），复用其 <code>RunningRule</code> 以保留历史指标窗口；否则 new 一个新 <code>RunningRule</code>。</p></blockquote><h3 id="%E6%B5%81%E5%A4%84%E7%90%86%E9%93%BE%E5%A6%82%E4%BD%95%E8%A7%A6%E5%8F%91%E5%91%8A%E8%AD%A6" tabindex="-1">流处理链如何触发告警</h3><p>这是告警与流处理的核心连接点：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/06-%E6%9F%A5%E8%AF%A2%E5%B1%82%E4%B8%8E%E5%91%8A%E8%AD%A6.md_4.png" alt="图5" /></p><ol><li><strong>指标落盘后通知告警</strong>：<code>MetricsStreamProcessor.minutePersistentWorker</code>（<code>:317</code>）创建 <code>AlarmNotifyWorker</code>，作为 <code>MetricsPersistentMinWorker</code> 的下游（<code>:320-323</code>）。分钟级指标持久化后调 <code>AlarmNotifyWorker.in(metrics)</code>。</li><li><code>AlarmNotifyWorker</code>（<code>AlarmNotifyWorker.java:30</code>）：<code>in</code> 若 metrics 实现 <code>WithMetadata</code>，调 <code>entrance.forward(metrics)</code>。</li><li><code>AlarmEntrance</code>（<code>AlarmEntrance.java:24</code>）：<code>forward</code> 先检查 <code>has(AlarmModule.NAME)</code>（没装告警模块跳过），再懒加载 <code>MetricsNotify</code> 调 <code>notify</code>。</li><li><code>NotifyHandler.notify</code>（<code>NotifyHandler.java:118-234</code>）：<ul><li>从 metrics 拿 <code>MetricsMetaInfo</code>（scope、metricsName、id）。</li><li>按 scope 构造对应 <code>MetaInAlarm</code>（Service/ServiceInstance/Endpoint 及 Relation），用 <code>IDManager</code> 解析 id 成可读名，通过 <code>MetadataQueryService</code> 查 <code>layers</code>。</li><li>只接受 6 种 catalog scope（service/instance/endpoint 及 relation），其它直接 return。</li><li><code>core.findRunningRule(metricsName)</code> 找命中该指标的规则（<code>AlarmCore.findRunningRule</code>，<code>AlarmCore.java:57-68</code>，遍历 <code>exprMetricsMap</code> 反查）。</li><li>对每个命中的 <code>RunningRule</code> 调 <code>rule.in(metaInAlarm, metrics)</code>。</li></ul></li><li><code>RunningRule.in</code>（<code>RunningRule.java:135-156</code>）：<ul><li>先按 <code>includeMetrics</code> 过滤指标名。</li><li>再按 include/exclude names + 正则过滤实体名（<code>validate</code> <code>:162-200</code>）。</li><li>为实体构造 <code>AlarmEntity</code>，<code>computeIfAbsent</code> 拿到/创建 <code>Window</code>，<code>window.add</code> 把指标塞进滑窗。</li></ul></li></ol><blockquote><p><strong>AlarmNotifyWorker 只接分钟级</strong><br /><code>AlarmNotifyWorker</code> 只接在<strong>分钟级</strong> worker 后，不接小时/天。<code>RunningRule</code> 注释：“only minute dimensionality metrics are expected to process”。告警基于分钟级指标。</p></blockquote><h3 id="alarmcore-%C2%B7-%E5%AE%9A%E6%97%B6%E6%A3%80%E6%9F%A5" tabindex="-1">AlarmCore · 定时检查</h3><p><code>AlarmCore.start</code>（<code>AlarmCore.java:70-122</code>）：单线程定时器，<strong>每 10 秒</strong>触发一次（<code>:121</code> <code>scheduleAtFixedRate(..., 10, 10, SECONDS)</code>）。每次：</p><ol><li>计算距上次执行的分钟数。</li><li>对每个 <code>RunningRule</code> 调 <code>moveTo(checkTime)</code> 推进滑窗；且只在每分钟的第 15 秒之后才真正 <code>check()</code>（<code>:85</code> <code>checkTime.getSecondOfMinute() &gt; 15</code>），注释说&quot;避免在每分钟头一刻触发误报&quot;——因为分钟级指标可能还没落盘完。</li><li>收集所有 <code>AlarmMessage</code>，分出 firing（<code>AlarmMessage</code>）和 recovery（<code>AlarmRecoveryMessage</code>），遍历所有 <code>AlarmCallback</code> 分别调 <code>doAlarm</code>/<code>doAlarmRecovery</code>（<code>:99-110</code>）。</li></ol><h3 id="%E5%91%8A%E8%AD%A6%E7%8A%B6%E6%80%81%E6%9C%BA" tabindex="-1">告警状态机</h3><blockquote><p><strong>window / 连续 N 次 / 静默 速成</strong></p><ul><li><strong>window（滑窗）</strong>：<code>RunningRule.Window</code> 是按分钟为粒度的滑动窗口，长度 = <code>period</code>。<code>window.add</code> 把每分钟一个指标值塞进 <code>LinkedList</code>，<code>moveTo</code> 随时间推进丢弃过期分钟、补 null。表达式 <code>sum(endpoint_percent &lt; 75) &gt;= 3</code> 就是在窗口的 N 个分钟值上算：先逐分钟比较 <code>&lt; 75</code> 得 0/1，再 <code>sum</code>，再 <code>&gt;= 3</code>。</li><li><strong>连续 N 次（period）</strong>：不是字面&quot;连续&quot;，而是&quot;在 period 分钟窗口内满足条件的次数 ≥ N&quot;。<code>period: 10</code> 表示窗口 10 分钟。</li><li><strong>silence-period（静默期）</strong>：告警触发后，接下来 <code>silence-period</code> 次检查即使条件仍满足也不再发新告警，避免轰炸。默认 <code>silence-period = period</code>（<code>RulesReader.java:114</code>）。</li><li><strong>recovery-observation-period（恢复观察期）</strong>：触发后条件不再满足时，不是立刻发恢复通知，而是观察 <code>recovery-observation-period</code> 分钟确认稳定恢复才发 <code>AlarmRecoveryMessage</code>。</li></ul></blockquote><p>状态机 5 个状态（<code>RunningRule.java:258-264</code>）：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/06-%E6%9F%A5%E8%AF%A2%E5%B1%82%E4%B8%8E%E5%91%8A%E8%AD%A6.md_5.png" alt="图6" /></p><p><code>AlarmStateMachine</code>（<code>RunningRule.java:522-639</code>）：</p><ul><li><code>onMatch</code>（<code>:538-565</code>）：<code>silenceCountdown--</code>。NORMAL→FIRING；FIRING 且还在静默期→SILENCED_FIRING；SILENCED_FIRING/OBSERVING_RECOVERY/RECOVERED 且静默到期→FIRING，否则→SILENCED_FIRING。</li><li><code>onMismatch</code>（<code>:567-597</code>）：<code>recoveryObservationCountdown--</code> + <code>silenceCountdown--</code>。FIRING/SILENCED_FIRING 且恢复观察期与静默期都到期→RECOVERED，否则→OBSERVING_RECOVERY。RECOVERED→NORMAL。</li><li><code>transitionTo</code>（<code>:599-622</code>）进入新状态时重置对应 countdown。</li></ul><p><code>Window.checkAlarm</code>（<code>:375-397</code>）：</p><ul><li><code>isMatch()</code> 用 <code>AlarmMQEVisitor</code> 在当前窗口指标值上求值 MQE 表达式，结果必须是布尔单值（<code>isMatch == 1</code> 视为命中，<code>:469</code>）。</li><li>命中 → <code>stateMachine.onMatch()</code>；未命中 → <code>stateMachine.onMismatch()</code>。</li><li>状态机为 <code>FIRING</code> 时构造 <code>AlarmMessage</code>（<code>buildAlarmMessage</code> <code>:399-416</code>，含 ruleName、message、period、tags、hooks、expression、<code>mqeMetricsSnapshot</code>）。</li><li>状态机为 <code>RECOVERED</code> 时构造 <code>AlarmRecoveryMessage</code>（包装上次的 <code>lastAlarmMessage</code>）。</li></ul><blockquote><p><strong>多 label 结果</strong><br />如果表达式结果是 labeled（如 <code>percentile{p='50,75'}</code>），任一 label 命中即整体命中（<code>RunningRule.java:434-464</code>，注释例子 <code>sum(percentile{p='50,75'} &gt; 1000) &gt;= 3</code>，P75 命中就算触发）。</p></blockquote><h3 id="hook-%E8%BE%93%E5%87%BA" tabindex="-1">Hook 输出</h3><p><code>NotifyHandler.init</code>（<code>:236-248</code>）注册所有回调：</p><ul><li><code>AlarmStandardPersistence</code>（来自 CoreModule，持久化告警到存储，<code>AlarmStandardPersistence.java:44</code>，<code>doAlarm</code> 写入告警记录，<code>doAlarmRecovery</code> 通过 <code>SourceReceiver</code> 发恢复事件）。</li><li>九种 hook：<code>WebhookCallback</code>、<code>GRPCCallback</code>、<code>SlackhookCallback</code>、<code>WechatHookCallback</code>、<code>DingtalkHookCallback</code>、<code>FeishuHookCallback</code>、<code>WeLinkHookCallback</code>、<code>PagerDutyHookCallback</code>、<code>DiscordHookCallback</code>。</li></ul><p><code>WebhookCallback</code>（<code>webhook/WebhookCallback.java:39</code>）继承 <code>HttpAlarmCallback</code>，<code>doAlarmCallback</code>（<code>:44-68</code>）：</p><ul><li>按 <code>messages</code> 上的 hooks 分组（<code>groupMessagesByHook</code>）。</li><li>对每个 hook 找对应 <code>WebhookSettings</code>，取 urls（恢复用 <code>recoveryUrls</code>），<code>post(URI.create(url), gson.toJson(messages), headers)</code>。</li><li><code>headers</code> 支持自定义（如 <code>Authorization: Bearer ...</code>）。</li></ul><p>每种 hook 有独立 <code>*Settings</code> + <code>*HookCallback</code>（如 <code>dingtalk/DingtalkSettings.java</code> + <code>DingtalkHookCallback.java</code>，支持 <code>secret</code> 签名、<code>text-template</code>/<code>recovery-text-template</code> 文案模板，<code>{name}</code> 等占位符由 <code>AlarmMessageFormatter</code> 替换）。</p><p>Hook 类型枚举 <code>AlarmHooksType</code>：<code>webhook</code>、<code>gRPC</code>、<code>slack</code>、<code>wechat</code>、<code>dingtalk</code>、<code>feishu</code>、<code>welink</code>、<code>pagerduty</code>、<code>discord</code>。</p><blockquote><p><strong>Hook 路由规则</strong><br />规则没指定 <code>hooks</code> 字段时，用所有 <code>is-default: true</code> 的 hook（<code>RulesReader.java:126-128</code> 的 <code>defaultHooks</code>）；指定了就只用指定的，且会校验 hook 名合法性。</p></blockquote><h3 id="%E5%91%8A%E8%AD%A6%E4%B8%8E-mqe-%E7%9A%84%E5%85%B3%E7%B3%BB" tabindex="-1">告警与 MQE 的关系</h3><p>当前版本告警系统的核心设计——<strong>告警规则表达式直接复用 MQE 引擎</strong>：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/06-%E6%9F%A5%E8%AF%A2%E5%B1%82%E4%B8%8E%E5%91%8A%E8%AD%A6.md_6.png" alt="图7" /></p><ol><li><strong>规则加载期校验</strong>：<code>AlarmRule.setExpression</code> 用 <code>AlarmMQEVerifyVisitor</code> 预编译，提取 <code>includeMetrics</code> 和 <code>maxTrendRange</code>，校验是布尔单值结果。</li><li><strong>运行期求值</strong>：<code>RunningRule.Window.isMatch</code>（<code>:418-474</code>）用 <code>AlarmMQEVisitor</code>（<code>expr/rt/AlarmMQEVisitor.java:53</code>），继承 <code>MQEVisitorBase</code>，但<strong>数据来源不同</strong>：查询侧 <code>MQEVisitor</code> 从存储读指标，而 <code>AlarmMQEVisitor</code> 从 <code>Window</code> 里已缓存的 <code>LinkedList&lt;Map&lt;String, Metrics&gt;&gt;</code> 取值，即告警在流处理内存里计算，不再回查存储。<code>Step.MINUTE</code> 固定（<code>:74</code>）。</li></ol><p>为什么告警在内存 Window 算而不是回查存储？因为告警是高频检查（�� 10 秒一次），如果每次都回查存储，等于每 10 秒对所有告警规则跑一轮查询，存储压力巨大。而分钟级指标本来就在流处理链里经过 <code>AlarmNotifyWorker</code>，顺手塞进 Window 是零成本——数据已经在内存里了，再用它算一次表达式即可。代价是 Window 占内存（每规则每实体一个 LinkedList），所以 period 不能设太大，默认 1 分钟窗口长度有限。<br />3. <strong>快��</strong>：<code>AlarmMQEVisitor</code> 产出 <code>mqeMetricsSnapshot</code>（<code>JsonObject</code>，<code>:66</code>），存到 <code>Window.mqeMetricsSnapshot</code>（<code>:287</code>），再塞进 <code>AlarmMessage.mqeMetricsSnapshot</code>（<code>AlarmMessage.java:52</code>）。对应 GraphQL <code>AlarmSnapshot</code> 类型，让 UI 展示触发告警时的指标曲线。<br />4. <strong>同一套语法</strong>：<code>execExpression</code>（GraphQL 查询）和告警规则表达式用同一个 ANTLR 文法 <code>mqe.rt.grammar.MQELexer/MQEParser</code>——你可以在 UI 上先用 <code>execExpression</code> 调试表达式、确认结果，再原样写进 <code>alarm-settings.yml</code> 当告警规则。这是 SkyWalking 9.5.0+ 的重要改进。</p><h3 id="alarmkernel-%C2%B7-%E8%BF%90%E8%A1%8C%E6%97%B6%E8%A7%84%E5%88%99%E9%87%8D%E7%BD%AE" tabindex="-1">AlarmKernel · 运行时规则重置</h3><p><code>AlarmKernel</code>（<code>AlarmKernel.java:47</code>）实现 <code>AlarmKernelService.reset(affectedMetricNames)</code>：当某指标的语义在运行时变化（如 OAL 规则热更新改变了指标定义），调用方传入受影响的指标名，<code>AlarmKernel</code> 遍历所有 <code>RunningRule</code>，凡是 <code>includeMetrics</code> 命中受影响指标的，调 <code>rule.resetWindows</code>（<code>:217-220</code>）清空所有 Window 的累积状态和状态机，避免旧语义下的告警状态污染新语义。</p><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><h3 id="%E6%9F%A5%E8%AF%A2%E5%B1%82" tabindex="-1">查询层</h3><ul><li><code>oap-server/server-core/.../query/QueryModule.java:26</code></li><li><code>oap-server/server-query-plugin/query-graphql-plugin/.../GraphQLQueryProvider.java:69</code></li><li><code>oap-server/server-query-plugin/query-graphql-plugin/.../GraphQLQueryHandler.java:41</code></li><li><code>oap-server/server-query-plugin/query-graphql-plugin/src/main/resources/query-protocol/common.graphqls</code></li><li><code>oap-server/server-query-plugin/query-graphql-plugin/src/main/resources/query-protocol/metrics-v3.graphqls:116</code></li><li><code>oap-server/server-query-plugin/query-graphql-plugin/.../resolver/MetricsExpressionQuery.java:44</code></li><li><code>oap-server/server-query-plugin/query-graphql-plugin/.../resolver/TraceQuery.java:49</code></li><li><code>oap-server/server-query-plugin/query-graphql-plugin/.../resolver/AlarmQuery.java:58</code></li><li><code>oap-server/server-query-plugin/promql-plugin/.../PromQLProvider.java:34</code> + <code>handler/PromQLApiHandler.java:116</code></li><li><code>oap-server/server-query-plugin/logql-plugin/.../LogQLProvider.java:33</code> + <code>handler/LogQLApiHandler.java:71</code></li><li><code>oap-server/server-query-plugin/traceql-plugin/.../TraceQLProvider.java:34</code></li><li><code>oap-server/server-query-plugin/zipkin-query-plugin/.../ZipkinQueryProvider.java:34</code> + <code>handler/ZipkinQueryHandler.java</code></li></ul><h3 id="%E5%91%8A%E8%AD%A6" tabindex="-1">告警</h3><ul><li><code>oap-server/server-core/.../alarm/AlarmModule.java:28</code></li><li><code>oap-server/server-alarm-plugin/.../AlarmModuleProvider.java:39</code></li><li><code>oap-server/server-alarm-plugin/.../RulesReader.java:53</code></li><li><code>oap-server/server-alarm-plugin/.../AlarmRule.java:53</code></li><li><code>oap-server/server-alarm-plugin/.../AlarmRulesWatcher.java:54</code></li><li><code>oap-server/server-alarm-plugin/.../NotifyHandler.java:57</code></li><li><code>oap-server/server-alarm-plugin/.../AlarmCore.java:41</code></li><li><code>oap-server/server-alarm-plugin/.../RunningRule.java:76</code>（含 <code>Window</code>、<code>AlarmStateMachine</code>）</li><li><code>oap-server/server-alarm-plugin/.../expr/rt/AlarmMQEVisitor.java:53</code></li><li><code>oap-server/server-alarm-plugin/.../expr/rt/AlarmMQEVerifyVisitor.java</code></li><li><code>oap-server/server-alarm-plugin/.../webhook/WebhookCallback.java:39</code></li><li><code>oap-server/server-alarm-plugin/src/test/resources/alarm-settings.yml</code>（真实规则示例）</li><li>流处理链入口：<code>oap-server/server-core/.../analysis/worker/MetricsStreamProcessor.java:317</code> + <code>AlarmNotifyWorker.java:30</code> + <code>AlarmEntrance.java:24</code></li><li>持久化回调：<code>oap-server/server-core/.../alarm/AlarmStandardPersistence.java:44</code></li><li>告警消息体：<code>oap-server/server-core/.../alarm/AlarmMessage.java:39</code></li></ul><blockquote><p><strong>存疑项</strong></p><ul><li><code>MetricsConverter</code>：在全仓库无匹配，本版本由 <code>NotifyHandler</code> 按 scope 构造 <code>MetaInAlarm</code> 取代。</li><li><code>alarm-rule.yml</code>：源码加载的是 <code>alarm-settings.yml</code>，无 <code>alarm-rule.yml</code>。</li></ul></blockquote><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5" tabindex="-1">接下来</h2><p>OAP 怎么多节点协调、怎么动态改配置、怎么自监控、AI 基线怎么接？详见 07-集群协调与动态配置。</p><blockquote><p><strong>心智模型</strong><br />把查询层想成&quot;展厅的多种导览方式&quot;：GraphQL 是原生导览员（按你点哪个展品讲哪个），PromQL/LogQL/TraceQL 是会多国语言的导览员（让 Grafana 这种外国游客也能听懂），Zipkin 是给老客户用的旧导览图（兼容老接口）。告警系统是&quot;质检报警器&quot;：指标落盘时顺带把样本塞进滑窗，每 10 秒巡检一次，状态机决定&quot;刚发现问题→报警&quot;“问题还在但静默期→不重复报”“问题消失→观察一阵→发恢复通知”，最后通过九种通知渠道（钉钉/Slack/Webhook…）发出去。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[六、Skywalking存储层]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/skywalking-cun-chu-ceng" />
                <id>tag:https://lymboy.com,2025-11-02:skywalking-cun-chu-ceng</id>
                <published>2025-11-02T21:50:10+08:00</published>
                <updated>2025-11-02T21:50:10+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E5%AD%98%E5%82%A8%E5%B1%82" tabindex="-1">存储层</h1><blockquote><p><strong>这一篇解决什么</strong><br />指标算好了，怎么存。OAP 用一套 <code>Model</code> + <code>StorageBuilder</code> + <code>DAO</code> 抽象把存储后端隔离开，同一份逻辑跑在自研 BanyanDB、Elasticsearch、关系库上。这篇讲存储抽象、BanyanDB 原生数据模型（为什么它是首选）、ES 怎么用文档库模拟指标聚合、JDBC 怎么用关系表模拟时序，最后对比三套实现。读完应该能回答：BanyanDB 凭什么更适合，ES 和 JDBC 各自怎么妥协。</p></blockquote><hr /><h2 id="%E5%AD%98%E5%82%A8%E5%B1%82%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" tabindex="-1">存储层解决什么问题</h2><p>OAP 存储层把流式分析产出的指标、原始记录（Trace/Log/Event）、管理数据、非流式配置数据持久化到外部数据库，查询时按时间范围、维度、TopN 读回来。它要同时伺候两类截然相反的负载：写密集的时序聚合（每分钟/小时/天滚动写入大量指标点），读密集的关联查询（按 traceId、service、endpoint 倒排检索原始 trace）。</p><blockquote><p><strong>为什么插件化</strong><br />OAP 核心只定义&quot;要存什么、要查什么&quot;——<code>StorageModule.services()</code> 列出 30+ 个接口（<code>oap-server/server-core/.../storage/StorageModule.java:69</code>），具体&quot;怎么存&quot;交给各 <code>ModuleProvider</code> 实现。核心和存储引擎彻底解耦，同一份分析逻辑跑在 BanyanDB/ES/MySQL 上，运维只改 <code>application.yml</code> 的 <code>storage.selector</code>。</p><p>BanyanDB 首选的原因就一句话：SkyWalking 的核心数据形态在它那里都是一等公民。<code>Measure</code>（指标聚合，按 seriesID + timestamp 自动覆盖合并）、<code>stream</code>（原始事件）、<code>trace</code>、<code>property</code>、<code>group</code>（TTL 与降采样隔离）、<code>TopNAggregation</code>（服务端预聚合 TopN）、<code>IndexRule</code>（显式倒排）——这六样恰好一一对应 SkyWalking 的核心数据形态。ES 是通用文档库，没有原生 measure，只能&quot;按时间分物理索引 + 文档 upsert + 查询期 aggregation&quot;硬模拟；JDBC 用关系表 + 按天分表模拟。BanyanDB 把这些语义下沉到存储引擎，省掉了 ES/JDBC 的大量模拟开销。</p></blockquote><hr /><h2 id="%E5%AD%98%E5%82%A8%E6%8A%BD%E8%B1%A1%E4%BD%93%E7%B3%BB" tabindex="-1">存储抽象体系</h2><h3 id="storagemodule-%E5%AE%9A%E4%B9%89%E7%9A%84%E8%83%BD%E5%8A%9B%E5%A5%91%E7%BA%A6" tabindex="-1">StorageModule 定义的能力契约</h3><p><code>StorageModule</code>（<code>oap-server/server-core/.../storage/StorageModule.java:60</code>）的 <code>services()</code> 返回一组接口，分四类：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/05-%E5%AD%98%E5%82%A8%E5%B1%82.md_0.png" alt="图1" /></p><p>写入/安装类（<code>StorageModule.java:71-113</code>）：<code>StorageBuilderFactory</code>、<code>StorageDAO</code>（DAO 工厂）、<code>IBatchDAO</code>（批量异步写入，被 <code>PersistenceTimer</code> 驱动）、<code>IHistoryDeleteDAO</code>（TTL 删除）、<code>ModelInstaller</code>（DDL 安装器，把 Model 创建成后端物理 schema）、<code>RuntimeRuleManagementDAO</code>（运行时规则持久化）。</p><p>查询类：<code>ITopologyQueryDAO</code>、<code>IMetricsQueryDAO</code>、<code>ITraceQueryDAO</code>、<code>IMetadataQueryDAO</code>、<code>IAggregationQueryDAO</code>、<code>IAlarmQueryDAO</code>、<code>IRecordQueryDAO</code>、<code>ILogQueryDAO</code>、<code>IEventQueryDAO</code>、<code>IBrowserLogQueryDAO</code>、<code>IZipkinQueryDAO</code>、各 profiling 查询 DAO、<code>ITagAutoCompleteQueryDAO</code>、<code>IHierarchyQueryDAO</code>——每个都是独立接口。</p><blockquote><p><strong>写入型 DAO vs 查询型 DAO</strong></p><ul><li>写入型（Metrics/Record/NoneStream/Management，被 <code>PersistenceTimer</code> 驱动）关心怎么把对象转成后端写入请求。</li><li>查询型（<code>storage/query/I*QueryDAO</code>，被 GraphQL query 模块调用）关心怎么把后端查询翻译成 OAP 返回结构。</li></ul></blockquote><h3 id="storagedao-%C2%B7-dao-%E5%B7%A5%E5%8E%82" tabindex="-1">StorageDAO · DAO 工厂</h3><p><code>StorageDAO</code>（<code>storage/StorageDAO.java:27</code>）是 DAO 工厂接口，四个方法：</p><pre><code class="language-">IMetricsDAO  newMetricsDao(StorageBuilder)IRecordDAO   newRecordDao(StorageBuilder)INoneStreamDAO newNoneStreamDao(StorageBuilder)IManagementDAO newManagementDao(StorageBuilder)</code></pre><p>OAP 核心按数据类型向存储要 DAO（<code>storageDAO.newMetricsDao(builder)</code>），存储端拿对应 <code>StorageBuilder</code> 返回自己的实现。各后端实现：BanyanDB（<code>BanyanDBStorageDAO.java:39</code>）、ES（<code>StorageEsDAO</code>）、JDBC（<code>JDBCStorageDAO</code>）。</p><h3 id="%E4%BA%94%E5%A4%A7%E6%A0%B8%E5%BF%83-dao-%E6%8E%A5%E5%8F%A3" tabindex="-1">五大核心 DAO 接口</h3><table><thead><tr><th>接口</th><th>职责</th><th>关键方法</th></tr></thead><tbody><tr><td><code>IMetricsDAO</code>（<code>IMetricsDAO.java:34</code>）</td><td>指标，唯一支持&quot;读改写&quot;</td><td><code>multiGet</code> 读旧值、<code>prepareBatchInsert</code>/<code>prepareBatchUpdate</code>、<code>isExpiredCache</code></td></tr><tr><td><code>IRecordDAO</code>（<code>IRecordDAO.java:29</code>）</td><td>原始记录只写不更新</td><td><code>prepareBatchInsert</code>（无 update）</td></tr><tr><td><code>INoneStreamDAO</code>（<code>INoneStreamDAO.java:28</code>）</td><td>非流式配置类</td><td><code>insert</code>（同步）</td></tr><tr><td><code>IManagementDAO</code>（<code>IManagementDAO.java:28</code>）</td><td>管理数据</td><td><code>insert</code>（同步）</td></tr><tr><td><code>IHistoryDeleteDAO</code>（<code>IHistoryDeleteDAO.java:28</code>）</td><td>TTL 删除</td><td><code>deleteHistory(model, timeBucketColumnName, ttl)</code></td></tr></tbody></table><blockquote><p><strong>IMetricsDAO 为什么有 multiGet</strong><br />指标是增量聚合——新数据来了要先读旧值、叠加、写回。<code>multiGet</code>（<code>IMetricsDAO.java:43</code>）按指标 ID 批量读回已有值。<code>prepareBatchInsert</code>（<code>:51</code>）转成 <code>InsertRequest</code>，<code>prepareBatchUpdate</code>（<code>:59</code>）转成 <code>UpdateRequest</code>。<code>isExpiredCache</code>（<code>:70</code>，default 方法）判断进程内缓存的指标是否超 TTL 该淘汰。</p></blockquote><h3 id="storagebuilder-%C2%B7-%E5%AE%9E%E4%BD%93%E2%86%94%E5%AD%98%E5%82%A8%E4%BA%92%E8%BD%AC" tabindex="-1">StorageBuilder · 实体↔存储互转</h3><p><code>StorageBuilder&lt;T&gt;</code>（<code>storage/type/StorageBuilder.java:28</code>）定义双向转换：</p><ul><li><code>storage2Entity(Convert2Entity converter)</code>（<code>:35</code>）：从存储返回结构还原成 OAP 实体。</li><li><code>entity2Storage(T entity, Convert2Storage converter)</code>（<code>:44</code>）：把 OAP 实体塞进 converter，再 <code>obtain()</code> 出后端需要的结构。</li></ul><p><code>Convert2Entity</code>/<code>Convert2Storage</code> 是转换器接口，不同后端不同实现：ES 用 <code>Map&lt;String,Object&gt;</code>，BanyanDB 用 <code>MeasureWrite</code>/<code>DataPoint</code>，JDBC 用 <code>HashMap</code>。</p><blockquote><p><strong>StorageBuilderFactory · DSL 与后端的解耦点</strong><br /><code>StorageBuilderFactory</code>（<code>storage/StorageBuilderFactory.java:34</code>）让存储端可覆盖默认 builder。OAL/MAL 在编译期为每个指标类生成一个 <code>*Builder</code>，默认把字段塞进 HashMap。存储插件若需更原生格式（BanyanDB 要 tag/field 分离、ES 要 mapping 友好结构），就通过 <code>StorageBuilderFactory</code> 替换 builder。这是&quot;DSL 生成代码&quot;和&quot;后端原生格式&quot;的解耦点。</p></blockquote><h3 id="model-%2F-modelcolumn-%C2%B7-%E5%90%8E%E7%AB%AF%E4%B8%AD%E7%AB%8B%E7%9A%84%E8%A1%A8%E5%AE%9A%E4%B9%89" tabindex="-1">Model / ModelColumn · 后端中立的表定义</h3><p><code>Model</code>（<code>storage/model/Model.java:36</code>）是后端中立的&quot;表定义&quot;，核心字段：<code>name</code>、<code>columns</code>（<code>List&lt;ModelColumn&gt;</code>）、<code>scopeId</code>、<code>downsampling</code>（降采样粒度）、<code>superDataset</code>（超大数据集）、<code>streamClass</code>（判断 isMetric/isRecord/isTimeSeries）、<code>timeRelativeID</code>、<code>allowBootReshape</code>（<code>Model.java:51-53</code>），以及三个后端专属扩展 <code>sqlDBModelExtension</code>/<code>banyanDBModelExtension</code>/<code>elasticSearchModelExtension</code>（<code>:54-56</code>）。</p><blockquote><p><strong>插件化的关键设计点</strong><br /><code>Model</code> 后端中立，但通过三个 <code>*ModelExtension</code> 给每个后端留专属配置挂载位。<code>BanyanDBModelExtension</code>（<code>storage/model/BanyanDBModelExtension.java:33</code>）挂 <code>timestampColumn</code>（<code>:42</code>）、<code>traceIdColumn</code>（<code>:51</code>）、<code>indexMode</code>（<code>:80</code>）、<code>streamGroup</code>（<code>:84</code>）、<code>traceGroup</code>（<code>:88</code>）；<code>ElasticSearchModelExtension</code> 挂 ES 专属；<code>SQLDatabaseModelExtension</code> 挂附加表、复合索引。</p></blockquote><p><code>ModelColumn</code>（<code>storage/model/ModelColumn.java:29</code>）描述一列：<code>columnName</code>、<code>type</code>（<code>:31</code>）、<code>storageOnly</code>（只存不查，<code>:36</code>）、<code>indexOnly</code>（只查不存，与 storageOnly 互斥，<code>:42</code>）、<code>length</code>（<code>:46</code>）。<code>byte[]</code> 和 <code>DataTable</code>（指标多值哈希）强制 <code>storageOnly=true</code>（<code>:86-87</code>）——这类字段永远不能做查询条件。</p><h3 id="modelinstaller-%C2%B7-%E5%9B%9B%E7%A7%8D%E5%AE%89%E8%A3%85%E7%AD%96%E7%95%A5" tabindex="-1">ModelInstaller · 四种安装策略</h3><p><code>ModelInstaller</code>（<code>storage/model/ModelInstaller.java:45</code>）订阅 <code>ModelRegistry</code> 的 <code>whenCreating</code>（<code>:50</code>）/<code>whenRemoving</code>（<code>:172</code>）事件——每注册一个 Model 触发后端建表。<code>whenCreating</code> 实现四种策略：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/05-%E5%AD%98%E5%82%A8%E5%B1%82.md_1.png" alt="图2" /></p><p>抽象方法 <code>isExists</code>/<code>createTable</code>/<code>dropTable</code> 由各后端实现。<code>dropTable</code> 默认空实现（<code>ModelInstaller.java:273,282-283</code>）——只有 BanyanDB（每逻辑模型一张 measure/stream）需要真删，JDBC/ES 是 append-only，模型移除时底层表/索引保留。</p><hr /><h2 id="banyandb-%E5%AD%98%E5%82%A8%EF%BC%88%E9%A6%96%E9%80%89%EF%BC%89" tabindex="-1">BanyanDB 存储（首选）</h2><h3 id="banyandb-%E6%98%AF%E4%BB%80%E4%B9%88" tabindex="-1">BanyanDB 是什么</h3><p>BanyanDB 是 SkyWalking 社区自研的时序 + 链路存储引擎，通过 gRPC 提供五类顶层资源（<code>MetadataRegistry.parseMetadata</code> <code>MetadataRegistry.java:808</code> 的 <code>Kind</code> 分支，<code>:1027</code> 定义 <code>MEASURE, STREAM, PROPERTY, TRACE</code>）：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/05-%E5%AD%98%E5%82%A8%E5%B1%82.md_2.png" alt="图3" /></p><ul><li><strong>measure</strong>（<code>Kind.MEASURE</code>）：指标聚合表，按 <code>seriesID</code>（一组 tag）+ timestamp 唯一定位，同 seriesID 同 timestamp 的新写覆盖/合并旧值——这正是 SkyWalking 指标增量聚合所需的原生语义。</li><li><strong>stream</strong>（<code>Kind.STREAM</code>）：原始事件流（普通 Record、Log、浏览器错误日志），按 timestamp 顺序追加。</li><li><strong>trace</strong>（<code>Kind.TRACE</code>）：链路，带 span 父子关系与 traceId 索引，是 stream 的特化。</li><li><strong>property</strong>（<code>Kind.PROPERTY</code>）：元数据键值（网络别名、服务标签），无时间维度。</li><li><strong>TopNAggregation</strong>：挂在 measure 上的服务端预聚合规则，按指定 field 排序取 Top-N。</li></ul><p>资源按 <strong>group</strong> 组织——group 是物理与配置隔离单位，每个 group 有独立 <code>IntervalRule</code>（生命周期 TTL）和 catalog。</p><h3 id="measure-vs-stream-%C2%B7-%E5%AF%B9%E6%96%B0%E6%89%8B%E6%9C%80%E5%85%B3%E9%94%AE%E7%9A%84%E5%8C%BA%E5%88%86" tabindex="-1">measure vs stream · 对新手最关键的区分</h3><blockquote><p><strong>财务类比</strong><br />measure 像&quot;每日汇总账&quot;——每天一行，第二天重算就覆盖。stream 像&quot;流水单&quot;——每笔交易一行，只增不改。BanyanDB 给这两种负载分别造了原生结构，ES 只有一种&quot;文档&quot;，两种都得用文档模拟，效率自然差。</p></blockquote><table><thead><tr><th></th><th>measure（指标）</th><th>stream（原始记录）</th></tr></thead><tbody><tr><td>对应 OAP 数据</td><td><code>Metrics</code> 子类（<code>ServiceAvgRespTime</code>）</td><td><code>Record</code> 子类（trace、log）</td></tr><tr><td>主键</td><td>seriesID（维度标签）+ timestamp</td><td>timestamp 顺序</td></tr><tr><td>更新语义</td><td>同 key 同时间覆盖合并</td><td>只追加不更新</td></tr><tr><td>查询方式</td><td>tag 列做 seriesID/过滤</td><td>倒排 IndexRule + 时间扫描</td></tr><tr><td>TopN 支持</td><td>服务端 <code>TopNAggregation</code> 预聚合</td><td>不支持服务端 TopN</td></tr></tbody></table><p>measure 对应 SkyWalking <code>Metrics</code> 子类。每分钟一个数据点，由 <code>(维度标签 = seriesID, 时间戳)</code> 唯一标识。同一分钟同一服务又有新数据，OAP 先 <code>multiGet</code> 读旧值、叠加、再 update（<code>BanyanDBMetricsDAO.multiGet</code> <code>measure/BanyanDBMetricsDAO.java:87</code>）。measure 用 tag 列做 seriesID 与过滤，用 field 列存数值（<code>MetadataRegistry.registerMeasureModel</code> <code>MetadataRegistry.java:189</code>，seriesID 列在 <code>:195</code>，indexMode 在 <code>:198</code> 强制 seriesID 含 <code>ID</code>）。field 不能做查询条件，<code>isIndexMode</code> 时甚至禁止有 field（<code>:234-239</code>）。</p><p>stream 对应 <code>Record</code> 子类。每个事件一条记录，只追加不更新。查询靠倒排 <code>IndexRule</code> + 时间范围扫描。不支持服务端 TopN 预聚合——<code>BanyanDBIndexInstaller.java:232</code> 注释写得很直白：“Stream not support server side TopN pre-aggregation”。</p><h3 id="banyandb-%E7%9A%84%E7%89%A9%E7%90%86%E5%AD%98%E5%82%A8%EF%BC%9A%E5%88%97%E5%AD%98%E3%80%81gorilla-%E7%BC%96%E7%A0%81%E3%80%81seriesid%E3%80%81segment-ttl" tabindex="-1">BanyanDB 的物理存储：列存、Gorilla 编码、SeriesID、segment TTL</h3><blockquote><p><strong>这部分讲的是 BanyanDB 服务端引擎的物理结构</strong><br />BanyanDB 服务端不在 SkyWalking 仓库里（OAP 只带 client proto，见 <code>.gitmodules</code> 的 <code>skywalking-banyandb-client-proto</code> 子模块）。下面这些是 BanyanDB 公开设计文档描述的物理层，OAP 插件层看不到也不需要关心——但它解释了&quot;为什么 BanyanDB 比 ES 更适合时序&quot;。能对照到的 OAP 侧证据我会标出来。</p></blockquote><p><strong>列存（columnar storage）</strong>——行存把一条记录的所有字段连续存在一起，读整行快但读单列要拖出整条；列存把同一列的值连续存在一起。时序负载的查询绝大多数只读少数几列（“给我某服务最近 1 小时的 avg response time”，只要 <code>service</code> + <code>value</code> 两列），列存让读盘只碰需要的列，IO 放大极小。ES 的底层 Lucene segment 本质是行式文档 + 倒排，读单列也要解整条 <code>_source</code>（除非用 doc_values 冷冻），列存优势拿不到。</p><p><strong>Gorilla 编码 / delta-of-delta</strong>——Facebook 2015 年论文《Gorilla: A Fast, Scalable, In-Memory Time Series Database》提出的浮点数压缩。两个机制叠加：</p><ol><li><strong>delta-of-delta 压缩时间戳</strong>。相邻点的时间戳通常等间隔（每分钟一个点），差值是常数。对差值再求差（delta-of-delta），等间隔序列就变成一串 0。Gorilla 用一个变长 bit 编码：0 存 1 个 bit，小扰动存 2-4 bit，大跳变才存更多。近乎 1 bit/点不是夸张，是等间隔序列的真实结果。</li><li><strong>XOR 编码浮点值</strong>。相邻采样点的浮点值通常很接近（响应时间不会突变），用相邻值的按位异或（XOR）结果存——XOR 后大多高位是 0，只存有效位。叠加后一个点平均 1.37 byte（论文实测），远小于一个 double 8 byte。</li></ol><p>BanyanDB 在 measure 的 field 列上用这类编码。ES 存指标要进 <code>_source</code> JSON（每个数值都是完整 ASCII 文本 + 字段名重复），再加倒排索引和 doc_values，膨胀一个数量级很正常。这是 BanyanDB 写入和存储成本碾压 ES 的根本原因。</p><p><strong>SeriesID</strong>——一组 tag 的组合（如 <code>service=order-svc, endpoint=/api/pay</code>）算出一个唯一 ID，作为 measure 的行键。同 seriesID 同 timestamp 自动覆盖合并，正是 SkyWalking 指标增量聚合所需。OAP 侧的对应逻辑在 <code>MetadataRegistry.parseEntityNames</code>（<code>:652-658</code> 收集 seriesID 列）和 <code>BanyanDBMetricsDAO.multiGet</code>（<code>BanyanDBMetricsDAO.java:98</code> 读 <code>ext.isSeriesID()</code> 列）。</p><p><strong>segment TTL</strong>——BanyanDB 把数据按时间切成 segment（一段时间的列存块），每个 group 的 <code>IntervalRule</code> 定义 segment 生命周期。过期整 segment 直接删文件，OAP 端什么都不用做。对比 ES 要 OAP 端定时扫描删整个物理索引、JDBC 要定时 drop 整张物理表——BanyanDB 把 TTL 变成存储原生能力。</p><p><strong>TopN 预聚合</strong>——<code>MetadataRegistry.parseTopNSpecs</code>（<code>MetadataRegistry.java:330</code>）读 <code>bydb-topn.yml</code> 配置，对单值指标（<code>ValueDataType.COMMON_VALUE</code>，<code>:340</code>）创建 <code>TopNAggregation</code> proto，指定 <code>sourceMeasure</code>/<code>fieldName</code>/<code>fieldValueSort</code>/<code>countersNumber</code>/<code>group_by_tag_names</code>（<code>:348-364</code>）。TopN 查询由 BanyanDB 服务端预聚合返回，OAP 不必全扫 measure。<code>BanyanDBStorageProvider.notifyAfterCompleted</code>（<code>BanyanDBStorageProvider.java:259</code>）还会 <code>cleanupUnusedTopNRules</code>（<code>:277</code>）清理多余规则。ES 没这个能力，TopN 全靠查询期 <code>terms</code> aggregation 全扫。</p><h3 id="group-%2F-index-%2F-property-%2F-tag-%E5%88%97%E6%A8%A1%E5%9E%8B" tabindex="-1">group / index / property / tag 列模型</h3><blockquote><p><strong>倒排索引通俗解释</strong><br />正排是&quot;给我 ID 为 X 的记录&quot;（按主键找内容）；倒排是&quot;给我所有 service=order-service 的 trace&quot;——先在&quot;service → ID 列表&quot;的索引里查到候选 ID，再回表取内容。BanyanDB 的 <code>IndexRule</code> 就是显式声明&quot;我要在哪个 tag 上建倒排&quot;，没声明则该 tag 只能顺序扫描，查询慢。</p></blockquote><ul><li><strong>group</strong>：物理隔离 + TTL 单位。group 名按数据语义固定（<code>MetadataRegistry.parseMetadata</code> <code>MetadataRegistry.java:808-930</code>）：指标按降采样粒度分 <code>METRICS_MINUTE</code>/<code>METRICS_HOUR</code>/<code>METRICS_DAY</code>（<code>:898,910,922</code>），元数据归 <code>PROPERTY</code> group（<code>:813</code>），Trace 按 <code>TraceGroup.TRACE</code>/<code>ZIPKIN_TRACE</code>（<code>:828,837</code>），Stream 按 <code>StreamGroup.RECORDS</code>/<code>RECORDS_LOG</code>/<code>RECORDS_BROWSER_ERROR_LOG</code>（<code>:871,853,862</code>）。</li><li><strong>tag</strong>：可被索引的列，组成 <code>seriesID</code> 或充当 <code>IndexRule</code> 索引列。</li><li><strong>field</strong>：仅 measure 有，存数值，不可查询。</li><li><strong>IndexRule + IndexRuleBinding</strong>：显式倒排索引——先定义 <code>IndexRule</code>（某 tag 上的索引类型，如 INVERTED/BLOOM），再用 <code>IndexRuleBinding</code> 绑到���体 measure/stream（<code>BanyanDBIndexInstaller.java:212-214,227-229,242-246</code>）。<code>defineIndexRule</code>/<code>defineIndexRuleBinding</code> 在 createTable 时调用（<code>:387-391,413-416,442-446</code>）。</li><li><strong>property</strong>：<code>registerPropertyModel</code>（<code>MetadataRegistry.java:255</code>），无时间维度，纯 tag 键值表。</li></ul><h3 id="model-%E2%86%92-schema-%E6%98%A0%E5%B0%84" tabindex="-1">Model → schema 映射</h3><p><code>BanyanDBStorageProvider</code>（<code>storage-banyandb-plugin/.../BanyanDBStorageProvider.java:111</code>）在 <code>prepare()</code> 注册全部 DAO 实现（<code>:141</code>），<code>start()</code> 连接 client 并 <code>modelInstaller.start()</code>（<code>:245</code>），然后 <code>ModelRegistry.addModelListener(modelInstaller)</code>（<code>:247</code>）——每个 Model 注册都回调 installer。</p><p><code>BanyanDBIndexInstaller.isExists</code>/<code>createTable</code>（<code>BanyanDBIndexInstaller.java:155,367</code>）按 <code>Model</code> 性质分流（<code>:200-258</code>）：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/05-%E5%AD%98%E5%82%A8%E5%B1%82.md_3.png" alt="图4" /></p><p>具体映射（<code>MetadataRegistry</code>）：</p><ul><li><code>registerStreamModel</code>（<code>:146</code>）：解析 seriesID 列（<code>:152</code>，必填）、tag 元数据、tagFamily、IndexRule，构造 <code>Stream</code> proto。</li><li><code>registerMeasureModel</code>（<code>:189</code>）：解析 seriesID，若 <code>isIndexMode</code> 强制 seriesID 含 <code>ID</code>（<code>:198-201</code>）；解析 tag + field；设 <code>interval</code> = 降采样粒度（<code>:228</code> 附近）；indexMode 时 <code>setIndexMode(true)</code> 且禁止 field（<code>:234-239</code>）。</li><li><code>registerTraceModel</code>（<code>:269</code>）、<code>registerPropertyModel</code>（<code>:255</code>）。</li><li><code>parseMetadata</code>（<code>:808</code>）决定 group 归属 + Kind + TTL 配置——这是上面路由的源头。</li></ul><h3 id="topn-%2F-histogram-%E5%8E%9F%E7%94%9F%E8%83%BD%E5%8A%9B" tabindex="-1">TopN / Histogram 原生能力</h3><ul><li><strong>TopNAggregation</strong>：见上文 <code>parseTopNSpecs</code>。TopN 查询由服务端预聚合返回，OAP 不必全扫 measure。</li><li><strong>IndexRule / IndexRuleBinding</strong>：由 <code>BanyanDBExtension.shouldIndex()</code> 决定哪些 tag 建索引。</li><li><strong>Histogram</strong>：SkyWalking 的 <code>DataTable</code> 类型字段（P99/百分比指标）在 BanyanDB 以 measure field 存储，按 bucket 维度表达。具体 Histogram 原生结构在 BanyanDB 服务端，OAP 插件层通过 measure field + value 列表达。</li></ul><h3 id="schemawatcher-%C2%B7-mod_revision-fence-%E6%9C%BA%E5%88%B6" tabindex="-1">SchemaWatcher · mod_revision fence 机制</h3><p>这是 BanyanDB 与 ES/JDBC 最大的架构差异。</p><blockquote><p><strong>为什么需要 schema fence</strong><br />BanyanDB 的 schema 存在它自己的 <code>_schema</code> 存储，<strong>异步</strong>传播到每个数据节点的内存缓存。DDL（Create/Update/Delete）只发给 schema-server 节点并<strong>立即返回一个 <code>mod_revision</code></strong>（现为 <code>time.Now().UnixNano()</code> 时间戳，删除未记录 tombstone 时为 0），<strong>不等数据节点</strong>。</p><p>若数据写入时 schema 还没传播到该数据节点，会被丢弃（<code>cannot find measure definition</code>，日志记录后跳过）。</p><p>所以 OAP 每次 DDL 后必须 fence（栅栏等待）。</p></blockquote><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/05-%E5%AD%98%E5%82%A8%E5%B1%82.md_4.png" alt="图5" /></p><p><code>BanyanDBIndexInstaller.fenceOnRevision</code>（<code>BanyanDBIndexInstaller.java:289</code>）调 <code>client.getSchemaWatcher().awaitRevisionApplied(maxRev, timeout)</code>，阻塞直到所有存活数据节点的 <code>notifiedModRevision</code> 水位达到 <code>maxRev</code>。</p><p>对返回 <code>mod_revision==0</code> 的删除，改用 <code>awaitSchemaDeleted(SchemaKey, timeout)</code>（基于 key 消失等待）——因为 revision-based fence 看不见没留 tombstone 的删除（<code>BanyanDBIndexInstaller.java:582-619</code>，<code>SchemaWatcher.java:109</code>）。</p><p><code>SchemaWatcher</code>（<code>server-library/library-banyandb-client/.../client/SchemaWatcher.java:60</code>）封装三个 gRPC（<code>SchemaBarrierService</code>）：<code>awaitRevisionApplied</code>（<code>:74</code>）、<code>awaitSchemaApplied</code>（<code>:92</code>，按 key 列表）、<code>awaitSchemaDeleted</code>（<code>:109</code>）。</p><p>fence 尽力而为：单次预算 2s（<code>FENCE_TIMEOUT</code>，<code>:268-275</code>），超时只记 WARN 列出 laggards 并继续（<code>:332-334</code>），非硬保证。批量运行时规则 apply 时 fence 延迟一次性——不在每个资源 fence，而是注册 flush 闭包，apply 结束后用累计 <code>maxModRevision</code> 一次 fence（<code>:292-301</code> 的 deferFence 闭包，实际执行在 <code>doDeferredFence</code> <code>:322-343</code>，用 opt 配置的更长 timeout）。</p><blockquote><p><strong>indexMode 与 allowBootReshape</strong></p><ul><li><strong>indexMode</strong>（<code>BanyanDBModelExtension.isIndexMode()</code>，<code>BanyanDBModelExtension.java:80</code>）：10.3.0 起，installer 自动给 indexMode measure 建一个虚拟 String tag <code>id</code> 作为 seriesID（<code>BanyanDBMetricsDAO.java:90-91,113-118</code>）。这种 measure 没有数值 field，把整条指标当倒排索引用——&quot;按 ID 快速查到记录&quot;的场景。</li><li><strong>allowBootReshape</strong>（<code>Model.java:51-53</code>）：仅 BanyanDB，为 true 时允许 installer 启动时做纯加性的 schema 变更（加 tag/加 field）；JDBC/ES 忽略此标记（append-only 本就接受加列）。</li></ul></blockquote><h3 id="banyandb-%E6%89%B9%E9%87%8F%E5%86%99%E5%85%A5" tabindex="-1">BanyanDB 批量写入</h3><p><code>BanyanDBBatchDAO</code>（<code>storage-banyandb-plugin/.../BanyanDBBatchDAO.java:37</code>）按请求类型分流到三个 BulkWriteProcessor：</p><table><thead><tr><th>请求类型</th><th>Processor</th></tr></thead><tbody><tr><td><code>BanyanDBStreamInsertRequest</code></td><td><code>StreamBulkWriteProcessor</code></td></tr><tr><td><code>BanyanDBMeasureInsertRequest</code></td><td><code>MeasureBulkWriteProcessor</code>（insert 回调 <code>onInsertCompleted</code>，<code>:82</code>）</td></tr><tr><td><code>BanyanDBMeasureUpdateRequest</code></td><td><code>MeasureBulkWriteProcessor</code></td></tr><tr><td><code>BanyanDBTraceInsertRequest</code></td><td><code>TraceBulkWriteProcessor</code></td></tr></tbody></table><p>三个 processor 懒初始化、各自加锁（<code>:97-128</code>），参数 <code>maxBulkSize</code>/<code>flushInterval</code>/<code>concurrency</code> 来自配置（<code>:47-51</code>）。这是 <code>PersistenceTimer.flush</code> 的真正执行端。</p><hr /><h2 id="elasticsearch-%E5%AD%98%E5%82%A8" tabindex="-1">Elasticsearch 存储</h2><h3 id="es-%E6%80%8E%E4%B9%88%E6%A8%A1%E6%8B%9F%E6%8C%87%E6%A0%87%E8%81%9A%E5%90%88" tabindex="-1">ES 怎么模拟指标聚合</h3><p>ES 是通用文档库，没有 measure 概念。OAP 用三招模拟：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/05-%E5%AD%98%E5%82%A8%E5%B1%82.md_5.png" alt="图6" /></p><ol><li><strong>时间分物理索引（rolling）</strong>：<code>TimeSeriesUtils</code>（<code>storage-elasticsearch-plugin/.../base/TimeSeriesUtils.java</code>）把逻辑表名 + 时间桶后缀拼成物理索引名。<code>writeIndexName(model, timeBucket)</code>（<code>:114</code>）按降采样粒度截取时间桶——Minute 取 <code>timeBucket/10000</code>（<code>:125</code>）、Hour 取 <code>/100</code>（<code>:123</code>）、Day 取 <code>timeBucket</code> 原值（<code>:127</code>）。注意：OAP 的 timeBucket 是 <code>yyyyMMddHHmm</code> 形态的 long， Minute 粒度要除 10000 丢掉时分、Hour 除 100 丢掉分。<code>DAY_STEP</code>（<code>:46</code>）控制几天合并一个物理索引，低流量场景设 <code>dayStep&gt;1</code> 让多个逻辑日落到一个物理索引，减少索引数量。</li><li><strong>文档 upsert（按 _id 覆盖）</strong>：<code>MetricsEsDAO.prepareBatchInsert</code>（<code>base/MetricsEsDAO.java:101</code>）把指标转成 <code>Map</code>，用 <code>IndexController.generateDocId</code>（<code>IndexController.java:80</code>）算出稳定 _id，写进对应时间物理索引。同 _id 的新写覆盖旧文档——模拟 measure 的&quot;同 key 同时间合并&quot;。但 ES 的 upsert 是 read-then-write，并发下要靠版本号或 retry，远不如 measure 的原生合并高效。</li><li><strong>查询期 aggregation</strong>：指标查询用 ES 的 <code>date_histogram</code> + <code>avg</code>/<code>sum</code> aggregation 在物理索引上聚合，而非像 BanyanDB 那样存储端就预聚合好。TopN 也靠查询期 <code>terms</code> aggregation，没有服务端预聚合。</li></ol><blockquote><p><strong>为什么 ES 不适合时序，一句话</strong><br />Lucene 为搜索而生，强在全文检索（日志 match query），但指标聚合要靠查询期扫全文档算、TTL 要靠 OAP 端定时删整个物理索引、TopN 要靠 <code>terms</code> 全扫。它没有列存、没有 Gorilla 编码、没有 measure 原生合并、没有 segment 级 TTL——这四样时序数据库的基本功它都没有，全靠 OAP 侧模拟补上。</p></blockquote><h3 id="index-%E6%A8%A1%E6%9D%BF%E3%80%81rolling%E3%80%81ttl-%E5%88%A0%E9%99%A4" tabindex="-1">index 模板、rolling、TTL 删除</h3><ul><li><strong>index 模板</strong>：<code>StorageEsInstaller</code>（<code>base/StorageEsInstaller.java:52</code>）<code>isExists</code>（<code>:86</code>）对时序表检查 <code>isExistsTemplate</code>（<code>:118</code>），对比 template 的 mappings 与 settings。<code>createMapping</code>/<code>createSetting</code> 生成 ES mapping 与 index settings，<code>ColumnTypeEsMapping</code>（<code>:55</code> 字段，<code>:69</code> 构造）负责 Java 类型→ES 类型映射。</li><li><strong>rolling</strong>：<code>IndexController</code>（<code>base/IndexController.java</code>）管理逻辑表名→物理表名映射。<code>LogicIndicesRegister</code>（<code>:136</code>）维护&quot;逻辑表→物理表&quot;目录，支持多个逻辑 metric 模型<strong>合并写��同一个物理索引</strong>——<code>METRICS_LOGIC_TABLE_NAME = &quot;metrics-all&quot;</code>（<code>:62</code>）。分钟/小时/天指标若配置合并，都进同一物理索引，靠 <code>metric_table</code> 列区分（<code>appendTableColumn</code> <code>:118-120</code>，列名常量 <code>METRIC_TABLE_NAME = &quot;metric_table&quot;</code> <code>:150</code>）。</li><li><strong>批量写入</strong>：<code>BatchProcessEsDAO</code>（<code>base/BatchProcessEsDAO.java:34</code>）用 ES <code>BulkProcessor</code>（<code>:35</code>），参数 <code>bulkActions</code>/<code>flushInterval</code>/<code>concurrentRequests</code>/<code>batchOfBytes</code>（<code>:36-39</code>）。</li><li><strong>TTL 删除（按时间删 index）</strong>：<code>HistoryDeleteEsDAO.deleteHistory</code>（<code>base/HistoryDeleteEsDAO.java:44</code>）算 deadline（<code>:57</code>），只对 <code>Minute</code> 降采样或 record 跑一次（<code>:48-55</code>，因为同日所有粒度在同一索引），取出别名下所有物理索引，用 <code>isolateTimeFromIndexName</code>（<code>TimeSeriesUtils.java:139</code>）解析索引名里的时间，早于 deadline 的 <code>deleteByIndexName</code>（<code>HistoryDeleteEsDAO.java:103-104</code>）。<strong>删的是整个物理索引而非文档</strong>，契合 ES 模型且高效。</li></ul><blockquote><p><strong>ES 的巧妙优化</strong></p><ul><li>多个降采样粒度合并写进同一物理索引，靠 <code>metric_table</code> 列区分，减少索引数量。</li><li>TTL 删整个物理索引（而非删文档），契合 ES 分索引模型。</li><li><code>dayStep</code> 让多个逻辑日合并一个物理索引，低流量场景省索引。</li></ul><p>这些都是为了弥补&quot;ES 没有原生时序聚合&quot;的妥协。</p></blockquote><h3 id="%E5%88%86%E8%AF%8D%E5%99%A8%E4%B8%8E-aggregation" tabindex="-1">分词器与 aggregation</h3><ul><li><strong>分词器</strong>：<code>AnalyzerSetting</code>（<code>base/AnalyzerSetting.java</code>）支持自定义 analyzer/tokenizer/filter/charFilter，通过 <code>ElasticSearchExtension</code> 在 ModelColumn 上声明（<code>needMatchQuery</code>），用于日志全文检索。</li><li><strong>aggregation</strong>：所有指标/TopN 聚合在查询期用 ES aggregation 完成（见上文第 3 点）。</li></ul><hr /><h2 id="jdbc-%E5%AD%98%E5%82%A8%EF%BC%88h2%2Fmysql%2Fpostgresql%EF%BC%89" tabindex="-1">JDBC 存储（H2/MySQL/PostgreSQL）</h2><h3 id="%E9%80%9A%E7%94%A8-jdbc-%E9%80%82%E9%85%8D" tabindex="-1">通用 JDBC 适配</h3><p><code>storage-jdbc-hikaricp-plugin</code> 用 HikariCP 连接池，一套 <code>common</code> 包适配多数据库，再在 <code>mysql</code>/<code>postgresql</code> 子包做方言特化（如 <code>PostgreSQLMetricsQueryDAO</code> 用 PG 特有 JSON/数组能力）。H2 是默认演示/测试后端。</p><h3 id="%E8%A1%A8%E7%BB%93%E6%9E%84%E7%94%9F%E6%88%90" tabindex="-1">表结构生成</h3><p><code>JDBCTableInstaller</code>（<code>common/JDBCTableInstaller.java:61</code>）是 JDBC 的 <code>ModelInstaller</code>：</p><ul><li><code>createTable(model)</code>（<code>:97</code>）算当天 dayTimeBucket，调 <code>createTable(model, timeBucket)</code>（<code>:103</code>）→ <code>createOrUpdateTable(table, columns, false)</code> + <code>createOrUpdateTableIndexes</code>。</li><li><code>createOrUpdateTable</code>（<code>:237</code>）：先查已有列，只加缺失列——因为某些 SQL 方言没有 <code>alter table add column if not exists</code>（<code>:238</code> 注释），用&quot;查后过滤&quot;兼容多数据库。</li><li><strong>按天分表</strong>：<code>TableHelper.getTableName</code> + timeBucket 拼 <code>表名_yyyyMMdd</code>，每天一张表。<code>additionalTables</code>（<code>:228-232</code>）支持附加表（如大数据列拆出去），同样按天分。</li><li><code>createOrUpdateTableIndexes</code>（<code>:138</code>）：对非 storageOnly 列建 <code>CREATE INDEX idx_&lt;hash&gt;</code>（<code>:145,159,182</code>），支持复合索引（<code>:201-212</code>）。</li></ul><h3 id="sql-%E7%94%9F%E6%88%90%E4%B8%8E%E6%89%B9%E9%87%8F%E6%8F%92%E5%85%A5" tabindex="-1">SQL 生成与批量插入</h3><ul><li><code>JDBCMetricsDAO</code>（<code>common/dao/JDBCMetricsDAO.java:40</code>）：<code>multiGet</code>（<code>:45</code>）按 ID 批查；<code>prepareBatchInsert</code>（<code>:56</code>）→ <code>getInsertExecutor</code>；<code>prepareBatchUpdate</code>（<code>:61</code>）→ <code>getUpdateExecutor</code>。</li><li><code>JDBCBatchDAO</code>（<code>common/dao/JDBCBatchDAO.java:41</code>）用 <code>BatchQueueManager</code> 建异步批量队列（<code>bufferSize=10000</code>、<code>maxIdleMs=20</code>）。<code>flush</code>（<code>:65</code>）把请求按 SQL 分组，用 <code>BatchSQLExecutor</code> 按 <code>maxBatchSqlSize</code> 批量执行 JDBC batch。</li></ul><h3 id="jdbc-ttl-%E5%88%A0%E9%99%A4%EF%BC%88%E6%8C%89%E5%A4%A9-drop-table%EF%BC%89" tabindex="-1">JDBC TTL 删除（按天 drop table）</h3><p><code>JDBCHistoryDeleteDAO.deleteHistory</code>（<code>common/dao/JDBCHistoryDeleteDAO.java:52</code>）：算时间范围，取出应保留的表，用 <code>conn.getMetaData().getTables</code> 查所有 <code>表名%</code> 物理表，过滤只匹配 <code>表名_\\\\d{8}</code> 的，对剩余 <code>drop table if exists</code>（<code>:89-92</code>）。额外表也按其 timeBucket 删。为下一天预建表（<code>:104-106</code>）。</p><hr /><h2 id="%E4%B8%89%E5%90%8E%E7%AB%AF%E5%AF%B9%E6%AF%94" tabindex="-1">三后端对比</h2><table><thead><tr><th>维度</th><th>BanyanDB</th><th>Elasticsearch</th><th>JDBC</th></tr></thead><tbody><tr><td><strong>指标聚合模型</strong></td><td>原生 measure（覆盖合并）</td><td>文档 upsert by _id 模拟</td><td>关系表行 + upsert</td></tr><tr><td><strong>原始记录模型</strong></td><td>原生 stream/trace</td><td>文档索引</td><td>关系表行</td></tr><tr><td><strong>元数据模型</strong></td><td>原生 property</td><td>文档索引（非时序）</td><td>关系表</td></tr><tr><td><strong>时序分片</strong></td><td>group（按降采样/TTL 分组）</td><td>按天物理索引（dayStep 可调）</td><td>按天物理表 <code>表名_yyyyMMdd</code></td></tr><tr><td><strong>TopN</strong></td><td>服务端 <code>TopNAggregation</code> 预聚合</td><td>查询期 ES <code>terms</code> aggregation</td><td>查询期 SQL <code>ORDER BY ... LIMIT</code></td></tr><tr><td><strong>倒排索引</strong></td><td>显式 <code>IndexRule</code>+<code>IndexRuleBinding</code></td><td>ES 原生倒排 + 自定义 analyzer</td><td>普通 <code>CREATE INDEX</code>（B-Tree）</td></tr><tr><td><strong>TTL 删除</strong></td><td>服务端 group <code>IntervalRule</code> 原生生命周期，OAP 端空实现</td><td>OAP 端按 deadline 删整个物理索引</td><td>OAP 端按 deadline drop 整张物理表 + 预建下一天</td></tr><tr><td><strong>DDL 同步性</strong></td><td>异步传播，需 <code>SchemaWatcher</code> fence on <code>mod_revision</code></td><td>协调节点同步生效，无需 fence</td><td>协调节点同步生效，无需 fence</td></tr><tr><td><strong>Schema 变更</strong></td><td>fence 等所有数据节点；shape mismatch 记 <code>SKIPPED_SHAPE_MISMATCH</code></td><td>对比 template mapping/settings，diff 报 WARN</td><td>查已有列，仅 ADD 缺失列</td></tr><tr><td><strong>批量写入</strong></td><td>三种 BulkWriteProcessor</td><td>ES <code>BulkProcessor</code></td><td><code>BatchQueue</code> + JDBC batch</td></tr><tr><td><strong>是否首选</strong></td><td>是</td><td>否（通用文档库，模拟 measure 有开销）</td><td>否（演示/测试/小规模）</td></tr></tbody></table><blockquote><p><strong>为什么 BanyanDB 更适合</strong><br />SkyWalking 核心数据形态在 BanyanDB 都是一等公民：</p><ul><li>measure 原生合并省掉 ES 的 upsert 模拟；</li><li>TopN 服务端预聚合省掉查询期全扫；</li><li>group + IntervalRule 让 TTL 由存储原生处理，省掉 OAP 端删索引/删表的定时扫描；</li><li>列存 + Gorilla 编码让写入和存储成本比 ES 低一个数量级；</li><li>显式 IndexRule 让索引精确可控。</li></ul><p>ES 为通用搜索而生，强在全文检索（日志 match query），但指标聚合、TTL、TopN 都得 OAP 侧模拟；JDBC 为关系数据而生，更不适合海量时序写入。三者映射同一套 <code>Model</code>/<code>ModelColumn</code>/<code>StorageBuilder</code> 抽象，运维只改 <code>application.yml</code> 即可切换，这正是存储层插件化的价值。</p></blockquote><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键类</th><th>路径</th></tr></thead><tbody><tr><td>核心抽象</td><td>StorageModule</td><td><code>oap-server/server-core/.../storage/StorageModule.java</code></td></tr><tr><td></td><td>StorageDAO / IBatchDAO / IMetricsDAO / IRecordDAO / IHistoryDeleteDAO</td><td><code>oap-server/server-core/.../storage/*.java</code></td></tr><tr><td></td><td>PersistenceTimer</td><td><code>oap-server/server-core/.../storage/PersistenceTimer.java</code></td></tr><tr><td></td><td>StorageBuilder / StorageBuilderFactory</td><td><code>oap-server/server-core/.../storage/type/StorageBuilder.java</code>、<code>storage/StorageBuilderFactory.java</code></td></tr><tr><td></td><td>Model / ModelColumn / ModelInstaller / BanyanDBModelExtension</td><td><code>oap-server/server-core/.../storage/model/*.java</code></td></tr><tr><td>BanyanDB</td><td>BanyanDBStorageProvider</td><td><code>oap-server/server-storage-plugin/storage-banyandb-plugin/.../BanyanDBStorageProvider.java</code></td></tr><tr><td></td><td>BanyanDBIndexInstaller / MetadataRegistry</td><td>同目录 <code>BanyanDBIndexInstaller.java</code>、<code>MetadataRegistry.java</code></td></tr><tr><td></td><td>BanyanDBBatchDAO / BanyanDBMetricsDAO</td><td>同目录 <code>BanyanDBBatchDAO.java</code>、<code>measure/BanyanDBMetricsDAO.java</code></td></tr><tr><td></td><td>SchemaWatcher</td><td><code>oap-server/server-library/library-banyandb-client/.../client/SchemaWatcher.java</code></td></tr><tr><td>ES</td><td>StorageModuleElasticsearchProvider</td><td><code>oap-server/server-storage-plugin/storage-elasticsearch-plugin/.../StorageModuleElasticsearchProvider.java</code></td></tr><tr><td></td><td>StorageEsInstaller / MetricsEsDAO / HistoryDeleteEsDAO / TimeSeriesUtils</td><td><code>.../elasticsearch/base/*.java</code></td></tr><tr><td>JDBC</td><td>JDBCTableInstaller</td><td><code>oap-server/server-storage-plugin/storage-jdbc-hikaricp-plugin/.../common/JDBCTableInstaller.java</code></td></tr><tr><td></td><td>JDBCMetricsDAO / JDBCBatchDAO / JDBCHistoryDeleteDAO</td><td><code>.../common/dao/*.java</code></td></tr></tbody></table><blockquote><p><strong>未确认项</strong></p><ul><li>BanyanDB 是否有区别于 measure field 的独立 Histogram 原生类型——OAP 侧仅以 <code>DataTable</code> field 使用，服务端原生结构不在本仓库。</li><li>列存/Gorilla 编码/SeriesID/segment TTL 是 BanyanDB 服务端引擎的物理结构，OAP 仓库只带 client proto（<code>skywalking-banyandb-client-proto</code> 子模块），服务端 Go 实现不在本仓库，上述物理层描述基于 BanyanDB 公开设计文档，OAP 侧可对照的证据已标注。</li></ul></blockquote><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5" tabindex="-1">接下来</h2><p>数据存好了，怎么查出来给 UI？告警又怎么基于这些数据触发？详见 06-查询层与告警。</p><blockquote><p><strong>心智模型</strong><br />把存储层想成三套仓库设计图：BanyanDB 是为 SkyWalking 量身定制的专用仓库（measure 货架自动合并、TopN 区有预分拣、TTL 自动清货、列存 + Gorilla 压缩省空间），ES 是租来的通用文档库（要把指标塞进文档、靠 _id 模拟合并、靠删整库做 TTL、TopN 现扫现算），JDBC 是用关系数据库当仓库（按天分表、靠 SQL upsert 模拟合并、靠 drop table 做 TTL）。同一份&quot;货物清单&quot;（Model），三套仓库各有各的摆法，但对外接口（DAO）一致。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[五、Skywalking流处理引擎 · Worker 链]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/skywalking-liu-chu-li-yin-qing-worker-lian" />
                <id>tag:https://lymboy.com,2025-10-19:skywalking-liu-chu-li-yin-qing-worker-lian</id>
                <published>2025-10-19T21:49:56+08:00</published>
                <updated>2025-10-19T21:49:56+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-%C2%B7-worker-%E9%93%BE" tabindex="-1">流处理引擎 · Worker 链</h1><blockquote><p><strong>这一篇解决什么</strong><br />Source 进来后，指标怎么从&quot;一条一条的原始事件&quot;变成&quot;存进数据库的聚合点&quot;？答案是 <code>StreamProcessor</code> + Worker 链：L1 内存聚合攒批、按 hash 分片到集群节点、L2 缓存做增量合并、降采样到小时天、最后批量落盘。这篇拆解四种 StreamProcessor、单条 metric 的完整 Worker DAG、L1/L2 缓存策略、集群分片机制、PersistenceTimer 的批量写入。这是 OAP 性能设计最核心的一篇。</p></blockquote><hr /><h2 id="%E5%9B%9B%E7%A7%8D-streamprocessor" tabindex="-1">四种 StreamProcessor</h2><p><code>StreamProcessor</code> 接口极简（<code>oap-server/server-core/.../analysis/StreamProcessor.java:21</code>）：只有 <code>void in(STREAM stream)</code>。它是所有流处理器的统一入口。</p><p><code>@Stream</code> 注解（<code>oap-server/server-core/.../analysis/Stream.java:41</code>）标注在数据类上，声明 <code>name</code>（流名/存储实体名）、<code>scopeId</code>、<code>builder</code>（实体↔存储转换器）、<code>processor</code>（用哪个 StreamProcessor）、<code>allowBootReshape</code>（BanyanDB 专用）。</p><p>四种 StreamProcessor，各管一类数据：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_0.png" alt="图1" /></p><blockquote><p><strong>四种数据形态对应的处理器</strong></p><ul><li><strong><code>MetricsStreamProcessor</code></strong>（<code>.../analysis/worker/MetricsStreamProcessor.java:65</code>）——最复杂最重要。处理 <code>Metrics</code>（可聚合统计量，如 QPS、延迟均值）。单例。<code>in(Metrics)</code>（<code>:141</code>）是热路径，按 <code>metrics.getClass()</code> 从 <code>entryWorkers</code> 找 <code>MetricsAggregateWorker</code> 转发。支持运行时热增删（<code>removeMetric</code> <code>:378</code>、<code>suspendDispatch</code> <code>:478</code>），用于 MAL/LAL runtime rule 热更新。</li><li><strong><code>RecordStreamProcessor</code></strong>（<code>RecordStreamProcessor.java:49</code>）——处理 <code>Record</code>（原始记录，如 trace segment、log），不聚合、不降采样，直接持久化，受 TTL 控制（<code>recordDataTTL</code> <code>:56</code>，<code>in</code> 里做过期判断 <code>:69-73</code>）。</li><li><strong><code>TopNStreamProcessor</code></strong>（<code>TopNStreamProcessor.java:51</code>）——处理 <code>TopN</code>（Top N 慢记录，如最慢的 N 条 SQL）。维护固定大小窗口，低频持久化（<code>topNWorkerReportCycle</code> <code>:59</code>，<code>topSize</code> <code>:62</code>）。</li><li><strong><code>NoneStreamProcessor</code></strong>（<code>NoneStreamProcessor.java:46</code>）——处理 <code>NoneStream</code>（UI 操作配置类实体，同步写、按 Record 模式 TTL 删除）。</li></ul></blockquote><h3 id="%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B%E7%BB%A7%E6%89%BF%E5%85%B3%E7%B3%BB" tabindex="-1">数据模型继承关系</h3><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_1.png" alt="图2" /></p><ul><li><code>Metrics</code>（<code>oap-server/server-core/.../analysis/metrics/Metrics.java:41</code>）：<code>extends StreamData implements StorageData, ToJson</code>。核心抽象方法：<code>combine(Metrics)</code>（<code>:68</code>，合并同 ID 同 timeBucket）、<code>calculate()</code>（<code>:73</code>，计算最终值）、<code>toHour()/toDay()</code>（<code>:80/83</code>，降采样克隆）、<code>id0()</code>（<code>:167</code>，存储主键）。子类众多：<code>CountMetrics</code>、<code>LongAvgMetrics</code>、<code>DoubleAvgMetrics</code>、<code>SumMetrics</code>、<code>PercentileMetrics</code>、<code>HistogramMetrics</code>、<code>ApdexMetrics</code>、<code>RateMetrics</code>、<code>CPMMetrics</code>。</li><li><code>Record</code>（<code>.../analysis/record/Record.java:32</code>）：<code>implements StorageData</code>，只有 <code>timeBucket</code>，子类如 <code>SegmentRecord</code>、<code>LogRecord</code>、<code>Event</code>、<code>LongText</code>。</li><li><code>TopN</code>（<code>.../analysis/topn/TopN.java:32</code>）：<code>extends Record implements ComparableStorageData</code>，增加 <code>latency</code>、<code>traceId</code>、<code>entityId</code>、<code>timestamp</code>，按 <code>latency</code> 排序（<code>:62</code>）。</li></ul><hr /><h2 id="worker-%E9%93%BE%E5%A6%82%E4%BD%95%E7%BC%96%E6%8E%92" tabindex="-1">Worker 链如何编排</h2><p>SkyWalking 的&quot;流处理 DAG&quot;不是用配置文件画的，而是 <code>MetricsStreamProcessor.create</code> 用代码组装出来的固定形态链，每个 metric 类一条链。</p><blockquote><p><strong>为什么是代码组装而非配置</strong><br />因为 worker 链的形态（要不要降采样、要不要告警、要不要导出）取决于 metric 的 <code>@MetricsExtension</code> 属性，运行时按属性分支组装。这比配置文件灵活，代价是新增链形态要改代码。</p></blockquote><h3 id="abstractworker-%C2%B7-%E5%9F%BA%E7%B1%BB" tabindex="-1">AbstractWorker · 基类</h3><p><code>AbstractWorker</code>（<code>oap-server/server-core/.../worker/AbstractWorker.java:30</code>）：所有 Worker 基类。持有 <code>ModuleDefineHolder</code>（<code>:33</code>，便于跨模块找 Service），核心方法 <code>in(INPUT)</code>（<code>:42</code>）是数据入口。</p><h3 id="%E5%8D%95%E6%9D%A1-metric-%E7%9A%84%E5%AE%8C%E6%95%B4-worker-dag" tabindex="-1">单条 metric 的完整 Worker DAG</h3><p><code>MetricsStreamProcessor.create</code>（<code>MetricsStreamProcessor.java:202-307</code>）组装的链：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_2.png" alt="图3" /></p><p>组装步骤（<code>MetricsStreamProcessor.java:202-307</code>）：</p><ol><li>拿 <code>StorageBuilderFactory</code> 生成 <code>StorageBuilder</code>，拿 <code>StorageDAO</code> 生成 <code>IMetricsDAO</code>（<code>:207-220</code>）。</li><li>拿 <code>ModelRegistry</code> 注册存储模型 <code>Model</code>，拿 <code>DownSamplingConfigService</code>/<code>TTLStatusQuery</code> 决定降采样和 TTL（<code>:222-228</code>）。</li><li>根据 <code>@MetricsExtension</code> 的 <code>supportDownSampling</code>/<code>supportUpdate</code>/<code>timeRelativeID</code>（<code>:235-246</code>）决定链形态。</li><li>若支持降采样：建 hour/day 的 <code>MetricsPersistentWorker</code>（<code>:329</code>），再用 <code>MetricsTransWorker</code>（<code>:267</code>）把 minute → hour/day 串起来。</li><li>建 minute 级 <code>MetricsPersistentMinWorker</code>（<code>:309</code>），内部挂 <code>AlarmNotifyWorker</code>（告警）、<code>ExportMetricsWorker</code>（导出）、<code>transWorker</code>（降采样转发）三个 next worker。</li><li>把 minute worker 注册到 <code>IWorkerInstanceSetter</code>（<code>:298-301</code>），名字为 <code>stream.getName()+&quot;_rec&quot;</code>——这是<strong>集群内 RPC 的接收端 worker 名</strong>。</li><li>建 <code>MetricsRemoteWorker</code>（<code>:303</code>，把数据按 hash 发到目标节点）。</li><li>建 <code>MetricsAggregateWorker</code>（<code>:304</code>，L1 入口），next worker = remote worker。</li><li>把 aggregateWorker 放入 <code>entryWorkers</code>（<code>:306</code>），<code>in(Metrics)</code> 据此路由。</li></ol><hr /><h2 id="l1-%E8%81%9A%E5%90%88%EF%BC%9Ametricsaggregateworker" tabindex="-1">L1 聚合：MetricsAggregateWorker</h2><p>L1 是内存里的第一道聚合，把高频小写入攒成低频批量。</p><p><code>MetricsAggregateWorker</code>（<code>.../analysis/worker/MetricsAggregateWorker.java:57</code>）：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_3.png" alt="图4" /></p><ul><li>用共享的 <code>BatchQueue</code>（<code>:98</code>，名为 <code>METRICS_L1_AGGREGATION</code>，CPU 核数线程、自适应分区、20k buffer）。</li><li><code>in(Metrics)</code>（<code>:146</code>）produce 到队列；<code>L1Handler.consume</code>（<code>:207</code>）调 <code>onWork</code>（<code>:152</code>）→ <code>mergeDataCache.accept</code> 合并相同 ID+timeBucket 的指标 → <code>flush()</code>（<code>:178</code>）按 <code>l1FlushPeriod</code>（默认 500ms，<code>:129</code>）周期性把合并结果喂给 next worker。</li><li><strong>OAL metric 权重 1.0，MAL meter 权重 0.05</strong>（<code>:141</code>，因 MAL 每分钟才来一批，多个类型可共享分区）。</li></ul><blockquote><p><strong>为什么要 L1</strong><br />同一个服务一秒可能收到几百条 trace，每条都要更新 resp_time 指标。如果每条都走&quot;读旧值→合并→写回&quot;，数据库扛不住。L1 先在内存把这一秒内的几百条合并成一个&quot;增量&quot;，500ms 才往后传一次。后面 L2 再做&quot;读旧值→叠加→写回&quot;。</p></blockquote><p>为什么 L1 用 <code>BatchQueue</code> 多线程消费，而不是单线程边收边合并？因为合并本身是 CPU 活（要算 ID 去重、调 <code>combine</code>），单线程会成为吞吐瓶颈。BatchQueue 把&quot;入队&quot;和&quot;合并&quot;解耦：接收线程只做极快的入队，多个消费线程并行合并，合并完再汇总。代价���合并顺序不确定，但 metric 的 <code>combine</code> 必须满足交换律和结合律——这也是为什么所有算子的 <code>combine</code> 都是累加式（计数相加、桶计数相加），不能用减法或除法这类顺序敏感的操作。</p><hr /><h2 id="%E9%9B%86%E7%BE%A4%E5%88%86%E7%89%87%EF%BC%9Ametricsremoteworker" tabindex="-1">集群分片：MetricsRemoteWorker</h2><p>L1 合并完的数据，要按 hash 决定去哪个 OAP 节点做持久化。</p><p><code>MetricsRemoteWorker</code>（<code>.../analysis/worker/MetricsRemoteWorker.java:33</code>）：<code>in(Metrics)</code>（<code>:44</code>）调 <code>remoteSender.send(workerName, metrics, Selector.HashCode)</code>，把数据按 hash 路由到某个 OAP 节点。</p><blockquote><p><strong>为什么必须 hash 分片</strong><br />同一个服务的指标可能被不同 OAP 节点收到。如果每个节点都存全量，指标会重复或冲突（combine 算两次）。<code>HashCodeSelector</code>（<code>.../remote/selector/HashCodeSelector.java:25</code>）用 <code>Math.abs(streamData.remoteHashCode() % size)</code>——保证同一 entity 的 metric 总是落到同一节点，保证聚合正确。</p></blockquote><p>三种 <code>Selector</code>（<code>.../remote/selector/</code>）：</p><table><thead><tr><th>Selector</th><th>行为</th><th>谁用</th></tr></thead><tbody><tr><td><code>HashCodeSelector</code></td><td><code>Math.abs(hash % size)</code>，同 hash 落同节点</td><td>Metrics（保证聚合正确）</td></tr><tr><td><code>RollingSelector</code></td><td>轮询</td><td>—</td></tr><tr><td><code>ForeverFirstSelector</code></td><td>永远选第一个</td><td>—</td></tr></tbody></table><hr /><h2 id="remoteclientmanager-%C2%B7-%E9%9B%86%E7%BE%A4%E8%BF%9E%E6%8E%A5%E7%AE%A1%E7%90%86" tabindex="-1">RemoteClientManager · 集群连接管理</h2><p><code>RemoteClientManager</code>（<code>oap-server/server-core/.../remote/client/RemoteClientManager.java:59</code>）管理本节点到集群内所有其他 OAP 节点的连接。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_4.png" alt="图5" /></p><ul><li><code>implements Service, ClusterWatcher</code>（<code>:59</code>）。</li><li><code>start()</code>（<code>:95</code>）起定时线程，每 10 秒 <code>refresh()</code>。</li><li><code>refresh()</code>（<code>:105</code>）→ 从 <code>ClusterModule</code> 拿 <code>ClusterNodesQuery</code>（<code>:107</code>）查询集群节点列表 → <code>refresh(instanceList)</code>（<code>:112</code>）。</li><li><code>refresh(List&lt;RemoteInstance&gt;)</code>（<code>:118</code>）去重、排序、比对，若变了就 <code>reBuildRemoteClients</code>（<code>:146</code>）重建连接，并通知 <code>ServerStatusService.rebalancedCluster</code>（<code>:147</code>，触发所有 <code>MetricsPersistentWorker</code> 的 <code>onClusterRebalanced</code>）。</li><li><code>reBuildRemoteClients</code>（<code>:202</code>）做 diff：不变的 <code>Unchanged</code>、新出现的 <code>Create</code>（本机建 <code>SelfRemoteClient</code>，远端建 <code>GRPCRemoteClient</code> 并 <code>connect()</code>，<code>:238-246</code>）、消失的 <code>Close</code>（<code>:255-261</code>）。最后排序保证顺序稳定（<code>:252</code>，为 <code>RollingSelector</code> 一致性）。</li></ul><blockquote><p><strong>不要直接 find ClusterModule</strong><br /><code>RemoteClientManager.refresh</code> 内部才 <code>find(ClusterModule.NAME)</code>。业务代码应通过 <code>CoreModule</code> 的 <code>RemoteClientManager</code> 间接使用。详见 07-集群协调与动态配置。</p></blockquote><h3 id="%E6%8E%A5%E6%94%B6%E4%BE%A7%EF%BC%9Aremoteservicehandler" tabindex="-1">接收侧：RemoteServiceHandler</h3><p><code>RemoteServiceHandler</code>（<code>oap-server/server-core/.../remote/RemoteServiceHandler.java:52</code>）是接收侧 gRPC 实现：</p><ul><li><code>call(StreamObserver)</code>（<code>:105</code>）返回一个 <code>StreamObserver&lt;RemoteMessage&gt;</code>。</li><li><code>onNext(RemoteMessage)</code>（<code>:118</code>）：取 <code>nextWorkerName</code>，用 <code>IWorkerInstanceGetter.get(nextWorkerName)</code>（<code>:125</code>）找到 <code>RemoteHandleWorker</code>，<code>newStreamDataInstance()</code> 反序列化（<code>:130-135</code>），<code>streamData.deserialize(remoteData)</code>（<code>:137</code>），最后 <code>nextWorker.in(streamData)</code>（<code>:143</code>）——即把远端发来的数据喂给本节点对应的 <code>MetricsPersistentMinWorker</code>（名字就是 <code>streamName+&quot;_rec&quot;</code>，见上面组装第 6 步）。</li></ul><p><code>IWorkerInstanceGetter/Setter</code>（<code>IWorkerInstanceGetter.java:26</code> / <code>IWorkerInstanceSetter.java:29</code>）：worker 名 → 实例的注册与查找表。<code>put</code> 在建链时调用（<code>MetricsStreamProcessor.java:298</code>），<code>get</code> 在收 RPC 时调用（<code>RemoteServiceHandler.java:125</code>）。</p><p>这就完成了集群内分发：<strong>发送端按 hash 选节点，接收端按 worker 名找到本地 worker 接续处理</strong>。</p><hr /><h2 id="l2-%E6%8C%81%E4%B9%85%E5%8C%96%EF%BC%9Ametricspersistentworker" tabindex="-1">L2 持久化：MetricsPersistentWorker</h2><p>L2 是真正落盘前的最后一道缓存，做&quot;读旧值→合并→写回&quot;。</p><p><code>PersistenceWorker</code>（<code>.../analysis/worker/PersistenceWorker.java:38</code>）抽象基类：持 <code>ReadWriteSafeCache</code>（<code>:40</code>），<code>onWork</code>（<code>:50</code>）写缓存，<code>buildBatchRequests</code>（<code>:64</code>）和 <code>endOfRound</code>（<code>:58</code>）由 <code>PersistenceTimer</code> 驱动。</p><p><code>MetricsPersistentWorker</code>（<code>.../analysis/worker/MetricsPersistentWorker.java:53</code>）核心逻辑：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_5.png" alt="图6" /></p><ul><li><code>in(Metrics)</code>（<code>:172</code>）：先查 TTL 过期（<code>:173</code>），再 <code>super.onWork</code> 入缓存。</li><li><code>buildBatchRequests</code>（<code>:188</code>）：受 <code>persistentMod</code> 控制（minute 级每轮都执行，降采样级每 4 轮执行一次，见 <code>:165</code>）。</li><li><code>buildBatchRequestsUnconditionally</code>（<code>:201</code>）：读缓存 → 对每批 <code>loadFromStorage</code>（<code>:249</code>，从 DB 读已有值到 <code>sessionCache</code>）→ <code>combine</code> 合并 → <code>calculate</code> → 生成 <code>prepareBatchUpdate/Insert</code>（<code>:271-285</code>）→ 喂给 alarm/export/trans worker（<code>:303</code>）。</li><li><code>sessionCache</code>（<code>MetricsSessionCache</code> <code>:115</code>）缓存最近写过的 metric，避免每次都读 DB；<code>endOfRound</code>（<code>:357</code>）清过期。</li></ul><blockquote><p><strong>启动优化：ServerStatusWatcher</strong><br /><code>MetricsPersistentWorker</code> 实现 <code>ServerStatusWatcher</code>（<code>:53</code>）：<code>onServerBooted</code>（<code>:428</code>）记录启动时间，<code>onClusterRebalanced</code>（<code>:434</code>）记录 rebalance 时间，<code>requireInitialization</code>（<code>:385</code>）用这个时间避免启动后无谓的 DB 回读——这是性能优化关键。OAP 刚启动时，缓存里没数据，如果不优化，每个 metric 都要回读 DB 拿旧值合并，启动会很慢。</p></blockquote><hr /><h2 id="%E9%99%8D%E9%87%87%E6%A0%B7%EF%BC%9A%E5%88%86%E9%92%9F-%E2%86%92-%E5%B0%8F%E6%97%B6-%E2%86%92-%E5%A4%A9" tabindex="-1">降采样：分钟 → 小时 → 天</h2><p>指标不只存一个粒度，分钟级再聚合成小时、天。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_6.png" alt="图7" /></p><ul><li><code>MetricsStreamProcessor.create</code> 在建链时按 <code>DownSamplingConfigService</code> 决定要不要建 hour/day worker（<code>:248,257</code>）。</li><li><code>MetricsTransWorker</code>（<code>:267</code>）把上一级聚合结果喂给下一级。</li><li>分钟级保留时间短（比如 7 天），小时级长一些（30 天），天级最长（半年）。查近期看细粒度，查历史看粗粒度，省存储。</li></ul><p>降采样枚举 <code>DownSampling</code>：<code>Minute/Hour/Day/Second/None</code>。<code>Metrics</code> 子类实现 <code>toHour()/toDay()</code> 做降采样克隆。</p><hr /><h2 id="persistencetimer-%C2%B7-%E6%89%B9%E9%87%8F%E5%86%99%E5%85%A5%E8%B0%83%E5%BA%A6" tabindex="-1">PersistenceTimer · 批量写入调度</h2><p>写入不是来一条写一条，而是攒着批量冲。<code>PersistenceTimer</code> 是写入侧的总调度。</p><p><code>PersistenceTimer</code>（<code>oap-server/server-core/.../storage/PersistenceTimer.java:45</code>，单例枚举）：</p><blockquote><p><strong>马桶式写入</strong><br />每个 Worker 在内存攒增量，<code>PersistenceTimer</code> 每 <code>persistentPeriod</code> 触发一次：多线程把所有 Worker 的内存对象&quot;翻译&quot;成数据库写入请求（prepare 阶段），再一把 flush 进去（execute 阶段）。这样把高频小写入变成低频批量写，降低数据库压力。</p></blockquote><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_7.png" alt="图8" /></p><p><code>start</code>（<code>:58</code>）用 <code>scheduleWithFixedDelay</code> 起定时器（初始延迟 5s，周期 = <code>moduleConfig.getPersistentPeriod()</code>，<code>:103</code>），每周期跑 <code>extractDataAndSave</code>（<code>:110</code>）：</p><ol><li>收集所有持久化 worker：<code>TopNStreamProcessor</code> + <code>MetricsStreamProcessor</code> 的 <code>getPersistentWorkers</code>（<code>:119-120</code>）。</li><li>用 <code>prepareExecutorService</code>（线程数 = <code>getPrepareThreads()</code>，<code>:94-96</code>）并发跑每个 worker 的 <code>buildBatchRequests</code>（<strong>prepare 阶段</strong>，产出 <code>List&lt;PrepareRequest&gt;</code>，<code>:135</code>）——这里 worker 内部调 <code>IMetricsDAO.prepareBatchInsert/Update</code> 把对象转成 <code>InsertRequest</code>/<code>UpdateRequest</code>。</li><li>把所有 <code>PrepareRequest</code> 交给 <code>IBatchDAO.flush(prepareRequests)</code>（<code>:146</code>，<strong>execute 阶段</strong>）真正批量写入。</li><li><code>endOfFlush</code>（<code>:152</code>）给后端清理机会。</li></ol><p><code>IBatchDAO.flush</code> 是异步返回 <code>CompletableFuture&lt;Void&gt;</code>（<code>IBatchDAO.java:47</code>），所以 prepare 与 execute 可以重叠。</p><hr /><h2 id="%E5%91%8A%E8%AD%A6%E4%B8%8E%E5%AF%BC%E5%87%BA%EF%BC%9A%E5%9C%A8-worker-%E9%93%BE%E9%87%8C%E7%9A%84%E4%BD%8D%E7%BD%AE" tabindex="-1">告警与导出：在 Worker 链里的位置</h2><p><code>MetricsStreamProcessor.minutePersistentWorker</code>（<code>:317</code>）创建 <code>AlarmNotifyWorker</code>，作为 <code>MetricsPersistentMinWorker</code> 的下游（<code>:320-323</code>）。即<strong>分钟级指标持久化后</strong>，会调用 <code>AlarmNotifyWorker.in(metrics)</code>。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_8.png" alt="图9" /></p><ul><li><code>AlarmNotifyWorker</code>（<code>.../analysis/worker/AlarmNotifyWorker.java:30</code>）：<code>in(Metrics)</code> 若 metrics 实现 <code>WithMetadata</code>，调 <code>entrance.forward(metrics)</code>。</li><li><code>AlarmEntrance</code>（<code>.../core/alarm/AlarmEntrance.java:24</code>）：<code>forward</code> 先检查 <code>has(AlarmModule.NAME)</code>（没装告警模块就跳过），再懒加载 <code>MetricsNotify</code> 服务，调 <code>metricsNotify.notify(metrics)</code>。</li></ul><blockquote><p><strong>AlarmNotifyWorker 只接在分钟级</strong><br /><code>AlarmNotifyWorker</code> 只接在<strong>分钟级</strong> worker 后，不接小时/天。这与 <code>RunningRule</code> 注释一致：“only minute dimensionality metrics are expected to process”。告警基于分钟级指标，不会用降采样后的数据。详见 06-查询层与告警。</p></blockquote><p><code>ExportMetricsWorker</code> 把指标外发给外部系统（OTel collector/Kafka），订阅驱动。详见 07-集群协调与动态配置。</p><hr /><h2 id="%E5%AE%8C%E6%95%B4%E6%95%B0%E6%8D%AE%E6%B5%81%E6%80%BB%E5%9B%BE" tabindex="-1">完整数据流总图</h2><p>把前面所有要素汇总：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/04-%E6%B5%81%E5%A4%84%E7%90%86%E5%BC%95%E6%93%8E-Worker%E9%93%BE.md_9.png" alt="图10" /></p><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键类</th><th>路径</th></tr></thead><tbody><tr><td>StreamProcessor</td><td>StreamProcessor 接口</td><td><code>oap-server/server-core/.../analysis/StreamProcessor.java</code></td></tr><tr><td></td><td>Stream 注解</td><td>同目录 <code>analysis/Stream.java</code></td></tr><tr><td>四种 Processor</td><td>Metrics/Record/TopN/None</td><td><code>oap-server/server-core/.../analysis/worker/{Metrics,Record,TopN,None}StreamProcessor.java</code></td></tr><tr><td>数据模型</td><td>Metrics/Record/TopN</td><td><code>oap-server/server-core/.../analysis/metrics/Metrics.java</code>、<code>record/Record.java</code>、<code>topn/TopN.java</code></td></tr><tr><td>Worker 基类</td><td>AbstractWorker/PersistenceWorker</td><td><code>oap-server/server-core/.../worker/AbstractWorker.java</code>、<code>analysis/worker/PersistenceWorker.java</code></td></tr><tr><td>L1</td><td>MetricsAggregateWorker</td><td><code>oap-server/server-core/.../analysis/worker/MetricsAggregateWorker.java</code></td></tr><tr><td>Remote</td><td>MetricsRemoteWorker</td><td><code>oap-server/server-core/.../analysis/worker/MetricsRemoteWorker.java</code></td></tr><tr><td>L2</td><td>MetricsPersistentWorker</td><td><code>oap-server/server-core/.../analysis/worker/MetricsPersistentWorker.java</code></td></tr><tr><td>集群</td><td>RemoteClientManager</td><td><code>oap-server/server-core/.../remote/client/RemoteClientManager.java</code></td></tr><tr><td></td><td>RemoteSenderService</td><td><code>oap-server/server-core/.../remote/RemoteSenderService.java</code></td></tr><tr><td></td><td>RemoteServiceHandler</td><td><code>oap-server/server-core/.../remote/RemoteServiceHandler.java</code></td></tr><tr><td></td><td>HashCodeSelector</td><td><code>oap-server/server-core/.../remote/selector/HashCodeSelector.java</code></td></tr><tr><td>批量写入</td><td>PersistenceTimer</td><td><code>oap-server/server-core/.../storage/PersistenceTimer.java</code></td></tr><tr><td>告警入口</td><td>AlarmNotifyWorker / AlarmEntrance</td><td><code>oap-server/server-core/.../analysis/worker/AlarmNotifyWorker.java</code>、<code>core/alarm/AlarmEntrance.java</code></td></tr><tr><td>Worker 注册</td><td>IWorkerInstanceGetter/Setter</td><td><code>oap-server/server-core/.../remote/IWorkerInstanceGetter.java</code>、<code>IWorkerInstanceSetter.java</code></td></tr></tbody></table><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5" tabindex="-1">接下来</h2><p>数据算好准备落盘了，具体怎么存？BanyanDB/ES/JDBC 三套实现差异在哪？详见 05-存储层。</p><blockquote><p><strong>心智模型</strong><br />把流处理链想成一条&quot;加工流水线 + 仓库调度&quot;：L1（AggregateWorker）是车间里的暂存货架，攒一会儿再往后送；RemoteWorker 是调度员，按零件编号（hash）决定送到哪个分厂；L2（PersistentWorker）是分厂的成品库，先核对旧账（multiGet）再合并入账；PersistenceTimer 是统一发货的铃铛，铃一响所有分厂一起发货。降采样是把成品再打包成&quot;日报/周报&quot;存到更长期的货架。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[四、Skywalking分析引擎与四大 DSL]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/skywalking-fen-xi-yin-qing-yu-si-da-dsl" />
                <id>tag:https://lymboy.com,2025-10-12:skywalking-fen-xi-yin-qing-yu-si-da-dsl</id>
                <published>2025-10-12T21:43:48+08:00</published>
                <updated>2025-10-12T21:43:48+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E5%88%86%E6%9E%90%E5%BC%95%E6%93%8E%E4%B8%8E%E5%9B%9B%E5%A4%A7-dsl" tabindex="-1">分析引擎与四大 DSL</h1><blockquote><p><strong>这一篇解决什么</strong><br />SkyWalking 有四种领域专用语言（DSL）：OAL、MAL、LAL 管写入侧的指标聚合，MQE 管读取侧的查询计算。讲清楚每种 DSL 解决什么问题、语法长什么样、怎么用 ANTLR 解析 + Javassist 生成字节码（MQE 例外，用 Visitor 即时求值），以及 analyzer 模块群如何把 trace 切分成 Source。</p></blockquote><hr /><h2 id="%E5%85%A8%E5%B1%80%E8%A7%86%E8%A7%92%EF%BC%9A%E5%86%99%E5%85%A5%E4%BE%A7-vs-%E8%AF%BB%E5%8F%96%E4%BE%A7" tabindex="-1">全局视角：写入侧 vs 读取侧</h2><p>OAP 的数据流是两条方向相反的流水线：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/03-%E5%88%86%E6%9E%90%E5%BC%95%E6%93%8E%E4%B8%8E%E5%9B%9B%E5%A4%A7DSL.md_0.png" alt="图1" /></p><p>OAL/MAL/LAL 把数据&quot;算好&quot;存进去，MQE 把存进去的数据&quot;取出来&quot;再加工。四者共享同一套&quot;指标类型&quot;和&quot;存储模型&quot;，所以写入侧定义的指标名（如 <code>service_resp_time</code>）能直接在 MQE 里引用——这是它们能协同的前提，不是巧合。</p><hr /><h2 id="oal-%C2%B7-observability-analysis-language" tabindex="-1">OAL · Observability Analysis Language</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" tabindex="-1">解决什么问题</h3><p>让用户用声明式语法声明&quot;从哪类 Source 取哪个字段、用什么聚合函数聚合成什么指标&quot;，OAP 在启动时把这些声明<strong>编译成 Java 字节码</strong>并接入流式聚合流水线。</p><blockquote><p><strong>什么是&quot;声明式聚合&quot;</strong><br />传统做法要为每个指标手写一个 Java 类（继承 <code>Metrics</code>）、手写一个 Dispatcher（把 Source 转成 Metrics）、手写 <code>id()/hashCode()/serialize()</code> 等样板方法，再注册到 <code>MetricsStreamProcessor</code>。</p><p>OAL 让你只写一行 <code>service_resp_time = from(Service.latency).longAvg();</code>，编译器替你生成全部样板代码。这样 SkyWalking 内置的上百个指标都集中在 <code>.oal</code> 文件里，可读、可改、可关闭。</p><p>为什么要编译成字节码而不是解释执行？流式聚合是高频热路径，每秒可能处理几十万条样本，解释执行的开销扛不住；编译成 Java 类后跑的是原生方法调用，和手写代码零差别。</p></blockquote><h3 id="%E7%9C%9F%E5%AE%9E%E8%84%9A%E6%9C%AC%E7%A4%BA%E4%BE%8B" tabindex="-1">真实脚本示例</h3><p><code>oap-server/server-starter/src/main/resources/oal/core.oal</code>（核心 OAL，所有 service/endpoint/关系拓扑指标都在这里）节选：</p><pre><code class="language-">// oal/core.oal:20service_resp_time = from(Service.latency).longAvg().decorator(&quot;ServiceDecorator&quot;);// oal/core.oal:21service_sla = from(Service.*).percent(status == true).decorator(&quot;ServiceDecorator&quot;);// oal/core.oal:23service_percentile = from(Service.latency).percentile2(10); // p50/p75/p90/p95/p99// oal/core.oal:29service_relation_client_cpm = from(ServiceRelation.*).filter(detectPoint == DetectPoint.CLIENT).cpm();// oal/core.oal:58endpoint_mq_consume_latency = from((str-&gt;long)Endpoint.tag[&quot;transmission.latency&quot;])                                .filter(type == RequestType.MQ)                                .filter(tag[&quot;transmission.latency&quot;] != null).longAvg();</code></pre><p>语法结构（见 <code>OALParser.g4</code>）：</p><ul><li><code>变量名 = from(Source.字段).filter(条件).聚合函数().decorator(&quot;装饰器&quot;);</code></li><li><code>Source</code> 是固定枚举（<code>Service</code>、<code>Endpoint</code>、<code>ServiceRelation</code>、<code>ServiceInstance</code>、<code>DatabaseAccess</code>、<code>CacheAccess</code>、<code>MQAccess</code>…，见 <code>OALParser.g4:56</code> 的 <code>source</code> 规则）。为什么是固定枚举而不是任意类型？因为 Source 是 trace 切分的产物，字段在编译期必须确定，enricher 要靠反射查 Source 类字段来生成 Dispatcher 代码（见下文）。</li><li>聚合函数：<code>longAvg()</code>、<code>cpm()</code>、<code>percent(status==true)</code>、<code>percentile2(10)</code>、<code>sum()</code>、<code>count()</code>、<code>apdex(name,status)</code> 等，每个对应 <code>server-core</code> 里一个度量基类（如 <code>LongAvgMetrics</code>）。</li><li><code>(str-&gt;long)</code> 是类型转换（<code>castStmt</code>，<code>OALParser.g4:214</code>），把字符串 tag 转成 long 再算平均。tag 来自探针上报的 key-value，原始都是字符串，算数值聚合前必须显式转换。</li><li><code>disable(segment);</code> 可关闭某条内置流（见 <code>oal/disable.oal</code>，<code>OALParser.g4:36</code> 的 <code>disableStatement</code>）。</li></ul><h3 id="antlr-%E8%AF%AD%E6%B3%95%E4%B8%8E-parser-%E7%94%9F%E6%88%90" tabindex="-1">ANTLR 语法与 parser 生成</h3><ul><li>语法定义：<code>oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALLexer.g4</code> 和 <code>OALParser.g4</code>。</li></ul><blockquote><p><strong>ANTLR / AST 速成</strong><br /><strong>ANTLR</strong> 是&quot;语法规则 → 解析器代码&quot;的生成器。你写 <code>.g4</code> 描述语法（lexer 词法 + parser 句法），构建时自动生成 <code>OALLexer.java</code>/<code>OALParser.java</code> 等类。生成的 parser 把一行 OAL 文本解析成&quot;解析树&quot;（parse tree）。</p><p><strong>AST</strong>（抽象语法树）是把解析树里&quot;对代码生成有用的信息&quot;提炼成精简的 Java 对象。OAL 里这个 AST 就是 <code>MetricDefinition</code>（不可变，字段全 <code>final</code>，见 <code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/model/MetricDefinition.java:39</code>），由 <code>OALListenerV2</code>（<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/parser/OALListenerV2.java:68</code>）在 <code>exitAggregationStatement</code>（<code>:123</code>）里用 Builder 拼出。</p></blockquote><h3 id="%E7%BC%96%E8%AF%91%E4%B8%8E%E6%89%A7%E8%A1%8C%E5%8E%9F%E7%90%86" tabindex="-1">编译与执行原理</h3><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/03-%E5%88%86%E6%9E%90%E5%BC%95%E6%93%8E%E4%B8%8E%E5%9B%9B%E5%A4%A7DSL.md_1.png" alt="图2" /></p><p>入口在 <code>server-core</code>，引擎实现在 <code>oal-rt</code>。这条链路分七步：</p><ol><li><strong>触发</strong>：<code>OALEngineLoaderService.load(OALDefine)</code>（<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.java:58</code>）。各 receiver 在 <code>start()</code> 里调用它，传入含 <code>.oal</code> 路径 + Source 包名的 <code>OALDefine</code>。核心 OAL 由 <code>CoreOALDefine.INSTANCE</code> 触发（指向 <code>oal/core.oal</code>）。每个 <code>OALDefine</code> 只激活一次（<code>:60</code> 的 <code>oalDefineSet.contains</code> 去重）。</li><li><strong>反射加载引擎</strong>：因为 <code>server-core</code> 在 Maven 反应堆里先于 <code>oal-rt</code> 编译，<code>server-core</code> 不能直接 <code>import</code> <code>oal-rt</code> 的类，只能用反射创建 <code>OALEngineV2</code>（<code>OALEngineLoaderService.java:88</code>，<code>Class.forName + getConstructor + newInstance</code>）。不这么做会形成 Maven 模块循环依赖，编译不过。</li><li><strong>解析</strong>：<code>OALEngineV2.start()</code>（<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/OALEngineV2.java:90</code>）读 <code>.oal</code> → <code>OALScriptParserV2.parse()</code>（<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/parser/OALScriptParserV2.java:99</code>）：建 <code>OALLexer</code>（<code>:101</code>）→ <code>CommonTokenStream</code>（<code>:104</code>）→ <code>OALParser</code>（<code>:107</code>）→ <code>parser.root()</code>（<code>:115</code>），再由 <code>ParseTreeWalker</code> 走 <code>OALListenerV2</code>（<code>OALScriptParserV2.java:123</code>）把解析树转成 <code>List&lt;MetricDefinition&gt;</code>。</li><li><strong>enrich</strong>：<code>MetricDefinitionEnricher.enrich()</code>（<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/generator/MetricDefinitionEnricher.java:96</code>）通过反射查 Source 类字段，给每个 metric 补上&quot;要复制哪些字段、哪些是持久化字段、scopeId&quot;等元信息，输出 <code>CodeGenModel</code>。这一步把 AST 从&quot;语法描述&quot;升级成&quot;代码生成所需的全部上下文&quot;。</li><li><strong>代码生成</strong>：<code>OALClassGeneratorV2.generateClassAtRuntime()</code>（<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/generator/OALClassGeneratorV2.java:143</code>）用 <strong>Javassist</strong>（运行时字节码操作库）+ <strong>FreeMarker 模板</strong>（<code>/code-templates-v2/*.ftl</code>）为每条 metric 生成三类 Java 类：<ul><li><strong>Metrics 类</strong>（<code>generateMetricsClass</code>，<code>:196</code>）：继承对应聚合基类（如 <code>LongAvgMetrics</code>），加 <code>@Column</code>/<code>@Stream</code> 注��、<code>id()/serialize()/deserialize()/toHour()/toDay()</code> 方法。类名如 <code>ServiceRespTimeMetrics</code>。</li><li><strong>MetricsBuilder 类</strong>（<code>generateMetricsBuilderClass</code>，<code>:341</code>）：实现 <code>StorageBuilder</code>，做实体↔存储互转。</li><li><strong>Dispatcher 类</strong>（<code>generateDispatcherClass</code>，<code>:393</code>）：实现 <code>SourceDispatcher&lt;Service&gt;</code>，把 Source 对象分发到对应 Metrics。</li></ul></li><li><strong>接线</strong>：<code>OALEngineV2.notifyAllListeners()</code>��<code>OALEngineV2.java:134</code>）把生成的 Metrics 类交给 <code>StreamAnnotationListener</code>，Dispatcher 类交给 <code>SourceReceiver</code> 的 <code>DispatcherDetectorListener</code>。</li><li><strong>接入流式聚合</strong>：<code>StreamAnnotationListener.notify</code>（<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/StreamAnnotationListener.java:50</code>）读生成类上的 <code>@Stream</code> 注解，按 <code>processor</code> 路由到对应 <code>StreamProcessor</code>（<code>:58-60</code> 分支 Record/Metrics）。OAL 生成的 metric 都标了 <code>MetricsStreamProcessor.class</code>，于是进入 <code>MetricsStreamProcessor.create</code>（<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/worker/MetricsStreamProcessor.java:165</code>，kind 默认 <code>OAL</code>），构建 worker 链。</li></ol><blockquote><p><strong>代码生成的小贴士</strong><br />这些类是运行时在内存里拼出 <code>.class</code> 字节码加载进 JVM 的，磁盘上看不到 <code>.java</code>。设环境变量 <code>SW_DYNAMIC_CLASS_ENGINE_DEBUG=true</code> 会把 <code>.class</code> + <code>.java</code> 旁车文件写到 <code>oal-rt/</code> 目录便于调试（见 <code>oal-rt/CLAUDE.md</code> 的 “Debug Output”）。</p></blockquote><hr /><h2 id="mal-%C2%B7-meter-analysis-language" tabindex="-1">MAL · Meter Analysis Language</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-1" tabindex="-1">解决什么问题</h3><p>让用户用类 PromQL/Groovy 风格的表达式把&quot;接收到的原始 meter 样本（Prometheus/OTel metric）&quot;加工成 SkyWalking 的指标。</p><blockquote><p><strong>OAL vs MAL 的区别</strong></p><table><thead><tr><th></th><th>OAL</th><th>MAL</th></tr></thead><tbody><tr><td>输入</td><td>OAP 内部 Source（trace 切分产物）</td><td>外部上报的 metric 样本（<code>SampleFamily</code>，带标签）</td></tr><tr><td>语法</td><td><code>from(Source.field).func()</code> 声明式</td><td><code>metric.sum(['tag']).rate('PT1M').service(['host'], Layer.OS_LINUX)</code> 表达式链 + 闭包</td></tr><tr><td>配置载体</td><td><code>.oal</code> 纯文本</td><td>YAML（<code>metricPrefix</code>/<code>expSuffix</code>/<code>filter</code>/<code>metricsRules</code>），<code>exp</code> 字段是 MAL 表达式</td></tr><tr><td>触发</td><td><code>OALEngineLoaderService.load(OALDefine)</code></td><td>receiver（如 OTel）启动时 <code>Rules.loadRules</code> + <code>new MetricConvert(rule, meterSystem)</code></td></tr><tr><td>落盘注册</td><td><code>StreamAnnotationListener</code> → <code>MetricsStreamProcessor</code></td><td><code>MeterSystem.create</code> → <code>MetricsStreamProcessor</code>（同一 processor，kind=MAL）</td></tr></tbody></table></blockquote><p>OAL 的 Source 是固定枚举（Service/Endpoint…），但外部系统（VM、MySQL、Redis、K8s…）上报的 metric 千变万化，OAL 无法表达；MAL 提供一套&quot;对样本做标签过滤、四则运算、聚合、直方图/分位数&quot;的表达式语言来填补。</p><h3 id="%E7%9C%9F%E5%AE%9E%E8%84%9A%E6%9C%AC%E7%A4%BA%E4%BE%8B-1" tabindex="-1">真实脚本示例</h3><p>MAL 表达式嵌在 YAML 里。<code>oap-server/server-starter/src/main/resources/otel-rules/vm.yaml</code>（VM node 监控）节选：</p><pre><code class="language-yaml"># otel-rules/vm.yaml:31-38filter: &quot;{ tags -&gt; tags.job_name == &#39;vm-monitoring&#39; }&quot;   # 闭包：只处理 job_name=vm-monitoring 的样本expSuffix: service([&#39;node_identifier_host_name&#39;], Layer.OS_LINUX)  # 公共后缀：绑定到 service scopemetricPrefix: meter_vm                                   # 指标名前缀metricsRules:  - name: cpu_total_percentage    exp: (node_cpu_seconds_total * 100).tagNotEqual(&#39;mode&#39;,&#39;idle&#39;).sum([&#39;node_identifier_host_name&#39;]).rate(&#39;PT1M&#39;)  - name: memory_used    exp: node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes</code></pre><p>最终指标名 = <code>metricPrefix + &quot;_&quot; + name</code> = <code>meter_vm_cpu_total_percentage</code>；最终表达式 = <code>expPrefix + exp + expSuffix</code>（见 <code>MetricConvert.formatExp</code>，<code>oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java:306</code>）。把 <code>expSuffix</code> 抽到文件级，是为了让同文件的多条规则共享同一 scope 绑定，避免每条重复写 <code>service(['host'], Layer.OS_LINUX)</code>。</p><p>MAL 表达式语法（<code>oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4</code>）支持：</p><ul><li>算术：<code>metric1 + metric2</code>、<code>(metric * 100)</code>（<code>additiveExpression:47</code>/<code>multiplicativeExpression:51</code>）。</li><li>方法链：<code>metric.sum(['tag']).rate('PT1M').histogram().histogram_percentile([50,75,90,95,99])</code>（<code>postfixExpression:64</code>）。</li><li>闭包（Groovy 风格）：<code>{ tags -&gt; tags.job_name == 'vm-monitoring' }</code>，用于 <code>tag({...})</code>、<code>forEach([...], {...})</code>、文件级 <code>filter</code>（<code>closureExpression:126</code>）。为什么用 Groovy 风格闭包而不是 Java lambda？因为 Javassist 不支持 lambda 语法，闭包要单独生成&quot;伴生类&quot;（<code>MalClosureCodegen</code>），Groovy 风格的 <code>{ args -&gt; body }</code> 更容易被 Javassist 拼出。</li><li>扩展函数 SPI：<code>metric.genai::estimateCost()</code>，<code>命名空间::方法()</code>（<code>:79</code>，<code>IDENTIFIER DOUBLE_COLON IDENTIFIER</code>），通过 <code>java.util.ServiceLoader</code> 发现，方便第三方插件加自定义函数。</li></ul><h3 id="%E7%BC%96%E8%AF%91%E4%B8%8E%E6%89%A7%E8%A1%8C%E5%8E%9F%E7%90%86-1" tabindex="-1">编译与执行原理</h3><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/03-%E5%88%86%E6%9E%90%E5%BC%95%E6%93%8E%E4%B8%8E%E5%9B%9B%E5%A4%A7DSL.md_2.png" alt="图3" /></p><p>核心类（均在 <code>oap-server/analyzer/meter-analyzer/.../v2/</code>）：</p><ol><li><strong>加载 YAML</strong>：<code>Rules.loadRules(&quot;otel-rules&quot;, enabledRules)</code>（<code>oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rules.java:60</code>）用 SnakeYAML（<code>:149</code> 的 <code>new Yaml().loadAs</code>）把每个 YAML 解析成 <code>Rule</code>。<code>OpenTelemetryMetricRequestProcessor.start</code>（<code>oap-server/server-receiver-plugin/otel-receiver-plugin/.../otlp/OpenTelemetryMetricRequestProcessor.java:218</code>）在 OTel receiver 启动时调它（<code>:226</code>），对每个 rule <code>new MetricConvert(rule, meterSystem)</code>（<code>:237</code>）。</li><li><strong>每条规则建一个 Analyzer，分两阶段</strong>（<code>MetricConvert.java:107</code> 构造函数）：<ul><li><strong>Phase 1 prepare</strong>：<code>Analyzer.prepare</code>（<code>oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java:176</code>）调 <code>DSL.parse()</code>（<code>:197</code>）→ <code>MALScriptParser.parse</code>（<code>oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParser.java:213</code>，内部建 <code>MALLexer:215</code> → <code>MALParser:217</code> → <code>parser.expression():233</code>，由 <code>MALExprVisitor</code>（<code>:280</code>）转 AST）得到 <code>MALExpressionModel.Expr</code>，再 <code>MALClassGenerator.compile</code>（<code>.../compiler/MALClassGenerator.java:150</code>）用 Javassist 生成实现 <code>MalExpression</code> 接口的类。闭包另生成&quot;伴生类&quot;（<code>MalClosureCodegen</code>）。同时 <code>MALMetadataExtractor.extractMetadata</code>（<code>.../compiler/MALMetadataExtractor.java:55</code>）静态从 AST 提取 <code>ExpressionMetadata</code>（输入 metric 名、scope、标签），供运行时 O(1) 过滤。</li><li><strong>Phase 2 register</strong>：<code>Analyzer.register</code>（<code>Analyzer.java:218</code>）调 <code>MeterSystem.create(metricName, functionName, scopeType, ...)</code>（<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.java:113</code>），再用 Javassist 生成存储侧 <code>Metrics</code> 子类并接入 <code>MetricsStreamProcessor.create(..., MetricStreamKind.MAL)</code>（<code>MetricsStreamProcessor.java:174</code>）——和 OAL 走的是同一个 <code>MetricsStreamProcessor</code>，只是 kind 标记为 <code>MAL</code>。</li><li>为什么要分两阶段？Phase 1 只编译不落盘，Phase 2 才触发存储 DDL。这样同文件某条规则编译失败时，前面已编译的规则不会留下&quot;已建表但无数据&quot;的脏状态，便于回滚（见 <code>MetricConvert.java:166-183</code> 的 <code>registered</code> 跟踪集）。</li></ul></li><li><strong>运行时执行</strong>：OTel receiver 收到一批 metric（按 Prometheus metric 名组织成 <code>ImmutableMap&lt;String, SampleFamily&gt;</code>），调 <code>MetricConvert.toMeter</code>（<code>MetricConvert.java:325</code>）广播给每个 <code>Analyzer.analyse</code>（<code>Analyzer.java:264</code>）：先用编译期提取的输入 metric 名做 O(1) 过滤（<code>:266-270</code>，避免对每条规则全量遍历），再过文件级 filter 闭包（<code>:297</code>），再调生成的 <code>expression.run(input)</code>（<code>:325</code>）算出 <code>Result</code>，最后按 metric ��型（<code>single</code>/<code>labeled</code>/<code>histogram</code>/<code>histogramPercentile</code>）调 <code>meterSystem.buildMetrics</code> + <code>doStreamingCalculation</code> 落盘。</li></ol><blockquote><p><strong>关键点</strong><br />MAL 和 OAL 最终都汇入同一个 <code>MetricsStreamProcessor</code>，差别只在&quot;Source 怎么来&quot;和&quot;Metrics 类怎么生成&quot;——OAL 由 OAL 引擎从 <code>.oal</code> 生成，MAL 由 <code>MeterSystem</code> 从 MAL 表达式生成。下游 worker 链、存储、查询对两者完全一致，这�� SkyWalking 能统一内部 trace 指标和外部 meter 指标的关键。</p></blockquote><hr /><h2 id="lal-%C2%B7-log-analysis-language" tabindex="-1">LAL · Log Analysis Language</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-2" tabindex="-1">解决什么问题</h3><p>让用户用 DSL 声明&quot;如何解析一条日志正文（JSON/YAML/正则）、从解析结果里提取 service/endpoint/layer/timestamp/tags、可选地从日志里提取指标（metrics 块）、以及如何 sink（保存/丢弃/采样）“。LAL 把&quot;无结构的日志流&quot;变成&quot;有结构的可查询日志 + 从日志里抽出来的指标”。</p><h3 id="%E7%9C%9F%E5%AE%9E%E8%84%9A%E6%9C%AC%E7%A4%BA%E4%BE%8B-2" tabindex="-1">真实脚本示例</h3><p><code>oap-server/server-starter/src/main/resources/lal/nginx.yaml</code>：</p><pre><code class="language-yaml"># lal/nginx.yaml:16-60rules:  - name: nginx-error-log    layer: NGINX    dsl: |      filter {        if (tag(&quot;LOG_KIND&quot;) == &quot;NGINX_ERROR_LOG&quot;) {       # 条件分发          text {                                          # 文本解析器            regexp $/(?&lt;time&gt;\\\\d{4}/\\\\d{2}/\\\\d{2} \\\\d{2}:\\\\d{2}:\\\\d{2}) \\\\[(?&lt;level&gt;.+)].*/$          }          extractor {                                    # 字段提取            tag level: parsed.level            timestamp parsed.time as String, &quot;yyyy/MM/dd HH:mm:ss&quot;            metrics {                                     # 从日志提取指标              timestamp log.timestamp as Long              labels level: parsed.level, service: log.service, service_instance_id: log.serviceInstance              name &quot;nginx_error_log_count&quot;              value 1            }          }          sink { }                                        # 保存（空 sink = 全保存）        }      }</code></pre><p>最简单的&quot;保存所有日志&quot;（<code>lal/default.yaml</code>）：</p><pre><code class="language-yaml">rules:  - name: default    layer: GENERAL    dsl: |      filter {        sink {        }      }</code></pre><p>LAL 语法结构（<code>oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALParser.g4</code>）：</p><ul><li>顶层 <code>root: filterBlock EOF</code>（<code>:38</code>），<code>filterBlock: FILTER L_BRACE filterContent R_BRACE</code>（<code>:43</code>），<code>filterContent: filterStatement*</code>（<code>:47</code>）。</li><li>三类块：<code>parserBlock</code>（<code>text{}</code>/<code>json{}</code>/<code>yaml{}</code>，<code>:62</code>）、<code>extractorBlock</code>（<code>:111</code>）、<code>sinkBlock</code>（<code>:227</code>）。</li><li><code>extractor</code> 里可设 <code>service</code>/<code>instance</code>/<code>endpoint</code>/<code>layer</code>/<code>traceId</code>/<code>timestamp</code>/<code>tag</code>/<code>metrics</code>/<code>def</code>（局部变量）。</li><li><code>metrics</code> 块（<code>:186</code> 的 <code>metricsBlock</code>）声明 <code>name</code>/<code>timestamp</code>/<code>labels</code>/<code>value</code>——这就是 <strong>LAL→MAL 的桥接点</strong>。</li><li><code>sink</code> 里可放 <code>sampler { rateLimit(&quot;id&quot;) { rpm 6000 } }</code>（限流采样）、<code>enforcer{}</code>（强制保存）、<code>dropper{}</code>（丢弃）（<code>sinkStatement:235</code>，分 <code>samplerBlock:242</code>/<code>enforcerStatement:262</code>/<code>dropperStatement</code>）。</li><li><code>if (condition) { ... } else if ... else ...</code> 控制流，条件支持 <code>tag(&quot;KEY&quot;)</code> 函数、<code>parsed?.field</code> 安全导航（<code>?.</code>）。</li></ul><h3 id="%E7%BC%96%E8%AF%91%E4%B8%8E%E6%89%A7%E8%A1%8C%E5%8E%9F%E7%90%86-2" tabindex="-1">编译与执行原理</h3><p>核心类（<code>oap-server/analyzer/log-analyzer/.../v2/</code>）：</p><ol><li><strong>加载与启动</strong>：<code>LogAnalyzerModuleProvider.start</code>（<code>oap-server/analyzer/log-analyzer/.../provider/LogAnalyzerModuleProvider.java:154</code>）调 <code>factory.loadStaticRules()</code>，把 <code>lal/</code> 目录下每个 YAML 的每条 rule 编译。同时把 <code>log-mal-rules/</code> 下的 YAML（从日志抽指标的 MAL 规则）<code>new MetricConvert(rule, meterSystem)</code>（<code>:160</code>）注册进 converter map——所以 LAL 模块同时承载 <code>lal</code> 和 <code>log-mal-rules</code> 两个 catalog。</li><li><strong>解析 DSL</strong>：<code>LALScriptParser.parse</code>（<code>oap-server/analyzer/log-analyzer/.../compiler/LALScriptParser.java:86</code>）用 ANTLR 解析：建 <code>LALLexer</code>（<code>:87</code>）→ <code>CommonTokenStream</code>（<code>:88</code>）→ <code>LALParser</code>（<code>:89</code>）→ <code>parser.root()</code>（<code>:105</code>），转成 <code>LALScriptModel</code>（不可变 AST）。<code>LALConfig</code> 是单条 rule 的配置 POJO（<code>name</code>/<code>dsl</code>/<code>layer</code>/<code>inputType</code>/<code>outputType</code>）。</li><li><strong>代码生成</strong>：<code>DSL.compile</code>（<code>oap-server/analyzer/log-analyzer/.../dsl/DSL.java:107</code>）调 <code>LALClassGenerator.compile</code> 为每条 LAL rule 生成<strong>单个类</strong>（实现 <code>LalExpression</code>，方法 <code>execute(FilterSpec, ExecutionContext)</code>），<code>extractor</code> 和 <code>sink</code> 块编成私有方法 <code>_extractor()</code>/<code>_sink()</code>。编译期还会做&quot;数据源类型分析&quot;：根据有无 <code>json{}</code>/<code>yaml{}</code>/<code>text{}</code> 决定 <code>parsed.*</code> 生成哪种 getter。为什么是单类而不是像 OAL 那样三类？LAL 的输入是单条日志，没有 Source→Metrics 的分发关系，一个 <code>execute</code> 方法顺序跑 parser→extractor→sink 就够了。</li><li><strong>运行时执行</strong>：日志 receiver 收到 <code>LogData</code>，对每个 layer 用 <code>LogFilterListener.Factory.create(layer)</code>（<code>oap-server/analyzer/log-analyzer/.../provider/log/listener/LogFilterListener.java:539</code>）建一个 <code>LogFilterListener</code>（<code>LogFilterListener.java:66</code>）。两阶段：<code>parse()</code>（<code>:114</code>）建 <code>ExecutionContext</code>，<code>build()</code>（<code>:89</code>）对每个 DSL 调 <code>dsl.evaluate(ctx)</code>（<code>DSL.java:134</code>）→ 生成的 <code>LalExpression.execute()</code>（<code>DSL.java:142</code>）→ 依次跑 <code>filterSpec.json/text/yaml(ctx)</code>、<code>_extractor()</code>、<code>filterSpec.sink(ctx)</code>。<code>ExecutionContext</code> 显式传参，无 ThreadLocal——这样能在多线程并发处理日志时避免线程间状态污染。</li></ol><blockquote><p><strong>LAL→MAL 桥</strong><br /><code>extractor</code> 里的 <code>metrics {}</code> 块由 <code>MetricExtractor</code>（<code>oap-server/analyzer/log-analyzer/.../dsl/spec/extractor/MetricExtractor.java</code>）处理：<code>prepareMetrics</code>（<code>:67</code>）建一个 <code>Sample</code>，<code>submitMetrics</code>（<code>:74</code>）把 <code>Sample</code> 包成 <code>SampleFamily</code>（<code>:78-79</code>），然后调 <code>provider.getMetricConverts()</code> 拿到 <code>log-mal-rules</code> 的所有 <code>MetricConvert</code>，调 <code>it.toMeter(map)</code>（<code>:97</code>）——这一步把 LAL 抽出来的样本直接喂给 MAL pipeline，复用 MAL 的 <code>Analyzer</code> 算出指标。</p><p>所以&quot;从日志里提取指标&quot;是 LAL（解析+提取样本）+ MAL（聚合样本成指标）的接力。</p></blockquote><hr /><h2 id="mqe-%C2%B7-metrics-query-engine" tabindex="-1">MQE · Metrics Query Engine</h2><h3 id="%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98-3" tabindex="-1">解决什么问题</h3><p>让用户在查询时用一条表达式把&quot;已经聚合好、存进存储的指标&quot;读出来再做二次计算（四则运算、聚合、趋势 increase/rate、TopN、排序、重命名标签、布尔逻辑、基线对比）。<strong>MQE 是读取侧的表达式语言，对应写入侧的 OAL/MAL。</strong></p><blockquote><p><strong>与 OAL 的关系</strong><br />OAL/MAL 写入，MQE 读出。OAL/MAL 定义并聚合了 <code>service_resp_time</code> 这样的指标，MQE 让你写 <code>service_resp_time / service_cpm</code> 或 <code>increase(http_count[5m])</code> 这样的表达式，OAP 读出底层指标再做运算返回给 UI/告警。</p></blockquote><h3 id="%E7%9C%9F%E5%AE%9E%E8%84%9A%E6%9C%AC%E7%A4%BA%E4%BE%8B-3" tabindex="-1">真实脚本示例</h3><p>MQE 没有 <code>.oal</code> 那样的&quot;配置文件&quot;，它是查询时由用户（GraphQL <code>metricsExpression</code> 查询、告警规则）传入的字符串。语法见 <code>oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4</code>。典型表达式：</p><pre><code class="language-">service_resp_time + service_cpm                    # 两个指标相加（addSubOp，:29）relabels(metric, label=&#39;a&#39;, replaceLabel=&#39;b&#39;)      # 重命名标签（relablesOP，:39）aggregate_labels(sum, metric, [tag])               # 跨标签聚合（aggregateLabelsOp，:40）increase(metric, 5)                                 # 趋势计算（trendOP，:35）top_n(metric, 10, des)                              # TopN（topN，:81）sort_values(metric, 10, des, avg)                  # 排序（sortValuesOP，:41）service_resp_time &gt; 100 ? service_cpm : 0          # 布尔/比较</code></pre><p>支持：标量 <code>scalar</code>（<code>:66</code>）、<code>metric</code>（可带 <code>labelList</code> 过滤，<code>:56</code>）、聚合 <code>avg/count/latest/sum/max/min</code>、数学函数 <code>abs/ceil/floor/round</code>、趋势 <code>increase/rate</code>、逻辑 <code>view_as_seq/is_present</code>、布尔 <code>and/or</code>���比较 <code>==/!=/&gt;/...&lt;/&gt;=</code>、<code>topNOf</code> 合并多个 TopN、<code>baseline</code>（AI 预测基线，需 <code>ai-pipeline</code> 模块）。</p><h3 id="%E7%BC%96%E8%AF%91%E4%B8%8E%E6%89%A7%E8%A1%8C%E5%8E%9F%E7%90%86-3" tabindex="-1">编译与执行原理</h3><blockquote><p><strong>MQE 与 OAL/MAL/LAL 的根本区别</strong><br />MQE <strong>不做代码生成</strong>，而是用 ANTLR 的 <strong>Visitor 模式直接在内存里&quot;边解析边执行&quot;</strong>。因为查询是高频、短生命周期、且要支持错误返回，没必要编译成字节码——编译一次的开销远超一次查询的执行时间，缓存这些临时类还会撑爆 PermGen。</p></blockquote><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/03-%E5%88%86%E6%9E%90%E5%BC%95%E6%93%8E%E4%B8%8E%E5%9B%9B%E5%A4%A7DSL.md_3.png" alt="图4" /></p><ol><li><strong>入口</strong>：<code>MQEExecutor.execute(expression, entity, duration, foreign)</code>（<code>oap-server/server-query-plugin/query-graphql-plugin/.../mqe/rt/MQEExecutor.java:71</code>）。GraphQL 查询 <code>MetricsExpressionQuery</code> 调它；告警侧 <code>AlarmMQEVisitor</code> 也复用同一套语法。</li><li><strong>解析</strong>：建 <code>MQELexer</code>（<code>:83</code>）→ <code>MQEParser</code>（<code>:85</code>）→ <code>parser.expression()</code>（<code>:89</code>）得到 parse tree，错误由 <code>ParseErrorListener</code> 捕获（<code>:84</code>/<code>:86</code>）。</li><li><strong>Visitor 执行</strong>：<code>MQEVisitor</code>（<code>oap-server/server-query-plugin/query-graphql-plugin/.../mqe/rt/MQEVisitor.java:65</code>）继承 <code>MQEVisitorBase</code>（<code>oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java:64</code>，一个 <code>MQEParserBaseVisitor&lt;ExpressionResult&gt;</code>）。每个语法备选对应一个 <code>visit*</code> 方法：<code>visitAddSubOp</code>（<code>MQEVisitorBase.java:110</code>）→ <code>BinaryOp.doBinaryOp</code>（<code>:124</code>），<code>visitAggregationOp</code>（<code>:178</code>）→ <code>AggregationOp.doAggregationOp</code>（<code>:188</code>），<code>visitTrendOP</code>（<code>:404</code>）→ <code>TrendOp.doTrendOp</code>（<code>:423</code>）。递归 <code>visit(ctx.expression())</code> 自底向上算，最终返回 <code>ExpressionResult</code>。把运算逻辑拆到 <code>BinaryOp</code>/<code>AggregationOp</code>/<code>TrendOp</code> 这些独立类，是为了让告警侧的 <code>AlarmMQEVisitor</code> 能复用同一套运算实现，不重复造轮子。</li><li><strong>叶子节点 <code>visitMetric</code></strong>：这是 MQE 真正&quot;读存储&quot;的地方（<code>MQEVisitor.java:110</code>）。<code>MQEVisitor</code> 持有 <code>MetricsQueryService</code>（<code>:69</code>，通过 <code>ModuleManager</code> 拿到 CoreModule 查询服务），叶子 metric 节点调 <code>getMetricsQueryService().readMetricsValues</code>（<code>:280</code>）/<code>readLabeledMetricsValues</code>（<code>:311</code>）把底层存储的指标读出来，包装成 <code>ExpressionResult</code> 往上传。</li><li><strong>调试</strong>：整个执行在 <code>DebuggingTraceContext</code> 里跑，便于 UI 展示表达式求值过程。</li></ol><blockquote><p><strong>对比 OAL</strong><br />OAL 是&quot;声明式 → 编译成 Java 类 → 长期运行聚合 worker&quot;；MQE 是&quot;表达式 → ANTLR 解析 → Visitor 即时求值 → 返回结果&quot;，无落盘、无代码生成。</p></blockquote><hr /><h2 id="%E5%9B%9B%E7%A7%8D-dsl-%E6%80%BB%E8%A7%88%E5%AF%B9%E6%AF%94" tabindex="-1">四种 DSL 总览对比</h2><table><thead><tr><th>DSL</th><th>方向</th><th>解决问题</th><th>语法载体</th><th>编译方式</th><th>运行时落点</th></tr></thead><tbody><tr><td><strong>OAL</strong></td><td>写入（聚合）</td><td>声明式从内部 Source 聚合指标</td><td><code>.oal</code> 文本</td><td>ANTLR→AST→Javassist+FreeMarker 生成 Metrics/Builder/Dispatcher 类</td><td><code>MetricsStreamProcessor</code>（经 <code>@Stream</code> 注解 + <code>StreamAnnotationListener</code>）</td></tr><tr><td><strong>MAL</strong></td><td>写入（聚合）</td><td>表达式处理外部 meter 样本</td><td>YAML（<code>exp</code> 字段）</td><td>ANTLR→AST→Javassist 生成 <code>MalExpression</code> 类 + 闭包伴生类</td><td><code>MeterSystem.create</code> → <code>MetricsStreamProcessor</code>（kind=MAL）</td></tr><tr><td><strong>LAL</strong></td><td>写入（解析+提取）</td><td>解析日志、提取字段、从日志抽指标</td><td>YAML（<code>dsl</code> 字段）</td><td>ANTLR→AST→Javassist 生成 <code>LalExpression</code> 类（单类+私有方法）</td><td><code>LogFilterListener</code> 跑 <code>execute()</code>；<code>metrics{}</code> 块经 <code>MetricExtractor</code> 喂给 MAL</td></tr><tr><td><strong>MQE</strong></td><td>读取（查询）</td><td>查询时对已存指标做表达式计算</td><td>查询字符串（无配置文件）</td><td>ANTLR→<strong>Visitor 直接执行</strong>（无代码生成）</td><td><code>MQEVisitor</code> 递归 visit，叶子 <code>visitMetric</code> 经 <code>MetricsQueryService</code> 读存储</td></tr></tbody></table><p>链路：探针 → agent-analyzer 切分成 Source / OTel → MAL 样本 / 日志 → LAL 解析 → OAL 或 MAL 聚合成指标 → <code>MetricsStreamProcessor</code> 落盘 → 用户写 MQE → <code>MQEVisitor</code> 读存储 + 二次计算 → UI/告警。</p><hr /><h2 id="analyzer-%E6%A8%A1%E5%9D%97%E7%BE%A4" tabindex="-1">analyzer 模块群</h2><p><code>oap-server/analyzer/</code> 下 7 个子模块：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/03-%E5%88%86%E6%9E%90%E5%BC%95%E6%93%8E%E4%B8%8E%E5%9B%9B%E5%A4%A7DSL.md_4.png" alt="图5" /></p><h3 id="agent-analyzer-%C2%B7-trace-%E5%88%87%E5%88%86" tabindex="-1">agent-analyzer · Trace 切分</h3><p>把 trace segment 切分成 Source 对象（<code>Service</code>/<code>Endpoint</code>/<code>ServiceInstance</code>/<code>ServiceRelation</code>/<code>EndpointRelation</code>/<code>DatabaseAccess</code>…），交给 <code>SourceReceiver</code> 分发到 OAL 生成的 Dispatcher。这是 OAL 数据的主要来源。</p><p>核心是&quot;监听器&quot;机制：一个 trace segment 进来，按 span 在 trace 里的角色（First/Entry/Exit/Local/Segment）创建不同 listener，每个在遇到自己关心的 <code>Point</code> 时提取对应 Source（<code>oap-server/analyzer/agent-analyzer/.../listener/</code>）。<code>AnalysisListener</code>（<code>AnalysisListener.java:24</code>）是根接口，<code>containsPoint(Point)</code>（<code>:34</code>）声明关心哪些点。</p><ul><li><code>FirstAnalysisListener</code>（<code>:27</code>）：trace 第一个 span，提取 trace 级信息。</li><li><code>EntryAnalysisListener</code>（<code>:27</code>）：入口 span（如 HTTP 入口），提取 endpoint 流量。</li><li><code>ExitAnalysisListener</code>（<code>:27</code>）：出口 span（如调下游/DB），提取关系。</li><li><code>LocalAnalysisListener</code>（<code>:27</code>）：本地 span，不产生跨服务关系。</li></ul><p>具体实现：</p><ul><li><code>SegmentAnalysisListener</code>（<code>:49</code>）：把 segment 原始数据落盘（<code>SegmentRecord</code>）+ 采样决策。实现多个接口，一个监听器身兼多职。</li><li><code>RPCAnalysisListener</code>（<code>:59</code>）：同时实现 Entry+Exit+Local，是提取服务间调用关系/拓扑的核心——出口侧建 <code>ServiceRelation</code>（client），入口侧建 <code>ServiceRelation</code>（server），这就是拓扑图的来源。</li><li><code>VirtualServiceProcessor</code>/<code>VirtualDatabaseProcessor</code>/<code>VirtualCacheProcessor</code>/<code>VirtualMQProcessor</code>/<code>VirtualGenAIProcessor</code>（<code>.../listener/vservice/</code>）：处理&quot;虚拟&quot; span（对缓存/DB/MQ/AI 服务的访问），转成 <code>CacheAccess</code>/<code>DatabaseAccess</code>/<code>MQAccess</code>/<code>GenAIProviderAccess</code> 等 Source，供 <code>core.oal</code> 对应 OAL 规则聚合。为什么叫&quot;虚拟&quot;？因为这些 span 不是真实的服务调用，而是探针埋点记录的&quot;对某类资源的访问&quot;，OAP 要把它映射成对应的 Source 类型才能进 OAL 聚合。</li></ul><blockquote><p><strong>根因分析</strong><br />“根因分析&quot;指通过 trace 拓扑定位错误/慢的根因 span。<code>RPCAnalysisListener</code> 同时处理 Exit（client 侧）和 Entry（server 侧），能把一个跨服务调用在两侧分别建关系 metric，从而在拓扑上区分&quot;谁发起、谁接收、错误归谁”。<code>SegmentStatusAnalyzer</code>（<code>.../listener/strategy/SegmentStatusAnalyzer.java</code>）按策略判定 segment 是否错误/慢。采样由 <code>TraceSegmentSampler</code>/<code>SamplingPolicy</code> 控制——只持久化慢/错误 trace，全量 trace 只聚合成指标，这是存储成本和可观测性的权衡。</p></blockquote><h3 id="meter-analyzer-%2F-log-analyzer" tabindex="-1">meter-analyzer / log-analyzer</h3><p>见上文 MAL / LAL 节。<code>meter-analyzer</code> 是 MAL 的宿主，被 OTel/Envoy/Telegraf/Zabbix/log-mal-rules 等所有处理外部 meter 的 receiver 共用。<code>log-analyzer</code> 承载 LAL 编译器，同时是 <code>log-mal-rules</code> catalog 的宿主。</p><h3 id="event-analyzer-%C2%B7-%E4%BA%8B%E4%BB%B6" tabindex="-1">event-analyzer · 事件</h3><p><code>EventAnalyzerModuleProvider</code> 处理 SkyWalking “事件”（服务实例启动/停止、JVM GC 事件、告警事件），注册 <code>EventRecordAnalyzerListener.Factory</code>，事件经 listener 转成 <code>Event</code> record 存储。事件不是指标，走 <code>RecordStreamProcessor</code>，不走 OAL/MAL——因为事件是离散的、不需要时间窗口聚合的原始记录，强行走聚合反而丢失语义。</p><h3 id="gen-ai-analyzer-%C2%B7-ai%2Fllm-%E5%8F%AF%E8%A7%82%E6%B5%8B" tabindex="-1">gen-ai-analyzer · AI/LLM 可观测</h3><p><code>GenAIAnalyzerModuleProvider</code>（<code>oap-server/analyzer/gen-ai-analyzer/.../GenAIAnalyzerModuleProvider.java:84</code>，<code>OALEngineLoaderService.load(GenAIOALDefine.INSTANCE)</code> 在 <code>:85</code>）加载 <code>oal/virtual-gen-ai.oal</code>，把 GenAI Provider/Model 的访问（<code>GenAIProviderAccess</code>/<code>GenAIModelAccess</code> Source）聚合成指标（token 数、成本、TTFT、延迟分位数）。它是一个专门��� OAL 触发器，配合 <code>VirtualGenAIProcessor</code> 工作——这是 OAL 可扩展性的体现：新可观测域只要定义 Source 类 + 写 <code>.oal</code> + 触发器，就能复用整套聚合/存储/查询链路。</p><h3 id="ios-analyzer-%C2%B7-ios-%E7%AB%AF" tabindex="-1">ios-analyzer · iOS 端</h3><p>监听器 <code>IOSHTTPSpanListener</code>、<code>IOSMetricKitSpanListener</code>（<code>.../analyzer/ios/listener/</code>），处理 iOS SDK 上报的 HTTP span 和 MetricKit 数据，机制类似 agent-analyzer 但针对 iOS 客户端语义。</p><h3 id="hierarchy-%C2%B7-%E5%B1%82%E7%BA%A7%E8%81%9A%E5%90%88" tabindex="-1">hierarchy · 层级聚合</h3><p>用第五种小 DSL（层级规则表达式）把服务按名称/短名/命名空间匹配成&quot;上下层级&quot;（如 <code>父服务.子服务</code>），用于在 UI 里把分布式服务/进程聚合成层级树。</p><p>语法（<code>oap-server/analyzer/hierarchy/src/main/antlr4/.../HierarchyRuleParser.g4</code>）是 Groovy 闭包风格：<code>{ (u, l) -&gt; u.name == l.name }</code>（<code>simpleExpression:46</code>）。<code>CompiledHierarchyRuleProvider</code>（<code>.../config/v2/compiler/CompiledHierarchyRuleProvider.java:48</code>）是 SPI 实现，被 <code>HierarchyDefinitionService</code> 通过 <code>ServiceLoader</code> 发现；<code>HierarchyRuleClassGenerator.compile</code> 用 ANTLR 解析 + Javassist 生成实现 <code>BiFunction&lt;Service, Service, Boolean&gt;</code> 的类，运行时由 <code>HierarchyService</code> 调这些匹配器建层级。</p><blockquote><p><strong>hierarchy 不属于四大 DSL</strong><br />hierarchy 用 ANTLR + Javassist 编译，但它是独立的&quot;匹配规则 DSL&quot;，只服务于&quot;层级&quot;这一个查询侧特性，不属于 OAL/MAL/LAL/MQE 四大 DSL。规则在 <code>hierarchy-definition.yml</code>（4 种内置模式：<code>name</code>/<code>short-name</code>/<code>lower-short-name-remove-namespace</code>/<code>lower-short-name-with-fqdn</code>）。</p></blockquote><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%BA%90%E7%A0%81%E7%B4%A2%E5%BC%95" tabindex="-1">关键源码索引</h2><h3 id="oal" tabindex="-1">OAL</h3><ul><li>语法：<code>oap-server/oal-grammar/src/main/antlr4/org/apache/skywalking/oal/rt/grammar/OALParser.g4:32</code>（<code>root</code>）、<code>:56</code>（<code>source</code>）、<code>:36</code>（<code>disableStatement</code>）、<code>:214</code>（<code>castStmt</code>）</li><li>触发：<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/oal/rt/OALEngineLoaderService.java:58</code>（<code>load</code>）、<code>:88</code>（反射加载）</li><li>引擎：<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/OALEngineV2.java:90</code>（<code>start</code>）、<code>:134</code>（<code>notifyAllListeners</code>）</li><li>解析：<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/parser/OALScriptParserV2.java:99</code>（<code>parse</code>）、<code>:123</code>（<code>OALListenerV2</code>）</li><li>AST：<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/model/MetricDefinition.java:39</code></li><li>enrich：<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/generator/MetricDefinitionEnricher.java:96</code></li><li>代码生成：<code>oap-server/oal-rt/src/main/java/org/apache/skywalking/oal/v2/generator/OALClassGeneratorV2.java:143</code>（<code>generateClassAtRuntime</code>）、<code>:196</code>（Metrics 类）、<code>:341</code>（Builder 类）、<code>:393</code>（Dispatcher 类）</li><li>接线：<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/StreamAnnotationListener.java:50</code></li><li>落盘：<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/worker/MetricsStreamProcessor.java:165</code>（OAL）、<code>:174</code>（MAL）</li><li>示例脚本：<code>oap-server/server-starter/src/main/resources/oal/core.oal</code>、<code>oal/disable.oal</code>、<code>oal/virtual-gen-ai.oal</code></li></ul><h3 id="mal" tabindex="-1">MAL</h3><ul><li>语法：<code>oap-server/analyzer/meter-analyzer/src/main/antlr4/org/apache/skywalking/mal/rt/grammar/MALParser.g4:47</code>（<code>additiveExpression</code>）、<code>:64</code>（<code>postfixExpression</code>）、<code>:79</code>（扩展函数）、<code>:126</code>（<code>closureExpression</code>）</li><li>加载：<code>oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/prometheus/rule/Rules.java:60</code>（<code>loadRules</code>）、<code>:149</code>（SnakeYAML）</li><li>OTel 触发：<code>oap-server/server-receiver-plugin/otel-receiver-plugin/.../otlp/OpenTelemetryMetricRequestProcessor.java:218</code>（<code>start</code>）、<code>:226</code>（<code>loadRules</code>）、<code>:237</code>（<code>new MetricConvert</code>）</li><li>编排：<code>oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/MetricConvert.java:107</code>（构造）、<code>:132</code>（Phase1）、<code>:166</code>（Phase2）、<code>:306</code>（<code>formatExp</code>）、<code>:325</code>（<code>toMeter</code>）</li><li>单规则：<code>oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/Analyzer.java:176</code>（<code>prepare</code>）、<code>:218</code>（<code>register</code>）、<code>:264</code>（<code>analyse</code>）、<code>:325</code>（<code>expression.run</code>）</li><li>解析+生成：<code>oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALScriptParser.java:213</code>（<code>parse</code>）、<code>:280</code>（<code>MALExprVisitor</code>）；<code>.../compiler/MALClassGenerator.java:150</code>（<code>compile</code>）；<code>.../compiler/MALMetadataExtractor.java:55</code></li><li>存储注册：<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/analysis/meter/MeterSystem.java:113</code>（<code>create</code>）</li><li>示例脚本：<code>oap-server/server-starter/src/main/resources/otel-rules/vm.yaml</code>、<code>meter-analyzer-config/java-agent.yaml</code></li></ul><h3 id="lal" tabindex="-1">LAL</h3><ul><li>语法：<code>oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALParser.g4:38</code>（<code>root</code>）、<code>:43</code>（<code>filterBlock</code>）、<code>:62</code>（<code>parserBlock</code>）、<code>:111</code>（<code>extractorBlock</code>）、<code>:186</code>（<code>metricsBlock</code>）、<code>:227</code>（<code>sinkBlock</code>）、<code>:235</code>（<code>sinkStatement</code>）</li><li>模块启动：<code>oap-server/analyzer/log-analyzer/.../provider/LogAnalyzerModuleProvider.java:154</code>（<code>start</code>）、<code>:160</code>（<code>new MetricConvert</code>）</li><li>解析：<code>oap-server/analyzer/log-analyzer/.../compiler/LALScriptParser.java:86</code>（<code>parse</code>）</li><li>代码生成：<code>oap-server/analyzer/log-analyzer/.../dsl/DSL.java:107</code>（<code>compile</code>）、<code>:134</code>（<code>evaluate</code>）、<code>:142</code>（<code>execute</code>）</li><li>运行时：<code>oap-server/analyzer/log-analyzer/.../provider/log/listener/LogFilterListener.java:66</code>（类）、<code>:89</code>（<code>build</code>）、<code>:114</code>（<code>parse</code>）、<code>:539</code>（<code>Factory.create</code>）</li><li>LAL→MAL 桥：<code>oap-server/analyzer/log-analyzer/.../dsl/spec/extractor/MetricExtractor.java:67</code>（<code>prepareMetrics</code>）、<code>:74</code>（<code>submitMetrics</code>）、<code>:97</code>（<code>toMeter</code>）</li><li>示例脚本：<code>oap-server/server-starter/src/main/resources/lal/nginx.yaml</code>、<code>lal/default.yaml</code></li></ul><h3 id="mqe" tabindex="-1">MQE</h3><ul><li>语法：<code>oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4:25</code>（<code>expression</code>）、<code>:29</code>（<code>addSubOp</code>）、<code>:35</code>（<code>trendOP</code>）、<code>:39</code>（<code>relablesOP</code>）、<code>:40</code>（<code>aggregateLabelsOp</code>）、<code>:41</code>（<code>sortValuesOP</code>）、<code>:56</code>（<code>metric</code>）、<code>:66</code>（<code>scalar</code>）、<code>:81</code>（<code>topN</code>）</li><li>入口：<code>oap-server/server-query-plugin/query-graphql-plugin/.../mqe/rt/MQEExecutor.java:71</code>（<code>execute</code>）、<code>:83</code>（lexer）、<code>:89</code>（<code>parser.expression()</code>）</li><li>Visitor 基类：<code>oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java:64</code>（类）、<code>:110</code>（<code>visitAddSubOp</code>）、<code>:178</code>（<code>visitAggregationOp</code>）、<code>:404</code>（<code>visitTrendOP</code>）</li><li>具体 Visitor：<code>oap-server/server-query-plugin/query-graphql-plugin/.../mqe/rt/MQEVisitor.java:65</code>（类）、<code>:110</code>（<code>visitMetric</code>）、<code>:280</code>（<code>readMetricsValues</code>）、<code>:311</code>（<code>readLabeledMetricsValues</code>）</li><li>结果模型：<code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/query/mqe/ExpressionResult.java</code></li></ul><hr />]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[三、Skywalking数据采集层-Receiver]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/skywalking-shu-ju-cai-ji-ceng--receiver" />
                <id>tag:https://lymboy.com,2025-10-09:skywalking-shu-ju-cai-ji-ceng--receiver</id>
                <published>2025-10-09T21:41:36+08:00</published>
                <updated>2025-10-09T21:41:36+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E5%B1%82-%C2%B7-receiver" tabindex="-1">数据采集层 · Receiver</h1><blockquote><p><strong>这一篇解决什么</strong><br />OAP 后端怎么收数据？答案是一堆 receiver 插件，每个对应一种协议或数据源。这篇先讲贯穿所有 receiver 的共同机制（共享服务器 + Source/Dispatcher 总线），再按&quot;协议大类&quot;分组讲每个插件，最后对比 push 和 pull 两种采集模式。读完应该能回答：trace/jvm/log/meter/otel/zipkin/envoy 各自怎么进来，差别在哪。</p></blockquote><hr /><h2 id="%E4%B8%80%E5%8F%A5%E8%AF%9D%E5%AE%9A%E4%BD%8D" tabindex="-1">一句话定位</h2><p>Receiver 体系解决的是&quot;用统一接口接收来自各种探针、协议、数据源的可观测数据，把它们规范化成内部 <code>Source</code> 模型送入流处理链&quot;。做成插件化是因为数据来源的协议（SkyWalking 原生 gRPC / OpenTelemetry OTLP / Zipkin / Envoy ALS / Kafka 拉取等）和上报方式彼此完全不同——每加一种协议都改一个 monolith 会越来越乱。插件化让 OAP 按需启用、独立演进、互不干扰，又能共享同一套底层服务器和 Source→Dispatcher 流转机制。</p><hr /><h2 id="%E8%B4%AF%E7%A9%BF%E6%89%80%E6%9C%89-receiver-%E7%9A%84%E5%85%B1%E5%90%8C%E6%9C%BA%E5%88%B6" tabindex="-1">贯穿所有 receiver 的共同机制</h2><p>先看骨架，再看具体插件。</p><h3 id="1.-%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%A0%87%E5%87%86%E7%94%A8%E6%B3%95" tabindex="-1">1. 模块系统的标准用法</h3><p>每个 receiver 是一个 <code>ModuleDefine</code> + <code>ModuleProvider</code>。启动流程固定四步：<code>prepare() → start() → notifyAfterCompleted()</code>，模块间依赖通过 <code>requiredModules()</code> 声明排序。几乎所有 receiver 的 <code>start()</code> 做的都是同一件事：从 <code>SharingServerModule</code> 拿到 <code>GRPCHandlerRegister</code>/<code>HTTPHandlerRegister</code>，然后 <code>addHandler(自己的 gRPC/HTTP 处理器)</code>。</p><h3 id="2.-%E6%95%B0%E6%8D%AE%E6%B5%81%E8%BD%AC%E6%80%BB%E7%BA%BF%EF%BC%9Asourcereceiver-%E2%86%92-dispatchermanager" tabindex="-1">2. 数据流转总线：SourceReceiver → DispatcherManager</h3><p>所有 receiver 收到原始数据后的统一去向：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/02-%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E5%B1%82-Receiver.md_0.png" alt="图1" /></p><ul><li>接口 <code>SourceReceiver</code>（<code>oap-server/server-core/.../source/SourceReceiver.java:28</code>）只有一个方法 <code>receive(ISource source)</code>。</li><li>实现 <code>SourceReceiverImpl</code>（<code>SourceReceiverImpl.java:29</code>）持 <code>DispatcherManager</code>，<code>receive()</code> 转发给 <code>dispatcherManager.forward(source)</code>。</li><li><code>DispatcherManager.forward</code>（<code>oap-server/server-core/.../analysis/DispatcherManager.java:46</code>）按 <code>source.scope()</code> 找到一组 <code>SourceDispatcher</code>，先 <code>source.prepare()</code>，再对每个 dispatcher 调 <code>dispatch(source)</code>（<code>:60-62</code>）。<strong>一个 Source 可被多个 dispatcher 处理</strong>——比如一个 <code>Service</code> 同时产生 resp_time、sla、cpm 等多个 metric。</li></ul><blockquote><p><strong>Source / Dispatcher 是什么</strong><br /><code>Source</code> 是&quot;一份数据事件的规范化形态&quot;，比如 <code>Service</code>、<code>ServiceRelation</code>、<code>Endpoint</code>、<code>Segment</code>、<code>Log</code>、<code>ServiceInstanceJVMMemory</code> 等几十种（<code>server-core/.../source/</code> 下有 60+ 个 Source 类）。每个有 <code>scope()</code> 整数 ID。</p><p><code>SourceDispatcher</code> 是&quot;针对某类 Source 做聚合计算的逻辑&quot;。OAL 引擎扫描用户写的 <code>.oal</code> 脚本，为每条指标规则生成一个 dispatcher 类注册进 <code>DispatcherManager</code>。这样 receiver 只管&quot;产 Source&quot;，不关心要算哪些指标——指标规则与数据接收彻底解耦。</p><p>启动时 <code>SourceReceiverImpl.scan()</code>（<code>:51</code>）用 Guava <code>ClassPath</code> 扫描 <code>org.apache.skywalking</code> 包下所有 <code>SourceDispatcher</code> 实现类自动注册。这就是&quot;为什么 OAL 生成的 dispatcher 不需要手动接线&quot;。</p></blockquote><p>为什么用&quot;一个 Source 可被多个 dispatcher 处理&quot;这种一发多收的设计？因为同一个原始事件往往要算多种指标——一次服务调用既算响应时间均值、又算成功率、又算 QPS。如果一对一，要么 receiver 重复构造多个 Source、要么把多种聚合塞进一个 dispatcher。一发多收让每条聚合规则独立成 dispatcher，新增一条规则只加一个 dispatcher，不动其他。这是观察者模式在流式聚合里的典型应用。</p><h3 id="3.-%E5%85%B1%E4%BA%AB%E6%9C%8D%E5%8A%A1%E5%99%A8%EF%BC%9Askywalking-sharing-server-plugin" tabindex="-1">3. 共享服务器：skywalking-sharing-server-plugin</h3><p>绝大多数 receiver 自己不建 gRPC/HTTP 服务器，而是向 <code>SharingServerModule</code> 注册 handler。这是整个 receiver 体系的&quot;地基&quot;。</p><ul><li>模块：<code>oap-server/server-receiver-plugin/skywalking-sharing-server-plugin/.../sharing/server/SharingServerModule.java</code></li><li>Provider：<code>SharingServerModuleProvider.java:41</code></li></ul><p>它做三件事：</p><blockquote><p><strong>SharingServer 的三段活</strong></p><ol><li><strong><code>prepare()</code></strong>（<code>:76</code>）：根据配置 <code>restPort</code>/<code>gRPCPort</code> 决定新建 <code>HTTPServer</code>/<code>GRPCServer</code>，还是用&quot;代理注册器&quot;模式（当本 receiver 没配端口时，把 handler 暂存，等有服务器的 CoreModule 启动后再委托注册）。<code>GRPCServer</code> 由 Netty 实现，注释明确说&quot;最多给 4 个 OAP 端点用：core-grpc、receiver-grpc、ebpf-grpc、als-grpc&quot;（<code>oap-server/server-library/library-server/.../grpc/GRPCServer.java:44</code>）。</li><li>把 <code>GRPCHandlerRegister</code> 和 <code>HTTPHandlerRegister</code> 作为 Service 注册出去，供其他 receiver 通过 <code>getManager().find(SharingServerModule.NAME).provider().getService(GRPCHandlerRegister.class)</code> 取用。</li><li><strong><code>notifyAfterCompleted()</code></strong>（<code>:173</code>）：真正 <code>grpcServer.start()</code> / <code>httpServer.start()</code>。</li></ol></blockquote><blockquote><p><strong>代理注册器 ReceiverGRPCHandlerRegister</strong><br /><code>oap-server/server-receiver-plugin/skywalking-sharing-server-plugin/.../sharing/server/ReceiverGRPCHandlerRegister.java:30</code>。</p><p>当 <code>SharingServerModuleProvider</code> 自己没配 gRPC 端口（比如某些部署把 receiver 合并进 core），它先创建一个空壳的 <code>ReceiverGRPCHandlerRegister</code> 注册成 Service，receiver 往里 <code>addHandler</code> 会先缓存；等 <code>start()</code> 时（<code>:160</code>）再从 <code>CoreModule</code> 取到真正的 <code>GRPCHandlerRegister</code> 赋值进来，缓存的处理就委托过去。</p><p>这让 receiver 代码完全不感知&quot;服务器在哪建&quot;，只管注册。还支持 <code>addFilter</code> 在 <code>addHandler</code> 之前调用，给每个 handler 绑上拦截器（如鉴权 <code>AuthenticationInterceptor</code>）。</p></blockquote><h3 id="4.-grpc-stream-%E4%B8%8E-proto-%E5%9F%BA%E7%A1%80" tabindex="-1">4. gRPC stream 与 proto 基础</h3><p>receiver handler 继承 proto 生成的 <code>*Grpc.*ImplBase</code>，实现 <code>GRPCHandler</code> 标记接口。</p><blockquote><p><strong>gRPC stream / StreamObserver / proto 速成</strong></p><ul><li><strong>proto</strong>：用 Protocol Buffers 描述语言写数据结构和接口契约，编译后生成 Java 类。比如 <code>Tracing.proto</code> 定义 <code>TraceSegmentReportService</code> 服务，含 <code>rpc collect(stream SegmentObject) returns (Commands)</code>。</li><li><strong><code>stream</code> 关键字</strong>：表示客户端流式上传——客户端可以连续 <code>onNext</code> 发多个对象，最后 <code>onCompleted</code>。<code>StreamObserver&lt;T&gt;</code> 就是 gRPC 给的回调接口：每来一条调 <code>onNext</code>，结束调 <code>onCompleted</code>，出错调 <code>onError</code>。</li><li><strong>unary RPC</strong>：一次请求一次响应，没有 <code>stream</code> 关键字，如 JVM 的 <code>collect(JVMMetricCollection) returns (Commands)</code>。</li></ul><p>SkyWalking 原生协议里，trace、mesh、meter、log 用 client-streaming（流量大、可背压），JVM/CLR/event 用 unary（定时批量、简单）。</p></blockquote><h3 id="5.-%E4%B8%8E-apm-protocol-%E7%9A%84%E5%85%B3%E7%B3%BB" tabindex="-1">5. 与 apm-protocol 的关系</h3><p><code>apm-protocol/apm-network/src/main/proto/</code> 是所有 SkyWalking 原生协议的 proto 源头。关键映射：</p><table><thead><tr><th>数据类型</th><th>proto 文件</th><th>关键 rpc</th></tr></thead><tbody><tr><td>链路追踪</td><td><code>language-agent/Tracing.proto</code></td><td><code>collect(stream SegmentObject)</code>、<code>collectInSync</code></td></tr><tr><td>JVM 监控</td><td><code>language-agent/JVMMetric.proto</code></td><td><code>collect(JVMMetricCollection)</code> unary</td></tr><tr><td>CLR(.NET)</td><td><code>language-agent/CLRMetric.proto</code></td><td>unary</td></tr><tr><td>自定义 Meter</td><td><code>language-agent/Meter.proto</code></td><td><code>collect(stream MeterData)</code></td></tr><tr><td>日志</td><td><code>logging/Logging.proto</code></td><td><code>collect(stream LogData)</code></td></tr><tr><td>事件</td><td><code>event/Event.proto</code></td><td>—</td></tr><tr><td>线程 Profiling</td><td><code>profile/Profile.proto</code></td><td>—</td></tr><tr><td>eBPF</td><td><code>ebpf/accesslog.proto</code>、<code>ebpf/profiling/*.proto</code></td><td>—</td></tr><tr><td>服务网格</td><td><code>service-mesh-probe/service-mesh.proto</code></td><td><code>collect(stream ServiceMeshMetrics)</code></td></tr><tr><td>浏览器 RUM</td><td><code>browser/BrowserPerf.proto</code></td><td>—</td></tr><tr><td>管理(注册)</td><td><code>management/Management.proto</code></td><td>—</td></tr><tr><td>公共响应</td><td><code>common/Command.proto</code></td><td><code>Commands</code> 是几乎所有响应的回包</td></tr></tbody></table><blockquote><p><strong>Compat 协议：向后兼容</strong><br />还有一组 <code>*Compat.proto</code>（TracingCompat / JVMMetricCompat / …）。它们用于协议向后兼容：proto 字段演进时，旧版 agent 仍能用兼容服务名上报，handler 里 <code>*Compat</code> 类（如 <code>TraceSegmentReportServiceHandlerCompat</code>）把旧协议请求转成新处理逻辑。每个原生 receiver 都成对注册主 handler 和 <code>*Compat</code> handler。</p></blockquote><p><code>receiver-proto/src/main/proto/</code> 存 OpenTelemetry OTLP 协议的 proto 和 Envoy 相关 proto，供 otel-receiver 和 envoy-metrics-receiver 编译使用。</p><hr /><h2 id="%E4%B8%89%E5%B1%82%E5%88%86%E5%B7%A5" tabindex="-1">三层分工</h2><p>理解 receiver 体系的关键是分清三层职责：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/02-%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E5%B1%82-Receiver.md_1.png" alt="图2" /></p><p>receiver 与 analyzer 通过 <code>ISegmentParserService</code>/<code>IMeterProcessService</code>/<code>ILogAnalyzerService</code> 等 Service 接口解耦。这就是 <code>requiredModules</code> 几乎都列 <code>AnalyzerModule</code>/<code>LogAnalyzerModule</code> 的原因。</p><hr /><h2 id="a-%E7%BB%84%EF%BC%9Askywalking-%E5%8E%9F%E7%94%9F-grpc-%E5%8D%8F%E8%AE%AE%E6%8E%A5%E6%94%B6%E5%99%A8" tabindex="-1">A 组：SkyWalking 原生 gRPC 协议接收器</h2><p>共同点：handler 继承 <code>*Grpc.*ImplBase</code>，通过 <code>SharingServerModule</code> 注册，数据流向 <code>SourceReceiver</code>。区别只在&quot;中间解析层&quot;放哪。</p><h3 id="a1.-trace-receiver-%C2%B7-%E9%93%BE%E8%B7%AF%E8%BF%BD%E8%B8%AA" tabindex="-1">A1. trace-receiver · 链路追踪</h3><ul><li><strong>Module/Provider</strong>：<code>TraceModule</code> / <code>TraceModuleProvider</code>（<code>.../trace/provider/TraceModuleProvider.java:39</code>）</li><li><strong>传输</strong>：gRPC client-streaming + HTTP REST</li><li><strong>关键类</strong>：<code>TraceSegmentReportServiceHandler</code>（<code>.../handler/v8/grpc/TraceSegmentReportServiceHandler.java:39</code>）继承 <code>TraceSegmentReportServiceGrpc.TraceSegmentReportServiceImplBase</code>，<code>collect</code> 返回 <code>StreamObserver</code>，每收到一个 <code>SegmentObject</code> 调 <code>segmentParserService.send(segment)</code>（<code>:73</code>）。</li></ul><p>数据流转（最完整的一条链）：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/02-%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E5%B1%82-Receiver.md_2.png" alt="图3" /></p><blockquote><p><strong>receiver 与 analyzer 的分工</strong><br />trace receiver 自身只做&quot;接收 + 转发&quot;，真正解析成 Source 的逻辑在 <code>analyzer</code> 模块。<code>TraceModuleProvider.java:46</code> 从 <code>AnalyzerModule</code> 取 <code>ISegmentParserService</code>；<code>SegmentParserServiceImpl.send</code>（<code>oap-server/analyzer/agent-analyzer/.../trace/parser/SegmentParserServiceImpl.java:38</code>）new 一个 <code>TraceAnalyzer</code> 并 <code>doAnalysis</code>；<code>TraceAnalyzer.doAnalysis</code>（<code>.../trace/parser/TraceAnalyzer.java:45</code>）遍历每个 <code>SpanObject</code>，按 <code>SpanType</code>（Entry/Exit/Local）分发到不同 listener，最后 <code>build()</code>。这就是为什么 <code>TraceModuleProvider.requiredModules()</code> 含 <code>AnalyzerModule.NAME</code>。</p></blockquote><h3 id="a2.-jvm-receiver-%C2%B7-jvm-%E7%9B%91%E6%8E%A7" tabindex="-1">A2. jvm-receiver · JVM 监控</h3><ul><li><strong>Module/Provider</strong>：<code>JVMModule</code> / <code>JVMModuleProvider</code>（<code>.../jvm/provider/JVMModuleProvider.java:32</code>）</li><li><strong>传输</strong>：gRPC <strong>unary</strong>（<code>collect(JVMMetricCollection) returns Commands</code>）</li><li><strong>关键类</strong>：<code>JVMMetricReportServiceHandler</code>（<code>.../jvm/provider/handler/JVMMetricReportServiceHandler.java:33</code>）</li></ul><p>与 trace 的区别：JVM 不走 <code>AnalyzerModule</code>，handler 自己直接 new <code>JVMSourceDispatcher</code>（在 analyzer 模块）。<code>collect()</code> 先做命名规范化（<code>NamingControl.formatServiceName</code>），再 <code>jvmSourceDispatcher.sendMetric(...)</code> 把每个 <code>JVMMetric</code> 转成 <code>ServiceInstanceJVMMemory</code>/<code>ServiceInstanceJVMCPU</code>/<code>ServiceInstanceJVMGC</code>/<code>ServiceInstanceJVMThread</code> 等 Source 并 <code>sourceReceiver.receive()</code>。</p><p>Provider <code>start()</code> 里 <code>OALEngineLoaderService.load(JVMOALDefine.INSTANCE)</code>（<code>JVMModuleProvider.java:57</code>）——JVM 的指标规则写在 <code>JVMOALDefine</code> 里，启动时编译成 dispatcher 注入 <code>DispatcherManager</code>。</p><h3 id="a3.-clr-receiver-%C2%B7-.net-clr" tabindex="-1">A3. clr-receiver · .NET CLR</h3><p>与 JVM 几乎同构，数据是 .NET CLR 指标（CPU/GC/Thread）。<code>CLRMetricReportServiceHandler</code> + <code>CLRSourceDispatcher</code>（<code>.../clr/provider/handler/CLRSourceDispatcher.java:36</code>），也是 gRPC unary。</p><h3 id="a4.-mesh-receiver-%C2%B7-%E6%9C%8D%E5%8A%A1%E7%BD%91%E6%A0%BC" tabindex="-1">A4. mesh-receiver · 服务网格</h3><blockquote><p><strong>易混点</strong><br />这个 receiver 处理的是 SkyWalking <strong>自定义的 service-mesh proto</strong>（<code>ServiceMeshMetrics</code>），<strong>不是</strong> Envoy ALS 的 <code>ALS</code> proto。Envoy ALS 在 D 组的 <code>envoy-metrics-receiver-plugin</code>。</p></blockquote><ul><li><strong>Module/Provider</strong>：<code>MeshReceiverModule</code> / <code>MeshReceiverProvider</code>（<code>.../mesh/MeshReceiverProvider.java:32</code>）</li><li><strong>传输</strong>：gRPC client-streaming（<code>collect(stream ServiceMeshMetrics) returns MeshProbeDownstream</code>）</li><li><strong>关键类</strong>：<code>MeshGRPCHandler</code>（<code>.../mesh/MeshGRPCHandler.java:30</code>），每条 <code>onNext</code> 调 <code>TelemetryDataDispatcher.process(metrics)</code>。</li><li><code>TelemetryDataDispatcher</code>（<code>.../mesh/TelemetryDataDispatcher.java:60</code>）静态工具，<code>init()</code> 时从 CoreModule 取 <code>SourceReceiver</code>/<code>NamingControl</code>。<code>dispatchHTTPMetrics</code>/<code>dispatchTCPMetrics</code> 把一条 mesh metric 拆成多个 Source（<code>Service</code>、<code>ServiceInstance</code>、<code>Endpoint</code>、<code>ServiceRelation</code>，或 TCP 情况的 <code>TCPService</code>/<code>TCPServiceRelation</code>），分别 <code>SOURCE_RECEIVER.receive(...)</code>。这是&quot;一条入参 → 多个 Source&quot;的典型。</li></ul><h3 id="a5.-log-receiver-%C2%B7-%E6%97%A5%E5%BF%97" tabindex="-1">A5. log-receiver · 日志</h3><ul><li><strong>Module/Provider</strong>：<code>LogModule</code> / <code>LogModuleProvider</code>（<code>.../log/provider/LogModuleProvider.java:36</code>）</li><li><strong>传输</strong>：gRPC client-streaming + HTTP REST</li><li><strong>关键类</strong>：<code>LogReportServiceGrpcHandler</code>（<code>.../log/provider/handler/grpc/LogReportServiceGrpcHandler.java:41</code>），每条 <code>onNext</code> 调 <code>logAnalyzerService.doAnalysis(...)</code>（<code>:96</code>）。细节：用 <code>setServiceName</code> 在 stream 内复用上一个非空 service 名（<code>:79</code>），因为日志流里 service 字段可能只第一条有。</li></ul><p>像 trace 一样依赖独立的 <code>LogAnalyzerModule</code>，取 <code>ILogAnalyzerService.doAnalysis()</code>。日志最终走 LAL 规则转成 <code>Log</code>/<code>LogMetadata</code> 等 Source。</p><h3 id="a6.-meter-receiver-%C2%B7-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8C%87%E6%A0%87" tabindex="-1">A6. meter-receiver · 自定义指标</h3><ul><li><strong>Module/Provider</strong>：<code>MeterReceiverModule</code> / <code>MeterReceiverProvider</code>（<code>.../meter/provider/MeterReceiverProvider.java:34</code>）</li><li><strong>传输</strong>：gRPC client-streaming（<code>collect(stream MeterData)</code>）+ 批量（<code>collectBatch(stream MeterDataCollection)</code>）</li><li><strong>关键类</strong>：<code>MeterServiceHandler</code>（<code>.../meter/provider/handler/MeterServiceHandler.java:42</code>）</li></ul><p>数据流转（委托 analyzer 的&quot;攒批后统一处理&quot;模式）：Provider 从 <code>AnalyzerModule</code> 取 <code>IMeterProcessService</code>（<code>MeterReceiverProvider.java:59</code>）。<code>collect()</code> 里 <code>processService.createProcessor()</code> 得到 <code>MeterProcessor</code>，每条 <code>MeterData</code> 调 <code>processor.read</code> 累积，<code>onCompleted()</code> 时 <code>processor.process()</code> 真正触发 MAL 规则计算 → Source → dispatcher。</p><h3 id="a7%E2%80%93a13.-%E5%85%B6%E4%BB%96%E5%8E%9F%E7%94%9F-receiver-%E9%80%9F%E8%A7%88" tabindex="-1">A7–A13. 其他原生 receiver 速览</h3><table><thead><tr><th>插件</th><th>传输</th><th>要点</th></tr></thead><tbody><tr><td>event-receiver</td><td>gRPC + HTTP</td><td>依赖 <code>EventAnalyzerModule</code>，事件经 analyzer 转 <code>Event</code> record，走 <code>RecordStreamProcessor</code> 不走 OAL</td></tr><tr><td>profile-receiver</td><td>gRPC（+Compat）</td><td>双向交互：OAP 不只收数据，还要把采集任务下发给 agent。线程栈快照存为 profiling 数据，不走标准 Source→OAL 链</td></tr><tr><td>browser-receiver</td><td>gRPC + HTTP</td><td>自带 listener 体系（<code>PerfDataParserListenerManager</code>、<code>ErrorLogParserListenerManager</code>），<code>start()</code> 里 <code>load(BrowserOALDefine.INSTANCE)</code></td></tr><tr><td>ebpf-receiver</td><td>可配独立 gRPC 端口</td><td><strong>弹性模式</strong>：有自己端口就用自己（线程名 <code>ebpf-grpc</code>），没配就 fallback 到 sharing。handler：进程上报/Profiling 数据/访问日志</td></tr><tr><td>pprof / async-profiler</td><td>gRPC（sharing）</td><td>收 Go pprof / Java async-profiler JFR 数据，存原始 profiling，不走 Source→OAL metrics 链</td></tr><tr><td>management-receiver</td><td>gRPC + HTTP</td><td>“管理面”：接收 agent 服务名/实例注册、心跳、属性上报，维护元数据 ID 分配，不产指标 Source</td></tr><tr><td>configuration-discovery-receiver</td><td>gRPC</td><td>“控制面”：让 agent 拉动态配置，向 <code>DynamicConfigurationService</code> 注册 <code>AgentConfigurationsWatcher</code></td></tr></tbody></table><hr /><h2 id="b-%E7%BB%84%EF%BC%9Aopentelemetry-%E5%8D%8F%E8%AE%AE%E6%8E%A5%E5%85%A5" tabindex="-1">B 组：OpenTelemetry 协议接入</h2><p><code>otel-receiver-plugin</code> 与原生协议的核心区别：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/02-%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E5%B1%82-Receiver.md_3.png" alt="图4" /></p><p>四点不同：</p><ol><li><strong>proto 完全不同</strong>：用 OpenTelemetry 官方 OTLP proto（<code>io.opentelemetry.proto.collector.*.*Grpc</code>），不是 SkyWalking 自己的 <code>apm.network.*</code>。proto 源在 <code>receiver-proto/src/main/proto/opentelemetry/proto/</code>。</li><li><strong>可拔插 Handler 机制</strong>：<code>OtelMetricReceiverProvider</code>（<code>.../otel/OtelMetricReceiverProvider.java:34</code>）在 <code>prepare()</code> 用 <code>ServiceLoader.load(Handler.class)</code>（<code>:80</code>）发现所有 <code>Handler</code> 实现，按 <code>enabledHandlers</code> 过滤；<code>start()</code> 里对每个 handler 调 <code>init()</code> + <code>active()</code>（<code>:91-93</code>）。<strong>加新 OTLP 数据类型只要实现 <code>Handler</code> 接口并注册 SPI</strong>。</li><li><strong>数据流转分两类</strong>：<ul><li><strong>Trace</strong>：<code>OpenTelemetryTraceHandler</code>（<code>.../otel/otlp/OpenTelemetryTraceHandler.java:72</code>）把 OTLP span <strong>先转成 Zipkin <code>Span</code></strong>（<code>convertSpan</code>，<code>:191</code>），再交给 <code>ZipkinReceiverModule</code> 的 <code>SpanForwardService.send()</code>（<code>:184</code>）——OTLP trace 复用 Zipkin 处理链。</li><li><strong>Metric</strong>：<code>OpenTelemetryMetricHandler</code>（<code>.../otel/otlp/OpenTelemetryMetricHandler.java:38</code>）调 <code>OpenTelemetryMetricRequestProcessor.processMetricsRequest()</code>，用 MAL 规则把 OTLP metrics 转成 SkyWalking Meter 体系。</li></ul></li><li><strong>gRPC 都是 unary</strong>：OTLP 的 <code>export(ExportTraceServiceRequest)</code> 是 unary，不像原生 trace 是 client-streaming。</li></ol><blockquote><p><strong>用 OTLP 接 trace 必须同时启用 zipkin receiver</strong><br /><code>OpenTelemetryTraceHandler</code> 把 OTLP span 转成 zipkin <code>Span</code> 后调 <code>SpanForwardService.send()</code>，这个 service 来自 <code>ZipkinReceiverModule</code>。所以用 OTLP 接 trace 时必须同时启用 zipkin receiver。</p></blockquote><p><strong>AWS Firehose</strong> 是个旁支：<code>AWSFirehoseReceiverModuleProvider</code> 自己起独立 HTTP 服务器（<code>firehose-http</code>），收 AWS Kinesis Firehose 推送，但内部复用 <code>OtelMetricReceiverModule</code> 的 <code>OpenTelemetryMetricRequestProcessor</code>——本质是&quot;另一种传输入口（AWS Firehose HTTP）+ OTLP 处理内核&quot;。</p><hr /><h2 id="c-%E7%BB%84%EF%BC%9Azipkin-%E5%8D%8F%E8%AE%AE" tabindex="-1">C 组：Zipkin 协议</h2><ul><li><strong>Module/Provider</strong>：<code>ZipkinReceiverModule</code> / <code>ZipkinReceiverProvider</code>（<code>.../zipkin/ZipkinReceiverProvider.java:37</code>）</li><li><strong>传输</strong>：HTTP REST（<code>ZipkinSpanHTTPHandler</code>，POST/GET）+ <strong>可选 Kafka 消费</strong>。<strong>不依赖 sharing server</strong>——自己 new <code>HTTPServer</code>（线程名 <code>zipkin-http</code>），是少数完全独立服务器的 receiver。</li></ul><p>核心流转类 <code>SpanForward</code>（<code>.../zipkin/trace/SpanForward.java:55</code>）实现 <code>SpanForwardService</code>：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/02-%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E5%B1%82-Receiver.md_4.png" alt="图5" /></p><p>OTLP trace 复用的就是这条链（见 B 组）。</p><hr /><h2 id="d-%E7%BB%84%EF%BC%9Aenvoy-als" tabindex="-1">D 组：Envoy ALS</h2><ul><li><strong>Module/Provider</strong>：<code>EnvoyMetricReceiverModule</code> / <code>EnvoyMetricReceiverProvider</code>（<code>.../envoy/EnvoyMetricReceiverProvider.java:42</code>）</li><li><strong>传输</strong>：gRPC，两类服务：<ul><li><strong>Envoy ALS（Access Log Service）</strong>：<code>AccessLogServiceGRPCHandler</code> + V3 兼容，收 Envoy 访问日志（L7 HTTP / L4 TCP），用 Envoy 官方 <code>als.proto</code>。</li><li><strong>Envoy Metrics Service</strong>：<code>MetricServiceGRPCHandler</code> + V3，可选，收 Envoy stats。</li></ul></li><li><strong>服务器策略</strong>：和 eBPF 一样可配独立 gRPC 端口（线程名 <code>als-grpc</code>），否则 fallback 到 sharing。</li><li><strong>依赖 mesh receiver</strong>：<code>requiredModules</code> 含 <code>MeshReceiverModule.NAME</code>——Envoy ALS 复用 mesh 的 OAL 规则与部分逻辑。还加载 <code>TCPOALDefine</code>（当配了 <code>alsTCPAnalysis</code>）和 <code>metadata-service-mapping.yaml</code> 字段映射。</li></ul><blockquote><p><strong>mesh-receiver vs envoy-metrics-receiver</strong></p><ul><li><code>skywalking-mesh-receiver-plugin</code>（A4）收 SkyWalking 私有 service-mesh proto（适合被 SkyWalking Rover/ALS adapter 转换过的数据）。</li><li><code>envoy-metrics-receiver-plugin</code>（D 组）收 Envoy 原生 ALS/Metrics proto（直接对接 Envoy）。</li></ul><p>两者都产出 mesh 类 Source，但入口协议不同。</p></blockquote><hr /><h2 id="e-%E7%BB%84%EF%BC%9A%E6%8B%89%E5%8F%96%E5%BC%8F-fetcher%EF%BC%88pull%EF%BC%89" tabindex="-1">E 组：拉取式 Fetcher（pull）</h2><p>与上面所有 receiver 不同——fetcher <strong>不开放端口等数据来</strong>，而是<strong>主动去外部系统拉数据</strong>。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/02-%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E5%B1%82-Receiver.md_5.png" alt="图6" /></p><h3 id="e1.-kafka-fetcher-%C2%B7-%E6%8A%8A%E5%8E%9F%E7%94%9F%E5%85%A5%E5%8F%A3%E6%8D%A2%E6%88%90-kafka" tabindex="-1">E1. kafka-fetcher · 把原生入口换成 Kafka</h3><ul><li><strong>Module/Provider</strong>：<code>KafkaFetcherModule</code> / <code>KafkaFetcherProvider</code>（<code>oap-server/server-fetcher-plugin/kafka-fetcher-plugin/.../provider/KafkaFetcherProvider.java:42</code>）</li><li><strong>机制</strong>：<code>prepare()</code> 创建 <code>KafkaFetcherHandlerRegister</code>（<code>:73</code>）；<code>start()</code> 注册一系列 <code>KafkaHandler</code>（<code>:78-89</code>）然后 <code>handlerRegister.start()</code>。这些 handler 与原生 receiver 一一对应，<strong>复用同一处理内核</strong>：</li></ul><table><thead><tr><th>KafkaHandler</th><th>消费内容</th><th>复用的 analyzer Service</th></tr></thead><tbody><tr><td><code>TraceSegmentHandler</code></td><td>Kafka 里的 <code>SegmentObject</code></td><td><code>ISegmentParserService</code>（同 A1）</td></tr><tr><td><code>JVMMetricsHandler</code></td><td>JVM 指标</td><td><code>JVMSourceDispatcher</code>（同 A2）</td></tr><tr><td><code>MeterServiceHandler</code></td><td>meter</td><td><code>IMeterProcessService</code>（同 A6）</td></tr><tr><td><code>LogHandler</code>/<code>JsonLogHandler</code></td><td>日志（proto/JSON）</td><td><code>ILogAnalyzerService</code>（同 A5）</td></tr><tr><td><code>ServiceManagementHandler</code></td><td>注册信息</td><td>同 A12</td></tr><tr><td><code>ProfileTaskHandler</code></td><td>profiling</td><td>—</td></tr></tbody></table><p>这意味着 agent 可以不直连 OAP，而是把数据写 Kafka，OAP 从 Kafka 消费——适合大流量/解耦部署。处理内核完全复用，所以 <code>requiredModules</code> 仍含 <code>AnalyzerModule</code>/<code>LogAnalyzerModule</code>。它<strong>不依赖 SharingServerModule</strong>（不需要 gRPC 端口）。</p><h3 id="e2.-cilium-fetcher-%C2%B7-grpc-stream-%E5%AE%A2%E6%88%B7%E7%AB%AF" tabindex="-1">E2. cilium-fetcher · gRPC stream 客户端</h3><ul><li><strong>Module/Provider</strong>：<code>CiliumFetcherModule</code> / <code>CiliumFetcherProvider</code>（<code>.../cilium/CiliumFetcherProvider.java:38</code>）</li><li><strong>机制</strong>：OAP 作为 gRPC <strong>client</strong> 主动连 Cilium Hubble Relay 的 observer gRPC stream，订阅 flow 事件。<code>start()</code> 创建 <code>CiliumNodeManager</code>（<code>:92</code>）管理到多个 Hubble 节点的连接，加 <code>CiliumFlowListener</code>（<code>:93</code>），flow 事件解析成 Cilium 相关 Source（<code>CiliumService</code>/<code>CiliumServiceRelation</code>/<code>CiliumServiceInstanceRelation</code>）。</li><li><strong>依赖</strong>：<code>CoreModule</code> + <code>ClusterModule</code>（多 OAP 节点要分摊 Hubble 连接），加载 <code>CiliumOALDefine</code>。</li></ul><h3 id="e3.-fetcher-proto" tabindex="-1">E3. fetcher-proto</h3><p>仅 proto 定义模块（<code>observer.proto</code> 等），编译出 Cilium Hubble 的 Java stub，无运行时逻辑。</p><hr /><h2 id="f-%E7%BB%84%EF%BC%9A%E7%A7%81%E6%9C%89-http-%E5%8D%8F%E8%AE%AE%E6%8E%A5%E6%94%B6%E5%99%A8" tabindex="-1">F 组：私有 HTTP 协议接收器</h2><h3 id="f1.-telegraf-receiver-%C2%B7-influxdb-line-protocol" tabindex="-1">F1. telegraf-receiver · InfluxDB line protocol</h3><p><code>TelegrafReceiverProvider</code>（<code>.../telegraf/provider/TelegrafReceiverProvider.java:43</code>）复用 sharing server 的 <code>HTTPHandlerRegister</code>。Telegraf 用 InfluxDB line protocol 格式上报指标，Provider <code>start()</code> 加载 MAL 规则（<code>Rules.loadRules</code>，<code>:83</code>），取 <code>MeterSystem</code>（<code>:92</code>）注入 handler。本质是&quot;telegraf 格式 → MAL 体系&quot;。</p><h3 id="f2.-zabbix-receiver-%C2%B7-zabbix-%E7%A7%81%E6%9C%89-json-%E5%8D%8F%E8%AE%AE" tabindex="-1">F2. zabbix-receiver · Zabbix 私有 JSON 协议</h3><p><code>ZabbixReceiverProvider</code>（<code>.../zabbix/provider/ZabbixReceiverProvider.java:37</code>）<strong>自实现 Zabbix 协议服务器</strong>（<code>ZabbixServer</code>，<code>:84</code> 自己 <code>start()</code>），不依赖 sharing server——因为 Zabbix agent 用 Zabbix 私有 JSON 协议，不是标准 HTTP/gRPC。Zabbix 指标进 <code>MeterSystem</code> → MAL。</p><hr /><h2 id="%E5%85%B3%E9%94%AE%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%B0%8F%E7%BB%93" tabindex="-1">关键设计模式小结</h2><p>写文档可用的六条结论：</p><ol><li><p><strong>统一入口注册，统一出口流转</strong>：几乎所有 receiver 在 <code>start()</code> 里 <code>getManager().find(SharingServerModule.NAME).provider().getService(GRPCHandlerRegister.class).addHandler(myHandler)</code>，数据最终都进 <code>SourceReceiver.receive()</code> → <code>DispatcherManager.forward()</code>。</p></li><li><p><strong>三层分工</strong>：receiver 管协议入口，analyzer 管语义解析，OAL/MAL/LAL 管指标计算。</p></li><li><p><strong>服务器复用的两种形态</strong>：</p><ul><li><strong>默认</strong>：receiver 向 <code>SharingServerModule</code> 注册 handler，共享 <code>receiver-grpc</code> 端口（trace/jvm/clr/mesh/log/meter/event/profile/browser/pprof/async-profiler/management/config-discovery/otel）。</li><li><strong>独立服务器</strong>：协议有特殊性能/隔离需求时，receiver 自建 <code>GRPCServer</code>/<code>HTTPServer</code>，如 <code>ebpf-grpc</code>、<code>als-grpc</code>、<code>zipkin-http</code>、<code>firehose-http</code>、Zabbix 私有协议。还有&quot;有配置就���立、没配置就 fallback 到 sharing&quot;的弹性模式（eBPF、Envoy）。</li></ul></li><li><p><strong>协议演进的兼容机制</strong>：<code>*Compat.proto</code> + <code>*HandlerCompat</code> 让旧版 agent 不断链。每个原生 receiver 注册一对（主 + Compat）handler。</p></li><li><p><strong>OTLP 与原生的本质区别</strong>：原生是&quot;私有 proto + 流式 + analyzer 直连&quot;；OTLP 是&quot;标准 proto + unary + 复用 Zipkin(trace)/MAL(metric) 链 + SPI 扩展&quot;。OTLP trace 必须配 zipkin receiver 才能工作。</p></li><li><p><strong>Receiver（push）vs Fetcher（pull）</strong>：receiver 开端口等数据，fetcher 主动连外部系统。kafka-fetcher 特殊——把&quot;原生 receiver 的处理内核&quot;接到&quot;Kafka 消费入口&quot;上，实现入口与处理解耦。</p></li></ol><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键类</th><th>路径</th></tr></thead><tbody><tr><td>共享服务器</td><td>SharingServerModuleProvider</td><td><code>oap-server/server-receiver-plugin/skywalking-sharing-server-plugin/.../sharing/server/SharingServerModuleProvider.java</code></td></tr><tr><td></td><td>ReceiverGRPCHandlerRegister</td><td>同目录 <code>ReceiverGRPCHandlerRegister.java</code></td></tr><tr><td>Source 总线</td><td>SourceReceiverImpl</td><td><code>oap-server/server-core/.../source/SourceReceiverImpl.java</code></td></tr><tr><td></td><td>DispatcherManager</td><td><code>oap-server/server-core/.../analysis/DispatcherManager.java</code></td></tr><tr><td>trace</td><td>TraceSegmentReportServiceHandler</td><td><code>oap-server/server-receiver-plugin/skywalking-trace-receiver-plugin/.../handler/v8/grpc/TraceSegmentReportServiceHandler.java</code></td></tr><tr><td>jvm</td><td>JVMMetricReportServiceHandler</td><td><code>oap-server/server-receiver-plugin/skywalking-jvm-receiver-plugin/.../jvm/provider/handler/JVMMetricReportServiceHandler.java</code></td></tr><tr><td>mesh</td><td>TelemetryDataDispatcher</td><td><code>oap-server/server-receiver-plugin/skywalking-mesh-receiver-plugin/.../mesh/TelemetryDataDispatcher.java</code></td></tr><tr><td>log</td><td>LogReportServiceGrpcHandler</td><td><code>oap-server/server-receiver-plugin/skywalking-log-receiver-plugin/.../log/provider/handler/grpc/LogReportServiceGrpcHandler.java</code></td></tr><tr><td>meter</td><td>MeterServiceHandler</td><td><code>oap-server/server-receiver-plugin/skywalking-meter-receiver-plugin/.../meter/provider/handler/MeterServiceHandler.java</code></td></tr><tr><td>otel</td><td>OpenTelemetryTraceHandler</td><td><code>oap-server/server-receiver-plugin/otel-receiver-plugin/.../otel/otlp/OpenTelemetryTraceHandler.java</code></td></tr><tr><td></td><td>OpenTelemetryMetricHandler</td><td>同目录 <code>OpenTelemetryMetricHandler.java</code></td></tr><tr><td>zipkin</td><td>SpanForward</td><td><code>oap-server/server-receiver-plugin/zipkin-receiver-plugin/.../zipkin/trace/SpanForward.java</code></td></tr><tr><td>envoy</td><td>EnvoyMetricReceiverProvider</td><td><code>oap-server/server-receiver-plugin/envoy-metrics-receiver-plugin/.../envoy/EnvoyMetricReceiverProvider.java</code></td></tr><tr><td>kafka fetcher</td><td>KafkaFetcherProvider</td><td><code>oap-server/server-fetcher-plugin/kafka-fetcher-plugin/.../provider/KafkaFetcherProvider.java</code></td></tr><tr><td>cilium fetcher</td><td>CiliumFetcherProvider</td><td><code>oap-server/server-fetcher-plugin/cilium-fetcher-plugin/.../cilium/CiliumFetcherProvider.java</code></td></tr><tr><td>proto 源</td><td>原生协议</td><td><code>apm-protocol/apm-network/src/main/proto/</code></td></tr><tr><td></td><td>OTLP/Envoy</td><td><code>oap-server/server-receiver-plugin/receiver-proto/src/main/proto/</code></td></tr></tbody></table><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5" tabindex="-1">接下来</h2><p>数据进来了，但&quot;算哪些指标&quot;由谁决定？答案是 OAL/MAL/LAL 三种 DSL——它们在启动时编译成 Dispatcher，接在 receiver 产出的 Source 后面。详见 03-分析引擎与四大DSL。</p><blockquote><p><strong>心智模型</strong><br />把 receiver 想成&quot;快递驿站&quot;——各种运输公司（gRPC/HTTP/Kafka/OTLP）把货送来，驿站只管签收、贴统一标签（转成 Source）。至于这批货要加工成什么产品（指标），那是后头工艺单（OAL/MAL/LAL）的事，驿站不操心。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[二、Skywalking模块系统与启动流程]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/skywalking-mo-kuai-xi-tong-yu-qi-dong-liu-cheng" />
                <id>tag:https://lymboy.com,2025-09-18:skywalking-mo-kuai-xi-tong-yu-qi-dong-liu-cheng</id>
                <published>2025-09-18T21:41:47+08:00</published>
                <updated>2025-09-18T21:41:47+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E4%B8%8E%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B" tabindex="-1">模块系统与启动流程</h1><blockquote><p><strong>这一篇解决什么</strong><br />OAP 后端有几十个功能模块（核心分析、存储、集群、告警、查询、各种 receiver 插件……），它们怎么拼成一个可运行的整体？答案是一套自研的&quot;模块系统&quot;：<code>ModuleDefine</code>（模块契约）+ <code>ModuleProvider</code>（模块实现）+ 标准 Java SPI 发现 + 拓扑排序启动。这篇拆解这套骨架，然后跟一遍 OAP 的启动 <code>main</code> 流程，最后看 <code>CoreModule</code> 这个&quot;大脑&quot;里装了什么。理解了这一篇，后面所有模块的代码都能对号入座。</p></blockquote><hr /><h2 id="%E4%B8%BA%E4%BB%80%E4%B9%88%E8%87%AA%E7%A0%94%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F" tabindex="-1">为什么自研模块系统</h2><p>SkyWalking 没用 Spring，而是自己写了一套极简的模块框架。原因是三个工程诉求 Spring 不好满足：</p><ul><li><strong>解耦</strong>：每个模块独立，对外只暴露 <code>Service</code> 接口，实现可替换。存储可以是 BanyanDB、ES、JDBC，配置一切换就行，调用方代码不动。</li><li><strong>可插拔</strong>：同一模块可以有多种实现（provider），运行时只装载配置选中的那一个。这让 OAP 能按部署形态裁剪——单机用 H2，集群用 BanyanDB，互不干扰。</li><li><strong>有序启动</strong>：模块之间有依赖，需要拓扑排序后按顺序启动，还要能检测循环依赖。Spring 的 <code>@DependsOn</code> 能表达依赖但不会在启动期就帮你拓扑排序和报循环错。</li></ul><p>整套代码在 <code>oap-server/server-library/library-module/src/main/java/org/apache/skywalking/oap/server/library/module/</code>，只有约 10 个类，非常精简。</p><hr /><h2 id="%E6%A0%B8%E5%BF%83%E6%8A%BD%E8%B1%A1%EF%BC%9A%E5%9B%9B%E4%B8%AA%E8%A7%92%E8%89%B2" tabindex="-1">核心抽象：四个角色</h2><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/01-%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E4%B8%8E%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.md_0.png" alt="图1" /></p><blockquote><p><strong>三个角色一句话</strong></p><ul><li><strong><code>Service</code></strong>：空接口（<code>Service.java:26</code>），是所有跨模块能力的根。模块对外承诺&quot;我能提供这些 Service&quot;。</li><li><strong><code>ModuleDefine</code></strong>：模块的&quot;契约&quot;，声明 <code>name()</code> 和 <code>services()</code>（对外暴露哪些 Service 接口）。抽象类，子类如 <code>CoreModule</code>。</li><li><strong><code>ModuleProvider</code></strong>：模块的&quot;实现&quot;，一个模块可以有多种 provider，但运行时只装一个。生命周期分 <code>prepare() → start() → notifyAfterCompleted()</code> 三段。</li><li><strong><code>ModuleManager</code></strong>：全局唯一总管，负责 SPI 加载、拓扑排序、驱动启动，并提供 <code>find(moduleName)</code> 给运行期跨模块查 Service。</li></ul></blockquote><hr /><h3 id="moduledefine-%C2%B7-%E5%A5%91%E7%BA%A6" tabindex="-1">ModuleDefine · 契约</h3><p><code>oap-server/server-library/library-module/src/main/java/org/apache/skywalking/oap/server/library/module/ModuleDefine.java:31</code></p><p>关键方法：</p><ul><li><code>name()</code>（<code>:46</code>）返回模块名，如 <code>&quot;core&quot;</code>、<code>&quot;storage&quot;</code>。</li><li><code>services()</code>（<code>:53</code>）是<strong>抽象方法</strong>，子类必须重写，返回这个模块对外暴露的所有 <code>Service</code> 接口的 Class 数组。这是模块的&quot;对外契约清单&quot;。</li><li><code>prepare(...)</code>（<code>:62</code>）是模块的 prepare 阶段入口：遍历所有候选 Provider，选出与配置匹配且 <code>module()</code> 等于当前模块的那个 Provider 装载进来。<strong>一个模块只能装一个 Provider</strong>，否则抛 <code>DuplicateProviderException</code>（<code>:79</code>），然后创建配置 Bean、调用 <code>provider.prepare()</code>。</li></ul><p>对新手来说要记住：<code>ModuleDefine</code> 是接口/契约，<code>ModuleProvider</code> 是实现。比如 <code>CoreModule</code> 只声明 <code>services()</code>，真正干活的是 <code>CoreModuleProvider</code>。</p><hr /><h3 id="moduleprovider-%C2%B7-%E5%AE%9E%E7%8E%B0" tabindex="-1">ModuleProvider · 实现</h3><p><code>oap-server/server-library/library-module/src/main/java/org/apache/skywalking/oap/server/library/module/ModuleProvider.java:31</code></p><p>生命周期三段是新手最需要理解的核心：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/01-%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E4%B8%8E%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.md_1.png" alt="图2" /></p><blockquote><p><strong>三阶段能干什么、不能干什么</strong></p><ul><li><strong><code>prepare()</code></strong>（<code>:84</code>）：只做&quot;不依赖其他模块&quot;的初始化，主要是 <code>registerServiceImplementation(...)</code> 把 Service 实现注册进来。此时<strong>不能</strong>访问别的模块（<code>find</code> 会抛错）。</li><li><strong><code>start()</code></strong>（<code>:89</code>）：此时所有模块都已 prepare 完，可以跨模块互操作，比如 <code>getManager().find(CoreModule.NAME).provider().getService(XxxService.class)</code>。</li><li><strong><code>notifyAfterCompleted()</code></strong>（<code>:94</code>）：所有模块都 start 完才执行，用于启动定时器、gRPC/HTTP server 等&quot;最后才能做的事&quot;。</li></ul></blockquote><p>声明方法：</p><ul><li><code>name()</code>（<code>:50</code>）Provider 名，如 <code>&quot;default&quot;</code>，对应 <code>application.yml</code> 里 selector 选中的名字。</li><li><code>module()</code>（<code>:55</code>）声明本 Provider 属于哪个 <code>ModuleDefine</code>。</li><li><code>newConfigCreator()</code>（<code>:61</code>）创建配置 Bean。</li><li><code>requiredModules()</code>（<code>:99</code>）声明依赖哪些模块——拓扑排序就靠它。</li></ul><p><code>registerServiceImplementation(...)</code>（<code>:105</code>）是 final 方法，把 Service 实现塞进内部 <code>services</code> Map。<code>requiredCheck(...)</code>（<code>:120</code>）在 start 阶段校验：注册的 Service 数量必须和 <code>ModuleDefine.services()</code> 声明的<strong>完全一致</strong>，多了少了都报错。</p><hr /><h3 id="%E6%A8%A1%E5%9D%97%E4%B9%8B%E9%97%B4%E6%80%8E%E4%B9%88%E6%B3%A8%E5%85%A5%E4%BE%9D%E8%B5%96" tabindex="-1">模块之间怎么注入依赖</h3><p>不是 Spring 那种字段注入，而是<strong>服务定位器（Service Locator）模式</strong>。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/01-%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E4%B8%8E%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.md_2.png" alt="图3" /></p><p>每个 Provider 持有 <code>getManager()</code>（<code>ModuleProvider.java:43</code>），需要别的模块时直接：</p><pre><code class="language-java">getManager().find(CoreModule.NAME).provider().getService(ConfigService.class)</code></pre><p>查找链是：<code>ModuleManager.find(&quot;core&quot;)</code> → <code>ModuleDefine</code>（同时也是 ModuleProviderHolder）→ <code>.provider()</code> → <code>ModuleProvider</code>（同时也是 ModuleServiceHolder）→ <code>.getService(XxxService.class)</code>。</p><p>这套 <code>ModuleDefineHolder</code> 接口被传给几乎所有 Worker，让 Worker 也能跨模块找 Service。所以&quot;依赖注入&quot;在 SkyWalking 里实质是&quot;持有 <code>ModuleManager</code> 引用，按名 + 接口查找&quot;。</p><p>为什么用服务定位器而不是 Spring 的字段注入？因为后者要在构造期就拿到依赖，而 OAP 的模块是按拓扑序逐个启动的，先启动的模块此刻后面的还没就绪。服务定位器把&quot;找依赖&quot;推迟到运行时调用那一刻，自然避开了这个时序问题。代价是调用方要主动 <code>find</code>，没有 Spring <code>@Autowired</code> 那种声明式简洁——但对一个要按 selector 动态裁剪 provider 的系统，这个代价是值得的。</p><hr /><h2 id="spi-%E6%9C%BA%E5%88%B6%EF%BC%9A%E6%A0%87%E5%87%86-java-spi" tabindex="-1">SPI 机制：标准 Java SPI</h2><p>模块系统用的是<strong>标准 JDK <code>java.util.ServiceLoader</code></strong>，不是自定义 SPI。</p><p>证据在 <code>oap-server/server-library/library-module/src/main/java/org/apache/skywalking/oap/server/library/module/ModuleManager.java:50-51</code>：</p><pre><code class="language-java">ServiceLoader.load(ModuleDefine.class);ServiceLoader.load(ModuleProvider.class);</code></pre><p>注册文件在各 jar 的 <code>META-INF/services/</code> 下，文件名即接口全限定名。</p><blockquote><p><strong>server-core 的 SPI 注册</strong><br /><code>oap-server/server-core/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine</code> 内容列出了 <code>StorageModule</code>、<code>ClusterModule</code>、<code>CoreModule</code>、<code>QueryModule</code>、<code>AlarmModule</code>、<code>ExporterModule</code> 六个模块定义类。</p><p>同目录的 <code>...ModuleProvider</code> 文件只有一行：<code>org.apache.skywalking.oap.server.core.CoreModuleProvider</code>。</p><p>这意味着 server-core 这个 jar 声明了自己实现了哪些模块（6 个 Define），但只自带了 1 个 Provider 实现（CoreModuleProvider）。其他模块（Storage/Cluster/Query/Alarm/Exporter）的 Provider 由各自的插件 jar 提供，靠 selector 决定装哪个。</p></blockquote><blockquote><p><strong>模块发现的两种顺序不要混</strong></p><ul><li><strong>发现顺序</strong>：由 <code>ServiceLoader</code> 遍历 <code>META-INF/services</code> 决定，这是<strong>无序的</strong>。</li><li><strong>启动顺序</strong>：由 <code>BootstrapFlow.makeSequence</code> 基于 <code>requiredModules()</code> 做拓扑排序决定，<strong>与发现顺序无关</strong>。</li></ul><p>模块之间正确的先后关系靠依赖图保证，不靠配置文件里写的顺序。</p></blockquote><hr /><h2 id="%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B%EF%BC%9A%E4%BB%8E-main-%E5%88%B0%E6%A8%A1%E5%9D%97%E5%85%A8%E5%B0%B1%E7%BB%AA" tabindex="-1">启动流程：从 main 到模块全就绪</h2><p>入口极简。<code>oap-server/server-starter/src/main/java/org/apache/skywalking/oap/server/starter/OAPServerStartUp.java:22</code> 的 <code>main</code> 只调 <code>OAPServerBootstrap.start()</code>。</p><p>真正的活在 <code>oap-server/server-starter/src/main/java/org/apache/skywalking/oap/server/starter/OAPServerBootstrap.java:38</code> 的 <code>start()</code>。</p><h3 id="%E5%AE%8C%E6%95%B4%E5%90%AF%E5%8A%A8%E6%97%B6%E5%BA%8F" tabindex="-1">完整启动时序</h3><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/01-%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E4%B8%8E%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.md_3.png" alt="图4" /></p><p><code>OAPServerBootstrap.start()</code>（<code>OAPServerBootstrap.java:38-69</code>）的步骤：</p><ol><li><code>new ModuleManager(&quot;Apache SkyWalking OAP&quot;)</code>（<code>:39</code>）创建内核。</li><li>读系统属性 <code>mode</code> 并 <code>RunningMode.setMode(mode)</code>（<code>:42-43</code>）。</li><li><code>new ApplicationConfigLoader(bootingParameters)</code>（<code>:45</code>），写入 Running Mode、Version（<code>:47-48</code>）。</li><li><code>configLoader.load()</code> 得到 <code>ApplicationConfiguration</code>（<code>:51</code>）。</li><li><code>manager.init(applicationConfiguration)</code>（<code>:52</code>）——这是整个模块启动流程。</li><li>启动成功后 <code>ServerStatusService.bootedNow(...)</code>（<code>:54-57</code>）标记启动完成时间，这对 <code>MetricsPersistentWorker</code> 的缓存策略很关键。</li><li>若 <code>RunningMode.isInitMode()</code>（<code>:59</code>）则只做初始化（建表）然后 <code>System.exit(0)</code>（<code>:61</code>）。</li><li>任何异常 <code>log.error</code> 后 <code>System.exit(1)</code>（<code>:63-65</code>）。</li><li><code>finally</code> 块打印 <code>bootingParameters</code> 表格（<code>:67</code>）。</li></ol><hr /><h3 id="runningmode-%C2%B7-%E8%BF%90%E8%A1%8C%E6%A8%A1%E5%BC%8F" tabindex="-1">RunningMode · 运行模式</h3><p><code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/RunningMode.java:26</code></p><p>通过 <code>-Dmode=init</code> / <code>-Dmode=no-init</code> 控制：</p><ul><li><code>isInitMode()</code>（<code>:44</code>）：init 模式只做存储初始化（建表/建 measure），建完即退出，用于首次部署或 schema 升级。</li><li><code>isNoInitMode()</code>（<code>:53</code>）：no-init 模式启动但不做存储初始化。</li></ul><p>在 <code>CoreModuleProvider.notifyAfterCompleted()</code>（<code>CoreModuleProvider.java:489</code>）处生效：<code>if (!RunningMode.isInitMode())</code> 才真正 <code>grpcServer.start()</code> 等网络服务。</p><hr /><h3 id="application.yml-%E6%80%8E%E4%B9%88%E8%A7%A3%E6%9E%90" tabindex="-1">application.yml 怎么解析</h3><p>配置加载在 <code>oap-server/server-starter/src/main/java/org/apache/skywalking/oap/server/starter/config/</code>。</p><p><code>ApplicationConfigLoader.load()</code>（<code>ApplicationConfigLoader.java:64</code>）两步：</p><ol><li><code>loadConfig</code>（<code>:72</code>）：用 SnakeYAML 把 classpath 下的 <code>application.yml</code> 读成 <code>Map&lt;String, Map&lt;String, Object&gt;&gt;</code>，结构是 <code>模块名 → (provider 名 → 配置)</code>。</li><li><code>overrideConfigBySystemEnv</code>（<code>:122</code>）：遍历 <code>System.getProperties()</code>，按 <code>moduleName.providerName.settingKey</code> 格式覆盖 YAML 配置（<code>overrideModuleSettings</code> <code>:161</code>），支持 int/String/long/boolean 类型转换。</li></ol><blockquote><p><strong>selector 机制</strong><br /><code>selectConfig</code>（<code>ApplicationConfigLoader.java:128-159</code>）是关键：每个模块下有 <code>selector</code> 字段，值为某个 provider 名（或 <code>-</code> 表示禁用）。</p><ul><li>解析 <code>selector</code>（支持 <code>${...}</code> 占位符替换系统属性，<code>:138</code>）。</li><li><code>removeIf</code> 把不等于 selector 值的所有 provider 配置删掉（<code>:141</code>）。</li><li>删完为空且 selector 不是 <code>-</code>，抛 <code>ProviderNotFoundException</code>（<code>:148</code>）。</li><li>selector 是 <code>-</code>，直接把这个模块从配置里移除（<code>:156</code>）——这就是&quot;禁用某模块&quot;的官方写法。</li></ul></blockquote><hr /><h3 id="bootstrapflow-%C2%B7-%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F" tabindex="-1">BootstrapFlow · 拓扑排序</h3><p><code>BootstrapFlow</code> 是 package-private 类（<code>BootstrapFlow.java:28</code>），只在 <code>ModuleManager.init</code> 内部用。</p><p><code>makeSequence</code>（<code>BootstrapFlow.java:56-120</code>）的核心：</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/01-%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E4%B8%8E%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.md_4.png" alt="图5" /></p><p>反复把&quot;所有依赖都已在序列中&quot;的 Provider 加入 <code>startupSequence</code>，直到全部排完。若一轮下来没有任何 Provider 能排进去（说明互相依赖），抛 <code>CycleDependencyException</code>（<code>:114</code>）。还会校验依赖的模块是否真的加载了（<code>:62-68</code>，缺失抛 <code>ModuleNotFoundException</code>）。</p><p>排好序后，<code>start</code>（<code>:40-48</code>）按拓扑序对每个 Provider 先 <code>requiredCheck</code>（校验 Service 注册齐全），再 <code>provider.start()</code>。<code>notifyAfterCompleted</code>（<code>:50-54</code>）按同一序调每个 Provider 的收尾方法。</p><hr /><h2 id="coremodule-%C2%B7-%E5%A4%A7%E8%84%91%E9%87%8C%E8%A3%85%E4%BA%86%E4%BB%80%E4%B9%88" tabindex="-1">CoreModule · 大脑里装了什么</h2><p><code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModule.java:80</code> 是整个 OAP 的&quot;大脑&quot;，<code>services()</code>（<code>:88</code>）列出约 60 个 Service 接口。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/01-%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E4%B8%8E%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.md_5.png" alt="图6" /></p><p>按职责分组（<code>CoreModule.java</code> 行号）：</p><ul><li><strong>配置类</strong>：<code>ConfigService</code>（<code>:90</code>）、<code>DownSamplingConfigService</code>（<code>:92</code>）、<code>NamingControl</code>（<code>:93</code>，命名长度控制）、<code>EndpointNameGroupService</code>（<code>:118</code>）。</li><li><strong>流处理骨架</strong>：<code>IWorkerInstanceGetter</code>/<code>IWorkerInstanceSetter</code>（<code>:99-100</code>，Worker 注册与查找，用于集群 RPC 路由）、<code>MeterSystem</code>（<code>:102</code>，MAL 入口）。</li><li><strong>服务端接口</strong>：<code>GRPCHandlerRegister</code>/<code>HTTPHandlerRegister</code>（<code>addServerInterface</code> <code>:173</code>，注册 gRPC/HTTP handler）。</li><li><strong>接收入口</strong>：<code>SourceReceiver</code>（<code>addReceiverInterface</code> <code>:191</code>，所有 source 数据的统一入口）。</li><li><strong>内部服务</strong>：<code>ModelRegistry</code>、<code>IModelManager</code>、<code>ModelManipulator</code>（存储模型注册表）、<code>RemoteClientManager</code>、<code>RemoteSenderService</code>（集群消息分发）。</li><li><strong>缓存</strong>：<code>NetworkAddressAliasCache</code>；<code>ProfileTaskCache</code>/<code>AsyncProfilerTaskCache</code>/<code>PprofTaskCache</code>（<code>:136-148</code>）。</li><li><strong>查询服务</strong>（面向 UI）：<code>TopologyQueryService</code>、<code>MetricsQueryService</code>、<code>TraceQueryService</code>、<code>LogQueryService</code>、<code>MetadataQueryService</code>、<code>AggregationQueryService</code>、<code>AlarmQueryService</code>、<code>TopNRecordsQueryService</code>、<code>BrowserLogQueryService</code>、<code>EventQueryService</code>、<code>TagAutoCompleteQueryService</code>、<code>RecordQueryService</code>、<code>HierarchyQueryService</code> 等。</li><li><strong>OAL runtime</strong>：<code>OALEngineLoaderService</code>（<code>:151</code>）。</li><li><strong>其他</strong>：<code>SpanListenerManager</code>（<code>:97</code>，trace span 监听器）、<code>CommandService</code>（<code>:116</code>）、<code>HierarchyService</code>（<code>:117</code>）。</li></ul><p>这些 Service 的默认实现全部在 <code>CoreModuleProvider.prepare()</code>（<code>CoreModuleProvider.java:185</code>）里通过 <code>registerServiceImplementation</code> 注册。</p><hr /><h3 id="coremoduleprovider-%E7%9A%84%E4%B8%89%E9%98%B6%E6%AE%B5%E5%AE%9E%E6%A0%B7" tabindex="-1">CoreModuleProvider 的三阶段实样</h3><p>以 <code>CoreModuleProvider</code> 为真实样本，看三阶段各做什么：</p><blockquote><p><strong>prepare / start / notifyAfterCompleted 的真实分工</strong><br /><strong><code>prepare()</code></strong>（<code>CoreModuleProvider.java:185</code>）：注册全部 Service 实现、建 <code>GRPCServer</code>/<code>HTTPServer</code> 但<strong>不启动</strong>、建 <code>RemoteClientManager</code>、扫描 Scope 注解、设置 StreamProcessor 参数——全是&quot;自我准备&quot;。</p><p><strong><code>start()</code></strong>（<code>CoreModuleProvider.java:417</code>）：把 <code>RemoteServiceHandler</code> 加进 gRPC、调 <code>receiver.scan()</code> 扫描 Dispatcher、启动 <code>ClusterCoordinator</code>、注册动态配置 watcher——开始跨模块协作。</p><p><strong><code>notifyAfterCompleted()</code></strong>（<code>CoreModuleProvider.java:481</code>）：<code>grpcServer.start()</code>、<code>httpServer.start()</code>、<code>remoteClientManager.start()</code>、<code>PersistenceTimer.INSTANCE.start(...)</code>、<code>DataTTLKeeperTimer</code>、<code>CacheUpdateTimer</code>——真正对外服务。</p></blockquote><p>这个三段式分工是 SkyWalking 所有 Provider 的通用模式：prepare 备料、start 接线、notifyAfterCompleted 通电。</p><hr /><h2 id="%E5%BC%82%E5%B8%B8%E4%BD%93%E7%B3%BB" tabindex="-1">异常体系</h2><p>启动期的校验失败都是可读异常，方便定位：</p><table><thead><tr><th>异常</th><th>触发场景</th></tr></thead><tbody><tr><td><code>ProviderNotFoundException</code></td><td>selector 指定的 provider 不存在</td></tr><tr><td><code>DuplicateProviderException</code></td><td>一个模块装了多个 Provider</td></tr><tr><td><code>ServiceNotProvidedException</code></td><td>Provider 注册的 Service 数与声明不符</td></tr><tr><td><code>ModuleNotFoundException</code></td><td><code>requiredModules</code> 声明的依赖没加载</td></tr><tr><td><code>CycleDependencyException</code></td><td>模块间循环依赖</td></tr></tbody></table><hr /><h2 id="%E5%85%B3%E9%94%AE%E6%96%87%E4%BB%B6%E9%80%9F%E6%9F%A5" tabindex="-1">关键文件速查</h2><table><thead><tr><th>子系统</th><th>关键类</th><th>路径</th></tr></thead><tbody><tr><td>模块系统</td><td>ModuleDefine</td><td><code>oap-server/server-library/library-module/.../module/ModuleDefine.java</code></td></tr><tr><td></td><td>ModuleProvider</td><td>同目录 <code>ModuleProvider.java</code></td></tr><tr><td></td><td>ModuleManager</td><td>同目录 <code>ModuleManager.java</code></td></tr><tr><td></td><td>BootstrapFlow</td><td>同目录 <code>BootstrapFlow.java</code></td></tr><tr><td></td><td>Service/ModuleConfig</td><td>同目录 <code>Service.java</code> / <code>ModuleConfig.java</code></td></tr><tr><td>启动入口</td><td>OAPServerStartUp</td><td><code>oap-server/server-starter/.../starter/OAPServerStartUp.java</code></td></tr><tr><td></td><td>OAPServerBootstrap</td><td>同目录 <code>OAPServerBootstrap.java</code></td></tr><tr><td></td><td>ApplicationConfigLoader</td><td><code>oap-server/server-starter/.../starter/config/ApplicationConfigLoader.java</code></td></tr><tr><td></td><td>RunningMode</td><td><code>oap-server/server-core/.../core/RunningMode.java</code></td></tr><tr><td>server-core</td><td>CoreModule</td><td><code>oap-server/server-core/.../core/CoreModule.java</code></td></tr><tr><td></td><td>CoreModuleProvider</td><td>同目录 <code>CoreModuleProvider.java</code></td></tr><tr><td>SPI 注册文件</td><td>—</td><td><code>oap-server/server-core/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.{ModuleDefine,ModuleProvider}</code></td></tr></tbody></table><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5" tabindex="-1">接下来</h2><p>骨架搞清楚了，下一步看数据怎么进来：02-数据采集层-Receiver。</p><blockquote><p><strong>贯穿全篇的心智模型</strong><br />把 <code>ModuleDefine</code> 想成&quot;插座规格&quot;（声明能插什么），<code>ModuleProvider</code> 想成&quot;插头&quot;（具体实现），<code>ModuleManager</code> 想成&quot;配电箱&quot;（管哪些插座通电、按什么顺序通电）。<code>Service</code> 就是插座上流出来的电。这个比喻贯穿后面所有模块——你看到的每一个插件（storage-banyandb、cluster-zookeeper、receiver-trace）都是一个插头，插进对应的插座。</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[一、Skywalking整体架构与端到端数据流]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/skywalking整体架构与端到端数据流" />
                <id>tag:https://lymboy.com,2025-09-11:skywalking整体架构与端到端数据流</id>
                <published>2025-09-11T20:38:28+08:00</published>
                <updated>2025-09-11T20:38:28+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81" tabindex="-1">整体架构与端到端数据流</h1><blockquote><p><strong>这一篇解决什么</strong><br />在拆细节之前，先用一条 trace 从产生到被看到的完整旅程，把 OAP 内部所有子系统串一遍。读完应该能回答：数据从哪来、中间被谁加工、最后落到哪、怎么被查出来。后面每一篇都是这条链上某一环的放大。</p></blockquote><hr /><h2 id="%E5%88%86%E5%B1%82%E8%A7%86%E5%9B%BE" tabindex="-1">分层视图</h2><p>OAP 后端是一条分层管道，每层只做一件事，层间用 Service 接口解耦。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/00-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md_0.png" alt="图1" /></p><p>六层的分工是理解后续所有内容的前提：</p><ul><li><strong>接收层</strong>把字节流解成 proto 对象，不碰业务语义。</li><li><strong>解析层</strong>把 proto 对象切成 <code>Source</code>——语义化的数据事件，比如&quot;这是一次服务调用&quot;。</li><li><strong>规则层</strong>把 OAL/MAL/LAL 脚本编译成 Dispatcher，决定对每类 Source 算哪些指标。</li><li><strong>聚合层</strong>用 Worker 链做内存聚合、降采样、落盘。</li><li><strong>存储层</strong>把对象翻译成具体数据库的写入请求。</li><li><strong>查询层</strong>按用户请求读存储，必要时用 MQE 做二次计算。</li></ul><p>为什么要分这么细？因为每层解决的问题性质不同：接收要扛并发，解析要懂业务语义，聚合要算数学，存储要懂引擎。混在一起做，任何一个改动都会牵连其他层。分层后，换存储引擎不动解析逻辑，加新指标不动接收代码。</p><hr /><h2 id="%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%97%B6%E5%BA%8F%EF%BC%9A%E4%B8%80%E6%9D%A1-trace-%E7%9A%84%E6%97%85%E7%A8%8B" tabindex="-1">端到端时序：一条 trace 的旅程</h2><p>跟一条 HTTP trace 走完整条链。假设用户调了 <code>order-service</code>，它又调了 <code>inventory-service</code>。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/00-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md_1.png" alt="图2" /></p><p>时序图里每一步背后都有具体机制，下面拆开。</p><hr /><h2 id="%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B%EF%BC%9Asource-%E2%86%92-metrics-%E2%86%92-storagedata" tabindex="-1">数据模型：Source → Metrics → StorageData</h2><p>整条链流转三种数据形态，搞清边界就理解了为什么这么分层。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/00-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md_2.png" alt="图3" /></p><blockquote><p><strong>三种核心数据形态</strong></p><ul><li><strong>Source</strong>：语义化的事件，带 <code>scope()</code>（作用域 ID）。由 Analyzer 构造，是&quot;发生了什么&quot;的抽象。比如 <code>Service</code>、<code>ServiceRelation</code>、<code>Endpoint</code>、<code>Log</code>、<code>ServiceInstanceJVMMemory</code>。定义在 <code>server-core/.../source/</code>。</li><li><strong>Metrics</strong>：可聚合的统计量。<code>combine()</code> 合并同 ID 同时间桶、<code>calculate()</code> 算最终值、<code>toHour()/toDay()</code> 降采样克隆。由 Dispatcher 从 Source 转换得到，是&quot;算出了什么&quot;。比如 <code>ServiceAvgRespTime</code>、<code>PercentileMetrics</code>。定义在 <code>server-core/.../analysis/metrics/</code>。</li><li><strong>Record</strong>：不可变的原始记录，只追加不聚合，受 TTL 控制。比如 <code>SegmentRecord</code>（trace）、<code>LogRecord</code>、<code>Event</code>。定义在 <code>server-core/.../analysis/record/</code>。</li></ul></blockquote><p>这三种形态对应三种写入语义：Source 是中转不落盘，Metrics 要&quot;同时间桶反复合并更新&quot;，Record 是&quot;来一条 append 一条&quot;。这种区分直接决定了存储层要有三套不同的 DAO（<code>IMetricsDAO</code> 有 update，<code>IRecordDAO</code> 没有），而不是一套通吃。</p><hr /><h2 id="scope-%E4%BD%93%E7%B3%BB%EF%BC%9A%E6%95%B0%E6%8D%AE%E7%9A%84%22%E5%BD%92%E5%B1%9E%E7%BB%B4%E5%BA%A6%22" tabindex="-1">Scope 体系：数据的&quot;归属维度&quot;</h2><p>每个 Source 都有一个 <code>scope()</code> 整数 ID，这是 SkyWalking 里很关键的概念。</p><blockquote><p><strong>什么是 Scope</strong><br />Scope 回答&quot;这条数据描述的是哪个层级的实体&quot;。<code>Service=1</code>、<code>ServiceInstance=2</code>、<code>Endpoint=3</code>、<code>ServiceRelation=4</code>。OAL 脚本里 <code>from(Service.latency).longAvg()</code> 的 <code>Service</code> 就是 scope。Scope 让指标规则和数据来源解耦——同一个 Source 可以触发多条 OAL 规则，产出多个指标。</p></blockquote><p>Scope ID 集中注册在 <code>oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/source/DefaultScopeDefine.java:31</code>，靠 <code>@ScopeDeclaration</code> 注解扫描进来。常量从 <code>SERVICE=1</code> 到 <code>GEN_AI_*</code>，区间 [0,10000) 留给 SkyWalking 官方，扩展从 10000 起。这个分区设计是为了避免官方和第三方扩展的 scope ID 撞车——第三方加新 source 从 10000 往上排，不会和官方未来新增的冲突。</p><p>Scope 还按 catalog 归类（service/serviceInstance/endpoint/relation），告警和查询用它分组。见 <code>DefaultScopeDefine.java:166-182</code>。</p><hr /><h2 id="%E9%9B%86%E7%BE%A4%E5%88%86%E7%89%87%EF%BC%9A%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-remoteclientmanager" tabindex="-1">集群分片：为什么需要 RemoteClientManager</h2><p>OAP 可以多节点部署。问题在于：同一个服务的指标可能被不同节点收到，如果每个节点都存全量，指标会重复或冲突。</p><p>SkyWalking 用按 hash 分片解决：同一实体的指标总是落到同一个 OAP 节点做持久化。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/00-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md_3.png" alt="图4" /></p><ul><li>节点 A/B 收到数据后，在 L1 内存聚合完，由 <code>MetricsRemoteWorker</code> 用 <code>HashCodeSelector</code>（<code>Math.abs(remoteHashCode % size)</code>）算出该数据应该去哪个节点。</li><li>通过 gRPC 把数据推给目标节点 C。</li><li>节点 C 的 <code>RemoteServiceHandler</code> 收到后，按 worker 名找到本地的 <code>MetricsPersistentMinWorker</code> 继续处理落盘。</li></ul><p>为什么必须分片而不是各存各的？因为 metric 是&quot;同 (entity, timeBucket) 要合并&quot;的——同一服务同一分钟的指标分散到多个节点各存一份，就没法合并，查出来的就是错的。分片保证同一 entity 的同一时间桶只在一个节点聚合，结果才正确。</p><p>核心类在 <code>oap-server/server-core/.../remote/</code>：</p><ul><li><code>RemoteClientManager.java:59</code> 每 10 秒刷新集群节点列表，维护到各节点的 gRPC 连接。</li><li><code>RemoteSenderService.java:39</code> 发送侧，按 selector 选 client。</li><li><code>RemoteServiceHandler.java:52</code> 接收侧，按 worker 名路由到本地 worker。</li></ul><blockquote><p><strong>不要直接 find ClusterModule</strong><br />业务代码不该直接 <code>moduleManager.find(ClusterModule.NAME)</code> 拿集群服务。应该走 <code>CoreModule</code> 的 <code>RemoteClientManager</code>——它是对外门面，内部再去找 <code>ClusterModule</code>。这样换集群实现（zookeeper/etcd/k8s）对业务透明。详见 07-集群协调与动态配置。</p></blockquote><hr /><h2 id="%E9%99%8D%E9%87%87%E6%A0%B7%EF%BC%9A%E5%88%86%E9%92%9F%E3%80%81%E5%B0%8F%E6%97%B6%E3%80%81%E5%A4%A9" tabindex="-1">降采样：分钟、小时、天</h2><p>指标不只存一个粒度。SkyWalking 把分钟级指标再聚合成小时、天级，这就是降采样（DownSampling）。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/00-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md_4.png" alt="图5" /></p><ul><li>分钟级保留时间短（比如 7 天），小时级长一些（30 天），天级最长（半年）。查近期看细粒度，查历史看粗粒度，省存储。</li><li><code>MetricsStreamProcessor.create</code> 在建链时按 <code>DownSamplingConfigService</code> 决定要不要建 hour/day worker（<code>MetricsStreamProcessor.java:248,257</code>）。</li><li><code>MetricsTransWorker</code> 把上一级聚合结果喂给下一级（<code>MetricsStreamProcessor.java:267</code>）。</li></ul><p>降采样不是简单地&quot;把分钟数据相加除以 60&quot;。不同算子降采样方式不同：Counter 类（count/sum）直接相加，Avg 类要重新用总 summation 除以总 count，Percentile 类要把各节点的桶计数合并后重算分位数。所以每个 Metrics 子类都要实现自己的 <code>toHour()/toDay()</code>，把&quot;如何降采样&quot;这个语义钉死在算子上。降采样枚举在 <code>DownSampling</code>：<code>Minute/Hour/Day/Second/None</code>。</p><hr /><h2 id="%E6%89%B9%E9%87%8F%E5%86%99%E5%85%A5%EF%BC%9Apersistencetimer" tabindex="-1">批量写入：PersistenceTimer</h2><p>写入不是来一条写一条，而是攒着批量冲。<code>PersistenceTimer</code> 是写入侧的总调度。</p><blockquote><p><strong>马桶式写入</strong><br />每个 Worker 在内存攒增量，<code>PersistenceTimer</code> 每 <code>persistentPeriod</code>（默认几秒）触发一次：多线程把所有 Worker 的内存对象&quot;翻译&quot;成数据库写入请求（prepare 阶段），再一把 flush 进去（execute 阶段）。高频小写入变成低频批量写，降低数据库压力。</p></blockquote><ul><li><code>PersistenceTimer.java:45</code> 单例，<code>start</code> 起定时器。</li><li><code>extractDataAndSave</code>（<code>:110</code>）每周期收集所有持久化 Worker，并发 <code>buildBatchRequests</code>，再 <code>IBatchDAO.flush</code>。</li><li><code>IBatchDAO.flush</code> 返回 <code>CompletableFuture</code>，prepare 和 execute 可以重叠。</li></ul><p>为什么要分 prepare 和 execute 两阶段？因为 prepare 是 CPU 密集（要把 metrics 对象序列化成存储格式、算 docId、合并旧值），execute 是 IO 密集（真正写库）。两阶段分开后，可以用多线程并发 prepare（吃满 CPU），再把 prepare 出的请求批量 execute（吃满 IO 带宽），两种资源都不浪费。</p><hr /><h2 id="%E6%9F%A5%E8%AF%A2%E4%BE%A7%EF%BC%9A%E8%AF%BB-%2B-%E4%BA%8C%E6%AC%A1%E8%AE%A1%E7%AE%97" tabindex="-1">查询侧：读 + 二次计算</h2><p>查询不是简单把存的数据原样取出来。用户可能要算 <code>service_resp_time / service_cpm</code>，或在 Grafana 里写 PromQL。所以查询侧也要一套表达式引擎，就是 MQE。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/00-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md_5.png" alt="图6" /></p><ul><li>MQE 用 ANTLR 解析表达式，用 Visitor 模式递归求值，<strong>不做代码生成</strong>（区别于 OAL/MAL/LAL 的编译方式），因为查询高频短命，生成 class 的开销不划算。</li><li>叶子节点 <code>visitMetric</code> 才真正读存储，通过 <code>MetricsQueryService</code> 调底层 <code>IMetricsQueryDAO</code>。</li><li>告警规则也复用 MQE 语法，但数据来源是内存滑动窗口而非存储。详见 06-查询层与告警。</li></ul><hr /><h2 id="%E4%B8%80%E5%BC%A0%E5%9B%BE%E6%94%B6%E6%9D%9F%E6%89%80%E6%9C%89%E6%A6%82%E5%BF%B5" tabindex="-1">一张图收束所有概念</h2><p>把前面所有要素汇总成一张关系图，作为本篇收尾，也是后续各篇的目录索引。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/00-%E6%95%B4%E4%BD%93%E6%9E%B6%E6%9E%84%E4%B8%8E%E7%AB%AF%E5%88%B0%E7%AB%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md_6.png" alt="图7" /></p><hr /><h2 id="%E6%8E%A5%E4%B8%8B%E6%9D%A5%E6%80%8E%E4%B9%88%E8%AF%BB" tabindex="-1">接下来怎么读</h2><p>全局观建立完毕。后面每一篇都是这张图里某一块的放大：</p><ul><li>想懂骨架怎么拼：01-模块系统与启动流程</li><li>想懂数据怎么进来：02-数据采集层-Receiver</li><li>想懂规则怎么编译：03-分析引擎与四大DSL</li><li>想懂聚合怎么算：04-流处理引擎-Worker链</li><li>想懂怎么落盘：05-存储层</li><li>想懂怎么查、怎么告警：06-查询层与告警</li><li>想懂集群、配置、自监控：07-集群协调与动态配置</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[面试经典 150 题 · Java 题解]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/mian-shi-jing-di-150-ti-java-ti-jie" />
                <id>tag:https://lymboy.com,2025-09-01:mian-shi-jing-di-150-ti-java-ti-jie</id>
                <published>2025-09-01T23:03:44+08:00</published>
                <updated>2025-09-01T23:03:44+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="%E9%9D%A2%E8%AF%95%E7%BB%8F%E5%85%B8-150-%E9%A2%98-%C2%B7-java-%E9%A2%98%E8%A7%A3" tabindex="-1">面试经典 150 题 · Java 题解</h1><blockquote><p>力扣官方精选 150 题，按 21 个专题整理。每题：思路脉络 + Mermaid 图 + Java 代码 + 复杂度 + 易错点。</p></blockquote><h2 id="%E9%A2%98%E5%9E%8B%E5%88%86%E5%B8%83" tabindex="-1">题型分布</h2><table><thead><tr><th>专题</th><th>题数</th><th>核心思维</th></tr></thead><tbody><tr><td>数组 / 字符串</td><td>24</td><td>双指针原地操作、区间合并、模拟</td></tr><tr><td>双指针</td><td>5</td><td>有序对撞、回文验证、有序两数</td></tr><tr><td>滑动窗口</td><td>4</td><td>定长/变长窗口 + 状态计数</td></tr><tr><td>矩阵</td><td>5</td><td>原地标记、螺旋遍历、坐标变换</td></tr><tr><td>哈希表</td><td>9</td><td>逆向映射、O(1) 查询、计数分组</td></tr><tr><td>区间</td><td>4</td><td>排序 + 合并/插入/贪心</td></tr><tr><td>栈</td><td>5</td><td>匹配消除、单调栈、表达式求值</td></tr><tr><td>链表</td><td>11</td><td>哨兵节点、快慢指针、指针操作</td></tr><tr><td>二叉树</td><td>17</td><td>递归三部曲、层序 BFS、构造</td></tr><tr><td>二叉搜索树</td><td>3</td><td>中序有序性、上下界</td></tr><tr><td>图</td><td>9</td><td>DFS/BFS 染色、拓扑排序、并查集</td></tr><tr><td>字典树</td><td>3</td><td>前缀检索、DFS 搜词</td></tr><tr><td>回溯</td><td>7</td><td>选→递→撤销</td></tr><tr><td>分治</td><td>4</td><td>找中点、递归两半、合并</td></tr><tr><td>Kadane 算法</td><td>2</td><td>最大子数组/环形子数组</td></tr><tr><td>二分查找</td><td>7</td><td>单调性 + 边界收缩</td></tr><tr><td>堆</td><td>4</td><td>Top-K、动态中位数</td></tr><tr><td>位运算</td><td>6</td><td>异或抵消、位统计、掩码</td></tr><tr><td>数学</td><td>6</td><td>数论、模拟、二分逼近</td></tr><tr><td>一维动态规划</td><td>5</td><td>状态 + 转移 + 滚动优化</td></tr><tr><td>多维动态规划</td><td>9</td><td>二维状态、区间 DP、股票系列</td></tr></tbody></table><h2 id="%E7%9B%AE%E5%BD%95" tabindex="-1">目录</h2><ul><li>[[#1. 数组 / 字符串]] · 24 题</li><li>[[#2. 双指针]] · 5 题</li><li>[[#3. 滑动窗口]] · 4 题</li><li>[[#4. 矩阵]] · 5 题</li><li>[[#5. 哈希表]] · 9 题</li><li>[[#6. 区间]] · 4 题</li><li>[[#7. 栈]] · 5 题</li><li>[[#8. 链表]] · 11 题</li><li>[[#9. 二叉树]] · 17 题</li><li>[[#10. 二叉搜索树]] · 3 题</li><li>[[#11. 图]] · 9 题</li><li>[[#12. 字典树]] · 3 题</li><li>[[#13. 回溯]] · 7 题</li><li>[[#14. 分治]] · 4 题</li><li>[[#15. Kadane 算法]] · 2 题</li><li>[[#16. 二分查找]] · 7 题</li><li>[[#17. 堆]] · 4 题</li><li>[[#18. 位运算]] · 6 题</li><li>[[#19. 数学]] · 6 题</li><li>[[#20. 一维动态规划]] · 5 题</li><li>[[#21. 多维动态规划]] · 9 题</li></ul><hr /><h1 id="1.-%E6%95%B0%E7%BB%84-%2F-%E5%AD%97%E7%AC%A6%E4%B8%B2" tabindex="-1">1. 数组 / 字符串</h1><p>数组和字符串题的核心是<strong>双指针原地操作</strong>——快慢指针做筛选、对撞指针做搜索、归并指针做合并。难点在边界处理和原地操作的指针顺序。</p><h2 id="88.-%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84" tabindex="-1"><a href="https://leetcode.cn/problems/merge-sorted-array/" target="_blank">88. 合并两个有序数组</a></h2><blockquote><p>两个非递减整数数组 <code>nums1</code>（末尾留 <code>m</code> 后补 0）、<code>nums2</code>，合并到 <code>nums1</code> 使其有序。原地，O(m+n)。</p></blockquote><p><strong>思路脉络</strong>：朴素是合并后排序 O((m+n)log(m+n))，没利用&quot;两个都有序&quot;。两数组都从后往前，用三个指针 <code>i=m-1, j=n-1, k=m+n-1</code>，每次把大的放进 <code>nums1[k]</code>，从后往前填不会覆盖未处理的 <code>nums1</code> 元素。这就是<strong>逆向双指针</strong>——正向填会覆盖，反向填天然安全。</p><div class="mermaid"><svg id="render1068252707" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="217.1707000732422" style="max-width: 1101.754638671875px;" viewBox="0 0 1101.754638671875 217.1707000732422"><style>#render1068252707 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1068252707 .error-icon{fill:#552222;}#render1068252707 .error-text{fill:#552222;stroke:#552222;}#render1068252707 .edge-thickness-normal{stroke-width:2px;}#render1068252707 .edge-thickness-thick{stroke-width:3.5px;}#render1068252707 .edge-pattern-solid{stroke-dasharray:0;}#render1068252707 .edge-pattern-dashed{stroke-dasharray:3;}#render1068252707 .edge-pattern-dotted{stroke-dasharray:2;}#render1068252707 .marker{fill:#333333;stroke:#333333;}#render1068252707 .marker.cross{stroke:#333333;}#render1068252707 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1068252707 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1068252707 .cluster-label text{fill:#333;}#render1068252707 .cluster-label span{color:#333;}#render1068252707 .label text,#render1068252707 span{fill:#333;color:#333;}#render1068252707 .node rect,#render1068252707 .node circle,#render1068252707 .node ellipse,#render1068252707 .node polygon,#render1068252707 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1068252707 .node .label{text-align:center;}#render1068252707 .node.clickable{cursor:pointer;}#render1068252707 .arrowheadPath{fill:#333333;}#render1068252707 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1068252707 .flowchart-link{stroke:#333333;fill:none;}#render1068252707 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1068252707 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1068252707 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1068252707 .cluster text{fill:#333;}#render1068252707 .cluster span{color:#333;}#render1068252707 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1068252707 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M188.296875,115.75L192.46354166666666,115.75C196.63020833333334,115.75,204.96354166666666,115.75,213.38020884195964,115.83333333333333C221.79687601725263,115.91666666666667,230.2968770345052,116.08333333333333,234.5468775431315,116.16666666666667L238.7968780517578,116.24999999999999" marker-end="url(#arrowhead4132)" style="fill:none"></path><defs><marker id="arrowhead4132" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M387.1695210066596,77.78124280841742L398.9976466966304,69.35936900701451C410.82577238660116,60.93749520561161,434.4820237665428,44.0937476028058,451.9188734148469,35.6718738014029C469.35572306315106,27.25,480.5731709798177,27.25,486.18189493815106,27.25L491.7906188964844,27.25" marker-end="url(#arrowhead4133)" style="fill:none"></path><defs><marker id="arrowhead4133" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M425.63827819824223,116.25L431.05494435628253,116.16666666666667C436.47161051432295,116.08333333333333,447.30494283040366,115.91666666666667,458.22160898844396,115.83333333333333C469.1382751464844,115.75,480.1382751464844,115.75,485.6382751464844,115.75L491.1382751464844,115.75" marker-end="url(#arrowhead4134)" style="fill:none"></path><defs><marker id="arrowhead4134" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M671.0796813964844,27.25L675.3550720214844,27.25C679.6304626464844,27.25,688.1812438964844,27.25,702.2254344534352,36.56453314706941C716.2696250103859,45.87906629413882,735.8072248742874,64.50813258827765,745.5760248062383,73.82266573534706L755.344824738189,83.13719888241647" marker-end="url(#arrowhead4135)" style="fill:none"></path><defs><marker id="arrowhead4135" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M671.7320251464844,115.75L675.898691813151,115.75C680.0653584798177,115.75,688.398691813151,115.75,696.8153587341309,115.83333333333333C705.2320256551106,115.91666666666667,713.7320261637369,116.08333333333333,717.9820264180502,116.16666666666667L722.2320266723632,116.24999999999999" marker-end="url(#arrowhead4136)" style="fill:none"></path><defs><marker id="arrowhead4136" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-B" style="opacity: 1;" id="L-E-B"><path class="path" d="M749.7897975523276,143.80777087996444L740.9468354846871,149.71480906663703C732.1038734170465,155.62184725330962,714.4179492817653,167.4359236266548,686.3588413807915,173.3429618133274C658.2997334798177,179.25,619.867441813151,179.25,580.101816813151,179.25C540.336191813151,179.25,499.2372334798177,179.25,468.0652190085502,173.9558701638131C436.8932045372826,168.66174032762623,415.6481339280809,158.07348065525244,405.02559862348,152.77935081906557L394.40306331887916,147.48522098287867" marker-end="url(#arrowhead4137)" style="fill:none"></path><defs><marker id="arrowhead4137" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M857.9421798706056,116.24999999999999L863.3588473002118,116.16666666666664C868.7755147298179,116.08333333333333,879.60884958903,115.91666666666667,890.525517018636,115.83333333333333C901.4421844482422,115.75,912.4421844482422,115.75,917.9421844482422,115.75L923.4421844482422,115.75" marker-end="url(#arrowhead4138)" style="fill:none"></path><defs><marker id="arrowhead4138" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(458.1382751464844,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000490188598633" height="18.500566482543945"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(458.1382751464844,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000490188598633" height="18.500566482543945"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(581.4351501464844,179.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000490188598633" height="18.500566482543945"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(890.4421844482422,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000490188598633" height="18.500566482543945"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5694" transform="translate(98.1484375,115.75)"><rect rx="0" ry="0" x="-90.1484375" y="-19.25" width="180.296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.1484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i=m-1, j=n-1, k=m+n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5695" transform="translate(331.7175750732422,115.75)"><polygon points="93.420703125,0 186.84140625,-93.420703125 93.420703125,-186.84140625 0,-93.420703125" transform="translate(-93.420703125,93.420703125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-74.55078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums1[i] &gt; nums2[j]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5697" transform="translate(581.4351501464844,27.25)"><rect rx="0" ry="0" x="-89.64453125" y="-19.25" width="179.2890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-79.64453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums1[k--]=nums1[i--]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5699" transform="translate(581.4351501464844,115.75)"><rect rx="0" ry="0" x="-90.296875" y="-19.25" width="180.59375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums1[k--]=nums2[j--]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5701" transform="translate(789.5871047973633,115.75)"><polygon points="67.855078125,0 135.71015625,-67.855078125 67.855078125,-135.71015625 0,-67.855078125" transform="translate(-67.855078125,67.855078125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-46.14453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i&gt;=0 且 j&gt;=0?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-5707" transform="translate(1008.5984344482422,115.75)"><rect rx="0" ry="0" x="-85.15625" y="-19.25" width="170.3125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-75.15625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">j&gt;=0 则拷剩余 nums2</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0088-merge-sorted.gif" alt="801" /></p><pre><code class="language-java">class Solution {    public void merge(int[] nums1, int m, int[] nums2, int n) {        int i = m - 1, j = n - 1, k = m + n - 1;        while (i &gt;= 0 &amp;&amp; j &gt;= 0) {            nums1[k--] = nums1[i] &gt; nums2[j] ? nums1[i--] : nums2[j--];        }        while (j &gt;= 0) nums1[k--] = nums2[j--]; // nums1 剩余的不用动(已在位)    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(m+n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />必须从后往前填。从前往后会覆盖 <code>nums1</code> 还没处理的元素。<code>nums1</code> 剩余不用拷（本来就在正确位置），只有 <code>nums2</code> 剩余要拷。</p></blockquote><h2 id="27.-%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0" tabindex="-1"><a href="https://leetcode.cn/problems/remove-element/" target="_blank">27. 移除元素</a></h2><blockquote><p>原地移除数组中值为 <code>val</code> 的元素，返回新长度。</p></blockquote><p><strong>思路脉络</strong>：快慢指针。<code>slow</code> 指向下一个保留元素该放的位置，<code>fast</code> 扫描全数组。遇 <code>nums[fast] != val</code> 就赋值给 <code>nums[slow]</code> 并 <code>slow++</code>，等于 <code>val</code> 就跳过。和热题 100 的移动零完全同构。</p><div class="mermaid"><svg id="render874455214" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="219.53009033203125" style="max-width: 734.2640380859375px;" viewBox="0 0 734.2640380859375 219.53009033203125"><style>#render874455214 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render874455214 .error-icon{fill:#552222;}#render874455214 .error-text{fill:#552222;stroke:#552222;}#render874455214 .edge-thickness-normal{stroke-width:2px;}#render874455214 .edge-thickness-thick{stroke-width:3.5px;}#render874455214 .edge-pattern-solid{stroke-dasharray:0;}#render874455214 .edge-pattern-dashed{stroke-dasharray:3;}#render874455214 .edge-pattern-dotted{stroke-dasharray:2;}#render874455214 .marker{fill:#333333;stroke:#333333;}#render874455214 .marker.cross{stroke:#333333;}#render874455214 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render874455214 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render874455214 .cluster-label text{fill:#333;}#render874455214 .cluster-label span{color:#333;}#render874455214 .label text,#render874455214 span{fill:#333;color:#333;}#render874455214 .node rect,#render874455214 .node circle,#render874455214 .node ellipse,#render874455214 .node polygon,#render874455214 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render874455214 .node .label{text-align:center;}#render874455214 .node.clickable{cursor:pointer;}#render874455214 .arrowheadPath{fill:#333333;}#render874455214 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render874455214 .flowchart-link{stroke:#333333;fill:none;}#render874455214 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render874455214 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render874455214 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render874455214 .cluster text{fill:#333;}#render874455214 .cluster span{color:#333;}#render874455214 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render874455214 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M106.43797109486778,120.90508270263672L116.44570507905648,114.82702255249023C126.45343906324518,108.74896240234375,146.4689070316226,96.59284210205078,160.7266415244376,90.59811528523763C174.98437601725263,84.60338846842448,183.48437703450523,84.77005513509116,187.73437754313156,84.85338846842448L191.98437805175786,84.93672180175783" marker-end="url(#arrowhead4165)" style="fill:none"></path><defs><marker id="arrowhead4165" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M322.84989733322493,62.92880358322492L331.93455087827334,59.138456619647066C341.0192044233218,55.34810965606922,359.1885115134187,47.76741572891351,373.77316505846716,43.977068765335666C388.3578186035156,40.18672180175781,399.3578186035156,40.18672180175781,404.8578186035156,40.18672180175781L410.3578186035156,40.18672180175781" marker-end="url(#arrowhead4166)" style="fill:none"></path><defs><marker id="arrowhead4166" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M322.84989298705415,106.94463826294584L331.9345472564644,110.56831885274784C341.0192015258746,114.19199944254983,359.1885100646951,121.43936062215381,386.271862250772,125.06304121195582C413.35521443684894,128.6867218017578,449.3526102701823,128.6867218017578,467.35130818684894,128.6867218017578L485.3500061035156,128.6867218017578" marker-end="url(#arrowhead4167)" style="fill:none"></path><defs><marker id="arrowhead4167" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M612.3421936035156,40.18672180175781L616.5088602701823,40.18672180175781C620.675526936849,40.18672180175781,629.0088602701823,40.18672180175781,640.6040484469714,51.72838846842448C652.1992366237605,63.27005513509115,667.0562796440053,86.35338846842448,674.4848011541277,97.89505513509114L681.9133226642501,109.43672180175781" marker-end="url(#arrowhead4168)" style="fill:none"></path><defs><marker id="arrowhead4168" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M537.3500061035156,128.6867218017578L554.015370686849,128.6867218017578C570.6807352701823,128.6867218017578,604.011464436849,128.6867218017578,624.843495686849,128.6867218017578C645.675526936849,128.6867218017578,654.0088602701823,128.6867218017578,658.175526936849,128.6867218017578L662.3421936035156,128.6867218017578" marker-end="url(#arrowhead4169)" style="fill:none"></path><defs><marker id="arrowhead4169" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M681.0673324233179,147.9367218017578L673.7798092866842,158.53561528523764C666.4922861500505,169.13450876871744,651.9172398767831,190.3322957356771,623.6310188234827,200.9311892191569C595.3447977701823,211.53008270263672,553.347401936849,211.53008270263672,510.0166727701823,211.53008270263672C466.6859436035156,211.53008270263672,422.0218811035156,211.53008270263672,381.45039621988934,211.53008270263672C340.878911336263,211.53008270263672,304.40000406901044,211.53008270263672,269.2544301350911,211.53008270263672C234.10885620117188,211.53008270263672,200.29661560058594,211.53008270263672,172.22397685677285,202.84258270263672C144.15133811295973,194.15508270263672,121.81830122591946,176.78008270263672,110.65178278239931,168.09258270263672L99.48526433887916,159.40508270263672" marker-end="url(#arrowhead4170)" style="fill:none"></path><defs><marker id="arrowhead4170" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(377.3578186035156,40.18672180175781)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000484466552734" height="18.500560760498047"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(377.3578186035156,128.6867218017578)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000484466552734" height="18.500560760498047"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5720" transform="translate(74.7421875,140.15508270263672)"><rect rx="0" ry="0" x="-66.7421875" y="-19.25" width="133.484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.7421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">slow=0,fast遍历</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5721" transform="translate(267.9210968017578,84.43672180175781)"><polygon points="76.43671875,0 152.8734375,-76.43671875 76.43671875,-152.8734375 0,-76.43671875" transform="translate(-76.43671875,76.43671875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-55.6796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[fast]≠val?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5723" transform="translate(511.3500061035156,40.18672180175781)"><rect rx="0" ry="0" x="-100.9921875" y="-19.25" width="201.984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-90.9921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[slow++]=nums[fast]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5725" transform="translate(511.3500061035156,128.6867218017578)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">跳过</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5727" transform="translate(694.3031311035156,128.6867218017578)"><rect rx="0" ry="0" x="-31.9609375" y="-19.25" width="63.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.9609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">fast++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int removeElement(int[] nums, int val) {        int slow = 0;        for (int fast = 0; fast &lt; nums.length; fast++) {            if (nums[fast] != val) nums[slow++] = nums[fast];        }        return slow;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路<br />快慢指针&quot;原地筛选&quot;：<code>slow</code> 永远指向下一个符合条件元素的落脚点。同款的还有删除有序数组重复项、移动零。</p></blockquote><h2 id="26.-%E5%88%A0%E9%99%A4%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E9%A1%B9" tabindex="-1"><a href="https://leetcode.cn/problems/remove-duplicates-from-sorted-array/" target="_blank">26. 删除有序数组中的重复项</a></h2><blockquote><p>有序数组原地去重，每个元素只留一个，返回新长度。</p></blockquote><p><strong>思路脉络</strong>：有序数组重复元素相邻。快慢指针：<code>slow</code> 指向最后一个不重复元素，<code>fast</code> 扫描。<code>nums[fast] != nums[slow]</code> 就 <code>slow++</code> 并赋值。</p><div class="mermaid"><svg id="render2167915445" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="259.8402404785156" style="max-width: 772.7921752929688px;" viewBox="0 0 772.7921752929688 259.8402404785156"><style>#render2167915445 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2167915445 .error-icon{fill:#552222;}#render2167915445 .error-text{fill:#552222;stroke:#552222;}#render2167915445 .edge-thickness-normal{stroke-width:2px;}#render2167915445 .edge-thickness-thick{stroke-width:3.5px;}#render2167915445 .edge-pattern-solid{stroke-dasharray:0;}#render2167915445 .edge-pattern-dashed{stroke-dasharray:3;}#render2167915445 .edge-pattern-dotted{stroke-dasharray:2;}#render2167915445 .marker{fill:#333333;stroke:#333333;}#render2167915445 .marker.cross{stroke:#333333;}#render2167915445 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2167915445 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2167915445 .cluster-label text{fill:#333;}#render2167915445 .cluster-label span{color:#333;}#render2167915445 .label text,#render2167915445 span{fill:#333;color:#333;}#render2167915445 .node rect,#render2167915445 .node circle,#render2167915445 .node ellipse,#render2167915445 .node polygon,#render2167915445 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2167915445 .node .label{text-align:center;}#render2167915445 .node.clickable{cursor:pointer;}#render2167915445 .arrowheadPath{fill:#333333;}#render2167915445 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2167915445 .flowchart-link{stroke:#333333;fill:none;}#render2167915445 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2167915445 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2167915445 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2167915445 .cluster text{fill:#333;}#render2167915445 .cluster span{color:#333;}#render2167915445 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2167915445 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M90.55201274774129,161.21523666381836L100.67094812311774,152.89772351582846C110.78988349849419,144.58021036783853,131.0277542492471,127.94518407185872,145.3966898789367,119.71100425720215C159.7656255086263,111.47682444254558,168.2656260172526,111.64349110921223,172.51562627156576,111.72682444254558L176.7656265258789,111.8101577758789" marker-end="url(#arrowhead4197)" style="fill:none"></path><defs><marker id="arrowhead4197" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M358.0676491529997,86.49186790299977L367.70403105279274,83.25324954847963C377.3404129525857,80.01463119395949,396.6131767521718,73.5373944849192,411.74955865196483,70.29877613039905C426.8859405517578,67.0601577758789,437.8859405517578,67.0601577758789,443.3859405517578,67.0601577758789L448.8859405517578,67.0601577758789" marker-end="url(#arrowhead4198)" style="fill:none"></path><defs><marker id="arrowhead4198" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M358.0676468491382,137.12844690086172L367.70402913290815,140.2003987133646C377.3404114166781,143.27235052586744,396.613175984218,149.4162541508732,424.24825618465457,152.48820596337603C451.8833363850911,155.5601577758789,487.8807322184245,155.5601577758789,505.8794301350911,155.5601577758789L523.8781280517578,155.5601577758789" marker-end="url(#arrowhead4199)" style="fill:none"></path><defs><marker id="arrowhead4199" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M650.8703155517578,67.0601577758789L655.0369822184244,67.0601577758789C659.2036488850912,67.0601577758789,667.5369822184244,67.0601577758789,679.1321703952136,78.60182444254558C690.7273585720027,90.14349110921223,705.5844015922476,113.22682444254558,713.0129231023699,124.76849110921223L720.4414446124923,136.3101577758789" marker-end="url(#arrowhead4200)" style="fill:none"></path><defs><marker id="arrowhead4200" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M575.8781280517578,155.5601577758789L592.5434926350912,155.5601577758789C609.2088572184244,155.5601577758789,642.5395863850912,155.5601577758789,663.3716176350912,155.5601577758789C684.2036488850912,155.5601577758789,692.5369822184244,155.5601577758789,696.7036488850912,155.5601577758789L700.8703155517578,155.5601577758789" marker-end="url(#arrowhead4201)" style="fill:none"></path><defs><marker id="arrowhead4201" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M721.4426245884262,174.8101577758789L713.8472397489813,187.64850425720215C706.2518549095366,200.4868507385254,691.0610852306472,226.16354370117188,662.4670024745359,239.00189018249512C633.8729197184244,251.84023666381836,591.8755238850912,251.84023666381836,548.5447947184244,251.84023666381836C505.2140655517578,251.84023666381836,460.5500030517578,251.84023666381836,415.49961217244464,251.84023666381836C370.44922129313153,251.84023666381836,325.0125020345052,251.84023666381836,280.9091161092122,251.84023666381836C236.80573018391928,251.84023666381836,194.03567759195963,251.84023666381836,162.41031772548945,243.15273666381836C130.78495785901927,234.46523666381836,110.30429071803853,217.09023666381836,100.06395714754815,208.40273666381836L89.82362357705779,199.71523666381836" marker-end="url(#arrowhead4202)" style="fill:none"></path><defs><marker id="arrowhead4202" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(415.8859405517578,67.0601577758789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000263214111328" height="18.500307083129883"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(415.8859405517578,155.5601577758789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000263214111328" height="18.500307083129883"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5744" transform="translate(67.1328125,180.46523666381836)"><rect rx="0" ry="0" x="-59.1328125" y="-19.25" width="118.265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-49.1328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">slow=0,fast=1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5745" transform="translate(279.5757827758789,111.3101577758789)"><polygon points="103.31015625,0 206.6203125,-103.31015625 103.31015625,-206.6203125 0,-103.31015625" transform="translate(-103.31015625,103.31015625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-85.5390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[fast]≠nums[slow]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5747" transform="translate(549.8781280517578,67.0601577758789)"><rect rx="0" ry="0" x="-100.9921875" y="-19.25" width="201.984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-90.9921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[++slow]=nums[fast]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5749" transform="translate(549.8781280517578,155.5601577758789)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">跳过</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5751" transform="translate(732.8312530517578,155.5601577758789)"><rect rx="0" ry="0" x="-31.9609375" y="-19.25" width="63.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.9609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">fast++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int removeDuplicates(int[] nums) {        if (nums.length == 0) return 0;        int slow = 0;        for (int fast = 1; fast &lt; nums.length; fast++) {            if (nums[fast] != nums[slow]) nums[++slow] = nums[fast];        }        return slow + 1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br /><code>slow</code> 从 0 开始，返回 <code>slow + 1</code>（长度）。比较 <code>nums[fast] != nums[slow]</code>，不是 <code>nums[fast] != nums[fast-1]</code>——前者更简洁，因为 <code>slow</code> 永远是最后一个保留元素。</p></blockquote><h2 id="80.-%E5%88%A0%E9%99%A4%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E9%A1%B9-ii" tabindex="-1"><a href="https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/" target="_blank">80. 删除有序数组中的重复项 II</a></h2><blockquote><p>有序数组，每个元素最多留两个，返回新长度。</p></blockquote><p><strong>思路脉络</strong>：和上一题的区别是允许重复两次。关键洞察：<code>slow</code> 位置要保留 <code>nums[fast]</code> 的条件是 <code>slow &lt; 2 || nums[fast] != nums[slow-2]</code>。<code>slow-2</code> 是&quot;如果保留这个，会不会变成第三个&quot;。前两个位置直接保留，从第三个起要比对倒数第二个。</p><div class="mermaid"><svg id="render3121745269" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="346.34063720703125" style="max-width: 876.7312622070312px;" viewBox="0 0 876.7312622070312 346.34063720703125"><style>#render3121745269 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3121745269 .error-icon{fill:#552222;}#render3121745269 .error-text{fill:#552222;stroke:#552222;}#render3121745269 .edge-thickness-normal{stroke-width:2px;}#render3121745269 .edge-thickness-thick{stroke-width:3.5px;}#render3121745269 .edge-pattern-solid{stroke-dasharray:0;}#render3121745269 .edge-pattern-dashed{stroke-dasharray:3;}#render3121745269 .edge-pattern-dotted{stroke-dasharray:2;}#render3121745269 .marker{fill:#333333;stroke:#333333;}#render3121745269 .marker.cross{stroke:#333333;}#render3121745269 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3121745269 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3121745269 .cluster-label text{fill:#333;}#render3121745269 .cluster-label span{color:#333;}#render3121745269 .label text,#render3121745269 span{fill:#333;color:#333;}#render3121745269 .node rect,#render3121745269 .node circle,#render3121745269 .node ellipse,#render3121745269 .node polygon,#render3121745269 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3121745269 .node .label{text-align:center;}#render3121745269 .node.clickable{cursor:pointer;}#render3121745269 .arrowheadPath{fill:#333333;}#render3121745269 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3121745269 .flowchart-link{stroke:#333333;fill:none;}#render3121745269 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3121745269 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3121745269 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3121745269 .cluster text{fill:#333;}#render3121745269 .cluster span{color:#333;}#render3121745269 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3121745269 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M94.07797366248181,227.75547790527344L106.14570721873484,215.7412846883138C118.21344077498787,203.72709147135416,142.34890788749394,179.6987050374349,158.66664246099955,167.7678451538086C174.9843770345052,155.83698527018228,183.4843790690104,156.00365193684894,187.73438008626303,156.08698527018228L191.9843811035156,156.1703186035156" marker-end="url(#arrowhead4229)" style="fill:none"></path><defs><marker id="arrowhead4229" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M458.272889475299,127.11820197529892L468.53157659725434,124.5018880800017C478.7902637192098,121.88557418470448,499.3076379631205,116.65294639411006,515.0663250850758,114.03663249881284C530.8250122070312,111.42031860351562,541.8250122070312,111.42031860351562,547.3250122070312,111.42031860351562L552.8250122070312,111.42031860351562" marker-end="url(#arrowhead4230)" style="fill:none"></path><defs><marker id="arrowhead4230" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M448.6043347934838,194.89097770651608L460.47444769574173,199.05589440630897C472.3445605979996,203.22081110610188,496.0847864025154,211.55064450568764,525.95359722144,215.71556120548053C555.8224080403646,219.88047790527344,591.8198038736979,219.88047790527344,609.8185017903646,219.88047790527344L627.8171997070312,219.88047790527344" marker-end="url(#arrowhead4231)" style="fill:none"></path><defs><marker id="arrowhead4231" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M754.8093872070312,111.42031860351562L758.9760538736979,111.42031860351562C763.1427205403646,111.42031860351562,771.4760538736979,111.42031860351562,783.4512626217243,126.28867848714192C795.4264713697506,141.15703837076822,811.0435555324698,170.89375813802084,818.8520976138294,185.76211802164713L826.660639695189,200.63047790527344" marker-end="url(#arrowhead4232)" style="fill:none"></path><defs><marker id="arrowhead4232" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M679.8171997070312,219.88047790527344L696.4825642903646,219.88047790527344C713.1479288736979,219.88047790527344,746.4786580403646,219.88047790527344,767.3106892903646,219.88047790527344C788.1427205403646,219.88047790527344,796.4760538736979,219.88047790527344,800.6427205403646,219.88047790527344L804.8093872070312,219.88047790527344" marker-end="url(#arrowhead4233)" style="fill:none"></path><defs><marker id="arrowhead4233" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M827.514064599514,239.13047790527344L819.5632850341002,255.6655044555664C811.6125054686863,272.2005310058594,795.7109463378588,305.2705841064453,766.7614688557784,321.8056106567383C737.8119913736979,338.34063720703125,695.8145955403646,338.34063720703125,652.4838663736979,338.34063720703125C609.1531372070312,338.34063720703125,564.4890747070312,338.34063720703125,512.0453236897787,338.34063720703125C459.60157267252606,338.34063720703125,399.3781331380208,338.34063720703125,340.48802693684894,338.34063720703125C281.59792073567706,338.34063720703125,224.04114786783853,338.34063720703125,183.19502787766623,326.3264439900716C142.34890788749394,314.312250773112,118.21344077498787,290.2838643391927,106.14570721873484,278.2696711222331L94.07797366248181,266.25547790527344" marker-end="url(#arrowhead4234)" style="fill:none"></path><defs><marker id="arrowhead4234" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(519.8250122070312,111.42031860351562)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00021743774414" height="18.50025177001953"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(519.8250122070312,219.88047790527344)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00021743774414" height="18.50025177001953"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5768" transform="translate(74.7421875,247.00547790527344)"><rect rx="0" ry="0" x="-66.7421875" y="-19.25" width="133.484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.7421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">slow=0,fast遍历</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5769" transform="translate(339.1546936035156,155.67031860351562)"><polygon points="147.6703125,0 295.340625,-147.6703125 147.6703125,-295.340625 0,-147.6703125" transform="translate(-147.6703125,147.6703125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-134.828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">slow&amp;lt;2或nums[fast]≠nums[slow-2]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5771" transform="translate(653.8171997070312,111.42031860351562)"><rect rx="0" ry="0" x="-100.9921875" y="-19.25" width="201.984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-90.9921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[slow++]=nums[fast]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5773" transform="translate(653.8171997070312,219.88047790527344)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">跳过</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5775" transform="translate(836.7703247070312,219.88047790527344)"><rect rx="0" ry="0" x="-31.9609375" y="-19.25" width="63.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.9609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">fast++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int removeDuplicates(int[] nums) {        int slow = 0;        for (int fast = 0; fast &lt; nums.length; fast++) {            if (slow &lt; 2 || nums[fast] != nums[slow - 2]) nums[slow++] = nums[fast];        }        return slow;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />判断条件 <code>nums[fast] != nums[slow-2]</code>（看倒数第二个），不是 <code>nums[slow-1]</code>。<code>slow &lt; 2</code> 保证前两个位置直接放，不用比。这题推广到&quot;最多留 k 个&quot;就把 <code>2</code> 换成 <code>k</code>。</p></blockquote><h2 id="169.-%E5%A4%9A%E6%95%B0%E5%85%83%E7%B4%A0" tabindex="-1"><a href="https://leetcode.cn/problems/majority-element/" target="_blank">169. 多数元素</a></h2><blockquote><p>数组中出现超过 <code>⌊n/2⌋</code> 次的元素，保证存在。</p></blockquote><p><strong>思路脉络</strong>：摩尔投票。想象两军对战，同阵营 <code>count+1</code>、异阵营 <code>count-1</code>，归零换候选。多数派超半数，抵消所有少数派后仍有盈余。和热题 100 相同。</p><div class="mermaid"><svg id="render3144875733" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="403.34393310546875" style="max-width: 536.6116943359375px;" viewBox="0 0 536.6116943359375 403.34393310546875"><style>#render3144875733 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3144875733 .error-icon{fill:#552222;}#render3144875733 .error-text{fill:#552222;stroke:#552222;}#render3144875733 .edge-thickness-normal{stroke-width:2px;}#render3144875733 .edge-thickness-thick{stroke-width:3.5px;}#render3144875733 .edge-pattern-solid{stroke-dasharray:0;}#render3144875733 .edge-pattern-dashed{stroke-dasharray:3;}#render3144875733 .edge-pattern-dotted{stroke-dasharray:2;}#render3144875733 .marker{fill:#333333;stroke:#333333;}#render3144875733 .marker.cross{stroke:#333333;}#render3144875733 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3144875733 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3144875733 .cluster-label text{fill:#333;}#render3144875733 .cluster-label span{color:#333;}#render3144875733 .label text,#render3144875733 span{fill:#333;color:#333;}#render3144875733 .node rect,#render3144875733 .node circle,#render3144875733 .node ellipse,#render3144875733 .node polygon,#render3144875733 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3144875733 .node .label{text-align:center;}#render3144875733 .node.clickable{cursor:pointer;}#render3144875733 .arrowheadPath{fill:#333333;}#render3144875733 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3144875733 .flowchart-link{stroke:#333333;fill:none;}#render3144875733 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3144875733 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3144875733 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3144875733 .cluster text{fill:#333;}#render3144875733 .cluster span{color:#333;}#render3144875733 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3144875733 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M43.738922669894485,237.97031021118164L52.07280014157874,208.08352661132812C60.40667761326299,178.1967430114746,77.07443255663149,118.42317581176758,89.65831015547231,88.61972554524739C102.24218775431315,58.81627527872721,110.74218800862631,58.98294194539388,114.99218813578288,59.06627527872721L119.24218826293946,59.149608612060554" marker-end="url(#arrowhead4273)" style="fill:none"></path><defs><marker id="arrowhead4273" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M220.5414054870606,59.14960861206054L225.95807202657068,59.066275278727204C231.37473856608077,58.98294194539388,242.20807164510094,58.81627527872721,256.009738944079,62.662075996398926C269.8114062430571,66.50787671407063,286.58140776199315,74.36614481608073,294.9664085214612,78.29527886708577L303.3514092809292,82.22441291809082" marker-end="url(#arrowhead4274)" style="fill:none"></path><defs><marker id="arrowhead4274" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M186.73989672367577,92.95111890132425L197.79014805708334,115.20454538112877C208.8403993904909,137.4579718609333,230.94090205730598,181.9648248205423,248.01504687419336,204.30158463368016C265.0891916910807,226.63834444681802,277.1369786580404,226.8050111134847,283.1608721415202,226.88834444681802L289.184765625,226.97167778015137" marker-end="url(#arrowhead4275)" style="fill:none"></path><defs><marker id="arrowhead4275" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M400.67929534912116,226.97167778015137L406.53652191162115,226.88834444681802C412.39374847412114,226.8050111134847,424.1082015991212,226.63834444681802,436.0006279324514,230.90894985198975C447.8930542657816,235.17955525716147,459.9634538074422,243.88743273417154,465.99865357827247,248.2413714726766L472.03385334910274,252.59531021118164" marker-end="url(#arrowhead4276)" style="fill:none"></path><defs><marker id="arrowhead4276" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M369.03358819223087,258.6173864629206L380.16509928087925,273.2593124928025C391.29661036952757,287.90123852268437,413.55963254682433,317.18509058244814,430.6104144688061,334.6151508897041C447.6611963907878,352.04521119696005,459.4997380574544,357.6214797517082,465.4190088907878,360.4096140290823L471.3382797241211,363.1977483064564" marker-end="url(#arrowhead4277)" style="fill:none"></path><defs><marker id="arrowhead4277" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M307.6838357410447,120.72441291809082L298.57676390489075,125.49501832326253C289.46969206873683,130.26562372843424,271.255548396429,139.80683453877768,248.2068751249316,144.57743994394937C225.15820185343424,149.3480453491211,197.2749989827474,149.3480453491211,170.7251294453939,149.3480453491211C144.17525990804037,149.3480453491211,118.95872370402019,149.3480453491211,98.76878521575998,164.1184228261312C78.57884672749975,178.88880030314127,63.4155059549995,208.42955525716147,55.83383556874938,223.19993273417154L48.252165182499255,237.97031021118164" marker-end="url(#arrowhead4278)" style="fill:none"></path><defs><marker id="arrowhead4278" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M472.03385334910274,291.09531021118164L465.99865357827247,295.4492489496867C459.9634538074422,299.8031876881917,447.8930542657816,308.51106516520184,426.626083661618,312.86500390370685C405.3591130574544,317.2189426422119,374.8955713907878,317.2189426422119,344.4320297241211,317.2189426422119C313.9684880574544,317.2189426422119,283.5049463907878,317.2189426422119,254.331574122111,317.2189426422119C225.15820185343424,317.2189426422119,197.2749989827474,317.2189426422119,170.7251294453939,317.2189426422119C144.17525990804037,317.2189426422119,118.95872370402019,317.2189426422119,100.08282289355759,310.42750390370685C81.206922083095,303.63606516520184,68.67165666618997,290.0531876881917,62.40402395773748,283.2617489496867L56.13639124928497,276.47031021118164" marker-end="url(#arrowhead4279)" style="fill:none"></path><defs><marker id="arrowhead4279" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-A" style="opacity: 1;" id="L-F-A"><path class="path" d="M471.3382797241211,382.460418326952L465.4190088907878,383.83683904616197C459.4997380574544,385.21325976537196,447.6611963907878,387.966101203792,426.5101547241211,389.3425219230019C405.3591130574544,390.7189426422119,374.8955713907878,390.7189426422119,344.4320297241211,390.7189426422119C313.9684880574544,390.7189426422119,283.5049463907878,390.7189426422119,254.331574122111,390.7189426422119C225.15820185343424,390.7189426422119,197.2749989827474,390.7189426422119,170.7251294453939,390.7189426422119C144.17525990804037,390.7189426422119,118.95872370402019,390.7189426422119,98.45265717273139,371.67750390370685C77.94659064144261,352.63606516520184,62.15099378288522,314.5531876881917,54.25319535360652,295.5117489496867L46.355396924327835,276.47031021118164" marker-end="url(#arrowhead4280)" style="fill:none"></path><defs><marker id="arrowhead4280" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(253.0414047241211,58.64960861206055)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000070571899414" height="18.50008201599121"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(253.0414047241211,226.47167778015137)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000070571899414" height="18.50008201599121"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(435.8226547241211,226.47167778015137)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000070571899414" height="18.50008201599121"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(435.8226547241211,346.4689426422119)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000070571899414" height="18.50008201599121"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5796" transform="translate(38.37109375,257.22031021118164)"><rect rx="0" ry="0" x="-30.37109375" y="-19.25" width="60.7421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20.37109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5797" transform="translate(169.39179611206055,58.64960861206055)"><polygon points="50.649609375000004,0 101.29921875000001,-50.649609375000004 50.649609375000004,-101.29921875000001 0,-50.649609375000004" transform="translate(-50.649609375000004,50.649609375000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-27.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cnt==0?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5799" transform="translate(344.4320297241211,101.47441291809082)"><rect rx="0" ry="0" x="-58.390625" y="-19.25" width="116.78125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48.390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cand=n,cnt=1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5801" transform="translate(344.4320297241211,226.47167778015137)"><polygon points="55.747265625000004,0 111.49453125000001,-55.747265625000004 55.747265625000004,-111.49453125000001 0,-55.747265625000004" transform="translate(-55.747265625000004,55.747265625000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-32.69140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">n==cand?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5803" transform="translate(498.7171859741211,271.84531021118164)"><rect rx="0" ry="0" x="-29.89453125" y="-19.25" width="59.7890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-19.89453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cnt++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-5805" transform="translate(498.7171859741211,376.0939426422119)"><rect rx="0" ry="0" x="-27.37890625" y="-19.25" width="54.7578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-17.37890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cnt--</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int majorityElement(int[] nums) {        int cand = 0, cnt = 0;        for (int n : nums) {            if (cnt == 0) { cand = n; cnt = 1; }            else if (n == cand) cnt++;            else cnt--;        }        return cand;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />摩尔投票只在&quot;保证有多数元素&quot;时直接返回候选。不保证时需二次遍历验证计数。</p></blockquote><h2 id="189.-%E8%BD%AE%E8%BD%AC%E6%95%B0%E7%BB%84" tabindex="-1"><a href="https://leetcode.cn/problems/rotate-array/" target="_blank">189. 轮转数组</a></h2><blockquote><p>数组右轮转 <code>k</code> 步，原地。</p></blockquote><p><strong>思路脉络</strong>：三次反转。整体反转 → 反转前 <code>k</code> 个 → 反转后 <code>n-k</code> 个，等价于右轮转 <code>k</code>。先 <code>k %= n</code>。和热题 100 相同。</p><div class="mermaid"><svg id="render3675165372" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 505.2578125px;" viewBox="0 0 505.2578125 54.5"><style>#render3675165372 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3675165372 .error-icon{fill:#552222;}#render3675165372 .error-text{fill:#552222;stroke:#552222;}#render3675165372 .edge-thickness-normal{stroke-width:2px;}#render3675165372 .edge-thickness-thick{stroke-width:3.5px;}#render3675165372 .edge-pattern-solid{stroke-dasharray:0;}#render3675165372 .edge-pattern-dashed{stroke-dasharray:3;}#render3675165372 .edge-pattern-dotted{stroke-dasharray:2;}#render3675165372 .marker{fill:#333333;stroke:#333333;}#render3675165372 .marker.cross{stroke:#333333;}#render3675165372 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3675165372 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3675165372 .cluster-label text{fill:#333;}#render3675165372 .cluster-label span{color:#333;}#render3675165372 .label text,#render3675165372 span{fill:#333;color:#333;}#render3675165372 .node rect,#render3675165372 .node circle,#render3675165372 .node ellipse,#render3675165372 .node polygon,#render3675165372 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3675165372 .node .label{text-align:center;}#render3675165372 .node.clickable{cursor:pointer;}#render3675165372 .arrowheadPath{fill:#333333;}#render3675165372 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3675165372 .flowchart-link{stroke:#333333;fill:none;}#render3675165372 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3675165372 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3675165372 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3675165372 .cluster text{fill:#333;}#render3675165372 .cluster span{color:#333;}#render3675165372 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3675165372 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M62.8046875,27.25L66.97135416666667,27.25C71.13802083333333,27.25,79.47135416666667,27.25,87.8046875,27.25C96.13802083333333,27.25,104.47135416666667,27.25,108.63802083333333,27.25L112.8046875,27.25" marker-end="url(#arrowhead4297)" style="fill:none"></path><defs><marker id="arrowhead4297" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M207.953125,27.25L212.11979166666666,27.25C216.28645833333334,27.25,224.61979166666666,27.25,232.953125,27.25C241.28645833333334,27.25,249.61979166666666,27.25,253.78645833333334,27.25L257.953125,27.25" marker-end="url(#arrowhead4298)" style="fill:none"></path><defs><marker id="arrowhead4298" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M352.4296875,27.25L356.5963541666667,27.25C360.7630208333333,27.25,369.0963541666667,27.25,377.4296875,27.25C385.7630208333333,27.25,394.0963541666667,27.25,398.2630208333333,27.25L402.4296875,27.25" marker-end="url(#arrowhead4299)" style="fill:none"></path><defs><marker id="arrowhead4299" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5818" transform="translate(35.40234375,27.25)"><rect rx="0" ry="0" x="-27.40234375" y="-19.25" width="54.8046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-17.40234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">k%=n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5819" transform="translate(160.37890625,27.25)"><rect rx="0" ry="0" x="-47.57421875" y="-19.25" width="95.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.57421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">反转0..n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5821" transform="translate(305.19140625,27.25)"><rect rx="0" ry="0" x="-47.23828125" y="-19.25" width="94.4765625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.23828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">反转0..k-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5823" transform="translate(449.84375,27.25)"><rect rx="0" ry="0" x="-47.4140625" y="-19.25" width="94.828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.4140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">反转k..n-1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public void rotate(int[] nums, int k) {        int n = nums.length;        k %= n;        reverse(nums, 0, n - 1);        reverse(nums, 0, k - 1);        reverse(nums, k, n - 1);    }    void reverse(int[] a, int l, int r) {        while (l &lt; r) { int t = a[l]; a[l] = a[r]; a[r] = t; l++; r--; }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />忘 <code>k %= n</code> 会越界。<code>k=0</code> 时反转 0 个元素是空操作，安全。</p></blockquote><h2 id="121.-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA" tabindex="-1"><a href="https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/" target="_blank">121. 买卖股票的最佳时机</a></h2><blockquote><p>一次买卖，求最大利润。</p></blockquote><p><strong>思路脉络</strong>：遍历维护&quot;到当前为止最低价&quot; <code>minPrice</code>，每天算&quot;今天卖能赚多少&quot;，取最大。和热题 100 相同。</p><div class="mermaid"><svg id="render2669824288" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 665.0390625px;" viewBox="0 0 665.0390625 89.5"><style>#render2669824288 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2669824288 .error-icon{fill:#552222;}#render2669824288 .error-text{fill:#552222;stroke:#552222;}#render2669824288 .edge-thickness-normal{stroke-width:2px;}#render2669824288 .edge-thickness-thick{stroke-width:3.5px;}#render2669824288 .edge-pattern-solid{stroke-dasharray:0;}#render2669824288 .edge-pattern-dashed{stroke-dasharray:3;}#render2669824288 .edge-pattern-dotted{stroke-dasharray:2;}#render2669824288 .marker{fill:#333333;stroke:#333333;}#render2669824288 .marker.cross{stroke:#333333;}#render2669824288 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2669824288 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2669824288 .cluster-label text{fill:#333;}#render2669824288 .cluster-label span{color:#333;}#render2669824288 .label text,#render2669824288 span{fill:#333;color:#333;}#render2669824288 .node rect,#render2669824288 .node circle,#render2669824288 .node ellipse,#render2669824288 .node polygon,#render2669824288 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2669824288 .node .label{text-align:center;}#render2669824288 .node.clickable{cursor:pointer;}#render2669824288 .arrowheadPath{fill:#333333;}#render2669824288 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2669824288 .flowchart-link{stroke:#333333;fill:none;}#render2669824288 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2669824288 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2669824288 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2669824288 .cluster text{fill:#333;}#render2669824288 .cluster span{color:#333;}#render2669824288 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2669824288 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M96.34375,37.053478653715835L100.51041666666667,35.419565544763195C104.67708333333333,33.785652435810555,113.01041666666667,30.51782621790528,121.34375,28.88391310895264C129.67708333333334,27.25,138.01041666666666,27.25,142.17708333333334,27.25L146.34375,27.25" marker-end="url(#arrowhead4315)" style="fill:none"></path><defs><marker id="arrowhead4315" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M379.6640625,27.25L383.8307291666667,27.25C387.9973958333333,27.25,396.3307291666667,27.25,407.2080813172043,28.5625C418.08543346774195,29.875,431.5068044354839,32.5,438.2174899193549,33.8125L444.9281754032258,35.125" marker-end="url(#arrowhead4316)" style="fill:none"></path><defs><marker id="arrowhead4316" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M444.9281754032258,73.625L438.2174899193548,74.9375C431.5068044354839,76.25,418.08543346774195,78.875,387.7647219422043,80.1875C357.4440104166667,81.5,310.2239583333333,81.5,263.00390625,81.5C215.78385416666666,81.5,168.56380208333334,81.5,140.787109375,79.86608689104736C113.01041666666667,78.23217378209472,104.67708333333333,74.96434756418944,100.51041666666667,73.3304344552368L96.34375,71.69652134628416" marker-end="url(#arrowhead4317)" style="fill:none"></path><defs><marker id="arrowhead4317" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5830" transform="translate(52.171875,54.375)"><rect rx="0" ry="0" x="-44.171875" y="-19.25" width="88.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历price</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5831" transform="translate(263.00390625,27.25)"><rect rx="0" ry="0" x="-116.66015625" y="-19.25" width="233.3203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-106.66015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">minPrice=min(minPrice,price)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5833" transform="translate(543.3515625,54.375)"><rect rx="0" ry="0" x="-113.6875" y="-19.25" width="227.375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-103.6875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans=max(ans,price-minPrice)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxProfit(int[] prices) {        int minPrice = Integer.MAX_VALUE, ans = 0;        for (int p : prices) {            minPrice = Math.min(minPrice, p);            ans = Math.max(ans, p - minPrice);        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="122.-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-ii" tabindex="-1"><a href="https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/" target="_blank">122. 买卖股票的最佳时机 II</a></h2><blockquote><p>无限次买卖，求最大利润。</p></blockquote><p><strong>思路脉络</strong>：贪心。只要今天比昨天贵就累加差值。所有上升段的和就是最大利润。为什么对？因为&quot;今天卖明天买&quot;和&quot;持有不卖&quot;等价，拆开统计上升段更简单。</p><div class="mermaid"><svg id="render2734867488" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="302.234375" style="max-width: 491.3515625px;" viewBox="0 0 491.3515625 302.234375"><style>#render2734867488 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2734867488 .error-icon{fill:#552222;}#render2734867488 .error-text{fill:#552222;stroke:#552222;}#render2734867488 .edge-thickness-normal{stroke-width:2px;}#render2734867488 .edge-thickness-thick{stroke-width:3.5px;}#render2734867488 .edge-pattern-solid{stroke-dasharray:0;}#render2734867488 .edge-pattern-dashed{stroke-dasharray:3;}#render2734867488 .edge-pattern-dotted{stroke-dasharray:2;}#render2734867488 .marker{fill:#333333;stroke:#333333;}#render2734867488 .marker.cross{stroke:#333333;}#render2734867488 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2734867488 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2734867488 .cluster-label text{fill:#333;}#render2734867488 .cluster-label span{color:#333;}#render2734867488 .label text,#render2734867488 span{fill:#333;color:#333;}#render2734867488 .node rect,#render2734867488 .node circle,#render2734867488 .node ellipse,#render2734867488 .node polygon,#render2734867488 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2734867488 .node .label{text-align:center;}#render2734867488 .node.clickable{cursor:pointer;}#render2734867488 .arrowheadPath{fill:#333333;}#render2734867488 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2734867488 .flowchart-link{stroke:#333333;fill:none;}#render2734867488 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2734867488 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2734867488 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2734867488 .cluster text{fill:#333;}#render2734867488 .cluster span{color:#333;}#render2734867488 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2734867488 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M66.30725308703965,196.859375L76.39536715586638,179.80859375C86.48348122469311,162.7578125,106.65970936234656,128.65625,120.99782343117327,111.68880208333333C135.3359375,94.72135416666667,143.8359375,94.88802083333333,148.0859375,94.97135416666667L152.3359375,95.0546875" marker-end="url(#arrowhead4340)" style="fill:none"></path><defs><marker id="arrowhead4340" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M325.4453125,95.0546875L330.8619791666667,94.97135416666667C336.2786458333333,94.88802083333333,347.1119791666667,94.72135416666667,361.54816745934824,101.55924479166667C375.9843557520299,108.39713541666667,394.02339900405985,122.23958333333333,403.0429206300748,129.16080729166666L412.06244225608975,136.08203125" marker-end="url(#arrowhead4341)" style="fill:none"></path><defs><marker id="arrowhead4341" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M277.165675072603,143.33432492739698L290.62894797716916,160.3384999394975C304.09222088173533,177.342674951598,331.01876669086766,211.351024975799,353.34922709543383,231.67186747655603C375.6796875,251.9927099773131,393.4140625,258.62604495462614,402.28125,261.9427124432827L411.1484375,265.25937993193924" marker-end="url(#arrowhead4342)" style="fill:none"></path><defs><marker id="arrowhead4342" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M412.06244225608975,174.58203125L403.0429206300748,181.50325520833334C394.02339900405985,188.42447916666666,375.9843557520299,202.26692708333334,347.0390528760149,209.18815104166666C318.09375,216.109375,278.2421875,216.109375,239.72395833333334,216.109375C201.20572916666666,216.109375,164.02083333333334,216.109375,141.26171875,216.109375C118.50260416666667,216.109375,110.16927083333333,216.109375,106.00260416666667,216.109375L101.8359375,216.109375" marker-end="url(#arrowhead4343)" style="fill:none"></path><defs><marker id="arrowhead4343" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M411.1484375,279.7853219323338L402.28125,281.42266411027816C393.4140625,283.0600062882225,375.6796875,286.3346906441113,346.88671875,287.9720328220556C318.09375,289.609375,278.2421875,289.609375,239.72395833333334,289.609375C201.20572916666666,289.609375,164.02083333333334,289.609375,136.58133370535714,280.5677083333333C109.14183407738096,271.5260416666667,91.44773065476191,253.44270833333334,82.6006789434524,244.40104166666666L73.75362723214286,235.359375" marker-end="url(#arrowhead4344)" style="fill:none"></path><defs><marker id="arrowhead4344" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(357.9453125,94.5546875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(357.9453125,245.359375)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5846" transform="translate(54.91796875,216.109375)"><rect rx="0" ry="0" x="-46.91796875" y="-19.25" width="93.8359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.91796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i=1..n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5847" transform="translate(238.390625,94.5546875)"><polygon points="86.5546875,0 173.109375,-86.5546875 86.5546875,-173.109375 0,-86.5546875" transform="translate(-86.5546875,86.5546875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-66.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">price[i]&gt;price[i-1]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5849" transform="translate(437.1484375,155.33203125)"><rect rx="0" ry="0" x="-46.203125" y="-19.25" width="92.40625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans+=差值</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5851" transform="translate(437.1484375,274.984375)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">跳过</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxProfit(int[] prices) {        int ans = 0;        for (int i = 1; i &lt; prices.length; i++) {            if (prices[i] &gt; prices[i - 1]) ans += prices[i] - prices[i - 1];        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 辨析<br />一次买卖(121)用&quot;维护最小值&quot;；无限次买卖(本题)用&quot;累加所有上升段&quot;；最多 k 次(188)才需 DP。难度递增。</p></blockquote><h2 id="55.-%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8F" tabindex="-1"><a href="https://leetcode.cn/problems/jump-game/" target="_blank">55. 跳跃游戏</a></h2><blockquote><p>每格表示最大跳步数，判断能否到末尾。</p></blockquote><p><strong>思路脉络</strong>：维护&quot;当前能到的最远位置&quot; <code>maxReach</code>。遍历时 <code>i &gt; maxReach</code> 则走不到返回 false，否则更新 <code>maxReach = max(maxReach, i+nums[i])</code>，覆盖末尾即 true。和热题 100 相同。</p><div class="mermaid"><svg id="render1905448115" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="258.2755889892578" style="max-width: 984.7406616210938px;" viewBox="0 0 984.7406616210938 258.2755889892578"><style>#render1905448115 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1905448115 .error-icon{fill:#552222;}#render1905448115 .error-text{fill:#552222;stroke:#552222;}#render1905448115 .edge-thickness-normal{stroke-width:2px;}#render1905448115 .edge-thickness-thick{stroke-width:3.5px;}#render1905448115 .edge-pattern-solid{stroke-dasharray:0;}#render1905448115 .edge-pattern-dashed{stroke-dasharray:3;}#render1905448115 .edge-pattern-dotted{stroke-dasharray:2;}#render1905448115 .marker{fill:#333333;stroke:#333333;}#render1905448115 .marker.cross{stroke:#333333;}#render1905448115 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1905448115 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1905448115 .cluster-label text{fill:#333;}#render1905448115 .cluster-label span{color:#333;}#render1905448115 .label text,#render1905448115 span{fill:#333;color:#333;}#render1905448115 .node rect,#render1905448115 .node circle,#render1905448115 .node ellipse,#render1905448115 .node polygon,#render1905448115 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1905448115 .node .label{text-align:center;}#render1905448115 .node.clickable{cursor:pointer;}#render1905448115 .arrowheadPath{fill:#333333;}#render1905448115 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1905448115 .flowchart-link{stroke:#333333;fill:none;}#render1905448115 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1905448115 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1905448115 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1905448115 .cluster text{fill:#333;}#render1905448115 .cluster span{color:#333;}#render1905448115 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1905448115 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M56.33892431201421,107.1572265625L61.87618692667851,101.84293619791667C67.4134495413428,96.52864583333333,78.4879747706714,85.90006510416667,88.2752373853357,80.66910807291667C98.0625,75.43815104166667,106.5625,75.60481770833333,110.8125,75.68815104166667L115.0625,75.771484375" marker-end="url(#arrowhead4377)" style="fill:none"></path><defs><marker id="arrowhead4377" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M229.00809392737685,55.17410955237685L237.85765639781403,51.14867202281405C246.70721886825126,47.12323449325123,264.40634380912564,39.072359434125616,293.96423961289616,35.04692190456281C323.5221354166667,31.021484375,364.9388020833333,31.021484375,385.6471354166667,31.021484375L406.35546875,31.021484375" marker-end="url(#arrowhead4378)" style="fill:none"></path><defs><marker id="arrowhead4378" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M229.00809392737685,96.36885919762315L237.85765639781403,100.22763006051929C246.70721886825126,104.08640092341544,264.40634380912564,111.80394264920771,278.7559062795628,115.66271351210385C293.10546875,119.521484375,304.10546875,119.521484375,309.60546875,119.521484375L315.10546875,119.521484375" marker-end="url(#arrowhead4379)" style="fill:none"></path><defs><marker id="arrowhead4379" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M601.66015625,119.521484375L605.8268229166666,119.521484375C609.9934895833334,119.521484375,618.3268229166666,119.521484375,631.1000828870955,123.77084759386149C643.8733428575243,128.02021081272298,661.0865294650488,136.51893725044596,669.693122768811,140.76830046930743L678.2997160725732,145.01766368816894" marker-end="url(#arrowhead4380)" style="fill:none"></path><defs><marker id="arrowhead4380" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M811.3968719482424,171.1572265625L816.813540140788,171.07389322916666C822.2302083333334,170.99055989583334,833.0635447184245,170.82389322916666,843.9802129109702,170.74055989583334C854.8968811035156,170.6572265625,865.8968811035156,170.6572265625,871.3968811035156,170.6572265625L876.8968811035156,170.6572265625" marker-end="url(#arrowhead4381)" style="fill:none"></path><defs><marker id="arrowhead4381" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M668.553000339556,187.55006760029818L661.5708596579633,189.25542742733182C654.5887189763706,190.96078725436544,640.6244376131854,194.37150690843274,605.5960729732593,196.07686673546638C570.5677083333334,197.7822265625,514.4752604166666,197.7822265625,457.0494791666667,197.7822265625C399.6236979166667,197.7822265625,340.8645833333333,197.7822265625,294.7731119791667,197.7822265625C248.681640625,197.7822265625,215.2578125,197.7822265625,183.16731770833334,197.7822265625C151.07682291666666,197.7822265625,120.31966145833333,197.7822265625,98.45588480097052,189.0947265625C76.5921081436077,180.4072265625,63.621716287215406,163.0322265625,57.136520359019265,154.3447265625L50.65132443082312,145.6572265625" marker-end="url(#arrowhead4382)" style="fill:none"></path><defs><marker id="arrowhead4382" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(282.10546875,31.021484375)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000133514404297" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(282.10546875,119.521484375)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000133514404297" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(843.8968811035156,170.6572265625)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000133514404297" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(282.10546875,197.7822265625)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000133514404297" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5868" transform="translate(36.28125,126.4072265625)"><rect rx="0" ry="0" x="-28.28125" y="-19.25" width="56.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5869" transform="translate(181.833984375,75.271484375)"><polygon points="67.271484375,0 134.54296875,-67.271484375 67.271484375,-134.54296875 0,-67.271484375" transform="translate(-67.271484375,67.271484375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-45.49609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i&gt;maxReach?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5871" transform="translate(458.3828125,31.021484375)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5873" transform="translate(458.3828125,119.521484375)"><rect rx="0" ry="0" x="-143.27734375" y="-19.25" width="286.5546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-133.27734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">maxReach=max(maxReach,i+nums[i])</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5875" transform="translate(731.2785186767578,170.6572265625)"><polygon points="79.618359375,0 159.23671875,-79.618359375 79.618359375,-159.23671875 0,-79.618359375" transform="translate(-79.618359375,79.618359375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-59.21484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">maxReach≥末尾?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-5877" transform="translate(926.8187561035156,170.6572265625)"><rect rx="0" ry="0" x="-49.921875" y="-19.25" width="99.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return true</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean canJump(int[] nums) {        int maxReach = 0;        for (int i = 0; i &lt; nums.length; i++) {            if (i &gt; maxReach) return false;            maxReach = Math.max(maxReach, i + nums[i]);        }        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="45.-%E8%B7%B3%E8%B7%83%E6%B8%B8%E6%88%8F-ii" tabindex="-1"><a href="https://leetcode.cn/problems/jump-game-ii/" target="_blank">45. 跳跃游戏 II</a></h2><blockquote><p>保证能到末尾，求最少跳跃次数。</p></blockquote><p><strong>思路脉络</strong>：BFS 思想的贪心。维护当前步覆盖范围 <code>[l,r]</code>，下一步能到的最远是范围内所有 <code>i+nums[i]</code> 的 max。每跳一步更新边界、步数+1，直到覆盖末尾。和热题 100 相同。</p><div class="mermaid"><svg id="render678602476" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="116.9429702758789" style="max-width: 898.6828002929688px;" viewBox="0 0 898.6828002929688 116.9429702758789"><style>#render678602476 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render678602476 .error-icon{fill:#552222;}#render678602476 .error-text{fill:#552222;stroke:#552222;}#render678602476 .edge-thickness-normal{stroke-width:2px;}#render678602476 .edge-thickness-thick{stroke-width:3.5px;}#render678602476 .edge-pattern-solid{stroke-dasharray:0;}#render678602476 .edge-pattern-dashed{stroke-dasharray:3;}#render678602476 .edge-pattern-dotted{stroke-dasharray:2;}#render678602476 .marker{fill:#333333;stroke:#333333;}#render678602476 .marker.cross{stroke:#333333;}#render678602476 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render678602476 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render678602476 .cluster-label text{fill:#333;}#render678602476 .cluster-label span{color:#333;}#render678602476 .label text,#render678602476 span{fill:#333;color:#333;}#render678602476 .node rect,#render678602476 .node circle,#render678602476 .node ellipse,#render678602476 .node polygon,#render678602476 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render678602476 .node .label{text-align:center;}#render678602476 .node.clickable{cursor:pointer;}#render678602476 .arrowheadPath{fill:#333333;}#render678602476 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render678602476 .flowchart-link{stroke:#333333;fill:none;}#render678602476 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render678602476 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render678602476 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render678602476 .cluster text{fill:#333;}#render678602476 .cluster span{color:#333;}#render678602476 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render678602476 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M137.96875,59L142.13541666666666,59C146.30208333333334,59,154.63541666666666,59,162.96875,59C171.30208333333334,59,179.63541666666666,59,183.80208333333334,59L187.96875,59" marker-end="url(#arrowhead4410)" style="fill:none"></path><defs><marker id="arrowhead4410" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M310.980437992126,39.75L317.0266670767717,37.666666666666664C323.0728961614173,35.583333333333336,335.1653543307087,31.416666666666668,345.378250082021,29.333333333333332C355.5911458333333,27.25,363.9244791666667,27.25,368.0911458333333,27.25L372.2578125,27.25" marker-end="url(#arrowhead4411)" style="fill:none"></path><defs><marker id="arrowhead4411" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M568.046875,27.25L572.2135416666666,27.25C576.3802083333334,27.25,584.7135416666666,27.25,595.6072368695233,30.147971209496976C606.5009320723798,33.04594241899395,619.9549891447595,38.841884837987905,626.6820176809493,41.739856047484885L633.4090462171392,44.63782725698186" marker-end="url(#arrowhead4412)" style="fill:none"></path><defs><marker id="arrowhead4412" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M718.432810974121,59.5L723.8494784037272,59.416666666666664C729.2661458333332,59.333333333333336,740.0994806925455,59.166666666666664,751.0161481221517,59.083333333333336C761.9328155517578,59,772.9328155517578,59,778.4328155517578,59L783.9328155517578,59" marker-end="url(#arrowhead4413)" style="fill:none"></path><defs><marker id="arrowhead4413" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M633.4090483607463,74.36217183486727L626.6820194672886,77.09347652905605C619.9549905738309,79.82478122324484,606.5009327869154,85.28739061162243,579.2914820184577,88.0186953058112C552.08203125,90.75,511.1171875,90.75,470.15234375,90.75C429.1875,90.75,388.22265625,90.75,361.69400529035437,88.66666666666667C335.1653543307087,86.58333333333333,323.0728961614173,82.41666666666667,317.0266670767717,80.33333333333333L310.980437992126,78.25" marker-end="url(#arrowhead4414)" style="fill:none"></path><defs><marker id="arrowhead4414" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(750.9328155517578,59)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000747680664062" height="18.500865936279297"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(470.15234375,90.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000747680664062" height="18.500865936279297"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5890" transform="translate(72.984375,59)"><rect rx="0" ry="0" x="-64.984375" y="-19.25" width="129.96875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-54.984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=0,steps=0</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5891" transform="translate(255.11328125,59)"><rect rx="0" ry="0" x="-67.14453125" y="-19.25" width="134.2890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-57.14453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">扫l..r算nextMax</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5893" transform="translate(470.15234375,27.25)"><rect rx="0" ry="0" x="-97.89453125" y="-19.25" width="195.7890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-87.89453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=r+1,r=nextMax,steps++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5895" transform="translate(667.9898452758789,59)"><polygon points="49.94296875,0 99.8859375,-49.94296875 49.94296875,-99.8859375 0,-49.94296875" transform="translate(-49.94296875,49.94296875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-26.2421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r≥末尾?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5897" transform="translate(837.3078155517578,59)"><rect rx="0" ry="0" x="-53.375" y="-19.25" width="106.75" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return steps</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int jump(int[] nums) {        int steps = 0, l = 0, r = 0;        while (r &lt; nums.length - 1) {            int nextMax = 0;            for (int i = l; i &lt;= r; i++) nextMax = Math.max(nextMax, i + nums[i]);            l = r + 1; r = nextMax; steps++;        }        return steps;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="274.-h-%E6%8C%87%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/h-index/" target="_blank">274. H 指数</a></h2><blockquote><p>学者有 <code>h</code> 篇论文被引至少 <code>h</code> 次，求最大 <code>h</code>。</p></blockquote><p><strong>思路脉络</strong>：排序后从高引到低引遍历，<code>h</code> 是&quot;至少有 h 篇被引 ≥ h 次&quot;。排序后第 <code>i</code> 篇（从0起）引文 <code>citations[i]</code>，若 <code>citations[i] &gt;= n-i</code> 则至少有 <code>n-i</code> 篇 ≥ 它，<code>h</code> 候选是 <code>n-i</code>。从后往前找第一个满足的，或正向遍历取 <code>min(citations[i], n-i)</code> 的最大值。</p><div class="mermaid"><svg id="render1291847162" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 612.703125px;" viewBox="0 0 612.703125 89.5"><style>#render1291847162 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1291847162 .error-icon{fill:#552222;}#render1291847162 .error-text{fill:#552222;stroke:#552222;}#render1291847162 .edge-thickness-normal{stroke-width:2px;}#render1291847162 .edge-thickness-thick{stroke-width:3.5px;}#render1291847162 .edge-pattern-solid{stroke-dasharray:0;}#render1291847162 .edge-pattern-dashed{stroke-dasharray:3;}#render1291847162 .edge-pattern-dotted{stroke-dasharray:2;}#render1291847162 .marker{fill:#333333;stroke:#333333;}#render1291847162 .marker.cross{stroke:#333333;}#render1291847162 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1291847162 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1291847162 .cluster-label text{fill:#333;}#render1291847162 .cluster-label span{color:#333;}#render1291847162 .label text,#render1291847162 span{fill:#333;color:#333;}#render1291847162 .node rect,#render1291847162 .node circle,#render1291847162 .node ellipse,#render1291847162 .node polygon,#render1291847162 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1291847162 .node .label{text-align:center;}#render1291847162 .node.clickable{cursor:pointer;}#render1291847162 .arrowheadPath{fill:#333333;}#render1291847162 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1291847162 .flowchart-link{stroke:#333333;fill:none;}#render1291847162 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1291847162 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1291847162 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1291847162 .cluster text{fill:#333;}#render1291847162 .cluster span{color:#333;}#render1291847162 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1291847162 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M121.9453125,35.52257564927329L126.11197916666667,34.14381304106107C130.27864583333334,32.76505043284886,138.61197916666666,30.00752521642443,146.9453125,28.628762608212213C155.27864583333334,27.25,163.61197916666666,27.25,167.77864583333334,27.25L171.9453125,27.25" marker-end="url(#arrowhead4437)" style="fill:none"></path><defs><marker id="arrowhead4437" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M228.5078125,27.25L232.67447916666666,27.25C236.84114583333334,27.25,245.17447916666666,27.25,253.5078125,27.25C261.8411458333333,27.25,270.1744791666667,27.25,274.3411458333333,27.25L278.5078125,27.25" marker-end="url(#arrowhead4438)" style="fill:none"></path><defs><marker id="arrowhead4438" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M513.359375,27.25L517.5260416666666,27.25C521.6927083333334,27.25,530.0260416666666,27.25,538.359375,29.724626525259435C546.6927083333334,32.19925305051887,555.0260416666666,37.148506101037746,559.1927083333334,39.62313262629718L563.359375,42.097759151556616" marker-end="url(#arrowhead4439)" style="fill:none"></path><defs><marker id="arrowhead4439" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M563.359375,66.65224084844338L559.1927083333334,69.12686737370282C555.0260416666666,71.60149389896226,546.6927083333334,76.55074694948114,518.7884114583334,79.02537347474056C490.8841145833333,81.5,443.4088541666667,81.5,395.93359375,81.5C348.4583333333333,81.5,300.9830729166667,81.5,268.365234375,81.5C235.74739583333334,81.5,217.98697916666666,81.5,200.2265625,81.5C182.46614583333334,81.5,164.70572916666666,81.5,151.65885416666666,80.12123739178779C138.61197916666666,78.74247478357557,130.27864583333334,75.98494956715113,126.11197916666667,74.60618695893892L121.9453125,73.22742435072671" marker-end="url(#arrowhead4440)" style="fill:none"></path><defs><marker id="arrowhead4440" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5908" transform="translate(64.97265625,54.375)"><rect rx="0" ry="0" x="-56.97265625" y="-19.25" width="113.9453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-46.97265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">排序citations</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5909" transform="translate(200.2265625,27.25)"><rect rx="0" ry="0" x="-28.28125" y="-19.25" width="56.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5911" transform="translate(395.93359375,27.25)"><rect rx="0" ry="0" x="-117.42578125" y="-19.25" width="234.8515625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-107.42578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">h=max(h,min(citations[i],n-i))</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5913" transform="translate(584.03125,54.375)"><rect rx="0" ry="0" x="-20.671875" y="-19.25" width="41.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-10.671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int hIndex(int[] citations) {        Arrays.sort(citations);        int h = 0, n = citations.length;        for (int i = 0; i &lt; n; i++) {            h = Math.max(h, Math.min(citations[i], n - i));        }        return h;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 进阶<br />计数排序可 O(n)：用 <code>cnt[i]</code> 记引文恰好 <code>i</code> 的篇数，超过 <code>n</code> 的计入 <code>cnt[n]</code>，从后往前累加。</p></blockquote><h2 id="380.-o(1)-%E6%97%B6%E9%97%B4%E6%8F%92%E5%85%A5%E3%80%81%E5%88%A0%E9%99%A4%E5%92%8C%E8%8E%B7%E5%8F%96%E9%9A%8F%E6%9C%BA%E5%85%83%E7%B4%A0" tabindex="-1"><a href="https://leetcode.cn/problems/insert-delete-getrandom-o1/" target="_blank">380. O(1) 时间插入、删除和获取随机元素</a></h2><blockquote><p>设计 <code>insert</code>/<code>remove</code>/<code>getRandom</code> 全 O(1) 的数据结构。</p></blockquote><p><strong>思路脉络</strong>：<code>ArrayList</code> 支持下标随机访问 O(1) 但按值删 O(n)；<code>HashSet</code> 插删 O(1) 但无法随机访问。两者结合：<code>List</code> 存值、<code>Map</code> 存值→下标。删除时把待删元素和末尾元素交换再删末尾，O(1)。</p><div class="mermaid"><svg id="render1898381784" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="231.5" style="max-width: 552.6828002929688px;" viewBox="0 0 552.6828002929688 231.5"><style>#render1898381784 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1898381784 .error-icon{fill:#552222;}#render1898381784 .error-text{fill:#552222;stroke:#552222;}#render1898381784 .edge-thickness-normal{stroke-width:2px;}#render1898381784 .edge-thickness-thick{stroke-width:3.5px;}#render1898381784 .edge-pattern-solid{stroke-dasharray:0;}#render1898381784 .edge-pattern-dashed{stroke-dasharray:3;}#render1898381784 .edge-pattern-dotted{stroke-dasharray:2;}#render1898381784 .marker{fill:#333333;stroke:#333333;}#render1898381784 .marker.cross{stroke:#333333;}#render1898381784 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1898381784 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1898381784 .cluster-label text{fill:#333;}#render1898381784 .cluster-label span{color:#333;}#render1898381784 .label text,#render1898381784 span{fill:#333;color:#333;}#render1898381784 .node rect,#render1898381784 .node circle,#render1898381784 .node ellipse,#render1898381784 .node polygon,#render1898381784 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1898381784 .node .label{text-align:center;}#render1898381784 .node.clickable{cursor:pointer;}#render1898381784 .arrowheadPath{fill:#333333;}#render1898381784 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1898381784 .flowchart-link{stroke:#333333;fill:none;}#render1898381784 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1898381784 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1898381784 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1898381784 .cluster text{fill:#333;}#render1898381784 .cluster span{color:#333;}#render1898381784 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1898381784 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M60,115.75L64.16666666666667,115.75C68.33333333333333,115.75,76.66666666666667,115.75,85.0833335876465,115.83333333333333C93.50000050862631,115.91666666666667,102.00000101725261,116.08333333333333,106.25000127156578,116.16666666666667L110.50000152587893,116.25000000000001" marker-end="url(#arrowhead4457)" style="fill:none"></path><defs><marker id="arrowhead4457" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M177.75860297245845,96.77110144657959L191.78373215234168,85.184251205483C205.8088613322249,73.5974009643864,233.85911969199137,50.4237004821932,264.7214884552079,38.8368502410966C295.5838572184245,27.25,329.2583363850911,27.25,346.0955759684245,27.25L362.9328155517578,27.25" marker-end="url(#arrowhead4458)" style="fill:none"></path><defs><marker id="arrowhead4458" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M197.2374984741211,116.24999999999999L208.0161450703939,116.16666666666664C218.7947916666667,116.08333333333333,240.35208485921225,115.91666666666667,261.9927106221517,115.83333333333333C283.6333363850911,115.75,305.3572947184245,115.75,316.2192738850911,115.75L327.0812530517578,115.75" marker-end="url(#arrowhead4459)" style="fill:none"></path><defs><marker id="arrowhead4459" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-E" style="opacity: 1;" id="L-B-E"><path class="path" d="M177.75860129138533,135.72889718273578L191.7837307514474,147.14908098561315C205.80886021150948,158.56926478849053,233.85911913163363,181.40963239424525,266.84258192502904,192.8298161971226C299.8260447184245,204.25,337.7427113850911,204.25,356.7010447184245,204.25L375.6593780517578,204.25" marker-end="url(#arrowhead4460)" style="fill:none"></path><defs><marker id="arrowhead4460" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(261.9093780517578,27.25)"><g transform="translate(-20.53515625,-9.25)" class="label"><rect rx="0" ry="0" width="41.06272888183594" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">insert</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(261.9093780517578,115.75)"><g transform="translate(-26.6875,-9.25)" class="label"><rect rx="0" ry="0" width="53.35967254638672" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">remove</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(261.9093780517578,204.25)"><g transform="translate(-40.171875,-9.25)" class="label"><rect rx="0" ry="0" width="80.32076263427734" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">getRandom</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5924" transform="translate(34,115.75)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">操作</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5925" transform="translate(153.3687515258789,115.75)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">类型?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5927" transform="translate(435.8820343017578,27.25)"><rect rx="0" ry="0" x="-72.94921875" y="-19.25" width="145.8984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-62.94921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">vals.add,map.put</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5929" transform="translate(435.8820343017578,115.75)"><rect rx="0" ry="0" x="-108.80078125" y="-19.25" width="217.6015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-98.80078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">交换末尾,删尾,map.remove</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5931" transform="translate(435.8820343017578,204.25)"><rect rx="0" ry="0" x="-60.22265625" y="-19.25" width="120.4453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.22265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">vals.get(rand)</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0380-randomized-set.gif" alt="" /></p><pre><code class="language-java">class RandomizedSet {    List&lt;Integer&gt; vals = new ArrayList&lt;&gt;();    Map&lt;Integer, Integer&gt; idx = new HashMap&lt;&gt;();    Random rand = new Random();    public boolean insert(int val) {        if (idx.containsKey(val)) return false;        idx.put(val, vals.size());        vals.add(val);        return true;    }    public boolean remove(int val) {        if (!idx.containsKey(val)) return false;        int i = idx.get(val), last = vals.get(vals.size() - 1);        vals.set(i, last); idx.put(last, i);   // 末尾填到待删位        vals.remove(vals.size() - 1); idx.remove(val);        return true;    }    public int getRandom() { return vals.get(rand.nextInt(vals.size())); }}</code></pre><blockquote><p>[!info] 复杂度<br />三操作均 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />删除时必须先把 <code>last</code> 填到位置 <code>i</code> 再删末尾。若 <code>i</code> 本来就是末尾，交换逻辑也对（自己换自己），但 <code>idx.remove(val)</code> 要在 <code>idx.put(last,i)</code> 之后，否则 <code>last==val</code> 时会先把 key 删了。</p></blockquote><h2 id="238.-%E9%99%A4%E8%87%AA%E8%BA%AB%E4%BB%A5%E5%A4%96%E6%95%B0%E7%BB%84%E7%9A%84%E4%B9%98%E7%A7%AF" tabindex="-1"><a href="https://leetcode.cn/problems/product-of-array-except-self/" target="_blank">238. 除自身以外数组的乘积</a></h2><blockquote><p>返回每位置&quot;除自身外其他元素乘积&quot;，不用除法，O(n)。</p></blockquote><p><strong>思路脉络</strong>：<code>res[i] = 左侧乘积 × 右侧乘积</code>。两趟扫描：先从左累乘把左乘积填进 <code>res</code>，再从右用一个变量累乘右乘积乘进去。和热题 100 相同。</p><div class="mermaid"><svg id="render908027622" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 803.3125px;" viewBox="0 0 803.3125 54.5"><style>#render908027622 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render908027622 .error-icon{fill:#552222;}#render908027622 .error-text{fill:#552222;stroke:#552222;}#render908027622 .edge-thickness-normal{stroke-width:2px;}#render908027622 .edge-thickness-thick{stroke-width:3.5px;}#render908027622 .edge-pattern-solid{stroke-dasharray:0;}#render908027622 .edge-pattern-dashed{stroke-dasharray:3;}#render908027622 .edge-pattern-dotted{stroke-dasharray:2;}#render908027622 .marker{fill:#333333;stroke:#333333;}#render908027622 .marker.cross{stroke:#333333;}#render908027622 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render908027622 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render908027622 .cluster-label text{fill:#333;}#render908027622 .cluster-label span{color:#333;}#render908027622 .label text,#render908027622 span{fill:#333;color:#333;}#render908027622 .node rect,#render908027622 .node circle,#render908027622 .node ellipse,#render908027622 .node polygon,#render908027622 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render908027622 .node .label{text-align:center;}#render908027622 .node.clickable{cursor:pointer;}#render908027622 .arrowheadPath{fill:#333333;}#render908027622 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render908027622 .flowchart-link{stroke:#333333;fill:none;}#render908027622 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render908027622 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render908027622 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render908027622 .cluster text{fill:#333;}#render908027622 .cluster span{color:#333;}#render908027622 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render908027622 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M124,27.25L128.16666666666666,27.25C132.33333333333334,27.25,140.66666666666666,27.25,149,27.25C157.33333333333334,27.25,165.66666666666666,27.25,169.83333333333334,27.25L174,27.25" marker-end="url(#arrowhead4477)" style="fill:none"></path><defs><marker id="arrowhead4477" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M365.8203125,27.25L369.9869791666667,27.25C374.1536458333333,27.25,382.4869791666667,27.25,390.8203125,27.25C399.1536458333333,27.25,407.4869791666667,27.25,411.6536458333333,27.25L415.8203125,27.25" marker-end="url(#arrowhead4478)" style="fill:none"></path><defs><marker id="arrowhead4478" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M531.8203125,27.25L535.9869791666666,27.25C540.1536458333334,27.25,548.4869791666666,27.25,556.8203125,27.25C565.1536458333334,27.25,573.4869791666666,27.25,577.6536458333334,27.25L581.8203125,27.25" marker-end="url(#arrowhead4479)" style="fill:none"></path><defs><marker id="arrowhead4479" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5938" transform="translate(66,27.25)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">第一趟左→右</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5939" transform="translate(269.91015625,27.25)"><rect rx="0" ry="0" x="-95.91015625" y="-19.25" width="191.8203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-85.91015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">res[i]=res[i-1]*nums[i-1]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5941" transform="translate(473.8203125,27.25)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">第二趟右→左</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5943" transform="translate(688.56640625,27.25)"><rect rx="0" ry="0" x="-106.74609375" y="-19.25" width="213.4921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-96.74609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">res[i]*=right,right*=nums[i]</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int[] productExceptSelf(int[] nums) {        int n = nums.length, res[] = new int[n];        res[0] = 1;        for (int i = 1; i &lt; n; i++) res[i] = res[i - 1] * nums[i - 1];        int right = 1;        for (int i = n - 1; i &gt;= 0; i--) { res[i] *= right; right *= nums[i]; }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="134.-%E5%8A%A0%E6%B2%B9%E7%AB%99" tabindex="-1"><a href="https://leetcode.cn/problems/gas-station/" target="_blank">134. 加油站</a></h2><blockquote><p>环形路上 <code>n</code> 个加油站，<code>gas[i]</code> 可加、<code>cost[i]</code> 耗，求能跑完一圈的起点下标，保证唯一。</p></blockquote><p><strong>思路脉络</strong>：总油量 <code>&gt;=</code> 总耗量则一定有解（题目保证唯一）。关键是找起点：从 0 开始累计 <code>gas-cost</code>，一旦累和 <code>&lt; 0</code>，说明 <code>0..i</code> 这段都不能作起点（油不够撑到 <code>i</code>），从 <code>i+1</code> 重新开始。最后看总剩油 <code>&gt;= 0</code> 则返回记录的起点。</p><div class="mermaid"><svg id="render1257000673" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="186.4796905517578" style="max-width: 795.9312744140625px;" viewBox="0 0.000003814697265625 795.9312744140625 186.4796905517578"><style>#render1257000673 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1257000673 .error-icon{fill:#552222;}#render1257000673 .error-text{fill:#552222;stroke:#552222;}#render1257000673 .edge-thickness-normal{stroke-width:2px;}#render1257000673 .edge-thickness-thick{stroke-width:3.5px;}#render1257000673 .edge-pattern-solid{stroke-dasharray:0;}#render1257000673 .edge-pattern-dashed{stroke-dasharray:3;}#render1257000673 .edge-pattern-dotted{stroke-dasharray:2;}#render1257000673 .marker{fill:#333333;stroke:#333333;}#render1257000673 .marker.cross{stroke:#333333;}#render1257000673 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1257000673 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1257000673 .cluster-label text{fill:#333;}#render1257000673 .cluster-label span{color:#333;}#render1257000673 .label text,#render1257000673 span{fill:#333;color:#333;}#render1257000673 .node rect,#render1257000673 .node circle,#render1257000673 .node ellipse,#render1257000673 .node polygon,#render1257000673 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1257000673 .node .label{text-align:center;}#render1257000673 .node.clickable{cursor:pointer;}#render1257000673 .arrowheadPath{fill:#333333;}#render1257000673 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1257000673 .flowchart-link{stroke:#333333;fill:none;}#render1257000673 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1257000673 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1257000673 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1257000673 .cluster text{fill:#333;}#render1257000673 .cluster span{color:#333;}#render1257000673 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1257000673 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M126.6796875,62.19457540692218L141.19921875,57.812667132925064C155.71875,53.43075885892794,184.7578125,44.6669423109337,203.44401041666666,40.28503403693657C222.13020833333334,35.90312576293945,230.46354166666666,35.90312576293945,234.63020833333334,35.90312576293945L238.796875,35.90312576293945" marker-end="url(#arrowhead4508)" style="fill:none"></path><defs><marker id="arrowhead4508" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M414.8828125,35.90312576293945L419.0494791666667,35.90312576293945C423.2161458333333,35.90312576293945,431.5494791666667,35.90312576293945,442.6779668804836,38.566304588632605C453.80645459430053,41.229483414325756,467.7300966886011,46.55584106571206,474.69191773575136,49.2190198914052L481.6537387829017,51.882198717098355" marker-end="url(#arrowhead4509)" style="fill:none"></path><defs><marker id="arrowhead4509" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M584.6890617370605,68.15312576293947L590.1057287851969,68.06979242960612C595.5223958333332,67.9864590962728,606.3557299296061,67.81979242960612,617.2723969777425,67.902562111615C628.1890640258789,67.98533179362387,639.1890640258789,68.31753782430827,644.6890640258789,68.48364083965049L650.1890640258789,68.64974385499269" marker-end="url(#arrowhead4510)" style="fill:none"></path><defs><marker id="arrowhead4510" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M481.65373989258336,84.42405239258338L474.6919186604861,86.92056462097605C467.73009742838894,89.41707684936874,453.8064549641945,94.41010130615409,428.00413894043055,96.90661353454676C402.2018229166667,99.40312576293945,364.5208333333333,99.40312576293945,326.83984375,99.40312576293945C289.1588541666667,99.40312576293945,251.47786458333334,99.40312576293945,218.11783854166666,95.79540909617428C184.7578125,92.18769242940908,155.71875,84.97225909587873,141.19921875,81.36454242911354L126.6796875,77.75682576234836" marker-end="url(#arrowhead4511)" style="fill:none"></path><defs><marker id="arrowhead4511" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M697.6461792099252,89.97968864440918L684.2366600125843,102.0341157913208C670.8271408152431,114.08854293823242,644.008102420561,138.19739723205566,615.1563955960634,150.25182437896729C586.3046887715658,162.3062515258789,555.4203135172526,162.3062515258789,525.8692715962728,162.3062515258789C496.31822967529297,162.3062515258789,468.1005210876465,162.3062515258789,435.15117200215656,162.3062515258789C402.2018229166667,162.3062515258789,364.5208333333333,162.3062515258789,326.83984375,162.3062515258789C289.1588541666667,162.3062515258789,251.47786458333334,162.3062515258789,217.44721617657981,150.25182437896729C183.41656776982632,138.19739723205566,153.03626053965266,114.08854293823242,137.84610692456582,102.0341157913208L122.655953309479,89.97968864440918" marker-end="url(#arrowhead4512)" style="fill:none"></path><defs><marker id="arrowhead4512" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(617.1890640258789,67.65312576293945)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000268936157227" height="18.50031089782715"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(326.83984375,99.40312576293945)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000268936157227" height="18.50031089782715"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5955" transform="translate(98.3984375,70.72968864440918)"><rect rx="0" ry="0" x="-28.28125" y="-19.25" width="56.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5956" transform="translate(326.83984375,35.90312576293945)"><rect rx="0" ry="0" x="-88.04296875" y="-19.25" width="176.0859375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-78.04296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">total+=diff,tank+=diff</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5958" transform="translate(524.5359382629395,67.65312576293945)"><polygon points="59.653125,0 119.30625,-59.653125 59.653125,-119.30625 0,-59.653125" transform="translate(-59.653125,59.653125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-37.03125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">tank&amp;lt;0?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5960" transform="translate(719.0601577758789,70.72968864440918)"><rect rx="0" ry="0" x="-68.87109375" y="-19.25" width="137.7421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.87109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">start=i+1,tank=0</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-5965" transform="translate(98.3984375,159.22968864440918)"><rect rx="0" ry="0" x="-90.3984375" y="-19.25" width="180.796875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.3984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return total≥0?start:-1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int canCompleteCircuit(int[] gas, int[] cost) {        int total = 0, tank = 0, start = 0;        for (int i = 0; i &lt; gas.length; i++) {            int diff = gas[i] - cost[i];            total += diff; tank += diff;            if (tank &lt; 0) { start = i + 1; tank = 0; } // 当前段作起点不行，重置        }        return total &gt;= 0 ? start : -1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />&quot;一旦 <code>tank &lt; 0</code> 就从 <code>i+1</code> 重起&quot;的正确性：若 <code>0..i</code> 段从任一 <code>j</code> 出发，走到 <code>i</code> 时累和必 <code>&lt; 0</code>（因为从 0 出发累到 <code>j</code> 是正的，从 <code>j</code> 出发少带这部分正油，更早见底）。所以整段都淘汰。</p></blockquote><h2 id="135.-%E5%88%86%E5%8F%91%E7%B3%96%E6%9E%9C" tabindex="-1"><a href="https://leetcode.cn/problems/candy/" target="_blank">135. 分发糖果</a></h2><blockquote><p><code>n</code> 个孩子 rating 不同，相邻中 rating 高的糖果更多，求最少糖果总数。</p></blockquote><p><strong>思路脉络</strong>：每人至少 1 颗。两次遍历：从左扫保证比左邻 rating 高的多一颗；从右扫保证比右邻高的多一颗，取两次的 max。两次取 max 同时满足左右约束。</p><div class="mermaid"><svg id="render1282183797" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 715.03125px;" viewBox="0 0 715.03125 54.5"><style>#render1282183797 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1282183797 .error-icon{fill:#552222;}#render1282183797 .error-text{fill:#552222;stroke:#552222;}#render1282183797 .edge-thickness-normal{stroke-width:2px;}#render1282183797 .edge-thickness-thick{stroke-width:3.5px;}#render1282183797 .edge-pattern-solid{stroke-dasharray:0;}#render1282183797 .edge-pattern-dashed{stroke-dasharray:3;}#render1282183797 .edge-pattern-dotted{stroke-dasharray:2;}#render1282183797 .marker{fill:#333333;stroke:#333333;}#render1282183797 .marker.cross{stroke:#333333;}#render1282183797 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1282183797 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1282183797 .cluster-label text{fill:#333;}#render1282183797 .cluster-label span{color:#333;}#render1282183797 .label text,#render1282183797 span{fill:#333;color:#333;}#render1282183797 .node rect,#render1282183797 .node circle,#render1282183797 .node ellipse,#render1282183797 .node polygon,#render1282183797 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1282183797 .node .label{text-align:center;}#render1282183797 .node.clickable{cursor:pointer;}#render1282183797 .arrowheadPath{fill:#333333;}#render1282183797 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1282183797 .flowchart-link{stroke:#333333;fill:none;}#render1282183797 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1282183797 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1282183797 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1282183797 .cluster text{fill:#333;}#render1282183797 .cluster span{color:#333;}#render1282183797 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1282183797 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M84.390625,27.25L88.55729166666667,27.25C92.72395833333333,27.25,101.05729166666667,27.25,109.390625,27.25C117.72395833333333,27.25,126.05729166666667,27.25,130.22395833333334,27.25L134.390625,27.25" marker-end="url(#arrowhead4529)" style="fill:none"></path><defs><marker id="arrowhead4529" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M289.046875,27.25L293.2135416666667,27.25C297.3802083333333,27.25,305.7135416666667,27.25,314.046875,27.25C322.3802083333333,27.25,330.7135416666667,27.25,334.8802083333333,27.25L339.046875,27.25" marker-end="url(#arrowhead4530)" style="fill:none"></path><defs><marker id="arrowhead4530" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M605.03125,27.25L609.1979166666666,27.25C613.3645833333334,27.25,621.6979166666666,27.25,630.03125,27.25C638.3645833333334,27.25,646.6979166666666,27.25,650.8645833333334,27.25L655.03125,27.25" marker-end="url(#arrowhead4531)" style="fill:none"></path><defs><marker id="arrowhead4531" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5972" transform="translate(46.1953125,27.25)"><rect rx="0" ry="0" x="-38.1953125" y="-19.25" width="76.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-28.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">每人1颗</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5973" transform="translate(211.71875,27.25)"><rect rx="0" ry="0" x="-77.328125" y="-19.25" width="154.65625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-67.328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左扫:比左邻高则+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5975" transform="translate(472.0390625,27.25)"><rect rx="0" ry="0" x="-132.9921875" y="-19.25" width="265.984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-122.9921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">右扫:比右邻高则max(当前,右邻+1)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5977" transform="translate(681.03125,27.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">求和</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0135-candy.gif" alt="" /></p><pre><code class="language-java">class Solution {    public int candy(int[] ratings) {        int n = ratings.length;        int[] candies = new int[n];        Arrays.fill(candies, 1);        for (int i = 1; i &lt; n; i++)           // 比左邻高则 +1            if (ratings[i] &gt; ratings[i - 1]) candies[i] = candies[i - 1] + 1;        for (int i = n - 2; i &gt;= 0; i--)      // 比右邻高则 max(+1)            if (ratings[i] &gt; ratings[i + 1]) candies[i] = Math.max(candies[i], candies[i + 1] + 1);        int sum = 0;        for (int c : candies) sum += c;        return sum;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点<br />从右扫时必须取 <code>max</code>，不能直接 <code>= candies[i+1]+1</code>——否则会破坏从左扫建立的左约束。比如 <code>[1,2,2,1]</code>，从右扫到 index1 时若直接覆盖会丢掉左扫的值。</p></blockquote><h2 id="42.-%E6%8E%A5%E9%9B%A8%E6%B0%B4" tabindex="-1"><a href="https://leetcode.cn/problems/trapping-rain-water/" target="_blank">42. 接雨水</a></h2><blockquote><p>柱状图接雨水，求能接住的水量。</p></blockquote><p><strong>思路脉络</strong>：每列水量 = <code>min(leftMax, rightMax) - height[i]</code>。双指针 O(1) 空间：哪边矮处理哪边，瓶颈在矮侧 max。和热题 100 相同。</p><div class="mermaid"><svg id="render3814666623" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="204.46484375" style="max-width: 387.70703125px;" viewBox="0 0 387.70703125 204.46484375"><style>#render3814666623 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3814666623 .error-icon{fill:#552222;}#render3814666623 .error-text{fill:#552222;stroke:#552222;}#render3814666623 .edge-thickness-normal{stroke-width:2px;}#render3814666623 .edge-thickness-thick{stroke-width:3.5px;}#render3814666623 .edge-pattern-solid{stroke-dasharray:0;}#render3814666623 .edge-pattern-dashed{stroke-dasharray:3;}#render3814666623 .edge-pattern-dotted{stroke-dasharray:2;}#render3814666623 .marker{fill:#333333;stroke:#333333;}#render3814666623 .marker.cross{stroke:#333333;}#render3814666623 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3814666623 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3814666623 .cluster-label text{fill:#333;}#render3814666623 .cluster-label span{color:#333;}#render3814666623 .label text,#render3814666623 span{fill:#333;color:#333;}#render3814666623 .node rect,#render3814666623 .node circle,#render3814666623 .node ellipse,#render3814666623 .node polygon,#render3814666623 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3814666623 .node .label{text-align:center;}#render3814666623 .node.clickable{cursor:pointer;}#render3814666623 .arrowheadPath{fill:#333333;}#render3814666623 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3814666623 .flowchart-link{stroke:#333333;fill:none;}#render3814666623 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3814666623 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3814666623 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3814666623 .cluster text{fill:#333;}#render3814666623 .cluster span{color:#333;}#render3814666623 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3814666623 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M68.37556535480554,99.08984375L76.64500237900462,90.1865234375C84.9144394032037,81.283203125,101.45331345160184,63.4765625,113.97275047580092,54.656575520833336C126.4921875,45.836588541666664,134.9921875,46.003255208333336,139.2421875,46.086588541666664L143.4921875,46.169921875" marker-end="url(#arrowhead4554)" style="fill:none"></path><defs><marker id="arrowhead4554" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M218.83203125,46.169921875L224.24869791666666,46.086588541666664C229.66536458333334,46.003255208333336,240.49869791666666,45.836588541666664,252.2386720814744,48.600748697916664C263.97864624628204,51.364908854166664,276.62526124256414,57.059895833333336,282.9485687407052,59.907389322916664L289.2718762388462,62.7548828125" marker-end="url(#arrowhead4555)" style="fill:none"></path><defs><marker id="arrowhead4555" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M196.58671593411918,68.41523719088083L205.71093515343264,81.61100495073403C214.83515437274613,94.80677271058721,233.08359281137305,121.1983082302936,248.00208286401985,136.52148449130868C262.9205729166667,151.8446607523238,274.5091145833333,156.09947775464755,280.3033854166667,158.22688625580943L286.09765625,160.35429475697134" marker-end="url(#arrowhead4556)" style="fill:none"></path><defs><marker id="arrowhead4556" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M289.2718762388462,101.2548828125L282.9485687407052,104.10237630208333C276.62526124256414,106.94986979166667,263.97864624628204,112.64485677083333,245.87701843564105,115.49235026041667C227.775390625,118.33984375,204.21875,118.33984375,181.99544270833334,118.33984375C159.77213541666666,118.33984375,138.88216145833334,118.33984375,124.2705078125,118.33984375C109.65885416666667,118.33984375,101.32552083333333,118.33984375,97.15885416666667,118.33984375L92.9921875,118.33984375" marker-end="url(#arrowhead4557)" style="fill:none"></path><defs><marker id="arrowhead4557" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M286.09765625,185.5384059111154L280.3033854166667,186.5886455509295C274.5091145833333,187.63888519074362,262.9205729166667,189.7393644703718,245.34798177083334,190.7896041101859C227.775390625,191.83984375,204.21875,191.83984375,181.99544270833334,191.83984375C159.77213541666666,191.83984375,138.88216145833334,191.83984375,120.1340835813492,182.79817708333334C101.38600570436508,173.75651041666666,84.77982390873017,155.67317708333334,76.4767330109127,146.63151041666666L68.17364211309524,137.58984375" marker-end="url(#arrowhead4558)" style="fill:none"></path><defs><marker id="arrowhead4558" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(251.33203125,45.669921875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000307083129883" height="18.500354766845703"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(251.33203125,147.58984375)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000307083129883" height="18.500354766845703"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-5988" transform="translate(50.49609375,118.33984375)"><rect rx="0" ry="0" x="-42.49609375" y="-19.25" width="84.9921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.49609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-5989" transform="translate(180.662109375,45.669921875)"><polygon points="37.669921875,0 75.33984375,-37.669921875 37.669921875,-75.33984375 0,-37.669921875" transform="translate(-37.669921875,37.669921875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-12.60546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">h[l]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-5991" transform="translate(332.01953125,82.0048828125)"><rect rx="0" ry="0" x="-47.6875" y="-19.25" width="95.375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.6875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">处理左:l++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-5993" transform="translate(332.01953125,177.21484375)"><rect rx="0" ry="0" x="-45.921875" y="-19.25" width="91.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-35.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">处理右:r--</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int trap(int[] height) {        int l = 0, r = height.length - 1, leftMax = 0, rightMax = 0, ans = 0;        while (l &lt; r) {            if (height[l] &lt; height[r]) {                if (height[l] &gt;= leftMax) leftMax = height[l];                else ans += leftMax - height[l];                l++;            } else {                if (height[r] &gt;= rightMax) rightMax = height[r];                else ans += rightMax - height[r];                r--;            }        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 关联<br />这题在热题 100 也选了。双指针 O(1) 空间是难点，DP 预处理两个数组是基础版。</p></blockquote><h2 id="13.-%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97%E8%BD%AC%E6%95%B4%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/roman-to-integer/" target="_blank">13. 罗马数字转整数</a></h2><blockquote><p>罗马数字字符串转整数。</p></blockquote><p><strong>思路脉络</strong>：规则是小数在大数左边是减、右边是加。遍历时若当前 <code>&lt;</code> 下一个就减当前，否则加当前。</p><div class="mermaid"><svg id="render1169799557" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="203.5085906982422" style="max-width: 347.50079345703125px;" viewBox="0 0 347.50079345703125 203.5085906982422"><style>#render1169799557 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1169799557 .error-icon{fill:#552222;}#render1169799557 .error-text{fill:#552222;stroke:#552222;}#render1169799557 .edge-thickness-normal{stroke-width:2px;}#render1169799557 .edge-thickness-thick{stroke-width:3.5px;}#render1169799557 .edge-pattern-solid{stroke-dasharray:0;}#render1169799557 .edge-pattern-dashed{stroke-dasharray:3;}#render1169799557 .edge-pattern-dotted{stroke-dasharray:2;}#render1169799557 .marker{fill:#333333;stroke:#333333;}#render1169799557 .marker.cross{stroke:#333333;}#render1169799557 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1169799557 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1169799557 .cluster-label text{fill:#333;}#render1169799557 .cluster-label span{color:#333;}#render1169799557 .label text,#render1169799557 span{fill:#333;color:#333;}#render1169799557 .node rect,#render1169799557 .node circle,#render1169799557 .node ellipse,#render1169799557 .node polygon,#render1169799557 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1169799557 .node .label{text-align:center;}#render1169799557 .node.clickable{cursor:pointer;}#render1169799557 .arrowheadPath{fill:#333333;}#render1169799557 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1169799557 .flowchart-link{stroke:#333333;fill:none;}#render1169799557 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1169799557 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1169799557 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1169799557 .cluster text{fill:#333;}#render1169799557 .cluster span{color:#333;}#render1169799557 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1169799557 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M50.48873794984065,98.13359069824219L57.00103162486721,89.30995814005534C63.51332529989376,80.48632558186848,76.53791264994688,62.83906046549479,87.30020657928658,54.098761240641274C98.06250050862631,45.35846201578776,106.56250101725261,45.52512868245443,110.81250127156578,45.60846201578776L115.06250152587893,45.691795349121094" marker-end="url(#arrowhead4581)" style="fill:none"></path><defs><marker id="arrowhead4581" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M189.4460922241211,45.691795349121094L194.8627586364746,45.60846201578776C200.27942504882813,45.52512868245443,211.11275787353517,45.35846201578776,222.38472692747277,48.08277829488119C233.65669598141037,50.80709457397461,245.36730126457851,56.422393798828125,251.22260390616262,59.23004341125488L257.0779065477467,62.03769302368164" marker-end="url(#arrowhead4582)" style="fill:none"></path><defs><marker id="arrowhead4582" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M167.46436698197644,67.67352364302354L176.54465426802074,80.83353481889331C185.62494155406503,93.9935459947631,203.78551612615362,120.31356834650263,218.3658034121979,135.6380753970027C232.9460906982422,150.96258244750274,243.9460906982422,155.29157419676326,249.4460906982422,157.45607007139355L254.9460906982422,159.62056594602382" marker-end="url(#arrowhead4583)" style="fill:none"></path><defs><marker id="arrowhead4583" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M257.0779065477467,100.53769302368164L251.22260390616262,103.3453426361084C245.36730126457851,106.15299224853516,233.65669598141037,111.76829147338867,216.10276078163943,114.57594108581543C198.5488255818685,117.38359069824219,175.15156046549478,117.38359069824219,153.08762868245444,117.38359069824219C131.02369689941406,117.38359069824219,110.29309844970703,117.38359069824219,95.76113255818684,117.38359069824219C81.22916666666667,117.38359069824219,72.89583333333333,117.38359069824219,68.72916666666667,117.38359069824219L64.5625,117.38359069824219" marker-end="url(#arrowhead4584)" style="fill:none"></path><defs><marker id="arrowhead4584" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M254.9460906982422,184.4722991202234L249.4460906982422,185.54084771655985C243.9460906982422,186.60939631289634,232.9460906982422,188.74649350556925,215.74745814005533,189.81504210190573C198.5488255818685,190.8835906982422,175.15156046549478,190.8835906982422,153.08762868245444,190.8835906982422C131.02369689941406,190.8835906982422,110.29309844970703,190.8835906982422,93.37335974072653,181.84192403157553C76.45362103174604,172.80025736490884,63.34474206349207,154.71692403157553,56.79030257936508,145.67525736490884L50.235863095238095,136.6335906982422" marker-end="url(#arrowhead4585)" style="fill:none"></path><defs><marker id="arrowhead4585" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(221.9460906982422,45.191795349121094)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00006103515625" height="18.500070571899414"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(221.9460906982422,146.6335906982422)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00006103515625" height="18.500070571899414"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6008" transform="translate(36.28125,117.38359069824219)"><rect rx="0" ry="0" x="-28.28125" y="-19.25" width="56.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6009" transform="translate(151.7542953491211,45.191795349121094)"><polygon points="37.191796875,0 74.38359375,-37.191796875 37.191796875,-74.38359375 0,-37.191796875" transform="translate(-37.191796875,37.191796875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-12.07421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">v[i]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6011" transform="translate(297.2234344482422,81.28769302368164)"><rect rx="0" ry="0" x="-41.01953125" y="-19.25" width="82.0390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.01953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans-=v[i]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6013" transform="translate(297.2234344482422,176.2585906982422)"><rect rx="0" ry="0" x="-42.27734375" y="-19.25" width="84.5546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.27734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans+=v[i]</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int romanToInt(String s) {        int[] v = new int[s.length()];        for (int i = 0; i &lt; s.length(); i++) v[i] = val(s.charAt(i));        int ans = 0;        for (int i = 0; i &lt; v.length; i++) {            if (i + 1 &lt; v.length &amp;&amp; v[i] &lt; v[i + 1]) ans -= v[i];            else ans += v[i];        }        return ans;    }    int val(char c) {        return switch (c) {            case &#39;I&#39; -&gt; 1; case &#39;V&#39; -&gt; 5; case &#39;X&#39; -&gt; 10; case &#39;L&#39; -&gt; 50;            case &#39;C&#39; -&gt; 100; case &#39;D&#39; -&gt; 500; case &#39;M&#39; -&gt; 1000;            default -&gt; 0;        };    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点<br />只比相邻一个。<code>IV</code>=4、<code>IX</code>=9，但 <code>IIX</code> 不是合法写法（8 写 <code>VIII</code>）。判断 <code>v[i] &lt; v[i+1]</code> 就减，不用管更远的。</p></blockquote><h2 id="12.-%E6%95%B4%E6%95%B0%E8%BD%AC%E7%BD%97%E9%A9%AC%E6%95%B0%E5%AD%97" tabindex="-1"><a href="https://leetcode.cn/problems/integer-to-roman/" target="_blank">12. 整数转罗马数字</a></h2><blockquote><p>整数转罗马数字，范围 1~3999。</p></blockquote><p><strong>思路脉络</strong>：罗马数字是&quot;贪心&quot;——从大到小枚举符号，能减就减。把 13 个值-符号对（含 <code>900=CM</code>、<code>400=CD</code>、<code>4=IV</code> 这种减法组合）按从大到小排列，每次减最大的可行值。</p><div class="mermaid"><svg id="render2429189202" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="188.17813110351562" style="max-width: 556.3031005859375px;" viewBox="0 0 556.3031005859375 188.17813110351562"><style>#render2429189202 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2429189202 .error-icon{fill:#552222;}#render2429189202 .error-text{fill:#552222;stroke:#552222;}#render2429189202 .edge-thickness-normal{stroke-width:2px;}#render2429189202 .edge-thickness-thick{stroke-width:3.5px;}#render2429189202 .edge-pattern-solid{stroke-dasharray:0;}#render2429189202 .edge-pattern-dashed{stroke-dasharray:3;}#render2429189202 .edge-pattern-dotted{stroke-dasharray:2;}#render2429189202 .marker{fill:#333333;stroke:#333333;}#render2429189202 .marker.cross{stroke:#333333;}#render2429189202 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2429189202 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2429189202 .cluster-label text{fill:#333;}#render2429189202 .cluster-label span{color:#333;}#render2429189202 .label text,#render2429189202 span{fill:#333;color:#333;}#render2429189202 .node rect,#render2429189202 .node circle,#render2429189202 .node ellipse,#render2429189202 .node polygon,#render2429189202 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2429189202 .node .label{text-align:center;}#render2429189202 .node.clickable{cursor:pointer;}#render2429189202 .arrowheadPath{fill:#333333;}#render2429189202 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2429189202 .flowchart-link{stroke:#333333;fill:none;}#render2429189202 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2429189202 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2429189202 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2429189202 .cluster text{fill:#333;}#render2429189202 .cluster span{color:#333;}#render2429189202 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2429189202 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M134.729929738713,105.66484832763672L146.0874414489275,100.43346786499023C157.444953159142,95.20208740234375,180.159976579571,84.73932647705078,195.7674887984118,79.59127934773763C211.37500101725257,74.44323221842448,219.87500203450517,74.60989888509114,224.12500254313144,74.69323221842448L228.37500305175774,74.77656555175781" marker-end="url(#arrowhead4606)" style="fill:none"></path><defs><marker id="arrowhead4606" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M345.8449470393131,59.69338453931312L353.7754777166802,57.24891470805391C361.70600839404733,54.80444487679468,377.5670697487815,49.915505214276244,390.9976004261486,47.47103538301703C404.4281311035156,45.02656555175781,415.4281311035156,45.02656555175781,420.9281311035156,45.02656555175781L426.4281311035156,45.02656555175781" marker-end="url(#arrowhead4607)" style="fill:none"></path><defs><marker id="arrowhead4607" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-B" style="opacity: 1;" id="L-C-B"><path class="path" d="M426.4281311035156,64.0011164499614L420.9281311035156,65.7136913002608C415.4281311035156,67.42626615056021,404.4281311035156,70.851415851159,393.51146291097007,72.64732403479174C382.5947947184245,74.44323221842448,371.7614583333334,74.60989888509114,366.3447901407878,74.69323221842448L360.9281219482422,74.77656555175781" marker-end="url(#arrowhead4608)" style="fill:none"></path><defs><marker id="arrowhead4608" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M340.4947087381501,95.2099787618499L349.31694579904433,99.09607656016789C358.13918285993867,102.98217435848586,375.7836569817271,110.75436995512185,394.5954773759547,119.14957191890176C413.4072977701823,127.54477388268168,433.38646443684894,136.56298221360555,443.3760477701823,141.07208637906749L453.3656311035156,145.58119054452942" marker-end="url(#arrowhead4609)" style="fill:none"></path><defs><marker id="arrowhead4609" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M453.3656311035156,166.22154427716833L443.3760477701823,167.77680874822622C433.38646443684894,169.3320732192841,413.4072977701823,172.44260216139983,386.87162017822266,173.99786663245774C360.335942586263,175.55313110351562,327.24375406901044,175.55313110351562,295.4848988850911,175.55313110351562C263.7260437011719,175.55313110351562,233.30052185058594,175.55313110351562,206.7302492150785,170.32175064086914C180.159976579571,165.09037017822266,157.444953159142,154.6276092529297,146.0874414489275,149.3962287902832L134.729929738713,144.16484832763672" marker-end="url(#arrowhead4610)" style="fill:none"></path><defs><marker id="arrowhead4610" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(393.4281311035156,45.02656555175781)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000532150268555" height="18.5006160736084"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(393.4281311035156,118.52656555175781)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000532150268555" height="18.5006160736084"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6028" transform="translate(92.9375,124.91484832763672)"><rect rx="0" ry="0" x="-84.9375" y="-19.25" width="169.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-74.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">从大到小枚举值-符号</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6029" transform="translate(294.1515655517578,74.27656555175781)"><polygon points="66.2765625,0 132.553125,-66.2765625 66.2765625,-132.553125 0,-66.2765625" transform="translate(-66.2765625,66.2765625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-44.390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">num≥vals[i]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6031" transform="translate(487.3656311035156,45.02656555175781)"><rect rx="0" ry="0" x="-60.9375" y="-19.25" width="121.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">追加符号,减值</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6035" transform="translate(487.3656311035156,160.92813110351562)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一个</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public String intToRoman(int num) {        int[] vals = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};        String[] syms = {&quot;M&quot;,&quot;CM&quot;,&quot;D&quot;,&quot;CD&quot;,&quot;C&quot;,&quot;XC&quot;,&quot;L&quot;,&quot;XL&quot;,&quot;X&quot;,&quot;IX&quot;,&quot;V&quot;,&quot;IV&quot;,&quot;I&quot;};        StringBuilder sb = new StringBuilder();        for (int i = 0; i &lt; vals.length; i++) {            while (num &gt;= vals[i]) { sb.append(syms[i]); num -= vals[i]; }        }        return sb.toString();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(1)（符号数固定），空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路<br />整数转&quot;贪心符号&quot;通用模板：值-符号对从大到小，能减就减。同样适合找零钱问题。</p></blockquote><h2 id="58.-%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E5%8D%95%E8%AF%8D%E7%9A%84%E9%95%BF%E5%BA%A6" tabindex="-1"><a href="https://leetcode.cn/problems/length-of-last-word/" target="_blank">58. 最后一个单词的长度</a></h2><blockquote><p>字符串末尾可能有空格，求最后一个单词长度。</p></blockquote><p><strong>思路脉络</strong>：从后往前跳过尾部空格，再数非空格。两步法，简洁。</p><div class="mermaid"><svg id="render84580515" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 566px;" viewBox="0 0 566 54.5"><style>#render84580515 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render84580515 .error-icon{fill:#552222;}#render84580515 .error-text{fill:#552222;stroke:#552222;}#render84580515 .edge-thickness-normal{stroke-width:2px;}#render84580515 .edge-thickness-thick{stroke-width:3.5px;}#render84580515 .edge-pattern-solid{stroke-dasharray:0;}#render84580515 .edge-pattern-dashed{stroke-dasharray:3;}#render84580515 .edge-pattern-dotted{stroke-dasharray:2;}#render84580515 .marker{fill:#333333;stroke:#333333;}#render84580515 .marker.cross{stroke:#333333;}#render84580515 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render84580515 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render84580515 .cluster-label text{fill:#333;}#render84580515 .cluster-label span{color:#333;}#render84580515 .label text,#render84580515 span{fill:#333;color:#333;}#render84580515 .node rect,#render84580515 .node circle,#render84580515 .node ellipse,#render84580515 .node polygon,#render84580515 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render84580515 .node .label{text-align:center;}#render84580515 .node.clickable{cursor:pointer;}#render84580515 .arrowheadPath{fill:#333333;}#render84580515 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render84580515 .flowchart-link{stroke:#333333;fill:none;}#render84580515 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render84580515 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render84580515 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render84580515 .cluster text{fill:#333;}#render84580515 .cluster span{color:#333;}#render84580515 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render84580515 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92,27.25L96.16666666666667,27.25C100.33333333333333,27.25,108.66666666666667,27.25,117,27.25C125.33333333333333,27.25,133.66666666666666,27.25,137.83333333333334,27.25L142,27.25" marker-end="url(#arrowhead4627)" style="fill:none"></path><defs><marker id="arrowhead4627" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M258,27.25L262.1666666666667,27.25C266.3333333333333,27.25,274.6666666666667,27.25,283,27.25C291.3333333333333,27.25,299.6666666666667,27.25,303.8333333333333,27.25L308,27.25" marker-end="url(#arrowhead4628)" style="fill:none"></path><defs><marker id="arrowhead4628" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M424,27.25L428.1666666666667,27.25C432.3333333333333,27.25,440.6666666666667,27.25,449,27.25C457.3333333333333,27.25,465.6666666666667,27.25,469.8333333333333,27.25L474,27.25" marker-end="url(#arrowhead4629)" style="fill:none"></path><defs><marker id="arrowhead4629" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6044" transform="translate(50,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">从后往前</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6045" transform="translate(200,27.25)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">跳过尾部空格</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6047" transform="translate(366,27.25)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">数非空格字符</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6049" transform="translate(516,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回长度</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int lengthOfLastWord(String s) {        int i = s.length() - 1, len = 0;        while (i &gt;= 0 &amp;&amp; s.charAt(i) == &#39; &#39;) i--; // 跳尾部空格        while (i &gt;= 0 &amp;&amp; s.charAt(i) != &#39; &#39;) { len++; i--; }        return len;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="14.-%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%89%8D%E7%BC%80" tabindex="-1"><a href="https://leetcode.cn/problems/longest-common-prefix/" target="_blank">14. 最长公共前缀</a></h2><blockquote><p>字符串数组的最长公共前缀。</p></blockquote><p><strong>思路脉络</strong>：拿第一个串作基准，逐字符和其余串比对。某串比基准短或字符不同就截断。</p><div class="mermaid"><svg id="render3034610126" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="211.6062469482422" style="max-width: 643.7468872070312px;" viewBox="0 0 643.7468872070312 211.6062469482422"><style>#render3034610126 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3034610126 .error-icon{fill:#552222;}#render3034610126 .error-text{fill:#552222;stroke:#552222;}#render3034610126 .edge-thickness-normal{stroke-width:2px;}#render3034610126 .edge-thickness-thick{stroke-width:3.5px;}#render3034610126 .edge-pattern-solid{stroke-dasharray:0;}#render3034610126 .edge-pattern-dashed{stroke-dasharray:3;}#render3034610126 .edge-pattern-dotted{stroke-dasharray:2;}#render3034610126 .marker{fill:#333333;stroke:#333333;}#render3034610126 .marker.cross{stroke:#333333;}#render3034610126 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3034610126 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3034610126 .cluster-label text{fill:#333;}#render3034610126 .cluster-label span{color:#333;}#render3034610126 .label text,#render3034610126 span{fill:#333;color:#333;}#render3034610126 .node rect,#render3034610126 .node circle,#render3034610126 .node ellipse,#render3034610126 .node polygon,#render3034610126 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3034610126 .node .label{text-align:center;}#render3034610126 .node.clickable{cursor:pointer;}#render3034610126 .arrowheadPath{fill:#333333;}#render3034610126 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3034610126 .flowchart-link{stroke:#333333;fill:none;}#render3034610126 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3034610126 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3034610126 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3034610126 .cluster text{fill:#333;}#render3034610126 .cluster span{color:#333;}#render3034610126 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3034610126 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M105.90625,142.48593521118164L110.07291666666667,142.48593521118164C114.23958333333333,142.48593521118164,122.57291666666667,142.48593521118164,130.90625,142.48593521118164C139.23958333333334,142.48593521118164,147.57291666666666,142.48593521118164,151.73958333333334,142.48593521118164L155.90625,142.48593521118164" marker-end="url(#arrowhead4655)" style="fill:none"></path><defs><marker id="arrowhead4655" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M231.4614678686875,123.23593521118164L239.7022648905729,117.02838325500488C247.94306191245832,110.82083129882812,264.4246559562292,98.40572738647461,276.91545323242775,92.28150876363118C289.4062505086263,86.15729014078777,297.9062510172526,86.32395680745442,302.15625127156574,86.40729014078777L306.4062515258789,86.4906234741211" marker-end="url(#arrowhead4656)" style="fill:none"></path><defs><marker id="arrowhead4656" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M446.1209864982173,70.22411149821734L454.2487382398881,67.9768634942013C462.3764899815589,65.72961549018525,478.6319934649005,61.23511948215317,492.2597452065714,58.98787147813713C505.8874969482422,56.740623474121094,516.8874969482422,56.740623474121094,522.3874969482422,56.740623474121094L527.8874969482422,56.740623474121094" marker-end="url(#arrowhead4657)" style="fill:none"></path><defs><marker id="arrowhead4657" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-C" style="opacity: 1;" id="L-D-C"><path class="path" d="M527.8874969482422,74.88682191035727L522.3874969482422,76.73745550431791C516.8874969482422,78.58808909827854,505.8874969482422,82.28935628619982,494.97083053588864,84.2233232134938C484.05416412353514,86.15729014078777,473.22083129882816,86.32395680745442,467.8041648864746,86.40729014078774L462.3874984741211,86.49062347412108" marker-end="url(#arrowhead4658)" style="fill:none"></path><defs><marker id="arrowhead4658" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M440.1569460561916,108.72117894380837L449.2787045382,112.30775303219383C458.40046302020846,115.89432712057926,476.6439799842253,123.06747529735019,495.10025087541317,132.46498663142248C513.5565217666011,141.86249796549478,532.2255465849602,153.4843724568685,541.5600589941396,159.29530970255533L550.8945714033191,165.1062469482422" marker-end="url(#arrowhead4659)" style="fill:none"></path><defs><marker id="arrowhead4659" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-B" style="opacity: 1;" id="L-E-B"><path class="path" d="M539.8171844482422,191.42230248881916L532.3289031982422,192.68212656538967C524.8406219482422,193.94195064196015,509.8640594482422,196.46159879510117,483.87734095255536,197.7214228716717C457.89062245686847,198.9812469482422,420.8937479654948,198.9812469482422,385.2302068074544,198.9812469482422C349.56666564941406,198.9812469482422,315.23645782470703,198.9812469482422,289.83055689046813,192.77369499206543C264.4246559562292,186.56614303588867,247.94306191245832,174.15103912353516,239.7022648905729,167.9434871673584L231.4614678686875,161.73593521118164" marker-end="url(#arrowhead4660)" style="fill:none"></path><defs><marker id="arrowhead4660" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(494.8874969482422,56.740623474121094)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00035285949707" height="18.500410079956055"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(494.8874969482422,130.2406234741211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00035285949707" height="18.500410079956055"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6062" transform="translate(56.953125,142.48593521118164)"><rect rx="0" ry="0" x="-48.953125" y="-19.25" width="97.90625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre=strs[0]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6063" transform="translate(205.90625,142.48593521118164)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历其他串</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6065" transform="translate(383.8968734741211,85.9906234741211)"><polygon points="77.99062500000001,0 155.98125000000002,-77.99062500000001 77.99062500000001,-155.98125000000002 0,-77.99062500000001" transform="translate(-77.99062500000001,77.99062500000001)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-57.40625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">startsWith(pre)?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6067" transform="translate(581.8171844482422,56.740623474121094)"><rect rx="0" ry="0" x="-53.9296875" y="-19.25" width="107.859375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.9296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre缩短一位</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6071" transform="translate(581.8171844482422,184.3562469482422)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一个串</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public String longestCommonPrefix(String[] strs) {        if (strs.length == 0) return &quot;&quot;;        String pre = strs[0];        for (int i = 1; i &lt; strs.length; i++) {            while (!strs[i].startsWith(pre)) pre = pre.substring(0, pre.length() - 1);        }        return pre;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(S)（S 是所有串总长），空间 O(1)。</p></blockquote><h2 id="151.-%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%9A%84%E5%8D%95%E8%AF%8D" tabindex="-1"><a href="https://leetcode.cn/problems/reverse-words-in-a-string/" target="_blank">151. 反转字符串中的单词</a></h2><blockquote><p><code>the sky is blue</code> → <code>blue is sky the</code>，单词间单空格，去首尾空格。</p></blockquote><p><strong>思路脉络</strong>：Java 直接 <code>split(&quot;\\s+&quot;)</code> 拆词，逆序拼。或原地双端：整体反转 → 每词反转 → 去多余空格。这里用 API 法最简洁。</p><div class="mermaid"><svg id="render1323221665" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 389.8125px;" viewBox="0 0 389.8125 54.5"><style>#render1323221665 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1323221665 .error-icon{fill:#552222;}#render1323221665 .error-text{fill:#552222;stroke:#552222;}#render1323221665 .edge-thickness-normal{stroke-width:2px;}#render1323221665 .edge-thickness-thick{stroke-width:3.5px;}#render1323221665 .edge-pattern-solid{stroke-dasharray:0;}#render1323221665 .edge-pattern-dashed{stroke-dasharray:3;}#render1323221665 .edge-pattern-dotted{stroke-dasharray:2;}#render1323221665 .marker{fill:#333333;stroke:#333333;}#render1323221665 .marker.cross{stroke:#333333;}#render1323221665 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1323221665 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1323221665 .cluster-label text{fill:#333;}#render1323221665 .cluster-label span{color:#333;}#render1323221665 .label text,#render1323221665 span{fill:#333;color:#333;}#render1323221665 .node rect,#render1323221665 .node circle,#render1323221665 .node ellipse,#render1323221665 .node polygon,#render1323221665 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1323221665 .node .label{text-align:center;}#render1323221665 .node.clickable{cursor:pointer;}#render1323221665 .arrowheadPath{fill:#333333;}#render1323221665 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1323221665 .flowchart-link{stroke:#333333;fill:none;}#render1323221665 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1323221665 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1323221665 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1323221665 .cluster text{fill:#333;}#render1323221665 .cluster span{color:#333;}#render1323221665 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1323221665 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M97.8125,27.25L101.97916666666667,27.25C106.14583333333333,27.25,114.47916666666667,27.25,122.8125,27.25C131.14583333333334,27.25,139.47916666666666,27.25,143.64583333333334,27.25L147.8125,27.25" marker-end="url(#arrowhead4672)" style="fill:none"></path><defs><marker id="arrowhead4672" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M247.8125,27.25L251.97916666666666,27.25C256.1458333333333,27.25,264.4791666666667,27.25,272.8125,27.25C281.1458333333333,27.25,289.4791666666667,27.25,293.6458333333333,27.25L297.8125,27.25" marker-end="url(#arrowhead4673)" style="fill:none"></path><defs><marker id="arrowhead4673" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6078" transform="translate(52.90625,27.25)"><rect rx="0" ry="0" x="-44.90625" y="-19.25" width="89.8125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.90625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">trim+split</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6079" transform="translate(197.8125,27.25)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">逆序拼单词</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6081" transform="translate(339.8125,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回结果</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public String reverseWords(String s) {        String[] words = s.trim().split(&quot;\\s+&quot;);        StringBuilder sb = new StringBuilder();        for (int i = words.length - 1; i &gt;= 0; i--) {            sb.append(words[i]);            if (i &gt; 0) sb.append(&quot; &quot;);        }        return sb.toString();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点<br /><code>split(&quot;\\s+&quot;)</code> 连续空格会产生空前缀（如 <code>&quot;  a&quot;</code> 拆成 <code>[&quot;&quot;, &quot;a&quot;]</code>），所以先 <code>trim()</code>。或用 <code>split(&quot; +&quot;)</code> 配合 trim。</p></blockquote><h2 id="6.-z-%E5%AD%97%E5%BD%A2%E5%8F%98%E6%8D%A2" tabindex="-1"><a href="https://leetcode.cn/problems/zigzag-conversion/" target="_blank">6. Z 字形变换</a></h2><blockquote><p>字符串按 Z 字形竖向排列，再逐行读取。</p></blockquote><p><strong>思路脉络</strong>：不必真画矩阵。用 <code>StringBuilder[numRows]</code> 模拟逐字符下填，到顶或底转向。方向变量 <code>goingDown</code> 控制指针上下移。</p><div class="mermaid"><svg id="render855737879" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="274.88201904296875" style="max-width: 679.8195190429688px;" viewBox="0 0 679.8195190429688 274.88201904296875"><style>#render855737879 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render855737879 .error-icon{fill:#552222;}#render855737879 .error-text{fill:#552222;stroke:#552222;}#render855737879 .edge-thickness-normal{stroke-width:2px;}#render855737879 .edge-thickness-thick{stroke-width:3.5px;}#render855737879 .edge-pattern-solid{stroke-dasharray:0;}#render855737879 .edge-pattern-dashed{stroke-dasharray:3;}#render855737879 .edge-pattern-dotted{stroke-dasharray:2;}#render855737879 .marker{fill:#333333;stroke:#333333;}#render855737879 .marker.cross{stroke:#333333;}#render855737879 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render855737879 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render855737879 .cluster-label text{fill:#333;}#render855737879 .cluster-label span{color:#333;}#render855737879 .label text,#render855737879 span{fill:#333;color:#333;}#render855737879 .node rect,#render855737879 .node circle,#render855737879 .node ellipse,#render855737879 .node polygon,#render855737879 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render855737879 .node .label{text-align:center;}#render855737879 .node.clickable{cursor:pointer;}#render855737879 .arrowheadPath{fill:#333333;}#render855737879 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render855737879 .flowchart-link{stroke:#333333;fill:none;}#render855737879 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render855737879 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render855737879 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render855737879 .cluster text{fill:#333;}#render855737879 .cluster span{color:#333;}#render855737879 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render855737879 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92.08990974584704,174.16152572631836L102.72206020487253,165.12477429707846C113.35421066389802,156.08802286783853,134.618511581949,138.01452000935873,149.50066229528764,129.06110191345215C164.3828130086263,120.10768381754558,172.88281351725257,120.27435048421223,177.1328137715657,120.35768381754558L181.38281402587887,120.4410171508789" marker-end="url(#arrowhead4700)" style="fill:none"></path><defs><marker id="arrowhead4700" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M379.08289427206375,94.2590661470638L388.8632196936794,91.16439131436631C398.6435451152951,88.06971648166883,418.20419595852644,81.88036681627388,433.4845213801421,78.78569198357638C448.7648468017578,75.6910171508789,459.7648468017578,75.6910171508789,465.2648468017578,75.6910171508789L470.7648468017578,75.6910171508789" marker-end="url(#arrowhead4701)" style="fill:none"></path><defs><marker id="arrowhead4701" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M378.1434172389696,147.56244213603026L388.080322166101,150.68312273441163C398.01722709323235,153.80380333279297,417.891036947495,160.04516452955565,434.9692179162931,163.16584512793702C452.0473988850911,166.28652572631836,466.3299509684245,166.28652572631836,473.4712270100911,166.28652572631836L480.6125030517578,166.28652572631836" marker-end="url(#arrowhead4702)" style="fill:none"></path><defs><marker id="arrowhead4702" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M542.4601593017578,75.6910171508789L546.6268259684244,75.6910171508789C550.7934926350912,75.6910171508789,559.1268259684244,75.6910171508789,571.7828855269131,87.58193524678548C584.4389450854018,99.47285334269206,601.4177308690457,123.2546895345052,609.9071237608676,135.14560763041177L618.3965166526897,147.03652572631836" marker-end="url(#arrowhead4703)" style="fill:none"></path><defs><marker id="arrowhead4703" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M532.6125030517578,166.28652572631836L538.4204457600912,166.28652572631836C544.2283884684244,166.28652572631836,555.8442738850912,166.28652572631836,565.8188832600912,166.28652572631836C575.7934926350912,166.28652572631836,584.1268259684244,166.28652572631836,588.2934926350912,166.28652572631836L592.4601593017578,166.28652572631836" marker-end="url(#arrowhead4704)" style="fill:none"></path><defs><marker id="arrowhead4704" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M619.762713846152,185.53652572631836L611.0456214220864,199.0941104888916C602.3285289980206,212.65169525146484,584.8943441498892,239.76686477661133,566.0359756841568,253.32444953918457C547.1776072184244,266.8820343017578,526.8950551350912,266.8820343017578,505.2791697184245,266.8820343017578C483.6632843017578,266.8820343017578,460.7140655517578,266.8820343017578,425.08261998494464,266.8820343017578C389.45117441813153,266.8820343017578,341.1375020345052,266.8820343017578,294.1571629842122,266.8820343017578C247.17682393391928,266.8820343017578,201.52981821695963,266.8820343017578,168.07416489945433,257.8452828725179C134.618511581949,248.80853144327799,113.35421066389802,230.7350285847982,102.72206020487253,221.69827715555826L92.08990974584704,212.66152572631836" marker-end="url(#arrowhead4705)" style="fill:none"></path><defs><marker id="arrowhead4705" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(437.7648468017578,75.6910171508789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000408172607422" height="18.500471115112305"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(437.7648468017578,166.28652572631836)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000408172607422" height="18.500471115112305"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6094" transform="translate(69.44140625,193.41152572631836)"><rect rx="0" ry="0" x="-61.44140625" y="-19.25" width="122.8828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-51.44140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">逐字符cur填行</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6095" transform="translate(292.8238296508789,119.9410171508789)"><polygon points="111.941015625,0 223.88203125,-111.941015625 111.941015625,-223.88203125 0,-111.941015625" transform="translate(-111.941015625,111.941015625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-95.12890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cur==0或cur==numRows-1?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6097" transform="translate(506.6125030517578,75.6910171508789)"><rect rx="0" ry="0" x="-35.84765625" y="-19.25" width="71.6953125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-25.84765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">翻转dir</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6099" transform="translate(506.6125030517578,166.28652572631836)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">继续</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6101" transform="translate(632.1398468017578,166.28652572631836)"><rect rx="0" ry="0" x="-39.6796875" y="-19.25" width="79.359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.6796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cur+=dir</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0006-zigzag.gif" alt="" /></p><pre><code class="language-java">class Solution {    public String convert(String s, int numRows) {        if (numRows == 1) return s;        StringBuilder[] rows = new StringBuilder[numRows];        for (int i = 0; i &lt; numRows; i++) rows[i] = new StringBuilder();        int cur = 0, dir = 1;        for (char c : s.toCharArray()) {            rows[cur].append(c);            if (cur == 0) dir = 1;            else if (cur == numRows - 1) dir = -1;            cur += dir;        }        StringBuilder ans = new StringBuilder();        for (StringBuilder r : rows) ans.append(r);        return ans.toString();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点<br /><code>numRows == 1</code> 时直接返回原串，否则 <code>cur += dir</code> 会越界（<code>numRows-1 = 0</code>，dir 一直在 1/-1 间跳但 cur 不动，但循环里 <code>cur == 0</code> 和 <code>cur == numRows-1</code> 同时成立会反复改 dir，实际 cur 始终 0，能跑但浪费。显式 return 更稳）。</p></blockquote><h2 id="28.-%E6%89%BE%E5%87%BA%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%8C%B9%E9%85%8D%E9%A1%B9%E7%9A%84%E4%B8%8B%E6%A0%87" tabindex="-1"><a href="https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/" target="_blank">28. 找出字符串中第一个匹配项的下标</a></h2><blockquote><p>实现 <code>indexOf</code>，返回 <code>needle</code> 在 <code>haystack</code> 中首次出现下标，没有返回 -1。</p></blockquote><p><strong>思路脉络</strong>：朴素匹配两层循环 O(nm)。KMP 能 O(n+m) 但实现长。面试若不要求 KMP，朴素法够用且不易错。这里给朴素法。</p><div class="mermaid"><svg id="render3938150672" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="145.125" style="max-width: 449.3984375px;" viewBox="0 0 449.3984375 145.125"><style>#render3938150672 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3938150672 .error-icon{fill:#552222;}#render3938150672 .error-text{fill:#552222;stroke:#552222;}#render3938150672 .edge-thickness-normal{stroke-width:2px;}#render3938150672 .edge-thickness-thick{stroke-width:3.5px;}#render3938150672 .edge-pattern-solid{stroke-dasharray:0;}#render3938150672 .edge-pattern-dashed{stroke-dasharray:3;}#render3938150672 .edge-pattern-dotted{stroke-dasharray:2;}#render3938150672 .marker{fill:#333333;stroke:#333333;}#render3938150672 .marker.cross{stroke:#333333;}#render3938150672 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3938150672 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3938150672 .cluster-label text{fill:#333;}#render3938150672 .cluster-label span{color:#333;}#render3938150672 .label text,#render3938150672 span{fill:#333;color:#333;}#render3938150672 .node rect,#render3938150672 .node circle,#render3938150672 .node ellipse,#render3938150672 .node polygon,#render3938150672 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3938150672 .node .label{text-align:center;}#render3938150672 .node.clickable{cursor:pointer;}#render3938150672 .arrowheadPath{fill:#333333;}#render3938150672 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3938150672 .flowchart-link{stroke:#333333;fill:none;}#render3938150672 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3938150672 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3938150672 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3938150672 .cluster text{fill:#333;}#render3938150672 .cluster span{color:#333;}#render3938150672 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3938150672 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M112.6015625,55.27255040679165L116.76822916666667,53.81045867232638C120.93489583333333,52.3483669378611,129.26822916666666,49.42418346893055,137.6015625,47.962091734465275C145.93489583333334,46.5,154.26822916666666,46.5,158.43489583333334,46.5L162.6015625,46.5" marker-end="url(#arrowhead4724)" style="fill:none"></path><defs><marker id="arrowhead4724" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M269.0234375,36.47846441947566L277.1901041666667,34.940387016229714C285.3567708333333,33.40230961298377,301.6901041666667,30.326154806491886,318.0234375,28.78807740324594C334.3567708333333,27.25,350.6901041666667,27.25,358.8567708333333,27.25L367.0234375,27.25" marker-end="url(#arrowhead4725)" style="fill:none"></path><defs><marker id="arrowhead4725" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M260.2771451271186,65.75L269.90152718926555,69.91666666666667C279.52590925141243,74.08333333333333,298.7746733757062,82.41666666666667,319.31832627118644,90.01985511844332C339.8619791666667,97.62304357021996,361.7005208333333,104.49608714043994,372.6197916666667,107.93260892554991L383.5390625,111.3691307106599" marker-end="url(#arrowhead4726)" style="fill:none"></path><defs><marker id="arrowhead4726" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M383.5390625,121.3827728426396L372.6197916666667,123.23564403553299C361.7005208333333,125.0885152284264,339.8619791666667,128.7942576142132,311.9075520833333,130.6471288071066C283.953125,132.5,249.8828125,132.5,219.8125,132.5C189.7421875,132.5,163.671875,132.5,141.9656827782201,125.89583333333333C120.2594905564402,119.29166666666667,102.9174186128804,106.08333333333333,94.24638264110051,99.47916666666667L85.5753466693206,92.875" marker-end="url(#arrowhead4727)" style="fill:none"></path><defs><marker id="arrowhead4727" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(318.0234375,27.25)"><g transform="translate(-24,-9.25)" class="label"><rect rx="0" ry="0" width="48" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">全匹配</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(318.0234375,90.75)"><g transform="translate(-16,-9.25)" class="label"><rect rx="0" ry="0" width="32" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">失配</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6114" transform="translate(60.30078125,73.625)"><rect rx="0" ry="0" x="-52.30078125" y="-19.25" width="104.6015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.30078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i遍历0..n-m</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6115" transform="translate(215.8125,46.5)"><rect rx="0" ry="0" x="-53.2109375" y="-19.25" width="106.421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.2109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">j匹配needle</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6117" transform="translate(404.2109375,27.25)"><rect rx="0" ry="0" x="-37.1875" y="-19.25" width="74.375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27.1875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6119" transform="translate(404.2109375,117.875)"><rect rx="0" ry="0" x="-20.671875" y="-19.25" width="41.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-10.671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int strStr(String haystack, String needle) {        int n = haystack.length(), m = needle.length();        for (int i = 0; i &lt;= n - m; i++) {            int j = 0;            for (; j &lt; m &amp;&amp; haystack.charAt(i + j) == needle.charAt(j); j++);            if (j == m) return i;        }        return -1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(nm)（朴素），空间 O(1)。</p></blockquote><blockquote><p>[!tip] 进阶<br />KMP 用 <code>next</code> 数组预处理 needle 的&quot;最长相同前后缀&quot;，失配时跳过已匹配部分，O(n+m)。适合大文本匹配，面试会追。</p></blockquote><h2 id="68.-%E6%96%87%E6%9C%AC%E5%B7%A6%E5%8F%B3%E5%AF%B9%E9%BD%90" tabindex="-1"><a href="https://leetcode.cn/problems/text-justification/" target="_blank">68. 文本左右对齐</a></h2><blockquote><p>单词数组按每行 <code>maxWidth</code> 字符左右对齐排版，末行左对齐。</p></blockquote><p><strong>思路脉络</strong>：贪心每行尽可能多放词。先确定一行放几个词（总长 + 至少单空格 ≤ maxWidth），再分配空格：行内词间空格尽量均匀，多出的左优先；末行左对齐补尾空格。</p><div class="mermaid"><svg id="render441197816" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="174.7375030517578" style="max-width: 726.6124877929688px;" viewBox="0 0 726.6124877929688 174.7375030517578"><style>#render441197816 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render441197816 .error-icon{fill:#552222;}#render441197816 .error-text{fill:#552222;stroke:#552222;}#render441197816 .edge-thickness-normal{stroke-width:2px;}#render441197816 .edge-thickness-thick{stroke-width:3.5px;}#render441197816 .edge-pattern-solid{stroke-dasharray:0;}#render441197816 .edge-pattern-dashed{stroke-dasharray:3;}#render441197816 .edge-pattern-dotted{stroke-dasharray:2;}#render441197816 .marker{fill:#333333;stroke:#333333;}#render441197816 .marker.cross{stroke:#333333;}#render441197816 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render441197816 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render441197816 .cluster-label text{fill:#333;}#render441197816 .cluster-label span{color:#333;}#render441197816 .label text,#render441197816 span{fill:#333;color:#333;}#render441197816 .node rect,#render441197816 .node circle,#render441197816 .node ellipse,#render441197816 .node polygon,#render441197816 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render441197816 .node .label{text-align:center;}#render441197816 .node.clickable{cursor:pointer;}#render441197816 .arrowheadPath{fill:#333333;}#render441197816 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render441197816 .flowchart-link{stroke:#333333;fill:none;}#render441197816 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render441197816 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render441197816 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render441197816 .cluster text{fill:#333;}#render441197816 .cluster span{color:#333;}#render441197816 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render441197816 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M156,87.3687515258789L160.16666666666666,87.3687515258789C164.33333333333334,87.3687515258789,172.66666666666666,87.3687515258789,181.08333358764648,87.45208485921223C189.5000005086263,87.53541819254558,198.00000101725263,87.70208485921223,202.25000127156576,87.78541819254558L206.5000015258789,87.8687515258789" marker-end="url(#arrowhead4748)" style="fill:none"></path><defs><marker id="arrowhead4748" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M342.8131931127213,65.44444311272136L351.96724476922736,61.723494514914286C361.12129642573353,58.002545917107206,379.42939973874564,50.56064872149306,397.23970139525176,46.83970012368599C415.0500030517578,43.118751525878906,432.3625030517578,43.118751525878906,441.0187530517578,43.118751525878906L449.6750030517578,43.118751525878906" marker-end="url(#arrowhead4749)" style="fill:none"></path><defs><marker id="arrowhead4749" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M342.81319092318654,110.29305907681352L351.96724294461507,113.84734115165776C361.12129496604365,117.40162322650197,379.4293990089007,124.51018737619044,394.0834510303293,128.06446945103468C408.7375030517578,131.6187515258789,419.7375030517578,131.6187515258789,425.2375030517578,131.6187515258789L430.7375030517578,131.6187515258789" marker-end="url(#arrowhead4750)" style="fill:none"></path><defs><marker id="arrowhead4750" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M581.6750030517578,43.118751525878906L588.9979197184244,43.118751525878906C596.3208363850912,43.118751525878906,610.9666697184244,43.118751525878906,623.8451419406467,47.28541819254557C636.723614162869,51.45208485921224,647.8347252739801,59.78541819254557,653.3902808295356,63.95208485921224L658.9458363850912,68.1187515258789" marker-end="url(#arrowhead4751)" style="fill:none"></path><defs><marker id="arrowhead4751" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M600.6125030517578,131.6187515258789L604.7791697184244,131.6187515258789C608.9458363850912,131.6187515258789,617.2791697184244,131.6187515258789,627.0013919406467,127.45208485921223C636.723614162869,123.28541819254558,647.8347252739801,114.95208485921223,653.3902808295356,110.78541819254558L658.9458363850912,106.6187515258789" marker-end="url(#arrowhead4752)" style="fill:none"></path><defs><marker id="arrowhead4752" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(397.7375030517578,43.118751525878906)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000286102294922" height="18.500329971313477"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(397.7375030517578,131.6187515258789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000286102294922" height="18.500329971313477"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6132" transform="translate(82,87.3687515258789)"><rect rx="0" ry="0" x="-74" y="-19.25" width="148" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-64,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">贪心确定每行词数</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6133" transform="translate(285.3687515258789,87.3687515258789)"><polygon points="79.36875,0 158.7375,-79.36875 79.36875,-158.7375 0,-79.36875" transform="translate(-79.36875,79.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-58.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">末行或单词一行?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6135" transform="translate(515.6750030517578,43.118751525878906)"><rect rx="0" ry="0" x="-66" y="-19.25" width="132" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左对齐补尾空格</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6137" transform="translate(515.6750030517578,131.6187515258789)"><rect rx="0" ry="0" x="-84.9375" y="-19.25" width="169.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-74.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">均匀分配空格,左优先</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6139" transform="translate(684.6125030517578,87.3687515258789)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一行</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;String&gt; fullJustify(String[] words, int maxWidth) {        List&lt;String&gt; res = new ArrayList&lt;&gt;();        int i = 0, n = words.length;        while (i &lt; n) {            int lineLen = words[i].length(), j = i + 1;            while (j &lt; n &amp;&amp; lineLen + 1 + words[j].length() &lt;= maxWidth) {                lineLen += 1 + words[j++].length();      // j 是下一行起点            }            int gap = j - i - 1;                          // 本行词间距数            StringBuilder sb = new StringBuilder(words[i]);            if (j == n || gap == 0) {                     // 末行 或 单词一行                for (int k = i + 1; k &lt; j; k++) sb.append(&#39; &#39;).append(words[k]);                while (sb.length() &lt; maxWidth) sb.append(&#39; &#39;);            } else {                int spaces = (maxWidth - lineLen + gap) / (gap); // 每个 gap 的基础空格                int extra = (maxWidth - lineLen + gap) % gap;     // 多出的分给左边                for (int k = i + 1; k &lt; j; k++) {                    int sp = spaces + (k - i &lt;= extra ? 1 : 0);                    sb.append(&quot; &quot;.repeat(sp)).append(words[k]);                }            }            res.add(sb.toString());            i = j;        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(总字符数)，空间 O(maxWidth)。</p></blockquote><blockquote><p>[!warning] 易错点<br />空格分配公式：<code>lineLen</code> 是&quot;词长+gap个单空格&quot;，所以 <code>maxWidth - lineLen</code> 是还需补的空格，分到 <code>gap</code> 个间距里，每个间距 <code>spaces</code> + 可能的 <code>extra</code>。<code>lineLen + gap</code> 是因为 <code>lineLen</code> 已含 gap 个单空格，再补的总空格 = <code>maxWidth - (lineLen - gap)</code> = <code>maxWidth - lineLen + gap</code>。</p></blockquote><hr /><h1 id="2.-%E5%8F%8C%E6%8C%87%E9%92%88" tabindex="-1">2. 双指针</h1><p>双指针的核心是<strong>用两个指针协同，把两层循环压成一层</strong>。这里聚焦三类：回文对撞、子序列匹配、有序数组两数。</p><h2 id="125.-%E9%AA%8C%E8%AF%81%E5%9B%9E%E6%96%87%E4%B8%B2" tabindex="-1"><a href="https://leetcode.cn/problems/valid-palindrome/" target="_blank">125. 验证回文串</a></h2><blockquote><p>字符串只看字母数字、忽略大小写，判断是否回文。</p></blockquote><p><strong>思路脉络</strong>：对撞指针，一左一右向中间走，跳过非字母数字，比较小写化后的字符。任一对不等就 false。</p><div class="mermaid"><svg id="render3583616239" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="223.37811279296875" style="max-width: 920.125px;" viewBox="0 0 920.125 223.37811279296875"><style>#render3583616239 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3583616239 .error-icon{fill:#552222;}#render3583616239 .error-text{fill:#552222;stroke:#552222;}#render3583616239 .edge-thickness-normal{stroke-width:2px;}#render3583616239 .edge-thickness-thick{stroke-width:3.5px;}#render3583616239 .edge-pattern-solid{stroke-dasharray:0;}#render3583616239 .edge-pattern-dashed{stroke-dasharray:3;}#render3583616239 .edge-pattern-dotted{stroke-dasharray:2;}#render3583616239 .marker{fill:#333333;stroke:#333333;}#render3583616239 .marker.cross{stroke:#333333;}#render3583616239 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3583616239 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3583616239 .cluster-label text{fill:#333;}#render3583616239 .cluster-label span{color:#333;}#render3583616239 .label text,#render3583616239 span{fill:#333;color:#333;}#render3583616239 .node rect,#render3583616239 .node circle,#render3583616239 .node ellipse,#render3583616239 .node polygon,#render3583616239 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3583616239 .node .label{text-align:center;}#render3583616239 .node.clickable{cursor:pointer;}#render3583616239 .arrowheadPath{fill:#333333;}#render3583616239 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3583616239 .flowchart-link{stroke:#333333;fill:none;}#render3583616239 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3583616239 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3583616239 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3583616239 .cluster text{fill:#333;}#render3583616239 .cluster span{color:#333;}#render3583616239 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3583616239 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92.9921875,133.75312042236328L97.15885416666667,133.75312042236328C101.32552083333333,133.75312042236328,109.65885416666667,133.75312042236328,117.9921875,133.75312042236328C126.32552083333333,133.75312042236328,134.65885416666666,133.75312042236328,138.82552083333334,133.75312042236328L142.9921875,133.75312042236328" marker-end="url(#arrowhead4790)" style="fill:none"></path><defs><marker id="arrowhead4790" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M241.68362133464245,114.50312042236328L251.40171569553536,108.7807248433431C261.1198100564283,103.05832926432292,280.55599877821413,91.61353810628255,294.52409364773337,85.9744758605957C308.4921885172526,80.33541361490886,316.9921895345052,80.50208028157552,321.24219004313153,80.58541361490884L325.4921905517578,80.66874694824217" marker-end="url(#arrowhead4791)" style="fill:none"></path><defs><marker id="arrowhead4791" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M448.4570839364323,59.29614643643226L457.43585017977426,55.39991318840058C466.41461642311634,51.5036799403689,484.3721489098004,43.71121344430554,502.0768266114757,39.814980196273865C519.781504313151,35.91874694824219,537.2333272298177,35.91874694824219,545.959238688151,35.91874694824219L554.6851501464844,35.91874694824219" marker-end="url(#arrowhead4792)" style="fill:none"></path><defs><marker id="arrowhead4792" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M448.45708823240665,102.04134926759353L457.43585375975294,105.77091554770163C466.41461928709924,109.50048182780976,484.37215034179184,116.95961438802597,498.8509158691381,120.68918066813409C513.3296813964844,124.41874694824219,524.3296813964844,124.41874694824219,529.8296813964844,124.41874694824219L535.3296813964844,124.41874694824219" marker-end="url(#arrowhead4793)" style="fill:none"></path><defs><marker id="arrowhead4793" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M620.0289001464844,35.91874694824219L627.4214782714844,35.91874694824219C634.8140563964844,35.91874694824219,649.5992126464844,35.91874694824219,664.3080512644216,49.24154882814836C679.0168898823589,62.56435070805455,693.6494108682333,89.20995446786691,700.9656713611704,102.53275634777309L708.2819318541077,115.85555822767927" marker-end="url(#arrowhead4794)" style="fill:none"></path><defs><marker id="arrowhead4794" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-B" style="opacity: 1;" id="L-F-B"><path class="path" d="M706.1514032324081,150.52015552122654L699.1902308430875,159.62148300474934C692.2290584537668,168.72281048827213,678.3067136751256,186.92546545531772,658.507650660805,196.0267929388405C638.7085876464844,205.12812042236328,613.0328063964844,205.12812042236328,586.023691813151,205.12812042236328C559.0145772298177,205.12812042236328,530.672129313151,205.12812042236328,498.97278086344403,205.12812042236328C467.273432413737,205.12812042236328,432.21718343098956,205.12812042236328,398.4942677815755,205.12812042236328C364.77135213216144,205.12812042236328,332.38176981608075,205.12812042236328,305.1107965214379,196.44062042236328C277.8398232267951,187.75312042236328,255.6874589535902,170.37812042236328,244.61127681698778,161.69062042236328L233.5350946803853,153.00312042236328" marker-end="url(#arrowhead4795)" style="fill:none"></path><defs><marker id="arrowhead4795" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-G" style="opacity: 1;" id="L-F-G"><path class="path" d="M746.7812431335446,134.25312042236328L752.1979096730547,134.16978708902994C757.6145762125649,134.08645375569662,768.4479092915852,133.91978708902994,779.3645758310953,133.83645375569662C790.2812423706055,133.75312042236328,801.2812423706055,133.75312042236328,806.7812423706055,133.75312042236328L812.2812423706055,133.75312042236328" marker-end="url(#arrowhead4796)" style="fill:none"></path><defs><marker id="arrowhead4796" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(502.3296813964844,35.91874694824219)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00022315979004" height="18.50025749206543"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(502.3296813964844,124.41874694824219)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00022315979004" height="18.50025749206543"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(502.3296813964844,205.12812042236328)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00022315979004" height="18.50025749206543"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(779.2812423706055,133.75312042236328)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00022315979004" height="18.50025749206543"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6156" transform="translate(50.49609375,133.75312042236328)"><rect rx="0" ry="0" x="-42.49609375" y="-19.25" width="84.9921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.49609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6157" transform="translate(208.9921875,133.75312042236328)"><rect rx="0" ry="0" x="-66" y="-19.25" width="132" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">跳过非字母数字</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6159" transform="translate(397.1609344482422,80.16874694824219)"><polygon points="72.16875,0 144.3375,-72.16875 72.16875,-144.3375 0,-72.16875" transform="translate(-72.16875,72.16875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-50.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">小写化后相等?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6161" transform="translate(587.3570251464844,35.91874694824219)"><rect rx="0" ry="0" x="-32.671875" y="-19.25" width="65.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-22.671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l++,r--</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6163" transform="translate(587.3570251464844,124.41874694824219)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6165" transform="translate(717.8328056335449,133.75312042236328)"><polygon points="28.4484375,0 56.896875,-28.4484375 28.4484375,-56.896875 0,-28.4484375" transform="translate(-28.4484375,28.4484375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-2.359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-G-6169" transform="translate(862.2031173706055,133.75312042236328)"><rect rx="0" ry="0" x="-49.921875" y="-19.25" width="99.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return true</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isPalindrome(String s) {        int l = 0, r = s.length() - 1;        while (l &lt; r) {            while (l &lt; r &amp;&amp; !Character.isLetterOrDigit(s.charAt(l))) l++;            while (l &lt; r &amp;&amp; !Character.isLetterOrDigit(s.charAt(r))) r--;            if (Character.toLowerCase(s.charAt(l++)) != Character.toLowerCase(s.charAt(r--))) return false;        }        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>跳过非字母数字时也要带 <code>l &lt; r</code> 条件，否则可能越界。<code>l++</code>/<code>r--</code> 放在比较时一起做，别漏。</li></ul><h2 id="392.-%E5%88%A4%E6%96%AD%E5%AD%90%E5%BA%8F%E5%88%97" tabindex="-1"><a href="https://leetcode.cn/problems/is-subsequence/" target="_blank">392. 判断子序列</a></h2><blockquote><p><code>s</code> 是否是 <code>t</code> 的子序列（按顺序出现，可不连续）。</p></blockquote><p><strong>思路脉络</strong>：双指针各一个，<code>i</code> 走 <code>s</code>、<code>j</code> 走 <code>t</code>。<code>s[i]==t[j]</code> 则 <code>i++</code>，<code>j</code> 总是 <code>++</code>。<code>i</code> 走到 <code>s.length()</code> 即是子序列。</p><div class="mermaid"><svg id="render2275895818" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="230.7117156982422" style="max-width: 715.7703247070312px;" viewBox="0 0.000003814697265625 715.7703247070312 230.7117156982422"><style>#render2275895818 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2275895818 .error-icon{fill:#552222;}#render2275895818 .error-text{fill:#552222;stroke:#552222;}#render2275895818 .edge-thickness-normal{stroke-width:2px;}#render2275895818 .edge-thickness-thick{stroke-width:3.5px;}#render2275895818 .edge-pattern-solid{stroke-dasharray:0;}#render2275895818 .edge-pattern-dashed{stroke-dasharray:3;}#render2275895818 .edge-pattern-dotted{stroke-dasharray:2;}#render2275895818 .marker{fill:#333333;stroke:#333333;}#render2275895818 .marker.cross{stroke:#333333;}#render2275895818 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2275895818 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2275895818 .cluster-label text{fill:#333;}#render2275895818 .cluster-label span{color:#333;}#render2275895818 .label text,#render2275895818 span{fill:#333;color:#333;}#render2275895818 .node rect,#render2275895818 .node circle,#render2275895818 .node ellipse,#render2275895818 .node polygon,#render2275895818 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2275895818 .node .label{text-align:center;}#render2275895818 .node.clickable{cursor:pointer;}#render2275895818 .arrowheadPath{fill:#333333;}#render2275895818 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2275895818 .flowchart-link{stroke:#333333;fill:none;}#render2275895818 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2275895818 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2275895818 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2275895818 .cluster text{fill:#333;}#render2275895818 .cluster span{color:#333;}#render2275895818 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2275895818 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M60.56469920102565,151.46171760559082L69.4914160008547,137.14439996083578C78.41813280068376,122.82708231608073,96.27156640034188,94.19244702657063,109.44828332732753,79.95846271514893C122.62500025431318,65.72447840372722,131.12500050862633,65.89114507039388,135.37500063578292,65.97447840372722L139.6250007629395,66.05781173706055" marker-end="url(#arrowhead4833)" style="fill:none"></path><defs><marker id="arrowhead4833" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M244.6503777136336,55.96756521363356L251.74875200704818,54.357606300871396C258.8471263004628,52.74764738810922,273.0438748872919,49.52772956258489,285.75097313903984,47.91777064982272C298.4580713907878,46.30781173706055,309.6755193074544,46.30781173706055,315.2842432657878,46.30781173706055L320.8929672241211,46.30781173706055" marker-end="url(#arrowhead4834)" style="fill:none"></path><defs><marker id="arrowhead4834" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M235.84755689825917,84.95088060174086L244.41306799423614,89.09370245762749C252.97857909021317,93.2365243135141,270.10960128216715,101.52216802528733,284.1751123781441,110.35045040198293C298.2406234741211,119.17873277867852,309.2406234741211,128.5496538202965,314.7406234741211,133.2351143411055L320.2406234741211,137.92057486191447" marker-end="url(#arrowhead4835)" style="fill:none"></path><defs><marker id="arrowhead4835" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M362.2367172241211,46.30781173706055L366.5121078491211,46.30781173706055C370.7874984741211,46.30781173706055,379.3382797241211,46.30781173706055,393.3327623579999,56.36437019764378C407.32724499187884,66.42092865822701,426.7654290096366,86.53404557939348,436.4845210185154,96.59060403997671L446.2036130273943,106.64716250055994" marker-end="url(#arrowhead4836)" style="fill:none"></path><defs><marker id="arrowhead4836" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M542.4265579223634,139.46171760559082L547.8432261149089,139.37838427225748C553.2598943074545,139.29505093892416,564.0932306925456,139.12838427225748,575.0098988850912,139.04505093892416C585.9265670776367,138.96171760559082,596.9265670776367,138.96171760559082,602.4265670776367,138.96171760559082L607.9265670776367,138.96171760559082" marker-end="url(#arrowhead4837)" style="fill:none"></path><defs><marker id="arrowhead4837" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M442.47852788341675,168.55118146312873L433.3802833985342,175.86960415353906C424.28203891365155,183.18802684394942,406.0855499438863,197.82487222477013,389.2666023340037,205.14329491518046C372.4476547241211,212.46171760559082,357.0062484741211,212.46171760559082,340.2315088907878,212.46171760559082C323.4567693074544,212.46171760559082,305.3486963907878,212.46171760559082,281.201691309611,212.46171760559082C257.05468622843426,212.46171760559082,226.8687489827474,212.46171760559082,198.0161450703939,212.46171760559082C169.16354115804037,212.46171760559082,141.64427057902017,212.46171760559082,121.99578798412087,208.71171760559082C102.34730538922156,204.96171760559082,90.56961077844312,197.46171760559082,84.68076347305389,193.71171760559082L78.79191616766467,189.96171760559082" marker-end="url(#arrowhead4838)" style="fill:none"></path><defs><marker id="arrowhead4838" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M320.2406234741211,161.82755854900063L314.7406234741211,163.30825172509898C309.2406234741211,164.78894490119737,298.2406234741211,167.75033125339408,277.64765485127765,169.23102442949246C257.05468622843426,170.71171760559082,226.8687489827474,170.71171760559082,198.0161450703939,170.71171760559082C169.16354115804037,170.71171760559082,141.64427057902017,170.71171760559082,123.71796862284343,170.71171760559082C105.79166666666667,170.71171760559082,97.45833333333333,170.71171760559082,93.29166666666667,170.71171760559082L89.125,170.71171760559082" marker-end="url(#arrowhead4839)" style="fill:none"></path><defs><marker id="arrowhead4839" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(287.2406234741211,46.30781173706055)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000104904174805" height="18.5001220703125"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(287.2406234741211,109.80781173706055)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000104904174805" height="18.5001220703125"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(574.9265670776367,138.96171760559082)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000104904174805" height="18.5001220703125"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(287.2406234741211,212.46171760559082)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000104904174805" height="18.5001220703125"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6184" transform="translate(48.5625,170.71171760559082)"><rect rx="0" ry="0" x="-40.5625" y="-19.25" width="81.125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.5625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i走s,j走t</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6185" transform="translate(196.68281173706055,65.55781173706055)"><polygon points="57.557812500000004,0 115.11562500000001,-57.557812500000004 57.557812500000004,-115.11562500000001 0,-57.557812500000004" transform="translate(-57.557812500000004,57.557812500000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-34.703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">s[i]==t[j]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6187" transform="translate(341.5648422241211,46.30781173706055)"><rect rx="0" ry="0" x="-20.671875" y="-19.25" width="41.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-10.671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6189" transform="translate(341.5648422241211,156.08671760559082)"><rect rx="0" ry="0" x="-21.32421875" y="-19.25" width="42.6484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-11.32421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">j++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6191" transform="translate(477.4078140258789,138.96171760559082)"><polygon points="64.51875,0 129.0375,-64.51875 64.51875,-129.0375 0,-64.51875" transform="translate(-64.51875,64.51875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-42.4375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i==s.length?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6193" transform="translate(657.8484420776367,138.96171760559082)"><rect rx="0" ry="0" x="-49.921875" y="-19.25" width="99.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return true</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isSubsequence(String s, String t) {        int i = 0;        for (int j = 0; j &lt; t.length() &amp;&amp; i &lt; s.length(); j++) {            if (s.charAt(i) == t.charAt(j)) i++;        }        return i == s.length();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n+m)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 进阶</p></blockquote><ul><li>若 <code>t</code> 固定、<code>s</code> 有大量查询，预处理 <code>t</code> 为&quot;每个位置之后各字母最早出现位置&quot;的 <code>next[i][26]</code> 表，每个 <code>s</code> 查询 O(|s|)。</li></ul><h2 id="167.-%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C-ii---%E8%BE%93%E5%85%A5%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84" tabindex="-1"><a href="https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/" target="_blank">167. 两数之和 II - 输入有序数组</a></h2><blockquote><p>有序数组找和为 <code>target</code> 的两数下标（1起），恰好一个解。</p></blockquote><p><strong>思路脉络</strong>：有序 → 对撞双指针。<code>l=0, r=n-1</code>，和小于 target 则 <code>l++</code>（要变大），和大于则 <code>r--</code>（要变小）。不像无序要哈希。</p><div class="mermaid"><svg id="render1265435648" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="282.4789123535156" style="max-width: 540.4945068359375px;" viewBox="0 0 540.4945068359375 282.4789123535156"><style>#render1265435648 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1265435648 .error-icon{fill:#552222;}#render1265435648 .error-text{fill:#552222;stroke:#552222;}#render1265435648 .edge-thickness-normal{stroke-width:2px;}#render1265435648 .edge-thickness-thick{stroke-width:3.5px;}#render1265435648 .edge-pattern-solid{stroke-dasharray:0;}#render1265435648 .edge-pattern-dashed{stroke-dasharray:3;}#render1265435648 .edge-pattern-dotted{stroke-dasharray:2;}#render1265435648 .marker{fill:#333333;stroke:#333333;}#render1265435648 .marker.cross{stroke:#333333;}#render1265435648 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1265435648 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1265435648 .cluster-label text{fill:#333;}#render1265435648 .cluster-label span{color:#333;}#render1265435648 .label text,#render1265435648 span{fill:#333;color:#333;}#render1265435648 .node rect,#render1265435648 .node circle,#render1265435648 .node ellipse,#render1265435648 .node polygon,#render1265435648 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1265435648 .node .label{text-align:center;}#render1265435648 .node.clickable{cursor:pointer;}#render1265435648 .arrowheadPath{fill:#333333;}#render1265435648 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1265435648 .flowchart-link{stroke:#333333;fill:none;}#render1265435648 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1265435648 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1265435648 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1265435648 .cluster text{fill:#333;}#render1265435648 .cluster span{color:#333;}#render1265435648 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1265435648 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M61.39836753152231,177.10391235351562L70.83067085960192,160.4494196573893C80.26297418768154,143.79492696126303,99.12758084384076,110.48594156901042,112.80988468054669,93.91478220621745C126.49218851725259,77.34362284342448,134.99218953450517,77.51028951009114,139.24219004313147,77.59362284342448L143.49219055175777,77.67695617675781" marker-end="url(#arrowhead4864)" style="fill:none"></path><defs><marker id="arrowhead4864" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M263.5280721412553,59.358931516255225L277.41115488496536,54.24526895967232C291.2942376286754,49.13160640308942,319.06040311609553,38.90428128992362,343.8568973181389,33.790618733340715C368.6533915201823,28.676956176757812,390.48021443684894,28.676956176757812,401.3936258951823,28.676956176757812L412.3070373535156,28.676956176757812" marker-end="url(#arrowhead4865)" style="fill:none"></path><defs><marker id="arrowhead4865" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M274.91256096638904,84.61048590861103L286.8982289059101,85.87156428663549C298.88389684543125,87.13264266465995,322.8552327244734,89.6547994207089,352.3116037889945,98.16276610091627C381.7679748535156,106.67073278112366,416.7093811035156,121.16450938548952,434.1800842285156,128.41139768767246L451.6507873535156,135.6582859898554" marker-end="url(#arrowhead4866)" style="fill:none"></path><defs><marker id="arrowhead4866" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-E" style="opacity: 1;" id="L-B-E"><path class="path" d="M247.41637330596532,112.10667356903468L263.9847391888904,128.52288003311483C280.5531050718154,144.939086497195,313.6898368376655,177.7714994253553,348.0231766789239,200.50080081275647C382.3565165201823,223.2301022001576,417.88646443684894,235.85629204679958,435.6514383951823,242.1693869701206L453.4164123535156,248.4824818934416" marker-end="url(#arrowhead4867)" style="fill:none"></path><defs><marker id="arrowhead4867" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M451.6507873535156,150.3939686728721L434.1800842285156,155.55395928631268C416.7093811035156,160.71394989975326,381.7679748535156,171.03393112663446,341.85436757405597,176.19392174007507C301.9407602945964,181.35391235351562,257.05495198567706,181.35391235351562,218.91588846842447,181.35391235351562C180.77682495117188,181.35391235351562,149.38450622558594,181.35391235351562,129.5216801961263,182.27989186621892C109.65885416666667,183.2058713789222,101.32552083333333,185.05783040432877,97.15885416666667,185.98380991703206L92.9921875,186.90978942973533" marker-end="url(#arrowhead4868)" style="fill:none"></path><defs><marker id="arrowhead4868" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M453.4164123535156,257.4399273782458L435.6514383951823,259.5089248741241C417.88646443684894,261.5779223700024,382.3565165201823,265.715917361759,342.14863840738934,267.7849148576373C301.9407602945964,269.8539123535156,257.05495198567706,269.8539123535156,218.91588846842447,269.8539123535156C180.77682495117188,269.8539123535156,149.38450622558594,269.8539123535156,125.3852559649755,260.81224568684894C101.38600570436508,251.77057902018228,84.77982390873017,233.68724568684897,76.4767330109127,224.64557902018228L68.17364211309524,215.60391235351562" marker-end="url(#arrowhead4869)" style="fill:none"></path><defs><marker id="arrowhead4869" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(346.8265686035156,28.676956176757812)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000133514404297" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(346.8265686035156,92.17695617675781)"><g transform="translate(-14.25,-9.25)" class="label"><rect rx="0" ry="0" width="28.4924259185791" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">sum</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(346.8265686035156,210.60391235351562)"><g transform="translate(-40.48046875,-9.25)" class="label"><rect rx="0" ry="0" width="80.93817901611328" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">sum&gt;target</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6210" transform="translate(50.49609375,196.35391235351562)"><rect rx="0" ry="0" x="-42.49609375" y="-19.25" width="84.9921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.49609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6211" transform="translate(212.1691436767578,77.17695617675781)"><polygon points="69.176953125,0 138.35390625,-69.176953125 69.176953125,-138.35390625 0,-69.176953125" transform="translate(-69.176953125,69.176953125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-47.61328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sum==target?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6213" transform="translate(472.4007873535156,28.676956176757812)"><rect rx="0" ry="0" x="-60.09375" y="-19.25" width="120.1875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.09375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return l+1,r+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6215" transform="translate(472.4007873535156,144.26543426513672)"><rect rx="0" ry="0" x="-20.75" y="-19.25" width="41.5" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-10.75,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6217" transform="translate(472.4007873535156,255.22891235351562)"><rect rx="0" ry="0" x="-18.984375" y="-19.25" width="37.96875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-8.984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r--</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int[] twoSum(int[] numbers, int target) {        int l = 0, r = numbers.length - 1;        while (l &lt; r) {            int sum = numbers[l] + numbers[r];            if (sum == target) return new int[]{l + 1, r + 1};            else if (sum &lt; target) l++;            else r--;        }        return new int[0];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>题目要求下标从 1 起，返回 <code>l+1, r+1</code>。有序是关键——无序的两数之和用哈希，有序的用对撞双指针省掉哈希空间。</li></ul><h2 id="11.-%E7%9B%9B%E6%9C%80%E5%A4%9A%E6%B0%B4%E7%9A%84%E5%AE%B9%E5%99%A8" tabindex="-1"><a href="https://leetcode.cn/problems/container-with-most-water/" target="_blank">11. 盛最多水的容器</a></h2><blockquote><p>数组表示竖线高度，两根线围成容器求最大盛水量。</p></blockquote><p><strong>思路脉络</strong>：双指针从两端出发。宽 <code>(r-l)</code> 在缩小，面积 = <code>min(h[l],h[r]) × 宽</code>。移动矮边才有可能让 <code>min</code> 变大；移动高边宽更小且 <code>min</code> 不升（被矮边卡住），面积必减。所以哪边矮移哪边。和热题 100 相同。</p><div class="mermaid"><svg id="render1777048589" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="204.46484375" style="max-width: 577.45703125px;" viewBox="0 0 577.45703125 204.46484375"><style>#render1777048589 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1777048589 .error-icon{fill:#552222;}#render1777048589 .error-text{fill:#552222;stroke:#552222;}#render1777048589 .edge-thickness-normal{stroke-width:2px;}#render1777048589 .edge-thickness-thick{stroke-width:3.5px;}#render1777048589 .edge-pattern-solid{stroke-dasharray:0;}#render1777048589 .edge-pattern-dashed{stroke-dasharray:3;}#render1777048589 .edge-pattern-dotted{stroke-dasharray:2;}#render1777048589 .marker{fill:#333333;stroke:#333333;}#render1777048589 .marker.cross{stroke:#333333;}#render1777048589 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1777048589 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1777048589 .cluster-label text{fill:#333;}#render1777048589 .cluster-label span{color:#333;}#render1777048589 .label text,#render1777048589 span{fill:#333;color:#333;}#render1777048589 .node rect,#render1777048589 .node circle,#render1777048589 .node ellipse,#render1777048589 .node polygon,#render1777048589 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1777048589 .node .label{text-align:center;}#render1777048589 .node.clickable{cursor:pointer;}#render1777048589 .arrowheadPath{fill:#333333;}#render1777048589 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1777048589 .flowchart-link{stroke:#333333;fill:none;}#render1777048589 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1777048589 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1777048589 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1777048589 .cluster text{fill:#333;}#render1777048589 .cluster span{color:#333;}#render1777048589 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1777048589 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M68.37556535480554,99.08984375L76.64500237900462,90.1865234375C84.9144394032037,81.283203125,101.45331345160184,63.4765625,113.8894171424676,54.5732421875C126.32552083333333,45.669921875,134.65885416666666,45.669921875,138.82552083333334,45.669921875L142.9921875,45.669921875" marker-end="url(#arrowhead4910)" style="fill:none"></path><defs><marker id="arrowhead4910" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M210.9921875,45.669921875L215.15885416666666,45.669921875C219.32552083333334,45.669921875,227.65885416666666,45.669921875,235.9921875,45.669921875C244.32552083333334,45.669921875,252.65885416666666,45.669921875,256.8255208333333,45.669921875L260.9921875,45.669921875" marker-end="url(#arrowhead4911)" style="fill:none"></path><defs><marker id="arrowhead4911" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M336.6171875,45.669921875L340.7838541666667,45.669921875C344.9505208333333,45.669921875,353.2838541666667,45.669921875,361.7005208333333,45.753255208333336C370.1171875,45.836588541666664,378.6171875,46.003255208333336,382.8671875,46.086588541666664L387.1171875,46.169921875" marker-end="url(#arrowhead4912)" style="fill:none"></path><defs><marker id="arrowhead4912" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M462.45703125,46.169921875L467.8736979166667,46.086588541666664C473.2903645833333,46.003255208333336,484.1236979166667,45.836588541666664,495.0403645833333,49.471251211240315C505.95703125,53.10591388081395,516.95703125,60.54190588662791,522.45703125,64.25990188953489L527.95703125,67.97789789244186" marker-end="url(#arrowhead4913)" style="fill:none"></path><defs><marker id="arrowhead4913" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M440.2117159341192,68.41523719088083L449.3359351534327,81.61100495073403C458.4601543727461,94.80677271058721,476.7085928113731,121.1983082302936,491.6270828640199,137.58766247270495C506.5455729166667,153.9770167151163,518.1341145833334,160.36418968023256,523.9283854166666,163.5577761627907L529.72265625,166.75136264534885" marker-end="url(#arrowhead4914)" style="fill:none"></path><defs><marker id="arrowhead4914" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M527.95703125,96.03186773255814L522.45703125,99.74986373546511C516.95703125,103.4678597383721,505.95703125,110.90385174418604,488.6787109375,114.62184774709301C471.400390625,118.33984375,447.84375,118.33984375,425.6204427083333,118.33984375C403.3971354166667,118.33984375,382.5071614583333,118.33984375,361.5934244791667,118.33984375C340.6796875,118.33984375,319.7421875,118.33984375,298.8046875,118.33984375C277.8671875,118.33984375,256.9296875,118.33984375,236.62760416666666,118.33984375C216.32552083333334,118.33984375,196.65885416666666,118.33984375,176.9921875,118.33984375C157.32552083333334,118.33984375,137.65885416666666,118.33984375,123.65885416666667,118.33984375C109.65885416666667,118.33984375,101.32552083333333,118.33984375,97.15885416666667,118.33984375L92.9921875,118.33984375" marker-end="url(#arrowhead4915)" style="fill:none"></path><defs><marker id="arrowhead4915" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-A" style="opacity: 1;" id="L-F-A"><path class="path" d="M529.72265625,182.38035973837208L523.9283854166666,183.95694040697674C518.1341145833334,185.53352107558138,506.5455729166667,188.6866824127907,488.9729817708333,190.26326308139537C471.400390625,191.83984375,447.84375,191.83984375,425.6204427083333,191.83984375C403.3971354166667,191.83984375,382.5071614583333,191.83984375,361.5934244791667,191.83984375C340.6796875,191.83984375,319.7421875,191.83984375,298.8046875,191.83984375C277.8671875,191.83984375,256.9296875,191.83984375,236.62760416666666,191.83984375C216.32552083333334,191.83984375,196.65885416666666,191.83984375,176.9921875,191.83984375C157.32552083333334,191.83984375,137.65885416666666,191.83984375,119.52242993551586,182.79817708333334C101.38600570436508,173.75651041666666,84.77982390873017,155.67317708333334,76.4767330109127,146.63151041666666L68.17364211309524,137.58984375" marker-end="url(#arrowhead4916)" style="fill:none"></path><defs><marker id="arrowhead4916" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(494.95703125,45.669921875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000307083129883" height="18.500354766845703"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(494.95703125,147.58984375)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000307083129883" height="18.500354766845703"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6236" transform="translate(50.49609375,118.33984375)"><rect rx="0" ry="0" x="-42.49609375" y="-19.25" width="84.9921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.49609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6237" transform="translate(176.9921875,45.669921875)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">算面积</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6239" transform="translate(298.8046875,45.669921875)"><rect rx="0" ry="0" x="-37.8125" y="-19.25" width="75.625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27.8125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">更新ans</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6241" transform="translate(424.287109375,45.669921875)"><polygon points="37.669921875,0 75.33984375,-37.669921875 37.669921875,-75.33984375 0,-37.669921875" transform="translate(-37.669921875,37.669921875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-12.60546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">h[l]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6243" transform="translate(548.70703125,82.0048828125)"><rect rx="0" ry="0" x="-20.75" y="-19.25" width="41.5" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-10.75,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6245" transform="translate(548.70703125,177.21484375)"><rect rx="0" ry="0" x="-18.984375" y="-19.25" width="37.96875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-8.984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r--</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxArea(int[] height) {        int l = 0, r = height.length - 1, ans = 0;        while (l &lt; r) {            ans = Math.max(ans, Math.min(height[l], height[r]) * (r - l));            if (height[l] &lt; height[r]) l++; else r--;        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="15.-%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/3sum/" target="_blank">15. 三数之和</a></h2><blockquote><p>找所有和为 0 的不重复三元组。</p></blockquote><p><strong>思路脉络</strong>：排序 + 固定一个 + 对撞双指针。固定 <code>i</code>，在 <code>i+1..n-1</code> 用双指针找 <code>nums[l]+nums[r] = -nums[i]</code>。难点在去重：<code>i/l/r</code> 三处都要跳过相邻重复。和热题 100 相同。</p><div class="mermaid"><svg id="render3162268887" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="292.7640686035156" style="max-width: 592.0218505859375px;" viewBox="0 1 592.0218505859375 292.7640686035156"><style>#render3162268887 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3162268887 .error-icon{fill:#552222;}#render3162268887 .error-text{fill:#552222;stroke:#552222;}#render3162268887 .edge-thickness-normal{stroke-width:2px;}#render3162268887 .edge-thickness-thick{stroke-width:3.5px;}#render3162268887 .edge-pattern-solid{stroke-dasharray:0;}#render3162268887 .edge-pattern-dashed{stroke-dasharray:3;}#render3162268887 .edge-pattern-dotted{stroke-dasharray:2;}#render3162268887 .marker{fill:#333333;stroke:#333333;}#render3162268887 .marker.cross{stroke:#333333;}#render3162268887 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3162268887 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3162268887 .cluster-label text{fill:#333;}#render3162268887 .cluster-label span{color:#333;}#render3162268887 .label text,#render3162268887 span{fill:#333;color:#333;}#render3162268887 .node rect,#render3162268887 .node circle,#render3162268887 .node ellipse,#render3162268887 .node polygon,#render3162268887 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3162268887 .node .label{text-align:center;}#render3162268887 .node.clickable{cursor:pointer;}#render3162268887 .arrowheadPath{fill:#333333;}#render3162268887 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3162268887 .flowchart-link{stroke:#333333;fill:none;}#render3162268887 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3162268887 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3162268887 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3162268887 .cluster text{fill:#333;}#render3162268887 .cluster span{color:#333;}#render3162268887 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3162268887 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M102.4375,167.35429573059082L106.60416666666667,167.35429573059082C110.77083333333333,167.35429573059082,119.10416666666667,167.35429573059082,127.4375,167.35429573059082C135.77083333333334,167.35429573059082,144.10416666666666,167.35429573059082,148.27083333333334,167.35429573059082L152.4375,167.35429573059082" marker-end="url(#arrowhead4950)" style="fill:none"></path><defs><marker id="arrowhead4950" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M236.75785218925313,148.10429573059082L243.1953455743776,144.59850152333578C249.6328389595021,141.09270731608072,262.507825729751,134.08111890157065,273.1953192420321,130.65865802764893C283.88281275431314,127.23619715372722,292.38281300862633,127.40286382039388,296.6328131357829,127.48619715372722L300.88281326293946,127.56953048706055" marker-end="url(#arrowhead4951)" style="fill:none"></path><defs><marker id="arrowhead4951" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M366.8362415633333,102.3838978133333L378.16926354846464,88.19491484444443C389.50228553359585,74.00593187555553,412.1683295038585,45.627965937777766,436.08468482232314,33.35099595590187C460.0010401407878,21.074025974025975,485.1677068074544,24.898051948051947,497.7510401407878,26.810064935064933L510.3343734741211,28.722077922077922" marker-end="url(#arrowhead4952)" style="fill:none"></path><defs><marker id="arrowhead4952" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M392.02187423706056,127.56953048706053L399.1572907765706,127.48619715372719C406.2927073160807,127.40286382039388,420.5635403951009,127.23619715372722,440.5765611012777,132.54269510094738C460.5895818074544,137.84919304816754,486.3447901407878,148.62885560927452,499.2223943074544,154.01868688982802L512.0999984741211,159.4085181703815" marker-end="url(#arrowhead4953)" style="fill:none"></path><defs><marker id="arrowhead4953" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-F" style="opacity: 1;" id="L-C-F"><path class="path" d="M366.83624224588067,152.75516400411948L378.16926411725404,166.77748016578641C389.5022859886274,180.79979632745335,412.1683297313743,208.84442865078722,430.7201016027477,225.08861981245414C449.2718734741211,241.3328109741211,463.7093734741211,245.7765609741211,470.9281234741211,247.9984359741211L478.1468734741211,250.2203109741211" marker-end="url(#arrowhead4954)" style="fill:none"></path><defs><marker id="arrowhead4954" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M510.3343734741211,35.02792207792208L497.7510401407878,36.939935064935064C485.1677068074544,38.85194805194805,460.0010401407878,42.67597402597403,432.604035059611,44.587987012987014C405.20702997843426,46.5,375.5796864827474,46.5,349.00442632039386,46.5C322.42916615804035,46.5,298.9059893290202,46.5,276.7793857583575,63.4340492884318C254.6527821876948,80.3680985768636,233.9227518753896,114.23619715372722,223.557736719237,131.17024644215903L213.19272156308438,148.10429573059082" marker-end="url(#arrowhead4955)" style="fill:none"></path><defs><marker id="arrowhead4955" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-B" style="opacity: 1;" id="L-E-B"><path class="path" d="M512.0999984741211,175.30007329080013L499.2223943074544,180.68990457135362C486.3447901407878,186.07973585190712,460.5895818074544,196.8593984130141,432.89830589294434,202.2492296935676C405.20702997843426,207.6390609741211,375.5796864827474,207.6390609741211,349.00442632039386,207.6390609741211C322.42916615804035,207.6390609741211,298.9059893290202,207.6390609741211,280.7069075293856,204.13326676686606C262.507825729751,200.627472559611,249.6328389595021,193.61588414510092,243.1953455743776,190.11008993784586L236.75785218925313,186.60429573059082" marker-end="url(#arrowhead4956)" style="fill:none"></path><defs><marker id="arrowhead4956" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-B" style="opacity: 1;" id="L-F-B"><path class="path" d="M478.1468734741211,274.5578109741211L470.9281234741211,275.6546859741211C463.7093734741211,276.7515609741211,449.2718734741211,278.9453109741211,427.23945172627765,280.0421859741211C405.20702997843426,281.1390609741211,375.5796864827474,281.1390609741211,349.00442632039386,281.1390609741211C322.42916615804035,281.1390609741211,298.9059893290202,281.1390609741211,276.90139565502267,265.38326676686603C254.89680198102522,249.627472559611,234.41079146205047,218.11588414510092,224.16778620256306,202.36008993784586L213.92478094307566,186.60429573059082" marker-end="url(#arrowhead4957)" style="fill:none"></path><defs><marker id="arrowhead4957" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(434.8343734741211,17.25)"><g transform="translate(-18.3125,-9.25)" class="label"><rect rx="0" ry="0" width="36.61797332763672" height="18.500396728515625"></rect><text><tspan xml:space="preserve" dy="1em" x="1">&amp;lt;0</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(434.8343734741211,127.06953048706055)"><g transform="translate(-8.390625,-9.25)" class="label"><rect rx="0" ry="0" width="16.7816104888916" height="18.500396728515625"></rect><text><tspan xml:space="preserve" dy="1em" x="1">&gt;0</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(434.8343734741211,236.8890609741211)"><g transform="translate(-8.390625,-9.25)" class="label"><rect rx="0" ry="0" width="16.7816104888916" height="18.500396728515625"></rect><text><tspan xml:space="preserve" dy="1em" x="1">=0</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6266" transform="translate(55.21875,167.35429573059082)"><rect rx="0" ry="0" x="-47.21875" y="-19.25" width="94.4375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.21875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">排序,固定i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6267" transform="translate(201.41015625,167.35429573059082)"><rect rx="0" ry="0" x="-48.97265625" y="-19.25" width="97.9453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.97265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=i+1,r=n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6269" transform="translate(345.95234298706055,127.06953048706055)"><polygon points="45.569531250000004,0 91.13906250000001,-45.569531250000004 45.569531250000004,-91.13906250000001 0,-45.569531250000004" transform="translate(-45.569531250000004,45.569531250000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-21.3828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sum?0</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6271" transform="translate(531.0843734741211,31.875)"><rect rx="0" ry="0" x="-20.75" y="-19.25" width="41.5" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-10.75,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6273" transform="translate(531.0843734741211,167.35429573059082)"><rect rx="0" ry="0" x="-18.984375" y="-19.25" width="37.96875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-8.984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r--</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6275" transform="translate(531.0843734741211,266.5140609741211)"><rect rx="0" ry="0" x="-52.9375" y="-19.25" width="105.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">记录,跳重复</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; threeSum(int[] nums) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        Arrays.sort(nums);        for (int i = 0; i &lt; nums.length - 2; i++) {            if (nums[i] &gt; 0) break;            if (i &gt; 0 &amp;&amp; nums[i] == nums[i - 1]) continue;            int l = i + 1, r = nums.length - 1;            while (l &lt; r) {                int sum = nums[i] + nums[l] + nums[r];                if (sum &lt; 0) l++;                else if (sum &gt; 0) r--;                else {                    res.add(Arrays.asList(nums[i], nums[l], nums[r]));                    while (l &lt; r &amp;&amp; nums[l] == nums[l + 1]) l++;                    while (l &lt; r &amp;&amp; nums[r] == nums[r - 1]) r--;                    l++; r--;                }            }        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(log n)（排序栈）。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>去重用 <code>nums[i] == nums[i-1]</code>（与前一个比），保留第一个、跳后续重复。<code>nums[i] == nums[i+1]</code> 会漏掉 <code>-1,-1,2</code> 中第一个 <code>-1</code>。</li></ul><hr /><h1 id="3.-%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3" tabindex="-1">3. 滑动窗口</h1><p>滑动窗口维护一个满足约束的动态区间。变长窗口（<code>r</code> 扩到违反、<code>l</code> 缩到合法）求最长/最短，定长窗口整体平移。核心是用一个&quot;状态量&quot;替代每次重算。</p><h2 id="209.-%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84" tabindex="-1"><a href="https://leetcode.cn/problems/minimum-size-subarray-sum/" target="_blank">209. 长度最小的子数组</a></h2><blockquote><p>正整数数组找和 ≥ <code>target</code> 的最短连续子数组长度。</p></blockquote><p><strong>思路脉络</strong>：正整数 → 窗口和单调。变长窗口：<code>r</code> 扩加入 <code>sum</code>，一旦 <code>sum ≥ target</code> 就缩 <code>l</code>（边缩边更新最短长度、减 <code>sum</code>）。正整数保证缩 <code>l</code> 和一定减，不会错过更优解。</p><div class="mermaid"><svg id="render1453532906" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="186.42733764648438" style="max-width: 516.5835876464844px;" viewBox="0 0 516.5835876464844 186.42733764648438"><style>#render1453532906 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1453532906 .error-icon{fill:#552222;}#render1453532906 .error-text{fill:#552222;stroke:#552222;}#render1453532906 .edge-thickness-normal{stroke-width:2px;}#render1453532906 .edge-thickness-thick{stroke-width:3.5px;}#render1453532906 .edge-pattern-solid{stroke-dasharray:0;}#render1453532906 .edge-pattern-dashed{stroke-dasharray:3;}#render1453532906 .edge-pattern-dotted{stroke-dasharray:2;}#render1453532906 .marker{fill:#333333;stroke:#333333;}#render1453532906 .marker.cross{stroke:#333333;}#render1453532906 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1453532906 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1453532906 .cluster-label text{fill:#333;}#render1453532906 .cluster-label span{color:#333;}#render1453532906 .label text,#render1453532906 span{fill:#333;color:#333;}#render1453532906 .node rect,#render1453532906 .node circle,#render1453532906 .node ellipse,#render1453532906 .node polygon,#render1453532906 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1453532906 .node .label{text-align:center;}#render1453532906 .node.clickable{cursor:pointer;}#render1453532906 .arrowheadPath{fill:#333333;}#render1453532906 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1453532906 .flowchart-link{stroke:#333333;fill:none;}#render1453532906 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1453532906 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1453532906 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1453532906 .cluster text{fill:#333;}#render1453532906 .cluster span{color:#333;}#render1453532906 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1453532906 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M120.36801649775146,104.35175323486328L130.57100333145954,99.1933224995931C140.77399016516765,94.03489176432292,161.1799638325838,83.71803029378255,175.6329511749182,78.6429328918457C190.08593851725263,73.56783548990886,198.58593953450523,73.73450215657552,202.8359400431315,73.81783548990886L207.08594055175783,73.90116882324219" marker-end="url(#arrowhead4978)" style="fill:none"></path><defs><marker id="arrowhead4978" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M322.90224794894107,58.9151385739411L330.81658581519827,56.45447694882461C338.7309236814555,53.993815323708134,354.5595994139699,49.07249207347516,367.9739372802272,46.61183044835868C381.3882751464844,44.15116882324219,392.3882751464844,44.15116882324219,397.8882751464844,44.15116882324219L403.3882751464844,44.15116882324219" marker-end="url(#arrowhead4979)" style="fill:none"></path><defs><marker id="arrowhead4979" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M317.6010188736333,94.1884342513667L326.39889491910844,98.09889001334595C335.19677096458366,102.00934577532519,352.792523055534,109.83025729928369,372.2733418093425,118.92335125336035C391.75416056315106,128.016445207437,413.1200459798177,138.38172159163176,423.80298868815106,143.56435978372917L434.4859313964844,148.74699797582656" marker-end="url(#arrowhead4980)" style="fill:none"></path><defs><marker id="arrowhead4980" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-B" style="opacity: 1;" id="L-C-B"><path class="path" d="M403.3882751464844,62.124575020476705L397.8882751464844,64.00400732093762C392.3882751464844,65.88343962139852,381.3882751464844,69.64230422232036,370.47160898844396,71.60506985611461C359.5549428304036,73.56783548990886,348.7216105143229,73.73450215657552,343.3049443562825,73.81783548990886L337.8882781982421,73.90116882324219" marker-end="url(#arrowhead4981)" style="fill:none"></path><defs><marker id="arrowhead4981" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M434.4859313964844,162.85077350647617L423.80298868815106,164.67603419647753C413.1200459798177,166.50129488647892,391.75416056315106,170.15181626648163,364.67102305094403,171.977076956483C337.587885538737,173.80233764648438,304.78749593098956,173.80233764648438,273.3204396565755,173.80233764648438C241.85338338216147,173.80233764648438,211.71966044108072,173.80233764648438,186.44981213683226,168.6439069112142C161.1799638325838,163.485476175944,140.77399016516765,153.16861470540366,130.57100333145954,148.01018397013345L120.36801649775146,142.85175323486328" marker-end="url(#arrowhead4982)" style="fill:none"></path><defs><marker id="arrowhead4982" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(370.3882751464844,44.15116882324219)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000469207763672" height="18.50054168701172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(370.3882751464844,117.65116882324219)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000469207763672" height="18.50054168701172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6292" transform="translate(82.29296875,123.60175323486328)"><rect rx="0" ry="0" x="-74.29296875" y="-19.25" width="148.5859375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-64.29296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r扩,sum+=nums[r]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6293" transform="translate(271.9871063232422,73.40116882324219)"><polygon points="65.401171875,0 130.80234375,-65.401171875 65.401171875,-130.80234375 0,-65.401171875" transform="translate(-65.401171875,65.401171875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-43.41796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sum≥target?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6295" transform="translate(455.9859313964844,44.15116882324219)"><rect rx="0" ry="0" x="-52.59765625" y="-19.25" width="105.1953125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.59765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans=min,缩l</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6297" transform="translate(455.9859313964844,159.17733764648438)"><rect rx="0" ry="0" x="-21.5" y="-19.25" width="43" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-11.5,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r++</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0209-min-subarray.gif" alt="" /></p><pre><code class="language-java">class Solution {    public int minSubArrayLen(int target, int[] nums) {        int l = 0, sum = 0, ans = Integer.MAX_VALUE;        for (int r = 0; r &lt; nums.length; r++) {            sum += nums[r];            while (sum &gt;= target) {                ans = Math.min(ans, r - l + 1);                sum -= nums[l++];            }        }        return ans == Integer.MAX_VALUE ? 0 : ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须正整数——含负数时窗口和不再单调，这法失效，得用前缀和+单调队列。本题保证正整数。</li></ul><h2 id="3.-%E6%97%A0%E9%87%8D%E5%A4%8D%E5%AD%97%E7%AC%A6%E7%9A%84%E6%9C%80%E9%95%BF%E5%AD%90%E4%B8%B2" tabindex="-1"><a href="https://leetcode.cn/problems/longest-substring-without-repeating-characters/" target="_blank">3. 无重复字符的最长子串</a></h2><blockquote><p>找不含重复字符的最长子串长度。</p></blockquote><p><strong>思路脉络</strong>：哈希记字符最近下标，<code>r</code> 遇重复时 <code>l</code> 跳到 <code>max(l, map[c]+1)</code>（不能后退）。和热题 100 相同。</p><div class="mermaid"><svg id="render871746712" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="195.1271514892578" style="max-width: 643.5398559570312px;" viewBox="0 0 643.5398559570312 195.1271514892578"><style>#render871746712 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render871746712 .error-icon{fill:#552222;}#render871746712 .error-text{fill:#552222;stroke:#552222;}#render871746712 .edge-thickness-normal{stroke-width:2px;}#render871746712 .edge-thickness-thick{stroke-width:3.5px;}#render871746712 .edge-pattern-solid{stroke-dasharray:0;}#render871746712 .edge-pattern-dashed{stroke-dasharray:3;}#render871746712 .edge-pattern-dotted{stroke-dasharray:2;}#render871746712 .marker{fill:#333333;stroke:#333333;}#render871746712 .marker.cross{stroke:#333333;}#render871746712 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render871746712 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render871746712 .cluster-label text{fill:#333;}#render871746712 .cluster-label span{color:#333;}#render871746712 .label text,#render871746712 span{fill:#333;color:#333;}#render871746712 .node rect,#render871746712 .node circle,#render871746712 .node ellipse,#render871746712 .node polygon,#render871746712 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render871746712 .node .label{text-align:center;}#render871746712 .node.clickable{cursor:pointer;}#render871746712 .arrowheadPath{fill:#333333;}#render871746712 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render871746712 .flowchart-link{stroke:#333333;fill:none;}#render871746712 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render871746712 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render871746712 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render871746712 .cluster text{fill:#333;}#render871746712 .cluster span{color:#333;}#render871746712 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render871746712 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M60.647339222893365,96.50214767456055L65.74257435241114,92.33512306213379C70.83780948192891,88.16809844970703,81.02827974096446,79.83404922485352,90.37351512479536,75.75035794576009C99.71875050862629,71.66666666666667,108.21875101725259,71.83333333333333,112.46875127156572,71.91666666666666L116.71875152587887,71.99999999999999" marker-end="url(#arrowhead5009)" style="fill:none"></path><defs><marker id="arrowhead5009" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M205.6203643202808,53.89302209615966L214.05486038327436,49.452518413466386C222.48935644626795,45.0120147307731,239.35834857225507,36.13100736538655,253.2928446352486,31.690503682693276C267.2273406982422,27.25,278.2273406982422,27.25,283.7273406982422,27.25L289.2273406982422,27.25" marker-end="url(#arrowhead5010)" style="fill:none"></path><defs><marker id="arrowhead5010" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M205.62036633925962,90.10697893661929L214.05486206575674,94.38081578051606C222.4893577922538,98.65465262441285,239.35834924524798,107.20232631220642,261.64375643007844,111.47616315610321C283.9291636149089,115.75,311.6309865315755,115.75,325.4818979899089,115.75L339.3328094482422,115.75" marker-end="url(#arrowhead5011)" style="fill:none"></path><defs><marker id="arrowhead5011" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M441.4382781982422,27.25L445.6049448649089,27.25C449.7716115315755,27.25,458.1049448649089,27.25,474.92842245730526,38.791666666666664C491.75190004970165,50.333333333333336,517.0655219011612,73.41666666666667,529.7223328268909,84.95833333333333L542.3791437526207,96.5" marker-end="url(#arrowhead5012)" style="fill:none"></path><defs><marker id="arrowhead5012" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M391.3328094482422,115.75L403.8503875732422,115.75C416.3679656982422,115.75,441.4031219482422,115.75,458.0873667399089,115.75C474.7716115315755,115.75,483.1049448649089,115.75,487.2716115315755,115.75L491.4382781982422,115.75" marker-end="url(#arrowhead5013)" style="fill:none"></path><defs><marker id="arrowhead5013" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M537.315030365705,135L525.5022383377944,143.6878579457601C513.689446309884,152.37571589152017,490.06386225406305,169.75143178304037,461.4001587678193,178.43928972880045C432.7364552815755,187.12714767456055,399.0346323649089,187.12714767456055,363.9994761149089,187.12714767456055C328.9643198649089,187.12714767456055,292.5958302815755,187.12714767456055,259.994202931722,187.12714767456055C227.3925755818685,187.12714767456055,198.55781046549478,187.12714767456055,171.05637868245444,187.12714767456055C143.55494689941406,187.12714767456055,117.38684844970703,187.12714767456055,97.71680699630711,178.43964767456055C78.04676554290718,169.75214767456055,64.87478108581436,152.37714767456055,58.28878885726795,143.68964767456055L51.70279662872154,135.00214767456055" marker-end="url(#arrowhead5014)" style="fill:none"></path><defs><marker id="arrowhead5014" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(256.2273406982422,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00017738342285" height="18.500205993652344"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(256.2273406982422,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00017738342285" height="18.500205993652344"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6314" transform="translate(37.109375,115.75214767456055)"><rect rx="0" ry="0" x="-29.109375" y="-19.25" width="58.21875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-19.109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r遍历</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6315" transform="translate(169.7230453491211,71.5)"><polygon points="53.504296875,0 107.00859375,-53.504296875 53.504296875,-107.00859375 0,-53.504296875" transform="translate(-53.504296875,53.504296875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-30.19921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">map含c?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6317" transform="translate(365.3328094482422,27.25)"><rect rx="0" ry="0" x="-76.10546875" y="-19.25" width="152.2109375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-66.10546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=max(l,map[c]+1)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6319" transform="translate(365.3328094482422,115.75)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">继续</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6321" transform="translate(563.4890594482422,115.75)"><rect rx="0" ry="0" x="-72.05078125" y="-19.25" width="144.1015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-62.05078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">map[c]=r,更新ans</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int lengthOfLongestSubstring(String s) {        Map&lt;Character, Integer&gt; map = new HashMap&lt;&gt;();        int l = 0, ans = 0;        for (int r = 0; r &lt; s.length(); r++) {            char c = s.charAt(r);            if (map.containsKey(c)) l = Math.max(l, map.get(c) + 1);            map.put(c, r);            ans = Math.max(ans, r - l + 1);        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(字符集)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>Math.max</code> 保证 <code>l</code> 只前进。<code>abba</code> 处理第二个 <code>b</code> 时 <code>l</code> 跳到 2，处理第二个 <code>a</code> 时 <code>map.get('a')+1=1</code> 不能让 <code>l</code> 退回。</li></ul><h2 id="30.-%E4%B8%B2%E8%81%94%E6%89%80%E6%9C%89%E5%8D%95%E8%AF%8D%E7%9A%84%E5%AD%90%E4%B8%B2" tabindex="-1"><a href="https://leetcode.cn/problems/substring-with-concatenation-of-all-words/" target="_blank">30. 串联所有单词的子串</a></h2><blockquote><p><code>words</code> 全部单词（同长）拼接的任意排列，在 <code>s</code> 中所有出现起点。</p></blockquote><p><strong>思路脉络</strong>：单词定长 <code>w</code>，把 <code>s</code> 按 <code>w</code> 切成&quot;词序列&quot;。窗口大小固定为 <code>words.length</code>，对每个起始偏移 <code>0..w-1</code> 各做一次定长滑动窗口 + 计数匹配。本质是&quot;定长窗口 + 状态计数&quot;。</p><div class="mermaid"><svg id="render2929511252" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="313.185546875" style="max-width: 1036.8023681640625px;" viewBox="0 0.000003814697265625 1036.8023681640625 313.185546875"><style>#render2929511252 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2929511252 .error-icon{fill:#552222;}#render2929511252 .error-text{fill:#552222;stroke:#552222;}#render2929511252 .edge-thickness-normal{stroke-width:2px;}#render2929511252 .edge-thickness-thick{stroke-width:3.5px;}#render2929511252 .edge-pattern-solid{stroke-dasharray:0;}#render2929511252 .edge-pattern-dashed{stroke-dasharray:3;}#render2929511252 .edge-pattern-dotted{stroke-dasharray:2;}#render2929511252 .marker{fill:#333333;stroke:#333333;}#render2929511252 .marker.cross{stroke:#333333;}#render2929511252 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2929511252 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2929511252 .cluster-label text{fill:#333;}#render2929511252 .cluster-label span{color:#333;}#render2929511252 .label text,#render2929511252 span{fill:#333;color:#333;}#render2929511252 .node rect,#render2929511252 .node circle,#render2929511252 .node ellipse,#render2929511252 .node polygon,#render2929511252 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2929511252 .node .label{text-align:center;}#render2929511252 .node.clickable{cursor:pointer;}#render2929511252 .arrowheadPath{fill:#333333;}#render2929511252 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2929511252 .flowchart-link{stroke:#333333;fill:none;}#render2929511252 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2929511252 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2929511252 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2929511252 .cluster text{fill:#333;}#render2929511252 .cluster span{color:#333;}#render2929511252 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2929511252 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M138.3125,158.5374984741211L142.47916666666666,158.5374984741211C146.64583333333334,158.5374984741211,154.97916666666666,158.5374984741211,163.3125,158.5374984741211C171.64583333333334,158.5374984741211,179.97916666666666,158.5374984741211,184.14583333333334,158.5374984741211L188.3125,158.5374984741211" marker-end="url(#arrowhead5056)" style="fill:none"></path><defs><marker id="arrowhead5056" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M283.93095141853075,139.2874984741211L297.1247511821089,127.03437360127766C310.31855094568715,114.78124872843425,336.7061504728436,90.27499898274739,354.1499503635784,78.1052074432373C371.59375025431314,65.93541590372722,380.0937505086263,66.10208257039388,384.3437506357829,66.18541590372722L388.59375076293946,66.26874923706055" marker-end="url(#arrowhead5057)" style="fill:none"></path><defs><marker id="arrowhead5057" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M504.1312492370605,66.26874923706055L509.5479157765706,66.18541590372722C514.9645823160807,66.10208257039388,525.7979153951009,65.93541590372722,541.2041652679444,70.3976185666304C556.6104151407877,74.85982122953358,576.5895818074545,83.9508932220066,586.5791651407877,88.49642921824312L596.5687484741211,93.04196521447963" marker-end="url(#arrowhead5058)" style="fill:none"></path><defs><marker id="arrowhead5058" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M467.9933860683777,102.40661393162226L479.4330298026682,121.57426135537207C490.87267353695887,140.74190877912187,513.75196100554,179.07720362662147,530.6916047398305,198.2448510503713C547.6312484741211,217.4124984741211,558.6312484741211,217.4124984741211,564.1312484741211,217.4124984741211L569.6312484741211,217.4124984741211" marker-end="url(#arrowhead5059)" style="fill:none"></path><defs><marker id="arrowhead5059" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M707.5062484741211,217.4124984741211L711.6729151407877,217.4124984741211C715.8395818074545,217.4124984741211,724.1729151407877,217.4124984741211,735.0208337858194,219.58541290810967C745.868752430851,221.75832734209823,759.2312563875807,226.10415621007533,765.9125083659457,228.2770706440639L772.5937603443106,230.4499850780525" marker-end="url(#arrowhead5060)" style="fill:none"></path><defs><marker id="arrowhead5060" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-G" style="opacity: 1;" id="L-F-G"><path class="path" d="M879.3023406982428,245.03749847412107L884.7190081278487,244.95416514078772C890.1356755574549,244.87083180745444,900.969010416667,244.70416514078775,911.8856778462729,244.62083180745444C922.8023452758789,244.5374984741211,933.8023452758789,244.5374984741211,939.3023452758789,244.5374984741211L944.8023452758789,244.5374984741211" marker-end="url(#arrowhead5061)" style="fill:none"></path><defs><marker id="arrowhead5061" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-B" style="opacity: 1;" id="L-F-B"><path class="path" d="M772.5937626620374,259.62501113615843L765.9125102973848,261.6312590258189C759.2312579327321,263.6375069154793,745.8687532034265,267.6500026948002,723.5312508387739,269.6562505844607C701.1937484741211,271.6624984741211,669.8812484741211,271.6624984741211,637.2354151407877,271.6624984741211C604.5895818074545,271.6624984741211,570.6104151407877,271.6624984741211,538.492706934611,271.6624984741211C506.37499872843426,271.6624984741211,476.1187489827474,271.6624984741211,447.19583257039386,271.6624984741211C418.27291615804035,271.6624984741211,390.6833330790202,271.6624984741211,363.07309782404053,256.0166651407878C335.46286256906075,240.37083180745444,307.83197513812155,209.07916514078775,294.0165314226519,193.43333180745444L280.2010877071823,177.7874984741211" marker-end="url(#arrowhead5062)" style="fill:none"></path><defs><marker id="arrowhead5062" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M596.5687484741211,131.264282496702L586.5791651407877,135.80981849293852C576.5895818074545,140.35535448917503,556.6104151407877,149.44642648164805,531.492706934611,153.99196247788458C506.37499872843426,158.5374984741211,476.1187489827474,158.5374984741211,447.19583257039386,158.5374984741211C418.27291615804035,158.5374984741211,390.6833330790202,158.5374984741211,372.72187487284344,158.5374984741211C354.7604166666667,158.5374984741211,346.4270833333333,158.5374984741211,342.2604166666667,158.5374984741211L338.09375,158.5374984741211" marker-end="url(#arrowhead5063)" style="fill:none"></path><defs><marker id="arrowhead5063" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(536.6312484741211,65.76874923706055)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00029945373535" height="18.500347137451172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(536.6312484741211,217.4124984741211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00029945373535" height="18.500347137451172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(911.8023452758789,244.5374984741211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00029945373535" height="18.500347137451172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(536.6312484741211,271.6624984741211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00029945373535" height="18.500347137451172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6342" transform="translate(73.15625,158.5374984741211)"><rect rx="0" ry="0" x="-65.15625" y="-19.25" width="130.3125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.15625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">每个偏移0..w-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6343" transform="translate(263.203125,158.5374984741211)"><rect rx="0" ry="0" x="-74.890625" y="-19.25" width="149.78125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-64.890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">按w切词,滑动窗口</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6345" transform="translate(445.86249923706055,65.76874923706055)"><polygon points="57.768750000000004,0 115.53750000000001,-57.768750000000004 57.768750000000004,-115.53750000000001 0,-57.768750000000004" transform="translate(-57.768750000000004,57.768750000000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-34.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">词在词表?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6347" transform="translate(638.5687484741211,112.15312385559082)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">重置窗口</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6349" transform="translate(638.5687484741211,217.4124984741211)"><rect rx="0" ry="0" x="-68.9375" y="-19.25" width="137.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">计数,缩到不超量</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6351" transform="translate(818.154296875,244.5374984741211)"><polygon points="60.648046875,0 121.29609375,-60.648046875 60.648046875,-121.29609375 0,-60.648046875" transform="translate(-60.648046875,60.648046875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-38.13671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">count==m?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-G-6353" transform="translate(986.8023452758789,244.5374984741211)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">记录起点</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;Integer&gt; findSubstring(String s, String[] words) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        int n = s.length(), m = words.length, w = words[0].length();        Map&lt;String, Integer&gt; need = new HashMap&lt;&gt;();        for (String wd : words) need.merge(wd, 1, Integer::sum);        for (int off = 0; off &lt; w; off++) {            int l = off, count = 0;            Map&lt;String, Integer&gt; win = new HashMap&lt;&gt;();            for (int r = off; r + w &lt;= n; r += w) {                String word = s.substring(r, r + w);                if (!need.containsKey(word)) { win.clear(); count = 0; l = r + w; continue; }                win.merge(word, 1, Integer::sum);                count++;                while (win.get(word) &gt; need.get(word)) {                    String out = s.substring(l, l + w);                    win.merge(out, -1, Integer::sum);                    count--; l += w;                }                if (count == m) res.add(l);            }        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n·w)，空间 O(m·w)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>起始偏移 <code>0..w-1</code> 各做一遍，不能只从 0 开始——词边界不同会漏解。遇到不在词表的词要重置窗口（<code>l</code> 跳过它）。</li></ul><h2 id="76.-%E6%9C%80%E5%B0%8F%E8%A6%86%E7%9B%96%E5%AD%90%E4%B8%B2" tabindex="-1"><a href="https://leetcode.cn/problems/minimum-window-substring/" target="_blank">76. 最小覆盖子串</a></h2><blockquote><p><code>s</code> 中覆盖 <code>t</code> 所有字符的最短子串。</p></blockquote><p><strong>思路脉络</strong>：变长窗口。<code>r</code> 扩到&quot;刚好覆盖 t&quot;，<code>l</code> 缩到&quot;刚好不覆盖&quot;，临界点更新最短。<code>valid</code> 记录窗口里满足需求计数的字符种类数。和热题 100 相同。</p><div class="mermaid"><svg id="render929187870" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="219.07345581054688" style="max-width: 831.9688110351562px;" viewBox="0 0 831.9688110351562 219.07345581054688"><style>#render929187870 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render929187870 .error-icon{fill:#552222;}#render929187870 .error-text{fill:#552222;stroke:#552222;}#render929187870 .edge-thickness-normal{stroke-width:2px;}#render929187870 .edge-thickness-thick{stroke-width:3.5px;}#render929187870 .edge-pattern-solid{stroke-dasharray:0;}#render929187870 .edge-pattern-dashed{stroke-dasharray:3;}#render929187870 .edge-pattern-dotted{stroke-dasharray:2;}#render929187870 .marker{fill:#333333;stroke:#333333;}#render929187870 .marker.cross{stroke:#333333;}#render929187870 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render929187870 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render929187870 .cluster-label text{fill:#333;}#render929187870 .cluster-label span{color:#333;}#render929187870 .label text,#render929187870 span{fill:#333;color:#333;}#render929187870 .node rect,#render929187870 .node circle,#render929187870 .node ellipse,#render929187870 .node polygon,#render929187870 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render929187870 .node .label{text-align:center;}#render929187870 .node.clickable{cursor:pointer;}#render929187870 .arrowheadPath{fill:#333333;}#render929187870 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render929187870 .flowchart-link{stroke:#333333;fill:none;}#render929187870 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render929187870 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render929187870 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render929187870 .cluster text{fill:#333;}#render929187870 .cluster span{color:#333;}#render929187870 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render929187870 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M94.1430291024673,109.99258041381836L103.96814925205608,103.70827039082845C113.79326940164486,97.42396036783855,133.44350970082243,84.85534032185872,150.37205386803342,79.57177269360011C167.30059803524443,74.2882050653415,181.50744607048887,76.28968985480408,188.6108700881111,77.29043224953537L195.71429410573333,78.29117464426666" marker-end="url(#arrowhead5103)" style="fill:none"></path><defs><marker id="arrowhead5103" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-A" style="opacity: 1;" id="L-B-A"><path class="path" d="M208.25055291424187,109.06852166424186L199.05775242853488,112.43086478917127C189.86495194282793,115.7932079141007,171.47935097141396,122.51789416395953,156.78655048570698,125.88023728888895C142.09375,129.24258041381836,131.09375,129.24258041381836,125.59375,129.24258041381836L120.09375,129.24258041381836" marker-end="url(#arrowhead5104)" style="fill:none"></path><defs><marker id="arrowhead5104" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M344.41718597412097,87.41172027587892L349.83385340372706,87.32838694254558C355.2505208333332,87.24505360921223,366.0838556925455,87.07838694254558,377.00052312215166,86.99505360921223C387.9171905517578,86.9117202758789,398.9171905517578,86.9117202758789,404.4171905517578,86.9117202758789L409.9171905517578,86.9117202758789" marker-end="url(#arrowhead5105)" style="fill:none"></path><defs><marker id="arrowhead5105" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M513.897997638372,67.6617202758789L519.1668214572696,65.57838694254558C524.4356452761673,63.49505360921224,534.9732929139626,59.32838694254557,544.4087833995269,57.24505360921224C553.8442738850912,55.161720275878906,562.1776072184244,55.161720275878906,566.3442738850912,55.161720275878906L570.5109405517578,55.161720275878906" marker-end="url(#arrowhead5106)" style="fill:none"></path><defs><marker id="arrowhead5106" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M672.8312530517578,55.161720275878906L676.9979197184244,55.161720275878906C681.1645863850912,55.161720275878906,689.4979197184244,55.161720275878906,702.086750652804,63.419699618802404C714.6755815871835,71.6776789617259,731.5199101226094,88.19363764757288,739.9420743903223,96.45161699049636L748.3642386580352,104.70959633341987" marker-end="url(#arrowhead5107)" style="fill:none"></path><defs><marker id="arrowhead5107" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-C" style="opacity: 1;" id="L-E-C"><path class="path" d="M723.3312545776366,129.74258041381836L719.0812543233236,129.65924708048502C714.8312540690104,129.5759137471517,706.3312535603841,129.40924708048502,689.3878939310711,129.3259137471517C672.4445343017578,129.24258041381836,647.0578155517578,129.24258041381836,621.6710968017578,129.24258041381836C596.2843780517578,129.24258041381836,570.8976593017578,129.24258041381836,550.9073344845667,125.39577039082845C530.9170096673756,121.54896036783855,516.3230787829935,113.85534032185872,509.0261133408024,110.00853029886882L501.72914789861136,106.1617202758789" marker-end="url(#arrowhead5108)" style="fill:none"></path><defs><marker id="arrowhead5108" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M747.9303969483001,154.3417258362395L739.5805396322097,162.0886782888259C731.2306823161193,169.83563074141227,714.5309676839385,185.32953564658501,693.4877509928482,193.07648809917143C672.4445343017578,200.8234405517578,647.0578155517578,200.8234405517578,621.6710968017578,200.8234405517578C596.2843780517578,200.8234405517578,570.8976593017578,200.8234405517578,544.8214874267578,200.8234405517578C518.7453155517578,200.8234405517578,491.9796905517578,200.8234405517578,463.8807322184245,200.8234405517578C435.7817738850911,200.8234405517578,406.3494822184245,200.8234405517578,372.981383005778,200.8234405517578C339.61328379313153,200.8234405517578,302.3093770345052,200.8234405517578,265.0054702758789,200.8234405517578C227.7015635172526,200.8234405517578,190.3976567586263,200.8234405517578,160.89573698614197,192.1016305287679C131.39381721365763,183.37982050577799,109.69388442731527,165.9362004597982,98.84391803414411,157.21439043680826L87.99395164097292,148.49258041381836" marker-end="url(#arrowhead5109)" style="fill:none"></path><defs><marker id="arrowhead5109" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(153.09375,129.24258041381836)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000228881835938" height="18.500267028808594"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(376.9171905517578,86.9117202758789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000228881835938" height="18.500267028808594"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(621.6710968017578,129.24258041381836)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000228881835938" height="18.500267028808594"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(465.2140655517578,200.8234405517578)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000228881835938" height="18.500267028808594"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6372" transform="translate(64.046875,129.24258041381836)"><rect rx="0" ry="0" x="-56.046875" y="-19.25" width="112.09375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-46.046875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r扩,加入字符</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6373" transform="translate(265.0054702758789,86.9117202758789)"><polygon points="78.91171875,0 157.8234375,-78.91171875 78.91171875,-157.8234375 0,-78.91171875" transform="translate(-78.91171875,78.91171875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-58.4296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">valid==needCnt?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6377" transform="translate(465.2140655517578,86.9117202758789)"><rect rx="0" ry="0" x="-55.296875" y="-19.25" width="110.59375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">缩l,更新最短</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6379" transform="translate(621.6710968017578,55.161720275878906)"><rect rx="0" ry="0" x="-51.16015625" y="-19.25" width="102.3203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-41.16015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">移除s[l],l++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6381" transform="translate(773.4000015258789,129.24258041381836)"><polygon points="50.56875,0 101.1375,-50.56875 50.56875,-101.1375 0,-50.56875" transform="translate(-50.56875,50.56875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-26.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">仍覆盖?</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public String minWindow(String s, String t) {        Map&lt;Character, Integer&gt; need = new HashMap&lt;&gt;();        for (char c : t.toCharArray()) need.merge(c, 1, Integer::sum);        Map&lt;Character, Integer&gt; win = new HashMap&lt;&gt;();        int l = 0, valid = 0, needCnt = need.size(), start = 0, minLen = Integer.MAX_VALUE;        for (int r = 0; r &lt; s.length(); r++) {            char c = s.charAt(r);            if (need.containsKey(c)) {                win.merge(c, 1, Integer::sum);                if (win.get(c).equals(need.get(c))) valid++;            }            while (valid == needCnt) {                if (r - l + 1 &lt; minLen) { minLen = r - l + 1; start = l; }                char d = s.charAt(l++);                if (need.containsKey(d)) {                    if (win.get(d).equals(need.get(d))) valid--;                    win.merge(d, -1, Integer::sum);                }            }        }        return minLen == Integer.MAX_VALUE ? &quot;&quot; : s.substring(start, start + minLen);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(字符集)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>valid</code> 只在&quot;刚好达到需求&quot;时 <code>++</code>、&quot;刚好跌破&quot;时 <code>--</code>。<code>needCnt</code> 用去重后的种类数 <code>need.size()</code>，不是 <code>t.length()</code>。</li></ul><hr /><h1 id="4.-%E7%9F%A9%E9%98%B5" tabindex="-1">4. 矩阵</h1><p>矩阵题集中在原地标记、边界模拟、坐标变换。关键是利用&quot;下标本身承载信息&quot;和&quot;锚点选位简化判断&quot;。</p><h2 id="36.-%E6%9C%89%E6%95%88%E7%9A%84%E6%95%B0%E7%8B%AC" tabindex="-1"><a href="https://leetcode.cn/problems/valid-sudoku/" target="_blank">36. 有效的数独</a></h2><blockquote><p>9×9 数独部分填好，判断当前状态是否合法（每行/每列/每 3×3 宫不重复 1-9）。</p></blockquote><p><strong>思路脉络</strong>：一次遍历，用三个集合数组记录每行/每列/每宫已见数字。宫号 = <code>(i/3)*3 + j/3</code>。遇已见数字即非法。</p><div class="mermaid"><svg id="render495971768" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="223.5734405517578" style="max-width: 940.5656127929688px;" viewBox="0 0 940.5656127929688 223.5734405517578"><style>#render495971768 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render495971768 .error-icon{fill:#552222;}#render495971768 .error-text{fill:#552222;stroke:#552222;}#render495971768 .edge-thickness-normal{stroke-width:2px;}#render495971768 .edge-thickness-thick{stroke-width:3.5px;}#render495971768 .edge-pattern-solid{stroke-dasharray:0;}#render495971768 .edge-pattern-dashed{stroke-dasharray:3;}#render495971768 .edge-pattern-dotted{stroke-dasharray:2;}#render495971768 .marker{fill:#333333;stroke:#333333;}#render495971768 .marker.cross{stroke:#333333;}#render495971768 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render495971768 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render495971768 .cluster-label text{fill:#333;}#render495971768 .cluster-label span{color:#333;}#render495971768 .label text,#render495971768 span{fill:#333;color:#333;}#render495971768 .node rect,#render495971768 .node circle,#render495971768 .node ellipse,#render495971768 .node polygon,#render495971768 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render495971768 .node .label{text-align:center;}#render495971768 .node.clickable{cursor:pointer;}#render495971768 .arrowheadPath{fill:#333333;}#render495971768 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render495971768 .flowchart-link{stroke:#333333;fill:none;}#render495971768 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render495971768 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render495971768 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render495971768 .cluster text{fill:#333;}#render495971768 .cluster span{color:#333;}#render495971768 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render495971768 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M71.65895678226477,117.58633041381836L79.89157856855398,110.88014539082845C88.12420035484318,104.17396036783855,104.5894439274216,90.76159032185872,119.8977602244127,85.08387745483357C135.20607652140382,79.40616458780842,149.3574655428076,81.46310889973795,156.4331600535095,82.49158105570271L163.50885456421142,83.52005321166747" marker-end="url(#arrowhead5146)" style="fill:none"></path><defs><marker id="arrowhead5146" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-A" style="opacity: 1;" id="L-B-A"><path class="path" d="M176.59549727448854,114.51503005036744L167.3386956454071,118.23524677760925C158.0818940163257,121.95546350485108,139.56829075816285,129.39589695933472,124.81148912908141,133.11611368657654C110.0546875,136.83633041381836,99.0546875,136.83633041381836,93.5546875,136.83633041381836L88.0546875,136.83633041381836" marker-end="url(#arrowhead5147)" style="fill:none"></path><defs><marker id="arrowhead5147" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M304.953125,92.4742202758789L310.3697916666667,92.39088694254558C315.7864583333333,92.30755360921223,326.6197916666667,92.14088694254558,337.5364583333333,92.05755360921223C348.453125,91.9742202758789,359.453125,91.9742202758789,364.953125,91.9742202758789L370.453125,91.9742202758789" marker-end="url(#arrowhead5148)" style="fill:none"></path><defs><marker id="arrowhead5148" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M544.5625,91.9742202758789L548.7291666666666,91.9742202758789C552.8958333333334,91.9742202758789,561.2291666666666,91.9742202758789,569.6458335876465,92.05755360921223C578.0625005086263,92.14088694254558,586.5625010172527,92.30755360921223,590.8125012715658,92.39088694254558L595.0625015258789,92.4742202758789" marker-end="url(#arrowhead5149)" style="fill:none"></path><defs><marker id="arrowhead5149" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M751.1444463591286,80.60772760912859L758.5388620579001,79.29380972025363C765.9332777566716,77.9798918313787,780.7221091542148,75.3520560536288,793.6165248529863,74.03813816475385C806.5109405517578,72.7242202758789,817.5109405517578,72.7242202758789,823.0109405517578,72.7242202758789L828.5109405517578,72.7242202758789" marker-end="url(#arrowhead5150)" style="fill:none"></path><defs><marker id="arrowhead5150" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M739.7327233270063,115.75243292299389L749.0290928644648,119.26641583813131C758.3254624019233,122.7803987532687,776.9182014768406,129.80836458354352,795.7999937702316,140.02853252167085C814.6817860636228,150.2487004597982,833.8526315754876,163.66107050577799,843.4380543314201,170.3672555287679L853.0234770873525,177.0734405517578" marker-end="url(#arrowhead5151)" style="fill:none"></path><defs><marker id="arrowhead5151" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-A" style="opacity: 1;" id="L-F-A"><path class="path" d="M830.5382843017578,204.92361512795114L824.7003936767578,205.92775269858558C818.8625030517578,206.93189026922005,807.1867218017578,208.94016541048893,781.8531277974447,209.94430298112334C756.5195337931315,210.9484405517578,717.5281270345052,210.9484405517578,679.8700536092123,210.9484405517578C642.2119801839193,210.9484405517578,605.8872400919596,210.9484405517578,569.0490887959799,210.9484405517578C532.2109375,210.9484405517578,494.859375,210.9484405517578,456.1744791666667,210.9484405517578C417.4895833333333,210.9484405517578,377.4713541666667,210.9484405517578,339.4290364583333,210.9484405517578C301.38671875,210.9484405517578,265.3203125,210.9484405517578,229.25390625,210.9484405517578C193.1875,210.9484405517578,157.12109375,210.9484405517578,130.0780401690786,201.8047555287679C103.0349865881572,192.66107050577799,85.0152856763144,174.3737004597982,76.005435220393,165.23001543680826L66.9955847644716,156.08633041381836" marker-end="url(#arrowhead5152)" style="fill:none"></path><defs><marker id="arrowhead5152" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(121.0546875,136.83633041381836)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000225067138672" height="18.500259399414062"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(337.453125,91.9742202758789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000225067138672" height="18.500259399414062"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(795.5109405517578,72.7242202758789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000225067138672" height="18.500259399414062"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(795.5109405517578,136.83633041381836)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000225067138672" height="18.500259399414062"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6400" transform="translate(48.02734375,136.83633041381836)"><rect rx="0" ry="0" x="-40.02734375" y="-19.25" width="80.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历(i,j)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6401" transform="translate(229.25390625,91.9742202758789)"><polygon points="75.19921875,0 150.3984375,-75.19921875 75.19921875,-150.3984375 0,-75.19921875" transform="translate(-75.19921875,75.19921875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-54.3046875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">board[i][j]=='.'?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6405" transform="translate(457.5078125,91.9742202758789)"><rect rx="0" ry="0" x="-87.0546875" y="-19.25" width="174.109375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-77.0546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">d=val-1,b=(i/3)*3+j/3</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6407" transform="translate(678.5367202758789,91.9742202758789)"><polygon points="83.97421875,0 167.9484375,-83.97421875 83.97421875,-167.9484375 0,-83.97421875" transform="translate(-83.97421875,83.97421875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-64.0546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">row/col/box已见?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6409" transform="translate(880.5382843017578,72.7242202758789)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6411" transform="translate(880.5382843017578,196.3234405517578)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">标记三集合</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0036-valid-sudoku.gif" alt="" /></p><pre><code class="language-java">class Solution {    public boolean isValidSudoku(char[][] board) {        boolean[][] row = new boolean[9][9], col = new boolean[9][9], box = new boolean[9][9];        for (int i = 0; i &lt; 9; i++) {            for (int j = 0; j &lt; 9; j++) {                char c = board[i][j];                if (c == &#39;.&#39;) continue;                int d = c - &#39;1&#39;, b = (i / 3) * 3 + j / 3;                if (row[i][d] || col[j][d] || box[b][d]) return false;                row[i][d] = col[j][d] = box[b][d] = true;            }        }        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(1)（固定 81 格），空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;校验重复&quot;通用：用 boolean 数组记已见。宫号公式 <code>(i/3)*3 + j/3</code> 把 3×3 子块映射到 0-8，记住。</li></ul><h2 id="54.-%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5" tabindex="-1"><a href="https://leetcode.cn/problems/spiral-matrix/" target="_blank">54. 螺旋矩阵</a></h2><blockquote><p>按顺时针螺旋顺序返回矩阵所有元素。</p></blockquote><p><strong>思路脉络</strong>：维护 <code>top/bottom/left/right</code> 四边界，按&quot;上→右→下→左&quot;走边，走完收缩。后两条边前判 <code>top≤bottom</code>/<code>left≤right</code> 防单行单列重复。和热题 100 相同。</p><div class="mermaid"><svg id="render973713996" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="247.49765014648438" style="max-width: 1140.089111328125px;" viewBox="0 0 1140.089111328125 247.49765014648438"><style>#render973713996 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render973713996 .error-icon{fill:#552222;}#render973713996 .error-text{fill:#552222;stroke:#552222;}#render973713996 .edge-thickness-normal{stroke-width:2px;}#render973713996 .edge-thickness-thick{stroke-width:3.5px;}#render973713996 .edge-pattern-solid{stroke-dasharray:0;}#render973713996 .edge-pattern-dashed{stroke-dasharray:3;}#render973713996 .edge-pattern-dotted{stroke-dasharray:2;}#render973713996 .marker{fill:#333333;stroke:#333333;}#render973713996 .marker.cross{stroke:#333333;}#render973713996 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render973713996 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render973713996 .cluster-label text{fill:#333;}#render973713996 .cluster-label span{color:#333;}#render973713996 .label text,#render973713996 span{fill:#333;color:#333;}#render973713996 .node rect,#render973713996 .node circle,#render973713996 .node ellipse,#render973713996 .node polygon,#render973713996 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render973713996 .node .label{text-align:center;}#render973713996 .node.clickable{cursor:pointer;}#render973713996 .arrowheadPath{fill:#333333;}#render973713996 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render973713996 .flowchart-link{stroke:#333333;fill:none;}#render973713996 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render973713996 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render973713996 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render973713996 .cluster text{fill:#333;}#render973713996 .cluster span{color:#333;}#render973713996 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render973713996 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M76,162.87323760986328L80.16666666666667,162.87323760986328C84.33333333333333,162.87323760986328,92.66666666666667,162.87323760986328,101,162.87323760986328C109.33333333333333,162.87323760986328,117.66666666666667,162.87323760986328,121.83333333333333,162.87323760986328L126,162.87323760986328" marker-end="url(#arrowhead5194)" style="fill:none"></path><defs><marker id="arrowhead5194" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M207.10426009045926,143.62323760986328L216.83688340871606,135.76916885375977C226.56950672697283,127.91510009765625,246.03475336348643,112.20696258544922,259.9340433484099,104.3528938293457C273.8333333333333,96.49882507324219,282.1666666666667,96.49882507324219,286.3333333333333,96.49882507324219L290.5,96.49882507324219" marker-end="url(#arrowhead5195)" style="fill:none"></path><defs><marker id="arrowhead5195" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M402.0234375,96.49882507324219L406.1901041666667,96.49882507324219C410.3567708333333,96.49882507324219,418.6901041666667,96.49882507324219,427.1067713419596,96.58215840657552C435.5234385172526,96.66549173990886,444.02343953450526,96.83215840657552,448.27344004313153,96.91549173990886L452.52344055175786,96.99882507324219" marker-end="url(#arrowhead5196)" style="fill:none"></path><defs><marker id="arrowhead5196" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M629.5210906982422,96.99882507324219L633.6044235229492,96.91549173990886C637.6877563476563,96.83215840657552,645.8544219970703,96.66549173990886,654.1877553304037,96.66549173990886C662.521088663737,96.66549173990886,671.0210896809897,96.83215840657552,675.2710901896161,96.91549173990886L679.5210906982423,96.99882507324219" marker-end="url(#arrowhead5197)" style="fill:none"></path><defs><marker id="arrowhead5197" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M834.6515533447266,96.99882507324219L838.7348861694336,96.91549173990886C842.8182189941407,96.83215840657552,850.9848846435547,96.66549173990886,863.9152233371929,103.13088796041457C876.845562030831,109.59628418092029,894.5395737686932,122.69374328859841,903.3865796376244,129.24247284243748L912.2335855065555,135.79120239627653" marker-end="url(#arrowhead5198)" style="fill:none"></path><defs><marker id="arrowhead5198" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-B" style="opacity: 1;" id="L-F-B"><path class="path" d="M912.2335855065555,190.95527282345006L903.3865796376244,197.33733571062245C894.5395737686932,203.71939859779482,876.845562030831,216.4835243721396,850.9043509413596,222.86558725931198C824.9631398518881,229.24765014648438,790.7747294108073,229.24765014648438,756.5863189697266,229.24765014648438C722.3979085286459,229.24765014648438,688.2094980875651,229.24765014648438,652.1988220214844,229.24765014648438C616.1881459554037,229.24765014648438,578.3552042643229,229.24765014648438,540.5222625732422,229.24765014648438C502.68932088216144,229.24765014648438,464.85637919108075,229.24765014648438,432.47962188720703,229.24765014648438C400.1028645833333,229.24765014648438,373.1822916666667,229.24765014648438,346.26171875,229.24765014648438C319.3411458333333,229.24765014648438,292.4205729166667,229.24765014648438,269.2276631400766,221.39358139038086C246.03475336348643,213.53951263427734,226.56950672697283,197.8313751220703,216.83688340871606,189.9773063659668L207.10426009045926,182.12323760986328" marker-end="url(#arrowhead5199)" style="fill:none"></path><defs><marker id="arrowhead5199" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-G" style="opacity: 1;" id="L-F-G"><path class="path" d="M1014.5890502929688,163.37323760986328L1020.0057169596354,163.28990427652994C1025.422383626302,163.20657094319662,1036.2557169596355,163.03990427652994,1047.172383626302,162.95657094319662C1058.0890502929688,162.87323760986328,1069.0890502929688,162.87323760986328,1074.5890502929688,162.87323760986328L1080.0890502929688,162.87323760986328" marker-end="url(#arrowhead5200)" style="fill:none"></path><defs><marker id="arrowhead5200" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(540.5222625732422,229.24765014648438)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000350952148438" height="18.500408172607422"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(1047.0890502929688,162.87323760986328)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000350952148438" height="18.500408172607422"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6428" transform="translate(42,162.87323760986328)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">四边界</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6429" transform="translate(183.25,162.87323760986328)"><rect rx="0" ry="0" x="-57.25" y="-19.25" width="114.5" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47.25,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">→顶行,top++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6431" transform="translate(346.26171875,96.49882507324219)"><rect rx="0" ry="0" x="-55.76171875" y="-19.25" width="111.5234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.76171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">↓右列,right--</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6433" transform="translate(540.5222625732422,96.49882507324219)"><polygon points="88.498828125,0 176.99765625,-88.498828125 88.498828125,-176.99765625 0,-88.498828125" transform="translate(-88.498828125,88.498828125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-69.08203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">top≤bottom?←底行</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6435" transform="translate(756.5863189697266,96.49882507324219)"><polygon points="77.565234375,0 155.13046875,-77.565234375 77.565234375,-155.13046875 0,-77.565234375" transform="translate(-77.565234375,77.565234375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-56.93359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">left≤right?↑左列</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6437" transform="translate(949.1203002929688,162.87323760986328)"><polygon points="64.96875,0 129.9375,-64.96875 64.96875,-129.9375 0,-64.96875" transform="translate(-64.96875,64.96875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-42.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">边界未交叉?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-G-6441" transform="translate(1106.0890502929688,162.87323760986328)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">结束</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;Integer&gt; spiralOrder(int[][] matrix) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        int top = 0, bottom = matrix.length - 1, left = 0, right = matrix[0].length - 1;        while (top &lt;= bottom &amp;&amp; left &lt;= right) {            for (int j = left; j &lt;= right; j++) res.add(matrix[top][j]); top++;            for (int i = top; i &lt;= bottom; i++) res.add(matrix[i][right]); right--;            if (top &lt;= bottom) { for (int j = right; j &gt;= left; j--) res.add(matrix[bottom][j]); bottom--; }            if (left &lt;= right) { for (int i = bottom; i &gt;= top; i--) res.add(matrix[i][left]); left++; }        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(1)。</p></blockquote><h2 id="48.-%E6%97%8B%E8%BD%AC%E5%9B%BE%E5%83%8F" tabindex="-1"><a href="https://leetcode.cn/problems/rotate-image/" target="_blank">48. 旋转图像</a></h2><blockquote><p><code>n×n</code> 矩阵顺时针旋转 90°，原地。</p></blockquote><p><strong>思路脉络</strong>：顺时针 90° = 主对角线转置 + 左右翻转。转置 <code>(i,j)↔(j,i)</code> 只走上三角，行翻转用双指针。和热题 100 相同。</p><div class="mermaid"><svg id="render1043894136" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 607.171875px;" viewBox="0 0 607.171875 54.5"><style>#render1043894136 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1043894136 .error-icon{fill:#552222;}#render1043894136 .error-text{fill:#552222;stroke:#552222;}#render1043894136 .edge-thickness-normal{stroke-width:2px;}#render1043894136 .edge-thickness-thick{stroke-width:3.5px;}#render1043894136 .edge-pattern-solid{stroke-dasharray:0;}#render1043894136 .edge-pattern-dashed{stroke-dasharray:3;}#render1043894136 .edge-pattern-dotted{stroke-dasharray:2;}#render1043894136 .marker{fill:#333333;stroke:#333333;}#render1043894136 .marker.cross{stroke:#333333;}#render1043894136 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1043894136 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1043894136 .cluster-label text{fill:#333;}#render1043894136 .cluster-label span{color:#333;}#render1043894136 .label text,#render1043894136 span{fill:#333;color:#333;}#render1043894136 .node rect,#render1043894136 .node circle,#render1043894136 .node ellipse,#render1043894136 .node polygon,#render1043894136 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1043894136 .node .label{text-align:center;}#render1043894136 .node.clickable{cursor:pointer;}#render1043894136 .arrowheadPath{fill:#333333;}#render1043894136 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1043894136 .flowchart-link{stroke:#333333;fill:none;}#render1043894136 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1043894136 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1043894136 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1043894136 .cluster text{fill:#333;}#render1043894136 .cluster span{color:#333;}#render1043894136 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1043894136 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M76,27.25L80.16666666666667,27.25C84.33333333333333,27.25,92.66666666666667,27.25,101,27.25C109.33333333333333,27.25,117.66666666666667,27.25,121.83333333333333,27.25L126,27.25" marker-end="url(#arrowhead5217)" style="fill:none"></path><defs><marker id="arrowhead5217" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M258,27.25L262.1666666666667,27.25C266.3333333333333,27.25,274.6666666666667,27.25,283,27.25C291.3333333333333,27.25,299.6666666666667,27.25,303.8333333333333,27.25L308,27.25" marker-end="url(#arrowhead5218)" style="fill:none"></path><defs><marker id="arrowhead5218" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M424,27.25L428.1666666666667,27.25C432.3333333333333,27.25,440.6666666666667,27.25,449,27.25C457.3333333333333,27.25,465.6666666666667,27.25,469.8333333333333,27.25L474,27.25" marker-end="url(#arrowhead5219)" style="fill:none"></path><defs><marker id="arrowhead5219" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6448" transform="translate(42,27.25)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">原矩阵</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6449" transform="translate(192,27.25)"><rect rx="0" ry="0" x="-66" y="-19.25" width="132" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">沿主对角线转置</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6451" transform="translate(366,27.25)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">每行左右翻转</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6453" transform="translate(536.5859375,27.25)"><rect rx="0" ry="0" x="-62.5859375" y="-19.25" width="125.171875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-52.5859375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">顺时针90°结果</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public void rotate(int[][] matrix) {        int n = matrix.length;        for (int i = 0; i &lt; n; i++)            for (int j = i + 1; j &lt; n; j++) {                int t = matrix[i][j]; matrix[i][j] = matrix[j][i]; matrix[j][i] = t;            }        for (int[] row : matrix) {            int l = 0, r = n - 1;            while (l &lt; r) { int t = row[l]; row[l] = row[r]; row[r] = t; l++; r--; }        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(1)。</p></blockquote><h2 id="73.-%E7%9F%A9%E9%98%B5%E7%BD%AE%E9%9B%B6" tabindex="-1"><a href="https://leetcode.cn/problems/set-matrix-zeroes/" target="_blank">73. 矩阵置零</a></h2><blockquote><p>若 <code>matrix[i][j]==0</code> 把它所在行列全置零，原地。</p></blockquote><p><strong>思路脉络</strong>：用第一行第一列自身记录&quot;哪些行列要置零&quot;，加两个布尔变量单独标记首行首列。先标记 → 清内部 → 最后清首行首列。和热题 100 相同。</p><div class="mermaid"><svg id="render3015515984" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 740.265625px;" viewBox="0 0 740.265625 54.5"><style>#render3015515984 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3015515984 .error-icon{fill:#552222;}#render3015515984 .error-text{fill:#552222;stroke:#552222;}#render3015515984 .edge-thickness-normal{stroke-width:2px;}#render3015515984 .edge-thickness-thick{stroke-width:3.5px;}#render3015515984 .edge-pattern-solid{stroke-dasharray:0;}#render3015515984 .edge-pattern-dashed{stroke-dasharray:3;}#render3015515984 .edge-pattern-dotted{stroke-dasharray:2;}#render3015515984 .marker{fill:#333333;stroke:#333333;}#render3015515984 .marker.cross{stroke:#333333;}#render3015515984 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3015515984 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3015515984 .cluster-label text{fill:#333;}#render3015515984 .cluster-label span{color:#333;}#render3015515984 .label text,#render3015515984 span{fill:#333;color:#333;}#render3015515984 .node rect,#render3015515984 .node circle,#render3015515984 .node ellipse,#render3015515984 .node polygon,#render3015515984 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3015515984 .node .label{text-align:center;}#render3015515984 .node.clickable{cursor:pointer;}#render3015515984 .arrowheadPath{fill:#333333;}#render3015515984 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3015515984 .flowchart-link{stroke:#333333;fill:none;}#render3015515984 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3015515984 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3015515984 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3015515984 .cluster text{fill:#333;}#render3015515984 .cluster span{color:#333;}#render3015515984 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3015515984 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M124,27.25L128.16666666666666,27.25C132.33333333333334,27.25,140.66666666666666,27.25,149,27.25C157.33333333333334,27.25,165.66666666666666,27.25,169.83333333333334,27.25L174,27.25" marker-end="url(#arrowhead5236)" style="fill:none"></path><defs><marker id="arrowhead5236" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M384.265625,27.25L388.4322916666667,27.25C392.5989583333333,27.25,400.9322916666667,27.25,409.265625,27.25C417.5989583333333,27.25,425.9322916666667,27.25,430.0989583333333,27.25L434.265625,27.25" marker-end="url(#arrowhead5237)" style="fill:none"></path><defs><marker id="arrowhead5237" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M550.265625,27.25L554.4322916666666,27.25C558.5989583333334,27.25,566.9322916666666,27.25,575.265625,27.25C583.5989583333334,27.25,591.9322916666666,27.25,596.0989583333334,27.25L600.265625,27.25" marker-end="url(#arrowhead5238)" style="fill:none"></path><defs><marker id="arrowhead5238" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6460" transform="translate(66,27.25)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">标记首行首列</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6461" transform="translate(279.1328125,27.25)"><rect rx="0" ry="0" x="-105.1328125" y="-19.25" width="210.265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-95.1328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历内部,用首行首列记0位</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6463" transform="translate(492.265625,27.25)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">据标记清内部</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6465" transform="translate(666.265625,27.25)"><rect rx="0" ry="0" x="-66" y="-19.25" width="132" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">最后清首行首列</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public void setZeroes(int[][] matrix) {        int m = matrix.length, n = matrix[0].length;        boolean firstRow = false, firstCol = false;        for (int j = 0; j &lt; n; j++) if (matrix[0][j] == 0) firstRow = true;        for (int i = 0; i &lt; m; i++) if (matrix[i][0] == 0) firstCol = true;        for (int i = 1; i &lt; m; i++)            for (int j = 1; j &lt; n; j++)                if (matrix[i][j] == 0) { matrix[i][0] = 0; matrix[0][j] = 0; }        for (int i = 1; i &lt; m; i++)            for (int j = 1; j &lt; n; j++)                if (matrix[i][0] == 0 || matrix[0][j] == 0) matrix[i][j] = 0;        if (firstRow) for (int j = 0; j &lt; n; j++) matrix[0][j] = 0;        if (firstCol) for (int i = 0; i &lt; m; i++) matrix[i][0] = 0;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须最后清首行首列，否则先清了会用被污染的标记清内部。</li></ul><h2 id="289.-%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F" tabindex="-1"><a href="https://leetcode.cn/problems/game-of-life/" target="_blank">289. 生命游戏</a></h2><blockquote><p>4 条规则演化细胞生死，原地更新矩阵（需同时更新所以不能边算边改）。</p></blockquote><p><strong>思路脉络</strong>：原地难点是&quot;同时更新&quot;——直接改 0/1 会污染后续判断。用<strong>复合状态</strong>：活→死记 <code>-1</code>、死→活记 <code>2</code>，统计活邻居时把 <code>-1</code> 也算活。最后扫一遍把 <code>-1→0</code>、<code>2→1</code>。</p><div class="mermaid"><svg id="render715679897" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="230.2624969482422" style="max-width: 944.9578247070312px;" viewBox="0 0 944.9578247070312 230.2624969482422"><style>#render715679897 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render715679897 .error-icon{fill:#552222;}#render715679897 .error-text{fill:#552222;stroke:#552222;}#render715679897 .edge-thickness-normal{stroke-width:2px;}#render715679897 .edge-thickness-thick{stroke-width:3.5px;}#render715679897 .edge-pattern-solid{stroke-dasharray:0;}#render715679897 .edge-pattern-dashed{stroke-dasharray:3;}#render715679897 .edge-pattern-dotted{stroke-dasharray:2;}#render715679897 .marker{fill:#333333;stroke:#333333;}#render715679897 .marker.cross{stroke:#333333;}#render715679897 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render715679897 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render715679897 .cluster-label text{fill:#333;}#render715679897 .cluster-label span{color:#333;}#render715679897 .label text,#render715679897 span{fill:#333;color:#333;}#render715679897 .node rect,#render715679897 .node circle,#render715679897 .node ellipse,#render715679897 .node polygon,#render715679897 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render715679897 .node .label{text-align:center;}#render715679897 .node.clickable{cursor:pointer;}#render715679897 .arrowheadPath{fill:#333333;}#render715679897 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render715679897 .flowchart-link{stroke:#333333;fill:none;}#render715679897 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render715679897 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render715679897 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render715679897 .cluster text{fill:#333;}#render715679897 .cluster span{color:#333;}#render715679897 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render715679897 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M272.06249027483443,124.88749694824219L280.46223147902873,113.83437220255534C288.861972683223,102.78124745686848,305.66145509161146,80.6749979654948,318.22786296247244,69.62187321980794C330.7942708333333,58.568748474121094,339.1276041666667,58.568748474121094,343.2942708333333,58.568748474121094L347.4609375,58.568748474121094" marker-end="url(#arrowhead5275)" style="fill:none"></path><defs><marker id="arrowhead5275" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M507.3984375,58.568748474121094L511.5651041666667,58.568748474121094C515.7317708333334,58.568748474121094,524.0651041666666,58.568748474121094,532.4817710876465,58.65208180745443C540.8984380086263,58.73541514078776,549.3984385172527,58.90208180745443,553.6484387715658,58.98541514078776L557.8984390258789,59.068748474121094" marker-end="url(#arrowhead5276)" style="fill:none"></path><defs><marker id="arrowhead5276" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M659.0359359741211,59.068748474121094L672.9864565531412,58.98541514078776C686.9369771321614,58.90208180745443,714.8380182902018,58.73541514078776,742.8223930358887,62.96517443823279C770.8067677815756,67.19493373567782,798.8744761149088,75.82111899723456,812.9083302815756,80.13421162801293L826.9421844482422,84.4473042587913" marker-end="url(#arrowhead5277)" style="fill:none"></path><defs><marker id="arrowhead5277" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M635.7728817363957,82.33180576360438L653.6005780217034,97.50775429437734C671.4282743070112,112.68370282515032,707.0836668776268,143.03559988669625,743.7798527462678,162.226886679998C780.4760386149088,181.4181734732997,818.2130177815756,189.44884999835722,837.0815073649088,193.464188260886L855.9499969482422,197.47952652341476" marker-end="url(#arrowhead5278)" style="fill:none"></path><defs><marker id="arrowhead5278" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M826.9421844482422,118.25894116357198L812.9083302815756,122.57203379435033C798.8744761149088,126.88512642512872,770.8067677815756,135.51131168668545,734.3109347025553,139.82440431746383C697.8151016235352,144.1374969482422,652.8911437988281,144.1374969482422,617.8343734741211,144.1374969482422C582.7776031494141,144.1374969482422,557.588020324707,144.1374969482422,527.4984372456869,144.1374969482422C497.4088541666667,144.1374969482422,462.4192708333333,144.1374969482422,427.4296875,144.1374969482422C392.4401041666667,144.1374969482422,357.4505208333333,144.1374969482422,335.7890625,144.1374969482422C314.1276041666667,144.1374969482422,305.7942708333333,144.1374969482422,301.6276041666667,144.1374969482422L297.4609375,144.1374969482422" marker-end="url(#arrowhead5279)" style="fill:none"></path><defs><marker id="arrowhead5279" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M855.9499969482422,205.74396336049875L837.0815073649088,207.726218958456C818.2130177815756,209.70847455641322,780.4760386149088,213.67298575232772,739.145570119222,215.65524135028497C697.8151016235352,217.6374969482422,652.8911437988281,217.6374969482422,617.8343734741211,217.6374969482422C582.7776031494141,217.6374969482422,557.588020324707,217.6374969482422,527.4984372456869,217.6374969482422C497.4088541666667,217.6374969482422,462.4192708333333,217.6374969482422,427.4296875,217.6374969482422C392.4401041666667,217.6374969482422,357.4505208333333,217.6374969482422,331.95633370535717,208.59583028157553C306.46214657738096,199.55416361490884,290.46335565476187,181.47083028157553,282.46396019345235,172.42916361490884L274.46456473214283,163.3874969482422" marker-end="url(#arrowhead5280)" style="fill:none"></path><defs><marker id="arrowhead5280" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-A" style="opacity: 1;" id="L-F-A"><path class="path" d="M167.40625,144.1374969482422L171.57291666666666,144.1374969482422C175.73958333333334,144.1374969482422,184.07291666666666,144.1374969482422,192.40625,144.1374969482422C200.73958333333334,144.1374969482422,209.07291666666666,144.1374969482422,213.23958333333334,144.1374969482422L217.40625,144.1374969482422" marker-end="url(#arrowhead5281)" style="fill:none"></path><defs><marker id="arrowhead5281" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(742.7390594482422,58.568748474121094)"><g transform="translate(-59.203125,-9.25)" class="label"><rect rx="0" ry="0" width="118.37741088867188" height="18.500377655029297"></rect><text><tspan xml:space="preserve" dy="1em" x="1">live&amp;lt;2或&amp;gt;3</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(742.7390594482422,173.3874969482422)"><g transform="translate(-37.703125,-9.25)" class="label"><rect rx="0" ry="0" width="75.38434600830078" height="18.500377655029297"></rect><text><tspan xml:space="preserve" dy="1em" x="1">live==2或3</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6481" transform="translate(257.43359375,144.1374969482422)"><rect rx="0" ry="0" x="-40.02734375" y="-19.25" width="80.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历(i,j)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6482" transform="translate(427.4296875,58.568748474121094)"><rect rx="0" ry="0" x="-79.96875" y="-19.25" width="159.9375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-69.96875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">统计8邻居(abs还原)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6484" transform="translate(607.9671859741211,58.568748474121094)"><polygon points="50.56875,0 101.1375,-50.56875 50.56875,-101.1375 0,-50.56875" transform="translate(-50.56875,50.56875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-26.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">活细胞?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6486" transform="translate(881.9499969482422,101.35312271118164)"><rect rx="0" ry="0" x="-55.0078125" y="-19.25" width="110.015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.0078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">记-1(活→死)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6488" transform="translate(881.9499969482422,203.0124969482422)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">不变</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6489" transform="translate(87.703125,144.1374969482422)"><rect rx="0" ry="0" x="-79.703125" y="-19.25" width="159.40625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-69.703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">死细胞live==3→记2</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0289-game-life.gif" alt="" /></p><pre><code class="language-java">class Solution {    public void gameOfLife(int[][] board) {        int m = board.length, n = board[0].length;        int[] dx = {-1,-1,-1,0,0,1,1,1}, dy = {-1,0,1,-1,1,-1,0,1};        for (int i = 0; i &lt; m; i++) {            for (int j = 0; j &lt; n; j++) {                int live = 0;                for (int k = 0; k &lt; 8; k++) {                    int x = i + dx[k], y = j + dy[k];                    if (x &gt;= 0 &amp;&amp; x &lt; m &amp;&amp; y &gt;= 0 &amp;&amp; y &lt; n &amp;&amp; Math.abs(board[x][y]) == 1) live++;                }                if (board[i][j] == 1 &amp;&amp; (live &lt; 2 || live &gt; 3)) board[i][j] = -1;                else if (board[i][j] == 0 &amp;&amp; live == 3) board[i][j] = 2;            }        }        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++)                board[i][j] = board[i][j] &gt; 0 ? 1 : 0;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;同时更新&quot;矩阵题通用技巧：用复合状态值（原值+新值编码），统计时 <code>abs()</code> 还原原值。比开额外数组省空间。</li></ul><hr /><h1 id="5.-%E5%93%88%E5%B8%8C%E8%A1%A8" tabindex="-1">5. 哈希表</h1><p>哈希的本质是<strong>建立逆向映射</strong>，把&quot;查找&quot;从 O(n) 降到 O(1)。这里聚焦三类：计数比较、模式映射、存在性查询。</p><h2 id="383.-%E8%B5%8E%E9%87%91%E4%BF%A1" tabindex="-1"><a href="https://leetcode.cn/problems/ransom-note/" target="_blank">383. 赎金信</a></h2><blockquote><p><code>ransomNote</code> 能否由 <code>magazine</code> 的字母组成（magazine 每个字母只用一次）。</p></blockquote><p><strong>思路脉络</strong>：统计 magazine 字母频次，遍历 ransomNote 扣减，某字母不够即 false。哈希或 <code>int[26]</code> 都行，字母范围固定用数组更快。</p><div class="mermaid"><svg id="render3812516495" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="150.10000610351562" style="max-width: 741.4828491210938px;" viewBox="0 0 741.4828491210938 150.10000610351562"><style>#render3812516495 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3812516495 .error-icon{fill:#552222;}#render3812516495 .error-text{fill:#552222;stroke:#552222;}#render3812516495 .edge-thickness-normal{stroke-width:2px;}#render3812516495 .edge-thickness-thick{stroke-width:3.5px;}#render3812516495 .edge-pattern-solid{stroke-dasharray:0;}#render3812516495 .edge-pattern-dashed{stroke-dasharray:3;}#render3812516495 .edge-pattern-dotted{stroke-dasharray:2;}#render3812516495 .marker{fill:#333333;stroke:#333333;}#render3812516495 .marker.cross{stroke:#333333;}#render3812516495 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3812516495 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3812516495 .cluster-label text{fill:#333;}#render3812516495 .cluster-label span{color:#333;}#render3812516495 .label text,#render3812516495 span{fill:#333;color:#333;}#render3812516495 .node rect,#render3812516495 .node circle,#render3812516495 .node ellipse,#render3812516495 .node polygon,#render3812516495 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3812516495 .node .label{text-align:center;}#render3812516495 .node.clickable{cursor:pointer;}#render3812516495 .arrowheadPath{fill:#333333;}#render3812516495 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3812516495 .flowchart-link{stroke:#333333;fill:none;}#render3812516495 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3812516495 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3812516495 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3812516495 .cluster text{fill:#333;}#render3812516495 .cluster span{color:#333;}#render3812516495 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3812516495 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M145.03543307086613,55.80000305175781L151.6545275590551,53.71666971842448C158.2736220472441,51.63333638509115,171.51181102362204,47.46666971842448,182.29757217847768,45.38333638509115C193.08333333333334,43.30000305175781,201.41666666666666,43.30000305175781,205.58333333333334,43.30000305175781L209.75,43.30000305175781" marker-end="url(#arrowhead5304)" style="fill:none"></path><defs><marker id="arrowhead5304" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M379.328125,43.30000305175781L383.4947916666667,43.30000305175781C387.6614583333333,43.30000305175781,395.9947916666667,43.30000305175781,407.2774212621185,45.80903961434631C418.5600508575704,48.3180761769348,432.79197671514083,53.33614930211181,439.907939643926,55.845185864700305L447.02390257271117,58.3542224272888" marker-end="url(#arrowhead5305)" style="fill:none"></path><defs><marker id="arrowhead5305" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M563.9281219482422,75.55000305175781L569.3447901407877,75.46666971842448C574.7614583333333,75.38333638509114,585.5947947184245,75.21666971842448,596.5114629109701,75.13333638509114C607.4281311035156,75.05000305175781,618.4281311035156,75.05000305175781,623.9281311035156,75.05000305175781L629.4281311035156,75.05000305175781" marker-end="url(#arrowhead5306)" style="fill:none"></path><defs><marker id="arrowhead5306" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M447.02390711090675,92.74578211090683L439.9079434257556,95.08815226771533C432.7919797406045,97.43052242452382,418.56005237030223,102.11526273814081,393.1459116018178,104.45763289494931C367.7317708333333,106.80000305175781,331.1354166666667,106.80000305175781,294.5390625,106.80000305175781C257.9427083333333,106.80000305175781,221.34635416666666,106.80000305175781,196.42908259514434,104.71666971842448C171.51181102362204,102.63333638509114,158.2736220472441,98.46666971842448,151.6545275590551,96.38333638509114L145.03543307086613,94.30000305175781" marker-end="url(#arrowhead5307)" style="fill:none"></path><defs><marker id="arrowhead5307" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(596.4281311035156,75.05000305175781)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000667572021484" height="18.50077247619629"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(294.5390625,106.80000305175781)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000667572021484" height="18.50077247619629"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6504" transform="translate(83.875,75.05000305175781)"><rect rx="0" ry="0" x="-75.875" y="-19.25" width="151.75" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-65.875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">统计magazine频次</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6505" transform="translate(294.5390625,43.30000305175781)"><rect rx="0" ry="0" x="-84.7890625" y="-19.25" width="169.578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-74.7890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历ransomNote扣减</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6507" transform="translate(496.3781280517578,75.05000305175781)"><polygon points="67.05,0 134.1,-67.05 67.05,-134.1 0,-67.05" transform="translate(-67.05,67.05)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-45.25,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">某字母&amp;lt;0?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6509" transform="translate(681.4554748535156,75.05000305175781)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean canConstruct(String ransomNote, String magazine) {        int[] cnt = new int[26];        for (char c : magazine.toCharArray()) cnt[c - &#39;a&#39;]++;        for (char c : ransomNote.toCharArray()) {            if (--cnt[c - &#39;a&#39;] &lt; 0) return false;        }        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(m+n)，空间 O(1)。</p></blockquote><h2 id="205.-%E5%90%8C%E6%9E%84%E5%AD%97%E7%AC%A6%E4%B8%B2" tabindex="-1"><a href="https://leetcode.cn/problems/isomorphic-strings/" target="_blank">205. 同构字符串</a></h2><blockquote><p><code>s</code> 和 <code>t</code> 能否双射——<code>s</code> 的每个字符映射到 <code>t</code> 对应字符，且映射一一对应。</p></blockquote><p><strong>思路脉络</strong>：双射要求&quot;同一字符映射恒定&quot;且&quot;无两个字符映射到同一目标&quot;。两个 map 互查：<code>s2t</code> 记 s→t、<code>t2s</code> 记 t→s。遇不一致即 false。</p><div class="mermaid"><svg id="render4146552499" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="218.8671875" style="max-width: 580.1015625px;" viewBox="0 1 580.1015625 218.8671875"><style>#render4146552499 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render4146552499 .error-icon{fill:#552222;}#render4146552499 .error-text{fill:#552222;stroke:#552222;}#render4146552499 .edge-thickness-normal{stroke-width:2px;}#render4146552499 .edge-thickness-thick{stroke-width:3.5px;}#render4146552499 .edge-pattern-solid{stroke-dasharray:0;}#render4146552499 .edge-pattern-dashed{stroke-dasharray:3;}#render4146552499 .edge-pattern-dotted{stroke-dasharray:2;}#render4146552499 .marker{fill:#333333;stroke:#333333;}#render4146552499 .marker.cross{stroke:#333333;}#render4146552499 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render4146552499 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render4146552499 .cluster-label text{fill:#333;}#render4146552499 .cluster-label span{color:#333;}#render4146552499 .label text,#render4146552499 span{fill:#333;color:#333;}#render4146552499 .node rect,#render4146552499 .node circle,#render4146552499 .node ellipse,#render4146552499 .node polygon,#render4146552499 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render4146552499 .node .label{text-align:center;}#render4146552499 .node.clickable{cursor:pointer;}#render4146552499 .arrowheadPath{fill:#333333;}#render4146552499 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render4146552499 .flowchart-link{stroke:#333333;fill:none;}#render4146552499 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render4146552499 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render4146552499 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render4146552499 .cluster text{fill:#333;}#render4146552499 .cluster span{color:#333;}#render4146552499 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render4146552499 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M58.980190998487146,92.99609375L64.0772424987393,88.67350260416667C69.17429399899143,84.35091145833333,79.36839699949572,75.70572916666667,88.71544849974786,71.46647135416667C98.0625,67.22721354166667,106.5625,67.39388020833333,110.8125,67.47721354166667L115.0625,67.560546875" marker-end="url(#arrowhead5338)" style="fill:none"></path><defs><marker id="arrowhead5338" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M205.84498483414959,47.6008442091496L214.58826861179134,42.542370174291335C223.33155238943309,37.483896139433064,240.81811994471653,27.366948069716532,264.28991934735825,22.308474034858268C287.76171875,17.25,317.21875,17.25,346.67578125,17.25C376.1328125,17.25,405.58984375,17.25,425.818359375,18.196019203381265C446.046875,19.14203840676253,457.046875,21.03407681352506,462.546875,21.980096016906327L468.046875,22.926115220287592" marker-end="url(#arrowhead5339)" style="fill:none"></path><defs><marker id="arrowhead5339" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M205.84498483414959,87.5202495408504L214.58826861179134,92.41205690904201C223.33155238943309,97.30386427723359,240.81811994471653,107.0874790136168,255.1447370556916,112.06261971514174C269.4713541666667,117.03776041666667,280.6380208333333,117.20442708333333,286.2213541666667,117.28776041666667L291.8046875,117.37109375" marker-end="url(#arrowhead5340)" style="fill:none"></path><defs><marker id="arrowhead5340" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-C" style="opacity: 1;" id="L-D-C"><path class="path" d="M386.77870418533934,101.60292293533931L394.8233993211161,98.28336025861609C402.86809445689295,94.96379758189288,418.95748472844645,88.32467222844643,435.6967311435253,79.91168507255655C452.43597755860424,71.49869791666667,469.8250801172085,61.311848958333336,478.51963139651053,56.218424479166664L487.21418267581265,51.125" marker-end="url(#arrowhead5341)" style="fill:none"></path><defs><marker id="arrowhead5341" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M388.3187781102713,131.5991906397287L396.10679425855943,134.2379323039406C403.8948104068475,136.87667396815246,419.4708427034238,142.15415729657624,435.39284125684304,149.11549010662145C451.3148398102622,156.07682291666666,467.5828046205244,164.72200520833334,475.71678702565555,169.04459635416666L483.85076943078667,173.3671875" marker-end="url(#arrowhead5342)" style="fill:none"></path><defs><marker id="arrowhead5342" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M478.07421875,199.8413341440024L470.9029947916667,201.07480970333532C463.7317708333333,202.30828526266828,449.3893229166667,204.77523638133414,427.4895833333333,206.00871194066704C405.58984375,207.2421875,376.1328125,207.2421875,346.67578125,207.2421875C317.21875,207.2421875,287.76171875,207.2421875,258.3046875,207.2421875C228.84765625,207.2421875,199.390625,207.2421875,171.26692708333334,207.2421875C143.14322916666666,207.2421875,116.35286458333333,207.2421875,95.87695858626485,194.61783854166666C75.40105258919637,181.99348958333334,61.23960517839274,156.74479166666666,54.158881472990934,144.12044270833334L47.078157767589126,131.49609375" marker-end="url(#arrowhead5343)" style="fill:none"></path><defs><marker id="arrowhead5343" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(346.67578125,17.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(258.3046875,116.87109375)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(435.046875,81.685546875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(435.046875,147.431640625)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6524" transform="translate(36.28125,112.24609375)"><rect rx="0" ry="0" x="-28.28125" y="-19.25" width="56.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6525" transform="translate(169.93359375,67.060546875)"><polygon points="55.37109375,0 110.7421875,-55.37109375 55.37109375,-110.7421875 0,-55.37109375" transform="translate(-55.37109375,55.37109375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-32.2734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">s2t[a]≠b?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6527" transform="translate(520.07421875,31.875)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6529" transform="translate(346.67578125,116.87109375)"><polygon points="55.37109375,0 110.7421875,-55.37109375 55.37109375,-110.7421875 0,-55.37109375" transform="translate(-55.37109375,55.37109375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-32.2734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">t2s[b]≠a?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6533" transform="translate(520.07421875,192.6171875)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">记录映射</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isIsomorphic(String s, String t) {        Map&lt;Character, Character&gt; s2t = new HashMap&lt;&gt;(), t2s = new HashMap&lt;&gt;();        for (int i = 0; i &lt; s.length(); i++) {            char a = s.charAt(i), b = t.charAt(i);            if (s2t.containsKey(a) &amp;&amp; s2t.get(a) != b) return false;            if (t2s.containsKey(b) &amp;&amp; t2s.get(b) != a) return false;            s2t.put(a, b); t2s.put(b, a);        }        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(字符集)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须两个 map 双向校验。单 map 只保证&quot;s→t 恒定&quot;，不保证&quot;不同 s 不映射到同一 t&quot;（如 <code>ab</code>→<code>aa</code> 单 map 会误判 true）。</li></ul><h2 id="290.-%E5%8D%95%E8%AF%8D%E8%A7%84%E5%BE%8B" tabindex="-1"><a href="https://leetcode.cn/problems/word-pattern/" target="_blank">290. 单词规律</a></h2><blockquote><p><code>pattern</code> 字母和 <code>s</code> 单词之间是否满足双射规律。</p></blockquote><p><strong>思路脉络</strong>：和同构字符串同构——把&quot;字母↔单词&quot;看成双射。两个 map 互查，注意拆词。</p><div class="mermaid"><svg id="render3105738275" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="117.13749694824219" style="max-width: 859.6218872070312px;" viewBox="0 0 859.6218872070312 117.13749694824219"><style>#render3105738275 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3105738275 .error-icon{fill:#552222;}#render3105738275 .error-text{fill:#552222;stroke:#552222;}#render3105738275 .edge-thickness-normal{stroke-width:2px;}#render3105738275 .edge-thickness-thick{stroke-width:3.5px;}#render3105738275 .edge-pattern-solid{stroke-dasharray:0;}#render3105738275 .edge-pattern-dashed{stroke-dasharray:3;}#render3105738275 .edge-pattern-dotted{stroke-dasharray:2;}#render3105738275 .marker{fill:#333333;stroke:#333333;}#render3105738275 .marker.cross{stroke:#333333;}#render3105738275 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3105738275 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3105738275 .cluster-label text{fill:#333;}#render3105738275 .cluster-label span{color:#333;}#render3105738275 .label text,#render3105738275 span{fill:#333;color:#333;}#render3105738275 .node rect,#render3105738275 .node circle,#render3105738275 .node ellipse,#render3105738275 .node polygon,#render3105738275 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3105738275 .node .label{text-align:center;}#render3105738275 .node.clickable{cursor:pointer;}#render3105738275 .arrowheadPath{fill:#333333;}#render3105738275 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3105738275 .flowchart-link{stroke:#333333;fill:none;}#render3105738275 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3105738275 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3105738275 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3105738275 .cluster text{fill:#333;}#render3105738275 .cluster span{color:#333;}#render3105738275 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3105738275 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M159.2570564516129,39.318748474121094L164.2454637096774,38.006248474121094C169.23387096774192,36.693748474121094,179.21068548387098,34.068748474121094,188.36575940860214,32.756248474121094C197.52083333333334,31.443748474121094,205.85416666666666,31.443748474121094,210.02083333333334,31.443748474121094L214.1875,31.443748474121094" marker-end="url(#arrowhead5373)" style="fill:none"></path><defs><marker id="arrowhead5373" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M348.4296875,31.443748474121094L353.9296875,31.443748474121094C359.4296875,31.443748474121094,370.4296875,31.443748474121094,381.4296875,31.443748474121094C392.4296875,31.443748474121094,403.4296875,31.443748474121094,408.9296875,31.443748474121094L414.4296875,31.443748474121094" marker-end="url(#arrowhead5374)" style="fill:none"></path><defs><marker id="arrowhead5374" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M530.4296875,31.443748474121094L534.5963541666666,31.443748474121094C538.7630208333334,31.443748474121094,547.0963541666666,31.443748474121094,557.7391826624781,33.82175356595615C568.3820111582896,36.1997586577912,581.3343348165791,40.955768841461314,587.810496645724,43.333773933296364L594.2866584748688,45.71177902513142" marker-end="url(#arrowhead5375)" style="fill:none"></path><defs><marker id="arrowhead5375" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M682.0671859741212,59.06874847412109L687.4838523864747,58.98541514078775C692.9005187988282,58.90208180745443,703.7338516235353,58.73541514078776,714.6505180358887,58.65208180745443C725.5671844482422,58.568748474121094,736.5671844482422,58.568748474121094,742.0671844482422,58.568748474121094L747.5671844482422,58.568748474121094" marker-end="url(#arrowhead5376)" style="fill:none"></path><defs><marker id="arrowhead5376" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M594.2866562291865,72.42571872918651L587.8104947743221,74.63705702000895C581.3343333194576,76.84839531083138,568.3820104097289,81.27107189247623,548.0725156215311,83.48241018329867C527.7630208333334,85.6937484741211,500.0963541666667,85.6937484741211,471.0963541666667,85.6937484741211C442.0963541666667,85.6937484741211,411.7630208333333,85.6937484741211,379.9095052083333,85.6937484741211C348.0559895833333,85.6937484741211,314.6822916666667,85.6937484741211,282.6419270833333,85.6937484741211C250.6015625,85.6937484741211,219.89453125,85.6937484741211,199.5526083669355,84.3812484741211C179.21068548387098,83.0687484741211,169.23387096774192,80.4437484741211,164.2454637096774,79.1312484741211L159.2570564516129,77.8187484741211" marker-end="url(#arrowhead5377)" style="fill:none"></path><defs><marker id="arrowhead5377" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(714.5671844482422,58.568748474121094)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000640869140625" height="18.50074005126953"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(381.4296875,85.6937484741211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000640869140625" height="18.50074005126953"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6546" transform="translate(86.09375,58.568748474121094)"><rect rx="0" ry="0" x="-78.09375" y="-19.25" width="156.1875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-68.09375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历pattern/words</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6547" transform="translate(281.30859375,31.443748474121094)"><rect rx="0" ry="0" x="-67.12109375" y="-19.25" width="134.2421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-57.12109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">长度不等→false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6549" transform="translate(472.4296875,31.443748474121094)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">双向映射校验</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6551" transform="translate(630.9984359741211,58.568748474121094)"><polygon points="50.56875,0 101.1375,-50.56875 50.56875,-101.1375 0,-50.56875" transform="translate(-50.56875,50.56875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-26.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">不一致?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6553" transform="translate(799.5945281982422,58.568748474121094)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean wordPattern(String pattern, String s) {        String[] words = s.split(&quot; &quot;);        if (words.length != pattern.length()) return false;        Map&lt;Character, String&gt; p2w = new HashMap&lt;&gt;();        Map&lt;String, Character&gt; w2p = new HashMap&lt;&gt;();        for (int i = 0; i &lt; pattern.length(); i++) {            char c = pattern.charAt(i); String w = words[i];            if (p2w.containsKey(c) &amp;&amp; !p2w.get(c).equals(w)) return false;            if (w2p.containsKey(w) &amp;&amp; w2p.get(w) != c) return false;            p2w.put(c, w); w2p.put(w, c);        }        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>长度不等先 return。字符串比较用 <code>.equals()</code>，字符可用 <code>!=</code>（基本类型）。<code>w2p.get(w) != c</code> 是 Character 拆箱成 char 比较，安全。</li></ul><h2 id="242.-%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D" tabindex="-1"><a href="https://leetcode.cn/problems/valid-anagram/" target="_blank">242. 有效的字母异位词</a></h2><blockquote><p><code>s</code> 和 <code>t</code> 是否字母异位词（字母构成相同）。</p></blockquote><p><strong>思路脉络</strong>：<code>int[26]</code> 计数，s 加 t 减，最后全 0 即是。</p><div class="mermaid"><svg id="render3629812378" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 525.6953125px;" viewBox="0 0 525.6953125 143"><style>#render3629812378 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3629812378 .error-icon{fill:#552222;}#render3629812378 .error-text{fill:#552222;stroke:#552222;}#render3629812378 .edge-thickness-normal{stroke-width:2px;}#render3629812378 .edge-thickness-thick{stroke-width:3.5px;}#render3629812378 .edge-pattern-solid{stroke-dasharray:0;}#render3629812378 .edge-pattern-dashed{stroke-dasharray:3;}#render3629812378 .edge-pattern-dotted{stroke-dasharray:2;}#render3629812378 .marker{fill:#333333;stroke:#333333;}#render3629812378 .marker.cross{stroke:#333333;}#render3629812378 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3629812378 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3629812378 .cluster-label text{fill:#333;}#render3629812378 .cluster-label span{color:#333;}#render3629812378 .label text,#render3629812378 span{fill:#333;color:#333;}#render3629812378 .node rect,#render3629812378 .node circle,#render3629812378 .node ellipse,#render3629812378 .node polygon,#render3629812378 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3629812378 .node .label{text-align:center;}#render3629812378 .node.clickable{cursor:pointer;}#render3629812378 .arrowheadPath{fill:#333333;}#render3629812378 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3629812378 .flowchart-link{stroke:#333333;fill:none;}#render3629812378 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3629812378 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3629812378 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3629812378 .cluster text{fill:#333;}#render3629812378 .cluster span{color:#333;}#render3629812378 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3629812378 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M83.2578125,71.5L87.42447916666667,71.5C91.59114583333333,71.5,99.92447916666667,71.5,108.2578125,71.5C116.59114583333333,71.5,124.92447916666667,71.5,129.09114583333334,71.5L133.2578125,71.5" marker-end="url(#arrowhead5397)" style="fill:none"></path><defs><marker id="arrowhead5397" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M203.3515625,71.5L207.51822916666666,71.5C211.68489583333334,71.5,220.01822916666666,71.5,228.43489583333334,71.58333333333333C236.8515625,71.66666666666667,245.3515625,71.83333333333333,249.6015625,71.91666666666667L253.8515625,72" marker-end="url(#arrowhead5398)" style="fill:none"></path><defs><marker id="arrowhead5398" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M331.37022933349033,55.22960433349034L339.5819619445753,50.56633694457528C347.79369455566024,45.90306955566023,364.21715977783015,36.57653477783011,378.2798038472484,31.913267388915056C392.3424479166667,27.25,404.0442708333333,27.25,409.8951822916667,27.25L415.74609375,27.25" marker-end="url(#arrowhead5399)" style="fill:none"></path><defs><marker id="arrowhead5399" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M331.37022933349033,88.77039566650966L339.5819619445753,93.26699638875805C347.79369455566024,97.76359711100645,364.21715977783015,106.75679855550322,377.9288923889151,111.25339927775161C391.640625,115.75,402.640625,115.75,408.140625,115.75L413.640625,115.75" marker-end="url(#arrowhead5400)" style="fill:none"></path><defs><marker id="arrowhead5400" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(380.640625,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(380.640625,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6564" transform="translate(45.62890625,71.5)"><rect rx="0" ry="0" x="-37.62890625" y="-19.25" width="75.2578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27.62890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">s计数++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6565" transform="translate(168.3046875,71.5)"><rect rx="0" ry="0" x="-35.046875" y="-19.25" width="70.09375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-25.046875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">t计数--</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6567" transform="translate(300.49609375,71.5)"><polygon points="47.14453125,0 94.2890625,-47.14453125 47.14453125,-94.2890625 0,-47.14453125" transform="translate(-47.14453125,47.14453125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-23.1328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">全为0?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6569" transform="translate(465.66796875,27.25)"><rect rx="0" ry="0" x="-49.921875" y="-19.25" width="99.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return true</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6571" transform="translate(465.66796875,115.75)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isAnagram(String s, String t) {        if (s.length() != t.length()) return false;        int[] cnt = new int[26];        for (char c : s.toCharArray()) cnt[c - &#39;a&#39;]++;        for (char c : t.toCharArray()) cnt[c - &#39;a&#39;]--;        for (int n : cnt) if (n != 0) return false;        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="49.-%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D%E5%88%86%E7%BB%84" tabindex="-1"><a href="https://leetcode.cn/problems/group-anagrams/" target="_blank">49. 字母异位词分组</a></h2><blockquote><p>把互为字母异位词的字符串归到一组。</p></blockquote><p><strong>思路脉络</strong>：异位词本质是&quot;字母构成相同&quot;，找规范化 key。排序法：字符串字符排序作 key，同 key 分组。和热题 100 相同。</p><div class="mermaid"><svg id="render3415130028" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 607.2734375px;" viewBox="0 0 607.2734375 54.5"><style>#render3415130028 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3415130028 .error-icon{fill:#552222;}#render3415130028 .error-text{fill:#552222;stroke:#552222;}#render3415130028 .edge-thickness-normal{stroke-width:2px;}#render3415130028 .edge-thickness-thick{stroke-width:3.5px;}#render3415130028 .edge-pattern-solid{stroke-dasharray:0;}#render3415130028 .edge-pattern-dashed{stroke-dasharray:3;}#render3415130028 .edge-pattern-dotted{stroke-dasharray:2;}#render3415130028 .marker{fill:#333333;stroke:#333333;}#render3415130028 .marker.cross{stroke:#333333;}#render3415130028 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3415130028 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3415130028 .cluster-label text{fill:#333;}#render3415130028 .cluster-label span{color:#333;}#render3415130028 .label text,#render3415130028 span{fill:#333;color:#333;}#render3415130028 .node rect,#render3415130028 .node circle,#render3415130028 .node ellipse,#render3415130028 .node polygon,#render3415130028 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3415130028 .node .label{text-align:center;}#render3415130028 .node.clickable{cursor:pointer;}#render3415130028 .arrowheadPath{fill:#333333;}#render3415130028 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3415130028 .flowchart-link{stroke:#333333;fill:none;}#render3415130028 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3415130028 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3415130028 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3415130028 .cluster text{fill:#333;}#render3415130028 .cluster span{color:#333;}#render3415130028 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3415130028 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M79.0390625,27.25L83.20572916666667,27.25C87.37239583333333,27.25,95.70572916666667,27.25,104.0390625,27.25C112.37239583333333,27.25,120.70572916666667,27.25,124.87239583333333,27.25L129.0390625,27.25" marker-end="url(#arrowhead5417)" style="fill:none"></path><defs><marker id="arrowhead5417" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M253.7265625,27.25L257.8932291666667,27.25C262.0598958333333,27.25,270.3932291666667,27.25,278.7265625,27.25C287.0598958333333,27.25,295.3932291666667,27.25,299.5598958333333,27.25L303.7265625,27.25" marker-end="url(#arrowhead5418)" style="fill:none"></path><defs><marker id="arrowhead5418" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M386.328125,27.25L390.4947916666667,27.25C394.6614583333333,27.25,402.9947916666667,27.25,411.328125,27.25C419.6614583333333,27.25,427.9947916666667,27.25,432.1614583333333,27.25L436.328125,27.25" marker-end="url(#arrowhead5419)" style="fill:none"></path><defs><marker id="arrowhead5419" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6578" transform="translate(43.51953125,27.25)"><rect rx="0" ry="0" x="-35.51953125" y="-19.25" width="71.0390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-25.51953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历str</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6579" transform="translate(191.3828125,27.25)"><rect rx="0" ry="0" x="-62.34375" y="-19.25" width="124.6875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-52.34375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">排序字符作key</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6581" transform="translate(345.02734375,27.25)"><rect rx="0" ry="0" x="-41.30078125" y="-19.25" width="82.6015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.30078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">map分组</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6583" transform="translate(517.80078125,27.25)"><rect rx="0" ry="0" x="-81.47265625" y="-19.25" width="162.9453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-71.47265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return map.values()</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;List&lt;String&gt;&gt; groupAnagrams(String[] strs) {        Map&lt;String, List&lt;String&gt;&gt; map = new HashMap&lt;&gt;();        for (String s : strs) {            char[] cs = s.toCharArray(); Arrays.sort(cs);            map.computeIfAbsent(new String(cs), k -&gt; new ArrayList&lt;&gt;()).add(s);        }        return new ArrayList&lt;&gt;(map.values());    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n·k log k)，空间 O(n·k)。</p></blockquote><h2 id="1.-%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/two-sum/" target="_blank">1. 两数之和</a></h2><blockquote><p>数组找和为 <code>target</code> 的两个元素下标。</p></blockquote><p><strong>思路脉络</strong>：边查边存。固定 <code>num</code> 要找的是 <code>target-num</code>，哈希 O(1) 查。先查再存避免用到自己。和热题 100 相同。</p><div class="mermaid"><svg id="render3063666294" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="228.1437530517578" style="max-width: 656.2921752929688px;" viewBox="0 0 656.2921752929688 228.1437530517578"><style>#render3063666294 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3063666294 .error-icon{fill:#552222;}#render3063666294 .error-text{fill:#552222;stroke:#552222;}#render3063666294 .edge-thickness-normal{stroke-width:2px;}#render3063666294 .edge-thickness-thick{stroke-width:3.5px;}#render3063666294 .edge-pattern-solid{stroke-dasharray:0;}#render3063666294 .edge-pattern-dashed{stroke-dasharray:3;}#render3063666294 .edge-pattern-dotted{stroke-dasharray:2;}#render3063666294 .marker{fill:#333333;stroke:#333333;}#render3063666294 .marker.cross{stroke:#333333;}#render3063666294 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3063666294 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3063666294 .cluster-label text{fill:#333;}#render3063666294 .cluster-label span{color:#333;}#render3063666294 .label text,#render3063666294 span{fill:#333;color:#333;}#render3063666294 .node rect,#render3063666294 .node circle,#render3063666294 .node ellipse,#render3063666294 .node polygon,#render3063666294 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3063666294 .node .label{text-align:center;}#render3063666294 .node.clickable{cursor:pointer;}#render3063666294 .arrowheadPath{fill:#333333;}#render3063666294 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3063666294 .flowchart-link{stroke:#333333;fill:none;}#render3063666294 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3063666294 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3063666294 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3063666294 .cluster text{fill:#333;}#render3063666294 .cluster span{color:#333;}#render3063666294 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3063666294 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M118.14165835770893,135.63906478881836L129.8094548814241,128.74245007832846C141.4772514051393,121.84583536783855,164.81284445256964,108.05260594685872,180.73064123059797,101.23932456970215C196.6484380086263,94.42604319254558,205.14843851725263,94.59270985921223,209.39843877156576,94.67604319254558L213.6484390258789,94.7593765258789" marker-end="url(#arrowhead5438)" style="fill:none"></path><defs><marker id="arrowhead5438" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M374.8334914283282,83.42567892832824L383.47244128223315,82.02296186125335C392.1113911361381,80.62024479417846,409.38929084394795,77.81481066002868,424.8615740311862,76.4120935929538C440.3338572184245,75.0093765258789,454.0005238850911,75.0093765258789,460.8338572184245,75.0093765258789L467.6671905517578,75.0093765258789" marker-end="url(#arrowhead5439)" style="fill:none"></path><defs><marker id="arrowhead5439" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M363.26378981362706,117.66277268637306L373.83102326998215,121.42965470344728C384.39825672633725,125.19653672052148,405.53272363904756,132.7303007546699,431.0367272671994,143.39379748223405C456.54073089535115,154.0572942097982,486.4142712389446,167.85052363077799,501.3510414107413,174.7471383412679L516.287811582538,181.6437530517578" marker-end="url(#arrowhead5440)" style="fill:none"></path><defs><marker id="arrowhead5440" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M491.2648468017578,208.32415449749794L480.4985707600911,209.52325425654126C469.7322947184245,210.72235401558456,448.1997426350911,213.1205535336712,416.223570505778,214.31965329271452C384.24739837646484,215.5187530517578,341.8276062011719,215.5187530517578,302.0744806925456,215.5187530517578C262.32135518391925,215.5187530517578,225.23489634195963,215.5187530517578,195.02387039726463,208.6221383412679C164.81284445256964,201.72552363077799,141.4772514051393,187.9322942097982,129.8094548814241,181.03567949930826L118.14165835770893,174.13906478881836" marker-end="url(#arrowhead5441)" style="fill:none"></path><defs><marker id="arrowhead5441" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(426.6671905517578,75.0093765258789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000219345092773" height="18.500253677368164"></rect><text><tspan xml:space="preserve" dy="1em" x="1">有</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(426.6671905517578,140.26406478881836)"><g transform="translate(-16,-9.25)" class="label"><rect rx="0" ry="0" width="31.992624282836914" height="18.500253677368164"></rect><text><tspan xml:space="preserve" dy="1em" x="1">没有</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6592" transform="translate(85.57421875,154.88906478881836)"><rect rx="0" ry="0" x="-77.57421875" y="-19.25" width="155.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-67.57421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i,num=nums[i]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6593" transform="translate(299.4078140258789,94.2593765258789)"><polygon points="86.259375,0 172.51875,-86.259375 86.259375,-172.51875 0,-86.259375" transform="translate(-86.259375,86.259375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-66.59375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">map有target-num?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6595" transform="translate(557.9796905517578,75.0093765258789)"><rect rx="0" ry="0" x="-90.3125" y="-19.25" width="180.625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.3125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回[map.get(need),i]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6597" transform="translate(557.9796905517578,200.8937530517578)"><rect rx="0" ry="0" x="-66.71484375" y="-19.25" width="133.4296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.71484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">map.put(num,i)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int[] twoSum(int[] nums, int target) {        Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();        for (int i = 0; i &lt; nums.length; i++) {            int need = target - nums[i];            if (map.containsKey(need)) return new int[]{map.get(need), i};            map.put(nums[i], i);        }        return new int[0];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>先查再存，否则 <code>[3,3] target=6</code> 会误用自己下标。</li></ul><h2 id="202.-%E5%BF%AB%E4%B9%90%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/happy-number/" target="_blank">202. 快乐数</a></h2><blockquote><p>反复把数替换成各位平方和，最终为 1 是快乐数，可能进入循环。</p></blockquote><p><strong>思路脉络</strong>：循环判定——用集合记出现过的数，遇重复说明进入循环非快乐数，遇 1 是快乐数。或者快慢指针（类似环形链表，但这里是数值链）。</p><div class="mermaid"><svg id="render3166703743" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="267.7076110839844" style="max-width: 626.0859375px;" viewBox="0 0 626.0859375 267.7076110839844"><style>#render3166703743 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3166703743 .error-icon{fill:#552222;}#render3166703743 .error-text{fill:#552222;stroke:#552222;}#render3166703743 .edge-thickness-normal{stroke-width:2px;}#render3166703743 .edge-thickness-thick{stroke-width:3.5px;}#render3166703743 .edge-pattern-solid{stroke-dasharray:0;}#render3166703743 .edge-pattern-dashed{stroke-dasharray:3;}#render3166703743 .edge-pattern-dotted{stroke-dasharray:2;}#render3166703743 .marker{fill:#333333;stroke:#333333;}#render3166703743 .marker.cross{stroke:#333333;}#render3166703743 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3166703743 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3166703743 .cluster-label text{fill:#333;}#render3166703743 .cluster-label span{color:#333;}#render3166703743 .label text,#render3166703743 span{fill:#333;color:#333;}#render3166703743 .node rect,#render3166703743 .node circle,#render3166703743 .node ellipse,#render3166703743 .node polygon,#render3166703743 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3166703743 .node .label{text-align:center;}#render3166703743 .node.clickable{cursor:pointer;}#render3166703743 .arrowheadPath{fill:#333333;}#render3166703743 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3166703743 .flowchart-link{stroke:#333333;fill:none;}#render3166703743 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3166703743 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3166703743 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3166703743 .cluster text{fill:#333;}#render3166703743 .cluster span{color:#333;}#render3166703743 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3166703743 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M107.82763002567776,114.9300765991211L116.1467229380648,110.65465354919434C124.46581585045185,106.37923049926758,141.10400167522593,97.82838439941406,153.6730948419261,93.63629468282063C166.24218800862627,89.44420496622722,174.74218851725254,89.61087163289388,178.9921887715657,89.69420496622722L183.24218902587884,89.77753829956055" marker-end="url(#arrowhead5470)" style="fill:none"></path><defs><marker id="arrowhead5470" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M252.00157123371935,70.07676425740097L260.70170049505913,62.938970214500806C269.40182975639885,55.801176171600645,286.8020882790783,41.52558808580032,301.8160844736049,34.38779404290016C316.83008066813153,27.25,329.4578145345052,27.25,335.7716814676921,27.25L342.0855484008789,27.25" marker-end="url(#arrowhead5471)" style="fill:none"></path><defs><marker id="arrowhead5471" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M252.0015695412626,109.4783109824191L260.70169908467847,116.44943858520277C269.40182862809434,123.42056618798642,286.8020877149261,137.36282139355376,301.0855508459884,144.41728232967077C315.36901397705077,151.47174326578775,326.5356811523437,151.63840993245444,332.1190147399902,151.72174326578778L337.7023483276367,151.80507659912112" marker-end="url(#arrowhead5472)" style="fill:none"></path><defs><marker id="arrowhead5472" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M437.45778199812776,141.95035707136998L444.51690166510645,140.30114365932852C451.5760213320852,138.65193024728703,465.69426066604257,135.35350342320405,478.3549428330213,133.70429001116258C491.015625,132.0550765991211,502.21875,132.0550765991211,507.8203125,132.0550765991211L513.421875,132.0550765991211" marker-end="url(#arrowhead5473)" style="fill:none"></path><defs><marker id="arrowhead5473" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M428.94800921307404,170.1695719636838L437.42542434422836,174.40048940292334C445.90283947538273,178.63140684216287,462.85766973769137,187.093241720642,479.4890381892847,195.5995822098083C496.1204066408782,204.1059226989746,512.4283132817563,212.65676879882812,520.5822666021954,216.93219184875488L528.7362199226345,221.20761489868164" marker-end="url(#arrowhead5474)" style="fill:none"></path><defs><marker id="arrowhead5474" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-A" style="opacity: 1;" id="L-F-A"><path class="path" d="M512.8125,249.44688986104993L507.3125,250.3861773673219C501.8125,251.32546487359386,490.8125,253.20403988613774,470.67832056681317,254.14332739240967C450.5441411336263,255.08261489868164,421.2757822672526,255.08261489868164,392.0074234008789,255.08261489868164C362.7390645345052,255.08261489868164,333.47070566813153,255.08261489868164,305.96484629313153,255.08261489868164C278.45898691813153,255.08261489868164,252.71562703450522,255.08261489868164,228.30560048421225,255.08261489868164C203.89557393391928,255.08261489868164,180.81888071695963,255.08261489868164,157.03721043828708,238.14052518208823C133.2555401596145,221.19843546549478,108.768892819229,187.31425603230795,96.52556914903626,170.3721663157145L84.2822454788435,153.4300765991211" marker-end="url(#arrowhead5475)" style="fill:none"></path><defs><marker id="arrowhead5475" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(304.2023468017578,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000268936157227" height="18.50031089782715"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(304.2023468017578,151.3050765991211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000268936157227" height="18.50031089782715"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(479.8125,132.0550765991211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000268936157227" height="18.50031089782715"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(479.8125,195.5550765991211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000268936157227" height="18.50031089782715"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6612" transform="translate(70.37109375,134.1800765991211)"><rect rx="0" ry="0" x="-62.37109375" y="-19.25" width="124.7421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-52.37109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">n→各位平方和</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6613" transform="translate(226.9722671508789,89.27753829956055)"><polygon points="44.230078125,0 88.46015625,-44.230078125 44.230078125,-88.46015625 0,-44.230078125" transform="translate(-44.230078125,44.230078125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-19.89453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">n==1?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6615" transform="translate(392.0074234008789,27.25)"><rect rx="0" ry="0" x="-49.921875" y="-19.25" width="99.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return true</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6617" transform="translate(392.0074234008789,151.3050765991211)"><polygon points="54.805078125,0 109.61015625,-54.805078125 54.805078125,-109.61015625 0,-54.805078125" transform="translate(-54.805078125,54.805078125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-31.64453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">seen含n?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6619" transform="translate(565.44921875,132.0550765991211)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6621" transform="translate(565.44921875,240.45761489868164)"><rect rx="0" ry="0" x="-52.63671875" y="-19.25" width="105.2734375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.63671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">seen.add(n)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isHappy(int n) {        Set&lt;Integer&gt; seen = new HashSet&lt;&gt;();        while (n != 1 &amp;&amp; !seen.contains(n)) {            seen.add(n);            n = next(n);        }        return n == 1;    }    int next(int n) {        int sum = 0;        while (n &gt; 0) { int d = n % 10; sum += d * d; n /= 10; }        return sum;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(log n)。</p></blockquote><blockquote><p>[!tip] 关联</p></blockquote><ul><li>快慢指针法 O(1) 空间：把&quot;数→平方和&quot;看成链表，循环即环，复用 [[#142. 环形链表 II]] 的快慢找环思路。</li></ul><h2 id="219.-%E5%AD%98%E5%9C%A8%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0-ii" tabindex="-1"><a href="https://leetcode.cn/problems/contains-duplicate-ii/" target="_blank">219. 存在重复元素 II</a></h2><blockquote><p>数组里是否存在 <code>nums[i] == nums[j]</code> 且 <code>|i-j| &lt;= k</code>。</p></blockquote><p><strong>思路脉络</strong>：哈希记每个值最近下标。遇同值时比下标差，<code>≤ k</code> 即 true，否则更新最近下标（更近的下标更可能满足后续，旧的丢弃）。</p><div class="mermaid"><svg id="render45824418" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="201.92733764648438" style="max-width: 668.8101806640625px;" viewBox="0 0 668.8101806640625 201.92733764648438"><style>#render45824418 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render45824418 .error-icon{fill:#552222;}#render45824418 .error-text{fill:#552222;stroke:#552222;}#render45824418 .edge-thickness-normal{stroke-width:2px;}#render45824418 .edge-thickness-thick{stroke-width:3.5px;}#render45824418 .edge-pattern-solid{stroke-dasharray:0;}#render45824418 .edge-pattern-dashed{stroke-dasharray:3;}#render45824418 .edge-pattern-dotted{stroke-dasharray:2;}#render45824418 .marker{fill:#333333;stroke:#333333;}#render45824418 .marker.cross{stroke:#333333;}#render45824418 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render45824418 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render45824418 .cluster-label text{fill:#333;}#render45824418 .cluster-label span{color:#333;}#render45824418 .label text,#render45824418 span{fill:#333;color:#333;}#render45824418 .node rect,#render45824418 .node circle,#render45824418 .node ellipse,#render45824418 .node polygon,#render45824418 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render45824418 .node .label{text-align:center;}#render45824418 .node.clickable{cursor:pointer;}#render45824418 .arrowheadPath{fill:#333333;}#render45824418 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render45824418 .flowchart-link{stroke:#333333;fill:none;}#render45824418 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render45824418 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render45824418 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render45824418 .cluster text{fill:#333;}#render45824418 .cluster span{color:#333;}#render45824418 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render45824418 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M64.5625,84.11102952705451L68.72916666666667,81.62813607641912C72.89583333333333,79.14524262578374,81.22916666666667,74.17945572451296,89.5625,71.69656227387757C97.89583333333333,69.21366882324219,106.22916666666667,69.21366882324219,110.39583333333333,69.21366882324219L114.5625,69.21366882324219" marker-end="url(#arrowhead5498)" style="fill:none"></path><defs><marker id="arrowhead5498" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M259.0390625,69.21366882324219L263.2057291666667,69.21366882324219C267.3723958333333,69.21366882324219,275.7057291666667,69.21366882324219,287.4082201096939,71.30284505550793C299.1107110527211,73.39202128777367,314.18235960544223,77.57037375230514,321.71818388180276,79.65954998457089L329.25400815816334,81.74872621683663" marker-end="url(#arrowhead5499)" style="fill:none"></path><defs><marker id="arrowhead5499" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M495.46640319824223,101.46366882324219L500.88306935628253,101.38033548990886C506.29973551432295,101.29700215657552,517.1330678304037,101.13033548990886,528.049733988444,101.04700215657552C538.9664001464844,100.96366882324219,549.9664001464844,100.96366882324219,555.4664001464844,100.96366882324219L560.9664001464844,100.96366882324219" marker-end="url(#arrowhead5500)" style="fill:none"></path><defs><marker id="arrowhead5500" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M329.2540033490293,121.17861272402939L321.7181798741911,123.10112207389818C314.18235639935284,125.02363142376699,299.11070944967645,128.86865012350458,275.3685057665049,130.79115947337337C251.62630208333334,132.7136688232422,219.21354166666666,132.7136688232422,186.80078125,132.7136688232422C154.38802083333334,132.7136688232422,121.97526041666667,132.7136688232422,101.60221354166667,130.23077537260681C81.22916666666667,127.74788192197143,72.89583333333333,122.78209502070064,68.72916666666667,120.29920157006525L64.5625,117.81630811942986" marker-end="url(#arrowhead5501)" style="fill:none"></path><defs><marker id="arrowhead5501" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(527.9664001464844,100.96366882324219)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00043296813965" height="18.500499725341797"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(186.80078125,132.7136688232422)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00043296813965" height="18.500499725341797"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6632" transform="translate(36.28125,100.96366882324219)"><rect rx="0" ry="0" x="-28.28125" y="-19.25" width="56.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6633" transform="translate(186.80078125,69.21366882324219)"><rect rx="0" ry="0" x="-72.23828125" y="-19.25" width="144.4765625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-62.23828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">map.put返回旧值</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6635" transform="translate(402.0027313232422,100.96366882324219)"><polygon points="92.963671875,0 185.92734375,-92.963671875 92.963671875,-185.92734375 0,-92.963671875" transform="translate(-92.963671875,92.963671875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-74.04296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">旧值≠null且i-旧值≤k?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6637" transform="translate(610.8882751464844,100.96366882324219)"><rect rx="0" ry="0" x="-49.921875" y="-19.25" width="99.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return true</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean containsNearbyDuplicate(int[] nums, int k) {        Map&lt;Integer, Integer&gt; idx = new HashMap&lt;&gt;();        for (int i = 0; i &lt; nums.length; i++) {            Integer prev = idx.put(nums[i], i);            if (prev != null &amp;&amp; i - prev &lt;= k) return true;        }        return false;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li><code>map.put</code> 返回旧值，巧妙用它一次性完成&quot;查旧值+更新&quot;。比 <code>containsKey</code> + <code>get</code> 更简洁。</li></ul><h2 id="128.-%E6%9C%80%E9%95%BF%E8%BF%9E%E7%BB%AD%E5%BA%8F%E5%88%97" tabindex="-1"><a href="https://leetcode.cn/problems/longest-consecutive-sequence/" target="_blank">128. 最长连续序列</a></h2><blockquote><p>未排序数组找最长连续整数序列长度，要求 O(n)。</p></blockquote><p><strong>思路脉络</strong>：所有数放 set，只从&quot;序列起点&quot;（<code>x-1</code> 不在 set）向上扩展。<code>x-1</code> 在则跳过避免 O(n²)。和热题 100 相同。</p><div class="mermaid"><svg id="render1042902395" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="163.8796844482422" style="max-width: 719.6765747070312px;" viewBox="0 0 719.6765747070312 163.8796844482422"><style>#render1042902395 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1042902395 .error-icon{fill:#552222;}#render1042902395 .error-text{fill:#552222;stroke:#552222;}#render1042902395 .edge-thickness-normal{stroke-width:2px;}#render1042902395 .edge-thickness-thick{stroke-width:3.5px;}#render1042902395 .edge-pattern-solid{stroke-dasharray:0;}#render1042902395 .edge-pattern-dashed{stroke-dasharray:3;}#render1042902395 .edge-pattern-dotted{stroke-dasharray:2;}#render1042902395 .marker{fill:#333333;stroke:#333333;}#render1042902395 .marker.cross{stroke:#333333;}#render1042902395 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1042902395 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1042902395 .cluster-label text{fill:#333;}#render1042902395 .cluster-label span{color:#333;}#render1042902395 .label text,#render1042902395 span{fill:#333;color:#333;}#render1042902395 .node rect,#render1042902395 .node circle,#render1042902395 .node ellipse,#render1042902395 .node polygon,#render1042902395 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1042902395 .node .label{text-align:center;}#render1042902395 .node.clickable{cursor:pointer;}#render1042902395 .arrowheadPath{fill:#333333;}#render1042902395 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1042902395 .flowchart-link{stroke:#333333;fill:none;}#render1042902395 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1042902395 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1042902395 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1042902395 .cluster text{fill:#333;}#render1042902395 .cluster span{color:#333;}#render1042902395 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1042902395 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M130.765625,95.53476333618164L134.93229166666666,95.53476333618164C139.09895833333334,95.53476333618164,147.43229166666666,95.53476333618164,155.765625,95.53476333618164C164.09895833333334,95.53476333618164,172.43229166666666,95.53476333618164,176.59895833333334,95.53476333618164L180.765625,95.53476333618164" marker-end="url(#arrowhead5533)" style="fill:none"></path><defs><marker id="arrowhead5533" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M237.30236320947694,76.28476333618164L243.3821776745641,71.87310981750488C249.4619921396513,67.46145629882812,261.6216210698256,58.63814926147461,274.6067536541428,55.42534454454769C287.59188623845995,52.21253982762076,301.4025224769199,54.610237431120424,308.3078405961499,55.809086232870264L315.21315871537985,57.0079350346201" marker-end="url(#arrowhead5534)" style="fill:none"></path><defs><marker id="arrowhead5534" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-B" style="opacity: 1;" id="L-C-B"><path class="path" d="M321.8413010299838,79.49989477998373L313.8312925249865,82.17237287268338C305.8212840199892,84.84485096538303,289.8012670099946,90.18980715078233,276.2912585049973,92.86228524348199C262.78125,95.53476333618164,251.78125,95.53476333618164,246.28125,95.53476333618164L240.78125,95.53476333618164" marker-end="url(#arrowhead5535)" style="fill:none"></path><defs><marker id="arrowhead5535" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M420.16093597412106,64.93984222412108L425.5776023864746,64.85650889078775C430.99426879882805,64.77317555745442,441.82760162353514,64.60650889078777,452.74426803588864,64.52317555745442C463.6609344482422,64.4398422241211,474.6609344482422,64.4398422241211,480.1609344482422,64.4398422241211L485.6609344482422,64.4398422241211" marker-end="url(#arrowhead5536)" style="fill:none"></path><defs><marker id="arrowhead5536" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M577.6765594482422,64.4398422241211L581.8432261149088,64.4398422241211C586.0098927815756,64.4398422241211,594.3432261149088,64.4398422241211,604.9749254973334,68.85149574279785C615.606624879758,73.26314926147461,628.5366903112739,82.08645629882812,635.0017230270317,86.49810981750488L641.4667557427897,90.90976333618164" marker-end="url(#arrowhead5537)" style="fill:none"></path><defs><marker id="arrowhead5537" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-B" style="opacity: 1;" id="L-E-B"><path class="path" d="M641.4667557427897,129.40976333618164L635.0017230270317,133.8214168548584C628.5366903112739,138.23307037353516,615.606624879758,147.05637741088867,597.3069567473334,151.46803092956543C579.0072886149088,155.8796844482422,555.3380177815756,155.8796844482422,530.3354136149088,155.8796844482422C505.3328094482422,155.8796844482422,478.9968719482422,155.8796844482422,450.92226282755536,155.8796844482422C422.84765370686847,155.8796844482422,393.0343729654948,155.8796844482422,363.2210922241211,155.8796844482422C333.4078114827474,155.8796844482422,303.5945307413737,155.8796844482422,281.5364984600551,149.03053092956543C259.4784661787365,142.18137741088867,245.17568235747305,128.48307037353516,238.0242904468413,121.6339168548584L230.87289853620956,114.78476333618164" marker-end="url(#arrowhead5538)" style="fill:none"></path><defs><marker id="arrowhead5538" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(273.78125,95.53476333618164)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000457763671875" height="18.500530242919922"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(452.6609344482422,64.4398422241211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000457763671875" height="18.500530242919922"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6652" transform="translate(69.3828125,95.53476333618164)"><rect rx="0" ry="0" x="-61.3828125" y="-19.25" width="122.765625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-51.3828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">全部数放入Set</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6653" transform="translate(210.7734375,95.53476333618164)"><rect rx="0" ry="0" x="-30.0078125" y="-19.25" width="60.015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20.0078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历x</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6655" transform="translate(363.2210922241211,64.4398422241211)"><polygon points="56.43984375,0 112.8796875,-56.43984375 56.43984375,-112.8796875 0,-56.43984375" transform="translate(-56.43984375,56.43984375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-33.4609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">x-1在Set?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6659" transform="translate(531.6687469482422,64.4398422241211)"><rect rx="0" ry="0" x="-46.0078125" y="-19.25" width="92.015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.0078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">从x向上数</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6661" transform="translate(669.6765594482422,110.15976333618164)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">更新最长</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int longestConsecutive(int[] nums) {        Set&lt;Integer&gt; set = new HashSet&lt;&gt;();        for (int n : nums) set.add(n);        int best = 0;        for (int x : set) {            if (set.contains(x - 1)) continue;            int cur = x, len = 1;            while (set.contains(cur + 1)) { cur++; len++; }            best = Math.max(best, len);        }        return best;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>漏 <code>x-1</code> 判断会退化 O(n²)。&quot;只从起点扩展&quot;是 O(n) 的灵魂。</li></ul><hr /><h1 id="6.-%E5%8C%BA%E9%97%B4" tabindex="-1">6. 区间</h1><p>区间题的核心是<strong>排序 + 一次遍历处理重叠</strong>。按起点排序后，重叠区间相邻，用一个&quot;当前合并区间&quot;吸收后续重叠即可。</p><h2 id="228.-%E6%B1%87%E6%80%BB%E5%8C%BA%E9%97%B4" tabindex="-1"><a href="https://leetcode.cn/problems/summary-ranges/" target="_blank">228. 汇总区间</a></h2><blockquote><p>有序无重复整数数组，把连续段汇总成 <code>a-&gt;b</code>、单点列自身。</p></blockquote><p><strong>思路脉络</strong>：一次遍历，<code>l</code> 标记段起点，遇到 <code>nums[i] != nums[i-1]+1</code>（断开）就输出 <code>[l, i-1]</code> 段并重置 <code>l=i</code>。</p><div class="mermaid"><svg id="render459176638" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="280.21875" style="max-width: 631px;" viewBox="0 0 631 280.21875"><style>#render459176638 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render459176638 .error-icon{fill:#552222;}#render459176638 .error-text{fill:#552222;stroke:#552222;}#render459176638 .edge-thickness-normal{stroke-width:2px;}#render459176638 .edge-thickness-thick{stroke-width:3.5px;}#render459176638 .edge-pattern-solid{stroke-dasharray:0;}#render459176638 .edge-pattern-dashed{stroke-dasharray:3;}#render459176638 .edge-pattern-dotted{stroke-dasharray:2;}#render459176638 .marker{fill:#333333;stroke:#333333;}#render459176638 .marker.cross{stroke:#333333;}#render459176638 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render459176638 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render459176638 .cluster-label text{fill:#333;}#render459176638 .cluster-label span{color:#333;}#render459176638 .label text,#render459176638 span{fill:#333;color:#333;}#render459176638 .node rect,#render459176638 .node circle,#render459176638 .node ellipse,#render459176638 .node polygon,#render459176638 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render459176638 .node .label{text-align:center;}#render459176638 .node.clickable{cursor:pointer;}#render459176638 .arrowheadPath{fill:#333333;}#render459176638 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render459176638 .flowchart-link{stroke:#333333;fill:none;}#render459176638 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render459176638 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render459176638 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render459176638 .cluster text{fill:#333;}#render459176638 .cluster span{color:#333;}#render459176638 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render459176638 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M73.34478303475618,215.84375L84.54513169563015,197.2109375C95.74548035650412,178.578125,118.14617767825206,141.3125,133.59652633912603,122.76302083333333C149.046875,104.21354166666667,157.546875,104.38020833333333,161.796875,104.46354166666667L166.046875,104.546875" marker-end="url(#arrowhead5568)" style="fill:none"></path><defs><marker id="arrowhead5568" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M345.67305045569486,92.07930045569486L353.167646213079,90.86556287974572C360.6622419704633,89.65182530379657,375.6514334852316,87.22435015189829,388.64602924261584,86.01061257594915C401.640625,84.796875,412.640625,84.796875,418.140625,84.796875L423.640625,84.796875" marker-end="url(#arrowhead5569)" style="fill:none"></path><defs><marker id="arrowhead5569" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M331.94371615374456,130.74378384625544L341.72653429478714,134.3607052885462C351.5093524358297,137.97762673083696,371.0749887179149,145.21146961541848,390.82134852562405,156.94299816183948C410.5677083333333,168.67452670826052,430.4947916666667,184.903740916521,440.4583333333333,193.01834802065127L450.421875,201.13295512478152" marker-end="url(#arrowhead5570)" style="fill:none"></path><defs><marker id="arrowhead5570" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M518.546875,84.796875L522.7135416666666,84.796875C526.8802083333334,84.796875,535.2135416666666,84.796875,546.5872041289941,100.23828125C557.9608665913216,115.6796875,572.3748581826433,146.5625,579.5818539783041,162.00390625L586.7888497739649,177.4453125" marker-end="url(#arrowhead5571)" style="fill:none"></path><defs><marker id="arrowhead5571" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M450.421875,222.36890051466304L440.4583333333333,224.48970876221918C430.4947916666667,226.61051700977535,410.5677083333333,230.85213350488766,379.0963541666667,232.97294175244383C347.625,235.09375,304.609375,235.09375,262.9270833333333,235.09375C221.24479166666666,235.09375,180.89583333333334,235.09375,156.5546875,235.09375C132.21354166666666,235.09375,123.88020833333333,235.09375,119.71354166666667,235.09375L115.546875,235.09375" marker-end="url(#arrowhead5572)" style="fill:none"></path><defs><marker id="arrowhead5572" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M582.4615258417813,215.9453125L575.9757507014843,225.32421875C569.4899755611875,234.703125,556.5184252805938,253.4609375,537.9571293069636,262.83984375C519.3958333333334,272.21875,495.2447916666667,272.21875,469.7604166666667,272.21875C444.2760416666667,272.21875,417.4583333333333,272.21875,382.5416666666667,272.21875C347.625,272.21875,304.609375,272.21875,262.9270833333333,272.21875C221.24479166666666,272.21875,180.89583333333334,272.21875,154.4000289351852,269.2395833333333C127.90422453703702,266.2604166666667,115.26157407407408,260.3020833333333,108.9402488425926,257.3229166666667L102.61892361111111,254.34375" marker-end="url(#arrowhead5573)" style="fill:none"></path><defs><marker id="arrowhead5573" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(390.640625,84.796875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(390.640625,152.4453125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6676" transform="translate(61.7734375,235.09375)"><rect rx="0" ry="0" x="-53.7734375" y="-19.25" width="107.546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.7734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i遍历,l=起点</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6677" transform="translate(261.59375,104.046875)"><polygon points="96.046875,0 192.09375,-96.046875 96.046875,-192.09375 0,-96.046875" transform="translate(-96.046875,96.046875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-77.46875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[i]+1≠nums[i+1]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6679" transform="translate(471.09375,84.796875)"><rect rx="0" ry="0" x="-47.453125" y="-19.25" width="94.90625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">输出[l,i]段</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6681" transform="translate(471.09375,217.96875)"><rect rx="0" ry="0" x="-20.671875" y="-19.25" width="41.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-10.671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6683" transform="translate(595.7734375,196.6953125)"><rect rx="0" ry="0" x="-27.2265625" y="-19.25" width="54.453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-17.2265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=i+1</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0228-summary-ranges.gif" alt="" /></p><pre><code class="language-java">class Solution {    public List&lt;String&gt; summaryRanges(int[] nums) {        List&lt;String&gt; res = new ArrayList&lt;&gt;();        for (int i = 0; i &lt; nums.length; i++) {            int l = i;            while (i + 1 &lt; nums.length &amp;&amp; nums[i + 1] == nums[i] + 1) i++;            if (l == i) res.add(String.valueOf(nums[l]));            else res.add(nums[l] + &quot;-&gt;&quot; + nums[i]);        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>单点（<code>l==i</code>）单独输出数字，连续段输出 <code>a-&gt;b</code>。<code>while</code> 推进 <code>i</code>，外层 <code>for</code> 的 <code>i++</code> 会跳到下一段起点。</li></ul><h2 id="56.-%E5%90%88%E5%B9%B6%E5%8C%BA%E9%97%B4" tabindex="-1"><a href="https://leetcode.cn/problems/merge-intervals/" target="_blank">56. 合并区间</a></h2><blockquote><p>合并所有重叠区间。</p></blockquote><p><strong>思路脉络</strong>：按起点排序，遍历时若当前区间起点 ≤ 上一已合并区间终点则合并（终点取 max），否则新开。和热题 100 相同。</p><div class="mermaid"><svg id="render414805295" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="295.4140625" style="max-width: 667.8671875px;" viewBox="0 0 667.8671875 295.4140625"><style>#render414805295 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render414805295 .error-icon{fill:#552222;}#render414805295 .error-text{fill:#552222;stroke:#552222;}#render414805295 .edge-thickness-normal{stroke-width:2px;}#render414805295 .edge-thickness-thick{stroke-width:3.5px;}#render414805295 .edge-pattern-solid{stroke-dasharray:0;}#render414805295 .edge-pattern-dashed{stroke-dasharray:3;}#render414805295 .edge-pattern-dotted{stroke-dasharray:2;}#render414805295 .marker{fill:#333333;stroke:#333333;}#render414805295 .marker.cross{stroke:#333333;}#render414805295 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render414805295 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render414805295 .cluster-label text{fill:#333;}#render414805295 .cluster-label span{color:#333;}#render414805295 .label text,#render414805295 span{fill:#333;color:#333;}#render414805295 .node rect,#render414805295 .node circle,#render414805295 .node ellipse,#render414805295 .node polygon,#render414805295 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render414805295 .node .label{text-align:center;}#render414805295 .node.clickable{cursor:pointer;}#render414805295 .arrowheadPath{fill:#333333;}#render414805295 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render414805295 .flowchart-link{stroke:#333333;fill:none;}#render414805295 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render414805295 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render414805295 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render414805295 .cluster text{fill:#333;}#render414805295 .cluster span{color:#333;}#render414805295 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render414805295 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M108,209.2890625L112.16666666666667,209.2890625C116.33333333333333,209.2890625,124.66666666666667,209.2890625,133,209.2890625C141.33333333333334,209.2890625,149.66666666666666,209.2890625,153.83333333333334,209.2890625L158,209.2890625" marker-end="url(#arrowhead5601)" style="fill:none"></path><defs><marker id="arrowhead5601" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M210.9167135063647,190.0390625L220.26392792197058,173.556640625C229.61114233757644,157.07421875,248.30557116878822,124.109375,261.9027855843941,107.71028645833333C275.5,91.31119791666667,284,91.47786458333333,288.25,91.56119791666667L292.5,91.64453125" marker-end="url(#arrowhead5602)" style="fill:none"></path><defs><marker id="arrowhead5602" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M458.7890625,91.64453125L464.2057291666667,91.56119791666667C469.6223958333333,91.47786458333333,480.4557291666667,91.31119791666667,497.19651659881794,97.86490885416667C513.9373040309694,104.41861979166667,536.5855455619386,117.69270833333333,547.9096663274232,124.32975260416667L559.233787092908,130.966796875" marker-end="url(#arrowhead5603)" style="fill:none"></path><defs><marker id="arrowhead5603" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M412.28723050082635,138.14636324917365L425.4542025006886,154.87847979097805C438.6211745005509,171.61059633278242,464.95511850027543,205.07482941639122,487.92026758347106,224.68693103687167C510.8854166666667,244.29903265735211,530.4817708333334,250.0590028147043,540.2799479166666,252.93898789338036L550.078125,255.81897297205643" marker-end="url(#arrowhead5604)" style="fill:none"></path><defs><marker id="arrowhead5604" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M559.233787092908,169.466796875L547.9096663274232,176.10384114583334C536.5855455619386,182.74088541666666,513.9373040309694,196.01497395833334,483.2557613904846,202.65201822916666C452.57421875,209.2890625,413.859375,209.2890625,376.4778645833333,209.2890625C339.0963541666667,209.2890625,303.0481770833333,209.2890625,280.857421875,209.2890625C258.6666666666667,209.2890625,250.33333333333334,209.2890625,246.16666666666666,209.2890625L242,209.2890625" marker-end="url(#arrowhead5605)" style="fill:none"></path><defs><marker id="arrowhead5605" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-B" style="opacity: 1;" id="L-E-B"><path class="path" d="M550.078125,274.2584737859468L540.2799479166666,275.68023857162234C530.4817708333334,277.10200335729786,510.8854166666667,279.9455329286489,481.7298177083333,281.3672977143245C452.57421875,282.7890625,413.859375,282.7890625,376.4778645833333,282.7890625C339.0963541666667,282.7890625,303.0481770833333,282.7890625,276.7820250496032,273.7473958333333C250.515873015873,264.7057291666667,234.03174603174602,246.62239583333334,225.78968253968253,237.58072916666666L217.54761904761904,228.5390625" marker-end="url(#arrowhead5606)" style="fill:none"></path><defs><marker id="arrowhead5606" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(491.2890625,91.14453125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(491.2890625,238.5390625)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6700" transform="translate(58,209.2890625)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">按起点排序</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6701" transform="translate(200,209.2890625)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历区间</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6703" transform="translate(375.14453125,91.14453125)"><polygon points="83.14453125,0 166.2890625,-83.14453125 83.14453125,-166.2890625 0,-83.14453125" transform="translate(-83.14453125,83.14453125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-63.1328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">起点≤末区间终点?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6705" transform="translate(592.078125,150.216796875)"><rect rx="0" ry="0" x="-67.7890625" y="-19.25" width="135.578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-57.7890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">合并:终点取max</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6707" transform="translate(592.078125,268.1640625)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">新开一段</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int[][] merge(int[][] intervals) {        Arrays.sort(intervals, (a, b) -&gt; a[0] - b[0]);        List&lt;int[]&gt; res = new ArrayList&lt;&gt;();        for (int[] iv : intervals) {            if (!res.isEmpty() &amp;&amp; iv[0] &lt;= res.get(res.size() - 1)[1]) {                res.get(res.size() - 1)[1] = Math.max(res.get(res.size() - 1)[1], iv[1]);            } else res.add(iv);        }        return res.toArray(new int[0][]);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n)，空间 O(排序栈)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>合并时终点取 <code>max</code>，不能直接覆盖。<code>[1,5]</code> 与 <code>[2,3]</code> 覆盖成 <code>[1,3]</code> 就错了。</li></ul><h2 id="57.-%E6%8F%92%E5%85%A5%E5%8C%BA%E9%97%B4" tabindex="-1"><a href="https://leetcode.cn/problems/insert-interval/" target="_blank">57. 插入区间</a></h2><blockquote><p>已有一组不重叠区间（按起点序），插入新区间 <code>newInterval</code> 后合并。</p></blockquote><p><strong>思路脉络</strong>：三段法。左边：终点 &lt; 新起点的直接加入；中间：与新区间有重叠的合并（起点取 min、终点取 max）；右边：起点 &gt; 新终点的直接加入。</p><div class="mermaid"><svg id="render3661368382" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="171.3484344482422" style="max-width: 779.2703247070312px;" viewBox="0 0 779.2703247070312 171.3484344482422"><style>#render3661368382 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3661368382 .error-icon{fill:#552222;}#render3661368382 .error-text{fill:#552222;stroke:#552222;}#render3661368382 .edge-thickness-normal{stroke-width:2px;}#render3661368382 .edge-thickness-thick{stroke-width:3.5px;}#render3661368382 .edge-pattern-solid{stroke-dasharray:0;}#render3661368382 .edge-pattern-dashed{stroke-dasharray:3;}#render3661368382 .edge-pattern-dotted{stroke-dasharray:2;}#render3661368382 .marker{fill:#333333;stroke:#333333;}#render3661368382 .marker.cross{stroke:#333333;}#render3661368382 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3661368382 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3661368382 .cluster-label text{fill:#333;}#render3661368382 .cluster-label span{color:#333;}#render3661368382 .label text,#render3661368382 span{fill:#333;color:#333;}#render3661368382 .node rect,#render3661368382 .node circle,#render3661368382 .node ellipse,#render3661368382 .node polygon,#render3661368382 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3661368382 .node .label{text-align:center;}#render3661368382 .node.clickable{cursor:pointer;}#render3661368382 .arrowheadPath{fill:#333333;}#render3661368382 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3661368382 .flowchart-link{stroke:#333333;fill:none;}#render3661368382 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3661368382 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3661368382 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3661368382 .cluster text{fill:#333;}#render3661368382 .cluster span{color:#333;}#render3661368382 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3661368382 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M122.03125,85.6742172241211L126.19791666666667,85.6742172241211C130.36458333333334,85.6742172241211,138.69791666666666,85.6742172241211,147.11458358764648,85.75755055745442C155.5312505086263,85.84088389078777,164.03125101725263,86.00755055745442,168.28125127156576,86.09088389078777L172.53125152587893,86.1742172241211" marker-end="url(#arrowhead5626)" style="fill:none"></path><defs><marker id="arrowhead5626" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M305.6941023302467,63.98863358024673L314.8083660165793,60.22789752089246C323.9226297029119,56.46716146153818,342.15115707557703,48.94568934282964,356.76542076190964,45.18495328347537C371.3796844482422,41.424217224121094,382.3796844482422,41.424217224121094,387.8796844482422,41.424217224121094L393.3796844482422,41.424217224121094" marker-end="url(#arrowhead5627)" style="fill:none"></path><defs><marker id="arrowhead5627" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M305.6941045103507,108.35980173964926L314.8083678333326,111.95387098706124C323.92263115631454,115.54794023447319,342.1511578022784,122.73607872929715,359.43208779192696,126.33014797670911C376.7130177815755,129.9242172241211,393.0463511149089,129.9242172241211,401.2130177815755,129.9242172241211L409.3796844482422,129.9242172241211" marker-end="url(#arrowhead5628)" style="fill:none"></path><defs><marker id="arrowhead5628" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M489.1296844482422,129.9242172241211L495.9630177815755,129.9242172241211C502.7963511149089,129.9242172241211,516.4630177815756,129.9242172241211,527.4630177815756,129.9242172241211C538.4630177815756,129.9242172241211,546.7963511149088,129.9242172241211,550.9630177815756,129.9242172241211L555.1296844482422,129.9242172241211" marker-end="url(#arrowhead5629)" style="fill:none"></path><defs><marker id="arrowhead5629" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(360.3796844482422,41.424217224121094)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000438690185547" height="18.500507354736328"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(360.3796844482422,129.9242172241211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000438690185547" height="18.500507354736328"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6720" transform="translate(65.015625,85.6742172241211)"><rect rx="0" ry="0" x="-57.015625" y="-19.25" width="114.03125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47.015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历intervals</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6721" transform="translate(249.7054672241211,85.6742172241211)"><polygon points="77.67421875000001,0 155.34843750000002,-77.67421875000001 77.67421875000001,-155.34843750000002 0,-77.67421875000001" transform="translate(-77.67421875000001,77.67421875000001)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-57.0546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">终点&amp;lt;新起点?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6723" transform="translate(449.2546844482422,41.424217224121094)"><rect rx="0" ry="0" x="-55.875" y="-19.25" width="111.75" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">直接加入(左)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6725" transform="translate(449.2546844482422,129.9242172241211)"><rect rx="0" ry="0" x="-39.875" y="-19.25" width="79.75" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">合并(中)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6727" transform="translate(663.1999969482422,129.9242172241211)"><rect rx="0" ry="0" x="-108.0703125" y="-19.25" width="216.140625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-98.0703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">起点&gt;新终点→直接加入(右)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int[][] insert(int[][] intervals, int[] newInterval) {        List&lt;int[]&gt; res = new ArrayList&lt;&gt;();        int i = 0, n = intervals.length;        while (i &lt; n &amp;&amp; intervals[i][1] &lt; newInterval[0]) res.add(intervals[i++]);        while (i &lt; n &amp;&amp; intervals[i][0] &lt;= newInterval[1]) {            newInterval[0] = Math.min(newInterval[0], intervals[i][0]);            newInterval[1] = Math.max(newInterval[1], intervals[i][1]);            i++;        }        res.add(newInterval);        while (i &lt; n) res.add(intervals[i++]);        return res.toArray(new int[0][]);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><h2 id="452.-%E7%94%A8%E6%9C%80%E5%B0%91%E6%95%B0%E9%87%8F%E7%9A%84%E7%AE%AD%E5%BC%95%E7%88%86%E6%B0%94%E7%90%83" tabindex="-1"><a href="https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/" target="_blank">452. 用最少数量的箭引爆气球</a></h2><blockquote><p>一组气球用区间表示，一支箭从 x 射穿所有覆盖 x 的气球，求最少箭数。</p></blockquote><p><strong>思路脉络</strong>：按终点排序贪心。从左到右，每支箭射在&quot;当前组的右边界最小值&quot;，这样能射穿最多气球。遇到起点 &gt; 当前右边界就需新箭。</p><div class="mermaid"><svg id="render330544824" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="244.5078125" style="max-width: 614.7109375px;" viewBox="0 0 614.7109375 244.5078125"><style>#render330544824 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render330544824 .error-icon{fill:#552222;}#render330544824 .error-text{fill:#552222;stroke:#552222;}#render330544824 .edge-thickness-normal{stroke-width:2px;}#render330544824 .edge-thickness-thick{stroke-width:3.5px;}#render330544824 .edge-pattern-solid{stroke-dasharray:0;}#render330544824 .edge-pattern-dashed{stroke-dasharray:3;}#render330544824 .edge-pattern-dotted{stroke-dasharray:2;}#render330544824 .marker{fill:#333333;stroke:#333333;}#render330544824 .marker.cross{stroke:#333333;}#render330544824 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render330544824 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render330544824 .cluster-label text{fill:#333;}#render330544824 .cluster-label span{color:#333;}#render330544824 .label text,#render330544824 span{fill:#333;color:#333;}#render330544824 .node rect,#render330544824 .node circle,#render330544824 .node ellipse,#render330544824 .node polygon,#render330544824 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render330544824 .node .label{text-align:center;}#render330544824 .node.clickable{cursor:pointer;}#render330544824 .arrowheadPath{fill:#333333;}#render330544824 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render330544824 .flowchart-link{stroke:#333333;fill:none;}#render330544824 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render330544824 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render330544824 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render330544824 .cluster text{fill:#333;}#render330544824 .cluster span{color:#333;}#render330544824 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render330544824 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M108,158.3828125L112.16666666666667,158.3828125C116.33333333333333,158.3828125,124.66666666666667,158.3828125,133,158.3828125C141.33333333333334,158.3828125,149.66666666666666,158.3828125,153.83333333333334,158.3828125L158,158.3828125" marker-end="url(#arrowhead5657)" style="fill:none"></path><defs><marker id="arrowhead5657" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M220.9340811875764,139.1328125L230.5492343229803,126.892578125C240.16438745838425,114.65234375,259.3946937291921,90.171875,273.25984686459606,78.01497395833333C287.125,65.85807291666667,295.625,66.02473958333333,299.875,66.10807291666667L304.125,66.19140625" marker-end="url(#arrowhead5658)" style="fill:none"></path><defs><marker id="arrowhead5658" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M419.5078125,66.19140625L424.9244791666667,66.10807291666667C430.3411458333333,66.02473958333333,441.1744791666667,65.85807291666667,455.7360931310316,70.29069010416667C470.2977070953966,74.72330729166667,488.58760169079324,83.75520833333333,497.73254898849154,88.27115885416667L506.8774962861899,92.787109375" marker-end="url(#arrowhead5659)" style="fill:none"></path><defs><marker id="arrowhead5659" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M386.422743813035,99.27647493696495L397.35358859419586,114.00253119747079C408.2844333753567,128.72858745797663,430.1461229376784,158.18069997898832,446.57696771883917,174.6428754436896C463.0078125,191.10505090839092,474.0078125,194.57728931678184,479.5078125,196.31340852097728L485.0078125,198.04952772517274" marker-end="url(#arrowhead5660)" style="fill:none"></path><defs><marker id="arrowhead5660" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M506.8774962861899,131.287109375L497.7325489884916,135.80305989583334C488.58760169079324,140.31901041666666,470.2977070953966,149.35091145833334,446.0375254226983,153.86686197916666C421.77734375,158.3828125,391.546875,158.3828125,362.6497395833333,158.3828125C333.7526041666667,158.3828125,306.1888020833333,158.3828125,288.240234375,158.3828125C270.2916666666667,158.3828125,261.9583333333333,158.3828125,257.7916666666667,158.3828125L253.625,158.3828125" marker-end="url(#arrowhead5661)" style="fill:none"></path><defs><marker id="arrowhead5661" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-B" style="opacity: 1;" id="L-E-B"><path class="path" d="M485.0078125,226.74038346478815L479.5078125,227.5974549706568C474.0078125,228.45452647652542,463.0078125,230.1686694882627,442.392578125,231.02574099413138C421.77734375,231.8828125,391.546875,231.8828125,362.6497395833333,231.8828125C333.7526041666667,231.8828125,306.1888020833333,231.8828125,283.4498077876984,222.84114583333334C260.7108134920635,213.79947916666666,242.796626984127,195.71614583333334,233.8395337301587,186.67447916666666L224.88244047619048,177.6328125" marker-end="url(#arrowhead5662)" style="fill:none"></path><defs><marker id="arrowhead5662" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(452.0078125,65.69140625)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(452.0078125,187.6328125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6740" transform="translate(58,158.3828125)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">按终点排序</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6741" transform="translate(205.8125,158.3828125)"><rect rx="0" ry="0" x="-47.8125" y="-19.25" width="95.625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.8125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历points</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6743" transform="translate(361.31640625,65.69140625)"><polygon points="57.69140625,0 115.3828125,-57.69140625 57.69140625,-115.3828125 0,-57.69140625" transform="translate(-57.69140625,57.69140625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-34.8515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">p[0]≤end?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6745" transform="translate(545.859375,112.037109375)"><rect rx="0" ry="0" x="-44.9375" y="-19.25" width="89.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">射中,继续</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6747" transform="translate(545.859375,217.2578125)"><rect rx="0" ry="0" x="-60.8515625" y="-19.25" width="121.703125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.8515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">新箭,end=p[1]</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0452-arrows.gif" alt="" /></p><pre><code class="language-java">class Solution {    public int findMinArrowShots(int[][] points) {        if (points.length == 0) return 0;        Arrays.sort(points, (a, b) -&gt; Integer.compare(a[1], b[1]));        int arrows = 1, end = points[0][1];        for (int[] p : points) {            if (p[0] &gt; end) { arrows++; end = p[1]; }        }        return arrows;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n)，空间 O(排序栈)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>按<strong>终点</strong>排序（不是起点），箭射在终点能覆盖最多。比较用 <code>Integer.compare</code> 防 <code>a[1]-b[1]</code> 溢出（坐标可能 INT_MIN）。<code>p[0] &gt; end</code> 严格大于才算不重叠（边界相切 <code>p[0]==end</code> 仍能一支箭射穿）。</li></ul><hr /><h1 id="7.-%E6%A0%88" tabindex="-1">7. 栈</h1><p>栈处理&quot;最近相关性&quot;问题：括号匹配、路径简化、表达式求值。后进先出天然适合&quot;配对/嵌套/撤销&quot;结构。</p><h2 id="20.-%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7" tabindex="-1"><a href="https://leetcode.cn/problems/valid-parentheses/" target="_blank">20. 有效的括号</a></h2><blockquote><p>判断括号串是否合法。</p></blockquote><p><strong>思路脉络</strong>：遇左括号入栈，遇右括号检查栈顶是否对应左括号——是则弹，否则非法。最后栈空才合法。和热题 100 相同。</p><div class="mermaid"><svg id="render1810237823" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="254.65310668945312" style="max-width: 610.6515502929688px;" viewBox="0 0 610.6515502929688 254.65310668945312"><style>#render1810237823 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1810237823 .error-icon{fill:#552222;}#render1810237823 .error-text{fill:#552222;stroke:#552222;}#render1810237823 .edge-thickness-normal{stroke-width:2px;}#render1810237823 .edge-thickness-thick{stroke-width:3.5px;}#render1810237823 .edge-pattern-solid{stroke-dasharray:0;}#render1810237823 .edge-pattern-dashed{stroke-dasharray:3;}#render1810237823 .edge-pattern-dotted{stroke-dasharray:2;}#render1810237823 .marker{fill:#333333;stroke:#333333;}#render1810237823 .marker.cross{stroke:#333333;}#render1810237823 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1810237823 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1810237823 .cluster-label text{fill:#333;}#render1810237823 .cluster-label span{color:#333;}#render1810237823 .label text,#render1810237823 span{fill:#333;color:#333;}#render1810237823 .node rect,#render1810237823 .node circle,#render1810237823 .node ellipse,#render1810237823 .node polygon,#render1810237823 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1810237823 .node .label{text-align:center;}#render1810237823 .node.clickable{cursor:pointer;}#render1810237823 .arrowheadPath{fill:#333333;}#render1810237823 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1810237823 .flowchart-link{stroke:#333333;fill:none;}#render1810237823 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1810237823 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1810237823 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1810237823 .cluster text{fill:#333;}#render1810237823 .cluster span{color:#333;}#render1810237823 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1810237823 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M66.93838536937508,102.875L76.60230030781257,117.20989576975505C86.26621524625006,131.5447915395101,105.59404512312501,160.21458307902017,119.50796031587568,174.63281218210855C133.42187550862633,189.05104128519693,141.92187601725266,189.21770795186362,146.17187627156582,189.301041285197L150.421876525879,189.3843746185303" marker-end="url(#arrowhead5695)" style="fill:none"></path><defs><marker id="arrowhead5695" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M219.31832556796903,157.14332671237815L230.10849996468122,137.93193892698181C240.89867436139343,118.72055114158543,262.4790231548178,80.29777557079272,284.06398909104007,59.34709064191566C305.6489550272624,38.396405713038604,327.23853810628253,34.917811426077215,338.03332964579266,33.17851428259652L348.82812118530273,31.43921713911582" marker-end="url(#arrowhead5696)" style="fill:none"></path><defs><marker id="arrowhead5696" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M251.55937347412117,189.38437461853027L256.97603988647467,189.30104128519693C262.39270629882816,189.21770795186362,273.2260391235352,189.05104128519693,284.22603899637863,189.05104128519693C295.22603886922207,189.0510412851969,306.39270579020194,189.21770795186353,311.9760392506919,189.30104128519687L317.5593727111818,189.3843746185302" marker-end="url(#arrowhead5697)" style="fill:none"></path><defs><marker id="arrowhead5697" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M414.1641881176215,170.45169155084895L422.73630183507845,166.14880539546252C431.30841555253545,161.84591924007606,448.45264298744934,153.24014692930317,466.86264732990634,143.57046482239522C485.2726516723633,133.90078271548728,504.9484329223633,123.16719081244428,514.7863235473633,117.80039486092278L524.6242141723633,112.43359890940127" marker-end="url(#arrowhead5698)" style="fill:none"></path><defs><marker id="arrowhead5698" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M422.9890640459783,199.49218328373343L430.09036510870914,200.9325485061996C437.1916661714399,202.3729137286657,451.3942682969016,205.253644173598,463.9955693596325,206.69400939606416C476.5968704223633,208.13437461853027,487.5968704223633,208.13437461853027,493.0968704223633,208.13437461853027L498.5968704223633,208.13437461853027" marker-end="url(#arrowhead5699)" style="fill:none"></path><defs><marker id="arrowhead5699" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M524.6242141723633,93.77790922037947L514.7863235473633,92.08575768364956C504.9484329223633,90.39360614691964,485.2726516723633,87.00930307345982,460.30663617451984,85.31715153672991C435.34062067667645,83.625,405.08437093098956,83.625,374.82812118530273,83.625C344.5718714396159,83.625,314.315621693929,83.625,285.25937207539874,83.625C256.20312245686847,83.625,228.34687296549478,83.625,201.82395680745444,83.625C175.30104064941406,83.625,150.11145782470703,83.625,133.34999974568686,83.625C116.58854166666667,83.625,108.25520833333333,83.625,104.08854166666667,83.625L99.921875,83.625" marker-end="url(#arrowhead5700)" style="fill:none"></path><defs><marker id="arrowhead5700" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M348.82812118530273,23.06078286088418L338.03332964579266,21.321485717403487C327.23853810628253,19.58218857392279,305.6489550272624,16.103594286961393,280.92603874206543,14.364297143480698C256.20312245686847,12.625,228.34687296549478,12.625,201.82395680745444,12.625C175.30104064941406,12.625,150.11145782470703,12.625,128.89641168083944,21.25C107.68136553697184,29.875,90.44085607394366,47.125,81.82060134242958,55.75L73.20034661091549,64.375" marker-end="url(#arrowhead5701)" style="fill:none"></path><defs><marker id="arrowhead5701" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(284.0593719482422,41.875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00029182434082" height="18.50033950805664"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(284.0593719482422,188.88437461853027)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00029182434082" height="18.50033950805664"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(465.5968704223633,144.63437461853027)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00029182434082" height="18.50033950805664"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(465.5968704223633,208.13437461853027)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00029182434082" height="18.50033950805664"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6766" transform="translate(53.9609375,83.625)"><rect rx="0" ry="0" x="-45.9609375" y="-19.25" width="91.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-35.9609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历字符c</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6767" transform="translate(200.4906234741211,188.88437461853027)"><polygon points="50.56875,0 101.1375,-50.56875 50.56875,-101.1375 0,-50.56875" transform="translate(-50.56875,50.56875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-26.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左括号?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6769" transform="translate(374.82812118530273,27.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">入栈</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6771" transform="translate(374.82812118530273,188.88437461853027)"><polygon points="57.768750000000004,0 115.53750000000001,-57.768750000000004 57.768750000000004,-115.53750000000001 0,-57.768750000000004" transform="translate(-57.768750000000004,57.768750000000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-34.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">栈顶匹配?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6773" transform="translate(550.6242141723633,98.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">弹栈</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6775" transform="translate(550.6242141723633,208.13437461853027)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isValid(String s) {        Map&lt;Character, Character&gt; m = Map.of(&#39;)&#39;, &#39;(&#39;, &#39;]&#39;, &#39;[&#39;, &#39;}&#39;, &#39;{&#39;);        Deque&lt;Character&gt; st = new ArrayDeque&lt;&gt;();        for (char c : s.toCharArray()) {            if (!m.containsKey(c)) st.push(c);            else if (st.isEmpty() || st.pop() != m.get(c)) return false;        }        return st.isEmpty();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><h2 id="71.-%E7%AE%80%E5%8C%96%E8%B7%AF%E5%BE%84" tabindex="-1"><a href="https://leetcode.cn/problems/simplify-path/" target="_blank">71. 简化路径</a></h2><blockquote><p>Unix 路径转规范绝对路径（去 <code>./..</code>、合并多余斜杠）。</p></blockquote><p><strong>思路脉络</strong>：按 <code>/</code> 拆段，遇 <code>.</code> 或空跳过，遇 <code>..</code> 弹栈（回上级），遇普通名压栈。最后用 <code>/</code> 拼接，补前导 <code>/</code>。</p><div class="mermaid"><svg id="render4208754209" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="231.5" style="max-width: 519.9187622070312px;" viewBox="0 0 519.9187622070312 231.5"><style>#render4208754209 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render4208754209 .error-icon{fill:#552222;}#render4208754209 .error-text{fill:#552222;stroke:#552222;}#render4208754209 .edge-thickness-normal{stroke-width:2px;}#render4208754209 .edge-thickness-thick{stroke-width:3.5px;}#render4208754209 .edge-pattern-solid{stroke-dasharray:0;}#render4208754209 .edge-pattern-dashed{stroke-dasharray:3;}#render4208754209 .edge-pattern-dotted{stroke-dasharray:2;}#render4208754209 .marker{fill:#333333;stroke:#333333;}#render4208754209 .marker.cross{stroke:#333333;}#render4208754209 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render4208754209 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render4208754209 .cluster-label text{fill:#333;}#render4208754209 .cluster-label span{color:#333;}#render4208754209 .label text,#render4208754209 span{fill:#333;color:#333;}#render4208754209 .node rect,#render4208754209 .node circle,#render4208754209 .node ellipse,#render4208754209 .node polygon,#render4208754209 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render4208754209 .node .label{text-align:center;}#render4208754209 .node.clickable{cursor:pointer;}#render4208754209 .arrowheadPath{fill:#333333;}#render4208754209 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render4208754209 .flowchart-link{stroke:#333333;fill:none;}#render4208754209 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render4208754209 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render4208754209 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render4208754209 .cluster text{fill:#333;}#render4208754209 .cluster span{color:#333;}#render4208754209 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render4208754209 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M84.390625,115.75L88.55729166666667,115.75C92.72395833333333,115.75,101.05729166666667,115.75,109.47395858764646,115.83333333333333C117.89062550862629,115.91666666666667,126.39062601725259,116.08333333333333,130.6406262715657,116.16666666666667L134.89062652587887,116.24999999999999" marker-end="url(#arrowhead5725)" style="fill:none"></path><defs><marker id="arrowhead5725" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M212.23185529394644,92.45373181982534L224.28123306966242,81.58644318318778C236.33061084537835,70.71915454655023,260.4293663968103,48.984577273275114,280.6454108391929,38.11728863663756C300.8614552815755,27.25,317.1947886149089,27.25,325.3614552815755,27.25L333.5281219482422,27.25" marker-end="url(#arrowhead5726)" style="fill:none"></path><defs><marker id="arrowhead5726" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M236.0281234741211,116.25L244.11145655314127,116.16666666666667C252.19478963216147,116.08333333333333,268.36145579020183,115.91666666666667,284.61145553588864,115.83333333333333C300.8614552815755,115.75,317.1947886149089,115.75,325.3614552815755,115.75L333.5281219482422,115.75" marker-end="url(#arrowhead5727)" style="fill:none"></path><defs><marker id="arrowhead5727" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-E" style="opacity: 1;" id="L-B-E"><path class="path" d="M212.2318569096306,140.04626961624825L224.28123441606587,150.74689134687355C236.33061192250113,161.44751307749883,260.42936693537166,182.84875653874943,280.6454111084736,193.54937826937473C300.8614552815755,204.25,317.1947886149089,204.25,325.3614552815755,204.25L333.5281219482422,204.25" marker-end="url(#arrowhead5728)" style="fill:none"></path><defs><marker id="arrowhead5728" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-F" style="opacity: 1;" id="L-C-F"><path class="path" d="M385.5281219482422,27.25L389.6947886149089,27.25C393.8614552815755,27.25,402.1947886149089,27.25,414.60302852192393,38.791666666666664C427.011268428939,50.333333333333336,443.49441490963574,73.41666666666667,451.73598814998417,84.95833333333333L459.9775613903326,96.5" marker-end="url(#arrowhead5729)" style="fill:none"></path><defs><marker id="arrowhead5729" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M385.5281219482422,115.75L389.6947886149089,115.75C393.8614552815755,115.75,402.1947886149089,115.75,410.5281219482422,115.75C418.8614552815755,115.75,427.1947886149089,115.75,431.3614552815755,115.75L435.5281219482422,115.75" marker-end="url(#arrowhead5730)" style="fill:none"></path><defs><marker id="arrowhead5730" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M385.5281219482422,204.25L389.6947886149089,204.25C393.8614552815755,204.25,402.1947886149089,204.25,414.60302852192393,192.70833333333334C427.011268428939,181.16666666666666,443.49441490963574,158.08333333333334,451.73598814998417,146.54166666666666L459.9775613903326,135" marker-end="url(#arrowhead5731)" style="fill:none"></path><defs><marker id="arrowhead5731" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(284.5281219482422,27.25)"><g transform="translate(-5.875,-9.25)" class="label"><rect rx="0" ry="0" width="11.750106811523438" height="18.500167846679688"></rect><text><tspan xml:space="preserve" dy="1em" x="1">..</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(284.5281219482422,115.75)"><g transform="translate(-18.9375,-9.25)" class="label"><rect rx="0" ry="0" width="37.867530822753906" height="18.500167846679688"></rect><text><tspan xml:space="preserve" dy="1em" x="1">.或空</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(284.5281219482422,204.25)"><g transform="translate(-24,-9.25)" class="label"><rect rx="0" ry="0" width="47.99262237548828" height="18.500167846679688"></rect><text><tspan xml:space="preserve" dy="1em" x="1">普通名</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6794" transform="translate(46.1953125,115.75)"><rect rx="0" ry="0" x="-38.1953125" y="-19.25" width="76.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-28.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">按/拆分</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6795" transform="translate(184.9593734741211,115.75)"><polygon points="50.56875,0 101.1375,-50.56875 50.56875,-101.1375 0,-50.56875" transform="translate(-50.56875,50.56875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-26.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">段类型?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6797" transform="translate(359.5281219482422,27.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">弹栈</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6799" transform="translate(359.5281219482422,115.75)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">跳过</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6801" transform="translate(359.5281219482422,204.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">压栈</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6803" transform="translate(473.7234344482422,115.75)"><rect rx="0" ry="0" x="-38.1953125" y="-19.25" width="76.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-28.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">用/拼接</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public String simplifyPath(String path) {        Deque&lt;String&gt; st = new ArrayDeque&lt;&gt;();        for (String p : path.split(&quot;/&quot;)) {            if (p.equals(&quot;..&quot;) &amp;&amp; !st.isEmpty()) st.pop();            else if (!p.equals(&quot;.&quot;) &amp;&amp; !p.equals(&quot;..&quot;) &amp;&amp; !p.isEmpty()) st.push(p);        }        StringBuilder sb = new StringBuilder();        while (!st.isEmpty()) sb.insert(0, &quot;/&quot; + st.pop());        return sb.length() == 0 ? &quot;/&quot; : sb.toString();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>..</code> 在栈空时（根目录）无效，不能弹。结果空返回 <code>/</code>（根）。<code>split(&quot;/&quot;)</code> 会产生空段，要跳过。</li></ul><h2 id="155.-%E6%9C%80%E5%B0%8F%E6%A0%88" tabindex="-1"><a href="https://leetcode.cn/problems/min-stack/" target="_blank">155. 最小栈</a></h2><blockquote><p>栈支持 <code>push</code>/<code>pop</code>/<code>top</code>/<code>getMin</code> 全 O(1)。</p></blockquote><p><strong>思路脉络</strong>：辅助栈 <code>minSt</code> 同步压栈，每步记&quot;当前栈内最小值&quot;。<code>pop</code> 同步弹。和热题 100 相同。</p><div class="mermaid"><svg id="render1612116667" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 535.1953125px;" viewBox="0 0 535.1953125 54.5"><style>#render1612116667 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1612116667 .error-icon{fill:#552222;}#render1612116667 .error-text{fill:#552222;stroke:#552222;}#render1612116667 .edge-thickness-normal{stroke-width:2px;}#render1612116667 .edge-thickness-thick{stroke-width:3.5px;}#render1612116667 .edge-pattern-solid{stroke-dasharray:0;}#render1612116667 .edge-pattern-dashed{stroke-dasharray:3;}#render1612116667 .edge-pattern-dotted{stroke-dasharray:2;}#render1612116667 .marker{fill:#333333;stroke:#333333;}#render1612116667 .marker.cross{stroke:#333333;}#render1612116667 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1612116667 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1612116667 .cluster-label text{fill:#333;}#render1612116667 .cluster-label span{color:#333;}#render1612116667 .label text,#render1612116667 span{fill:#333;color:#333;}#render1612116667 .node rect,#render1612116667 .node circle,#render1612116667 .node ellipse,#render1612116667 .node polygon,#render1612116667 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1612116667 .node .label{text-align:center;}#render1612116667 .node.clickable{cursor:pointer;}#render1612116667 .arrowheadPath{fill:#333333;}#render1612116667 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1612116667 .flowchart-link{stroke:#333333;fill:none;}#render1612116667 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1612116667 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1612116667 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1612116667 .cluster text{fill:#333;}#render1612116667 .cluster span{color:#333;}#render1612116667 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1612116667 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M160.5859375,27.25L164.75260416666666,27.25C168.91927083333334,27.25,177.25260416666666,27.25,185.5859375,27.25C193.91927083333334,27.25,202.25260416666666,27.25,206.41927083333334,27.25L210.5859375,27.25" marker-end="url(#arrowhead5743)" style="fill:none"></path><defs><marker id="arrowhead5743" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M310.875,27.25L315.0416666666667,27.25C319.2083333333333,27.25,327.5416666666667,27.25,335.875,27.25C344.2083333333333,27.25,352.5416666666667,27.25,356.7083333333333,27.25L360.875,27.25" marker-end="url(#arrowhead5744)" style="fill:none"></path><defs><marker id="arrowhead5744" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6812" transform="translate(84.29296875,27.25)"><rect rx="0" ry="0" x="-76.29296875" y="-19.25" width="152.5859375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-66.29296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">push:st+minSt同步</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6813" transform="translate(260.73046875,27.25)"><rect rx="0" ry="0" x="-50.14453125" y="-19.25" width="100.2890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.14453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pop:同步弹</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6815" transform="translate(444.03515625,27.25)"><rect rx="0" ry="0" x="-83.16015625" y="-19.25" width="166.3203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-73.16015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">getMin:minSt.peek()</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class MinStack {    Deque&lt;Integer&gt; st = new ArrayDeque&lt;&gt;(), minSt = new ArrayDeque&lt;&gt;();    public MinStack() { minSt.push(Integer.MAX_VALUE); }    public void push(int v) { st.push(v); minSt.push(Math.min(minSt.peek(), v)); }    public void pop() { st.pop(); minSt.pop(); }    public int top() { return st.peek(); }    public int getMin() { return minSt.peek(); }}</code></pre><blockquote><p>[!info] 复杂度<br />各操作 O(1)，空间 O(n)。</p></blockquote><h2 id="150.-%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC" tabindex="-1"><a href="https://leetcode.cn/problems/evaluate-reverse-polish-notation/" target="_blank">150. 逆波兰表达式求值</a></h2><blockquote><p>后缀表达式（token 数组）求值，<code>+ - * /</code>。</p></blockquote><p><strong>思路脉络</strong>：遇数字入栈，遇运算符弹两个（注意顺序：先弹的是右操作数）计算后入栈。除法用 <code>truncate toward zero</code>（Java <code>/</code> 默认就是）。</p><div class="mermaid"><svg id="render498587358" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="250.86251831054688" style="max-width: 586.7374877929688px;" viewBox="0 0 586.7374877929688 250.86251831054688"><style>#render498587358 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render498587358 .error-icon{fill:#552222;}#render498587358 .error-text{fill:#552222;stroke:#552222;}#render498587358 .edge-thickness-normal{stroke-width:2px;}#render498587358 .edge-thickness-thick{stroke-width:3.5px;}#render498587358 .edge-pattern-solid{stroke-dasharray:0;}#render498587358 .edge-pattern-dashed{stroke-dasharray:3;}#render498587358 .edge-pattern-dotted{stroke-dasharray:2;}#render498587358 .marker{fill:#333333;stroke:#333333;}#render498587358 .marker.cross{stroke:#333333;}#render498587358 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render498587358 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render498587358 .cluster-label text{fill:#333;}#render498587358 .cluster-label span{color:#333;}#render498587358 .label text,#render498587358 span{fill:#333;color:#333;}#render498587358 .node rect,#render498587358 .node circle,#render498587358 .node ellipse,#render498587358 .node polygon,#render498587358 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render498587358 .node .label{text-align:center;}#render498587358 .node.clickable{cursor:pointer;}#render498587358 .arrowheadPath{fill:#333333;}#render498587358 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render498587358 .flowchart-link{stroke:#333333;fill:none;}#render498587358 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render498587358 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render498587358 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render498587358 .cluster text{fill:#333;}#render498587358 .cluster span{color:#333;}#render498587358 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render498587358 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M71.73193227953868,110.48750305175781L80.68806856628224,100.63437779744466C89.64420485302578,90.78125254313152,107.5564774265129,71.0750020345052,120.7626139675696,61.30521011352539C133.9687505086263,51.53541819254557,142.46875101725263,51.70208485921224,146.71875127156576,51.78541819254557L150.96875152587893,51.868751525878906" marker-end="url(#arrowhead5774)" style="fill:none"></path><defs><marker id="arrowhead5774" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M237.7062484741211,51.8687515258789L243.1229159037272,51.78541819254556C248.53958333333335,51.70208485921224,259.37291819254557,51.53541819254557,277.7505231221517,56.34644409657985C296.1281280517578,61.157470000614126,322.0500030517578,70.94618847534933,335.0109405517578,75.84054771271694L347.9718780517578,80.73490695008455" marker-end="url(#arrowhead5775)" style="fill:none"></path><defs><marker id="arrowhead5775" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M209.84229179967923,79.73270820032081L219.902952008359,97.8793406755603C229.96361221703876,116.02597315079981,250.0849326343983,152.3192381012788,265.6455928430781,170.4658705765183C281.2062530517578,188.6125030517578,292.2062530517578,188.6125030517578,297.7062530517578,188.6125030517578L303.2062530517578,188.6125030517578" marker-end="url(#arrowhead5776)" style="fill:none"></path><defs><marker id="arrowhead5776" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M444.7375030517578,188.6125030517578L448.9041697184245,188.6125030517578C453.0708363850911,188.6125030517578,461.4041697184245,188.6125030517578,469.7375030517578,190.29938116121056C478.0708363850911,191.98625927066328,486.4041697184245,195.36001548956878,490.5708363850911,197.04689359902147L494.7375030517578,198.73377170847422" marker-end="url(#arrowhead5777)" style="fill:none"></path><defs><marker id="arrowhead5777" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M347.9718780517578,100.37134762755217L335.0109405517578,105.26570686491978C322.0500030517578,110.16006610228737,296.1281280517578,119.9487845770226,270.43906529744464,124.8431438143902C244.7500025431315,129.7375030517578,219.29375203450522,129.7375030517578,195.17083485921225,129.7375030517578C171.04791768391928,129.7375030517578,148.25833384195963,129.7375030517578,132.69687525431314,129.7375030517578C117.13541666666667,129.7375030517578,108.80208333333333,129.7375030517578,104.63541666666667,129.7375030517578L100.46875,129.7375030517578" marker-end="url(#arrowhead5778)" style="fill:none"></path><defs><marker id="arrowhead5778" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M494.7375030517578,232.7412343950414L490.5708363850911,234.42811250449415C486.4041697184245,236.11499061394684,478.0708363850911,239.48874683285234,457.9432322184245,241.17562494230506C437.8156280517578,242.8625030517578,405.8937530517578,242.8625030517578,372.6385447184245,242.8625030517578C339.3833363850911,242.8625030517578,304.7947947184245,242.8625030517578,274.772398630778,242.8625030517578C244.7500025431315,242.8625030517578,219.29375203450522,242.8625030517578,195.17083485921225,242.8625030517578C171.04791768391928,242.8625030517578,148.25833384195963,242.8625030517578,127.01142117972752,227.21666971842447C105.7645085174954,211.57083638509116,86.06026703499079,180.27916971842447,76.2081462937385,164.63333638509116L66.35602555248619,148.9875030517578" marker-end="url(#arrowhead5779)" style="fill:none"></path><defs><marker id="arrowhead5779" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(270.2062530517578,51.368751525878906)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000198364257812" height="18.50023078918457"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(270.2062530517578,188.6125030517578)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000198364257812" height="18.50023078918457"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6828" transform="translate(54.234375,129.7375030517578)"><rect rx="0" ry="0" x="-46.234375" y="-19.25" width="92.46875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历token</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6829" transform="translate(193.8375015258789,51.368751525878906)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">数字?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6831" transform="translate(373.9718780517578,90.55312728881836)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">入栈</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6833" transform="translate(373.9718780517578,188.6125030517578)"><rect rx="0" ry="0" x="-70.765625" y="-19.25" width="141.53125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-60.765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">弹b,弹a,算a op b</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6835" transform="translate(536.7375030517578,215.7375030517578)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">结果入栈</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0150-rpn.gif" alt="" /></p><pre><code class="language-java">class Solution {    public int evalRPN(String[] tokens) {        Deque&lt;Integer&gt; st = new ArrayDeque&lt;&gt;();        for (String t : tokens) {            if (t.equals(&quot;+&quot;) || t.equals(&quot;-&quot;) || t.equals(&quot;*&quot;) || t.equals(&quot;/&quot;)) {                int b = st.pop(), a = st.pop();                st.push(t.equals(&quot;+&quot;) ? a + b : t.equals(&quot;-&quot;) ? a - b                     : t.equals(&quot;*&quot;) ? a * b : a / b);            } else st.push(Integer.parseInt(t));        }        return st.pop();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>操作数顺序：先弹的是右操作数 <code>b</code>、后弹的是左操作数 <code>a</code>，<code>a - b</code>、<code>a / b</code> 顺序反了结果错。Java <code>a/b</code> 对负数是向 0 取整，符合题意。</li></ul><h2 id="224.-%E5%9F%BA%E6%9C%AC%E8%AE%A1%E7%AE%97%E5%99%A8" tabindex="-1"><a href="https://leetcode.cn/problems/basic-calculator/" target="_blank">224. 基本计算器</a></h2><blockquote><p>中缀表达式含 <code>+ - ( )</code> 和空格，求值。</p></blockquote><p><strong>思路脉络</strong>：用栈处理括号和符号。核心是维护&quot;当前符号 <code>sign</code>&quot;——遇 <code>+</code> sign=1、<code>-</code> sign=-1，数字按 <code>sign*数字</code> 累加。遇 <code>(</code> 把当前结果和 sign 入栈、重置；遇 <code>)</code> 弹栈合并。</p><div class="mermaid"><svg id="render1172855155" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="376.86248779296875" style="max-width: 490.9093933105469px;" viewBox="0 1 490.9093933105469 376.86248779296875"><style>#render1172855155 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1172855155 .error-icon{fill:#552222;}#render1172855155 .error-text{fill:#552222;stroke:#552222;}#render1172855155 .edge-thickness-normal{stroke-width:2px;}#render1172855155 .edge-thickness-thick{stroke-width:3.5px;}#render1172855155 .edge-pattern-solid{stroke-dasharray:0;}#render1172855155 .edge-pattern-dashed{stroke-dasharray:3;}#render1172855155 .edge-pattern-dotted{stroke-dasharray:2;}#render1172855155 .marker{fill:#333333;stroke:#333333;}#render1172855155 .marker.cross{stroke:#333333;}#render1172855155 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1172855155 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1172855155 .cluster-label text{fill:#333;}#render1172855155 .cluster-label span{color:#333;}#render1172855155 .label text,#render1172855155 span{fill:#333;color:#333;}#render1172855155 .node rect,#render1172855155 .node circle,#render1172855155 .node ellipse,#render1172855155 .node polygon,#render1172855155 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1172855155 .node .label{text-align:center;}#render1172855155 .node.clickable{cursor:pointer;}#render1172855155 .arrowheadPath{fill:#333333;}#render1172855155 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1172855155 .flowchart-link{stroke:#333333;fill:none;}#render1172855155 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1172855155 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1172855155 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1172855155 .cluster text{fill:#333;}#render1172855155 .cluster span{color:#333;}#render1172855155 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1172855155 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M66.45745242699317,183.9875030517578L74.88121035582765,174.13437779744467C83.30496828466211,164.2812525431315,100.15248414233106,144.57500203450522,112.82624232547869,134.8052101135254C125.50000050862631,125.03541819254558,134.00000101725263,125.20208485921226,138.25000127156576,125.28541819254559L142.50000152587893,125.36875152587893" marker-end="url(#arrowhead5814)" style="fill:none"></path><defs><marker id="arrowhead5814" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M204.3207832638643,100.45203326386437L214.44492406184656,86.58502771988697C224.5690648598288,72.71802217590958,244.8173464557933,44.98401108795479,265.29890912877556,32.30893360385321C285.7804718017578,19.633856119751645,306.4953155517578,22.017712239503293,316.8527374267578,23.209640299379114L327.2101593017578,24.401568359254934" marker-end="url(#arrowhead5815)" style="fill:none"></path><defs><marker id="arrowhead5815" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M229.23749847412108,125.3687515258789L235.2088534037272,125.28541819254558C241.18020833333333,125.20208485921223,253.1229181925456,125.03541819254558,269.86973648386277,128.27448081970215C286.61655477517996,131.51354344685873,308.16748149860206,138.15833536783853,318.94294486031316,141.48073132832846L329.7184082220242,144.80312728881836" marker-end="url(#arrowhead5816)" style="fill:none"></path><defs><marker id="arrowhead5816" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-E" style="opacity: 1;" id="L-B-E"><path class="path" d="M204.3207819654379,150.28546803456206L214.44492297982455,163.9858072040947C224.5690639942112,177.68614637362734,244.81734602298448,205.08682471269256,264.0404453707045,220.90822185234876C283.2635447184245,236.72961899200493,301.4614613850911,240.97173493225205,310.5604197184245,243.09279290237563L319.6593780517578,245.2138508724992" marker-end="url(#arrowhead5817)" style="fill:none"></path><defs><marker id="arrowhead5817" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-F" style="opacity: 1;" id="L-B-F"><path class="path" d="M198.40014322050592,156.206106779494L209.5110573590479,183.6696728248713C220.6219714975899,211.13323887024862,242.84379977467384,266.06037096100323,268.1357034965492,296.82966716559855C293.4276072184245,327.59896337019387,321.7895863850911,334.21042368863,335.9705759684245,337.51615384784805L350.1515655517578,340.8218840070661" marker-end="url(#arrowhead5818)" style="fill:none"></path><defs><marker id="arrowhead5818" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M327.2101593017578,39.34843164074507L316.8527374267578,40.54035970062089C306.4953155517578,41.73228776049671,285.7804718017578,44.11614388024836,262.14023717244464,45.30807194012417C238.5000025431315,46.5,211.93437703450522,46.5,187.25677235921225,46.5C162.57916768391928,46.5,139.78958384195963,46.5,118.59957962322925,69.41458384195964C97.40957540449887,92.32916768391927,77.81915080899772,138.15833536783853,68.02393851124715,161.0729192097982L58.228726213496586,183.9875030517578" marker-end="url(#arrowhead5819)" style="fill:none"></path><defs><marker id="arrowhead5819" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M329.7184082220242,183.30312728881836L318.94294486031316,186.62552324930826C308.16748149860206,189.9479192097982,286.61655477517996,196.59271113077799,262.55827865915575,199.9151070912679C238.5000025431315,203.2375030517578,211.93437703450522,203.2375030517578,187.25677235921225,203.2375030517578C162.57916768391928,203.2375030517578,139.78958384195963,203.2375030517578,124.22812525431316,203.2375030517578C108.66666666666667,203.2375030517578,100.33333333333333,203.2375030517578,96.16666666666667,203.2375030517578L92,203.2375030517578" marker-end="url(#arrowhead5820)" style="fill:none"></path><defs><marker id="arrowhead5820" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M319.6593780517578,279.01115523101646L310.5604197184245,281.13221320114C301.4614613850911,283.2532711712636,283.2635447184245,287.4953871115107,260.881773630778,289.61644508163425C238.5000025431315,291.7375030517578,211.93437703450522,291.7375030517578,187.25677235921225,291.7375030517578C162.57916768391928,291.7375030517578,139.78958384195963,291.7375030517578,119.65703297559376,280.1958363850911C99.52448210922786,268.6541697184245,82.04896421845574,245.57083638509116,73.31120527306967,234.02916971842447L64.57344632768361,222.4875030517578" marker-end="url(#arrowhead5821)" style="fill:none"></path><defs><marker id="arrowhead5821" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-A" style="opacity: 1;" id="L-F-A"><path class="path" d="M350.1515655517578,355.44584663078285L335.9705759684245,357.077789367612C321.7895863850911,358.7097321044412,293.4276072184245,361.9736175780995,265.963804880778,363.60556031492865C238.5000025431315,365.2375030517578,211.93437703450522,365.2375030517578,187.25677235921225,365.2375030517578C162.57916768391928,365.2375030517578,139.78958384195963,365.2375030517578,118.55502854649421,341.4458363850911C97.32047325102882,317.6541697184245,77.64094650205762,270.0708363850911,67.80118312757202,246.27916971842447L57.96141975308642,222.4875030517578" marker-end="url(#arrowhead5822)" style="fill:none"></path><defs><marker id="arrowhead5822" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(265.0656280517578,17.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000133514404297" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(265.0656280517578,124.8687515258789)"><g transform="translate(-11.328125,-9.25)" class="label"><rect rx="0" ry="0" width="22.65643882751465" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">+/-</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(265.0656280517578,232.4875030517578)"><g transform="translate(-2.9375,-9.25)" class="label"><rect rx="0" ry="0" width="5.875049114227295" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">(</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(265.0656280517578,320.9875030517578)"><g transform="translate(-2.9375,-9.25)" class="label"><rect rx="0" ry="0" width="5.875049114227295" height="18.500154495239258"></rect><text><tspan xml:space="preserve" dy="1em" x="1">)</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6858" transform="translate(50,203.2375030517578)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历字符</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6859" transform="translate(185.3687515258789,124.8687515258789)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">数字?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6861" transform="translate(392.1515655517578,31.875)"><rect rx="0" ry="0" x="-64.94140625" y="-19.25" width="129.8828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-54.94140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">num=num*10+d</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6863" transform="translate(392.1515655517578,164.05312728881836)"><rect rx="0" ry="0" x="-90.7578125" y="-19.25" width="181.515625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.7578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">res+=sign*num,sign=±1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6865" transform="translate(392.1515655517578,262.1125030517578)"><rect rx="0" ry="0" x="-72.4921875" y="-19.25" width="144.984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-62.4921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">入栈res,sign,重置</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6867" transform="translate(392.1515655517578,350.6125030517578)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">弹栈合并</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0224-calculator.gif" alt="" /></p><pre><code class="language-java">class Solution {    public int calculate(String s) {        Deque&lt;Integer&gt; st = new ArrayDeque&lt;&gt;();        int res = 0, sign = 1, num = 0;        for (char c : s.toCharArray()) {            if (Character.isDigit(c)) num = num * 10 + (c - &#39;0&#39;);            else if (c == &#39;+&#39;) { res += sign * num; num = 0; sign = 1; }            else if (c == &#39;-&#39;) { res += sign * num; num = 0; sign = -1; }            else if (c == &#39;(&#39;) { st.push(res); st.push(sign); res = 0; sign = 1; }            else if (c == &#39;)&#39;) { res += sign * num; num = 0; res *= st.pop(); res += st.pop(); }        }        return res + sign * num;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>最后 <code>return res + sign*num</code> 处理末尾未结算的数字。遇 <code>(</code> 入栈顺序是 <code>res</code> 先、<code>sign</code> 后，遇 <code>)</code> 弹出是 <code>sign</code> 先（乘）、<code>res</code> 后（加）——LIFO。<code>num</code> 用完后归零。</li></ul><hr /><h1 id="8.-%E9%93%BE%E8%A1%A8" tabindex="-1">8. 链表</h1><p>链表题通用心法：<strong>哨兵节点 dummy</strong> 统一头被改的情况、<strong>快慢指针</strong> 找中点/倒数/环、<strong>画图改指针</strong> 不靠脑内想象。</p><h2 id="141.-%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/linked-list-cycle/" target="_blank">141. 环形链表</a></h2><blockquote><p>判断链表是否有环。</p></blockquote><p><strong>思路脉络</strong>：快慢指针，<code>fast</code> 每次两步、<code>slow</code> 一步。有环 fast 终追上 slow；无环 fast 先到 null。</p><div class="mermaid"><svg id="render2102328297" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="258.5121154785156" style="max-width: 615.9484252929688px;" viewBox="0 -0.000003814697265625 615.9484252929688 258.5121154785156"><style>#render2102328297 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2102328297 .error-icon{fill:#552222;}#render2102328297 .error-text{fill:#552222;stroke:#552222;}#render2102328297 .edge-thickness-normal{stroke-width:2px;}#render2102328297 .edge-thickness-thick{stroke-width:3.5px;}#render2102328297 .edge-pattern-solid{stroke-dasharray:0;}#render2102328297 .edge-pattern-dashed{stroke-dasharray:3;}#render2102328297 .edge-pattern-dotted{stroke-dasharray:2;}#render2102328297 .marker{fill:#333333;stroke:#333333;}#render2102328297 .marker.cross{stroke:#333333;}#render2102328297 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2102328297 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2102328297 .cluster-label text{fill:#333;}#render2102328297 .cluster-label span{color:#333;}#render2102328297 .label text,#render2102328297 span{fill:#333;color:#333;}#render2102328297 .node rect,#render2102328297 .node circle,#render2102328297 .node ellipse,#render2102328297 .node polygon,#render2102328297 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2102328297 .node .label{text-align:center;}#render2102328297 .node.clickable{cursor:pointer;}#render2102328297 .arrowheadPath{fill:#333333;}#render2102328297 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2102328297 .flowchart-link{stroke:#333333;fill:none;}#render2102328297 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2102328297 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2102328297 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2102328297 .cluster text{fill:#333;}#render2102328297 .cluster span{color:#333;}#render2102328297 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2102328297 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M97.95261662635873,102.14765739440918L106.83811802196561,96.59778753916423C115.7236194175725,91.04791768391927,133.49462220878624,79.94817797342937,146.6301237315497,74.48164145151775C159.76562525431316,69.01510492960612,168.2656255086263,69.18177159627278,172.51562563578287,69.26510492960612L176.76562576293946,69.34843826293945" marker-end="url(#arrowhead5848)" style="fill:none"></path><defs><marker id="arrowhead5848" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M288.10574924748704,58.991686747487044L295.2485412938857,57.42614533339577C302.39133334028435,55.860603919304516,316.6769174330816,52.72952109112199,335.4852043983035,51.16397967703072C354.2934913635254,49.59843826293945,377.6244812011719,49.59843826293945,389.2899761199951,49.59843826293945L400.95547103881836,49.59843826293945" marker-end="url(#arrowhead5849)" style="fill:none"></path><defs><marker id="arrowhead5849" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M273.31159051761506,94.49934698238486L282.920075685659,101.24444298855558C292.528560853703,107.98953899472629,311.74553118979094,121.47973100706774,330.6514811394453,133.35223195971642C349.5574310890997,145.22473291236514,368.1523606523204,155.4795428053211,377.4498254339308,160.60694775179908L386.7472902155412,165.73435269827706" marker-end="url(#arrowhead5850)" style="fill:none"></path><defs><marker id="arrowhead5850" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M490.4484382629394,188.01914215087888L495.86510531107587,187.93580881754556C501.2817723592122,187.85247548421225,512.115106455485,187.68580881754556,523.0317735036214,187.60247548421225C533.9484405517578,187.5191421508789,544.9484405517578,187.5191421508789,550.4484405517578,187.5191421508789L555.9484405517578,187.5191421508789" marker-end="url(#arrowhead5851)" style="fill:none"></path><defs><marker id="arrowhead5851" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M372.7909155839203,196.34755544598084L365.8195132409134,197.3136532301305C358.8481108979065,198.2797510142802,344.90530621189265,200.21194658257954,322.2924974917292,201.1780443667292C299.67968877156574,202.1441421508789,268.3968760172526,202.1441421508789,238.4473965962728,202.1441421508789C208.49791717529297,202.1441421508789,179.88177108764648,202.1441421508789,154.89444631534346,191.89472802480063C129.90712154304043,181.64531389872232,108.54861808608086,161.14648564656576,97.8693663576011,150.89707152048746L87.1901146291213,140.64765739440918" marker-end="url(#arrowhead5852)" style="fill:none"></path><defs><marker id="arrowhead5852" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(330.9625015258789,49.59843826293945)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00026512145996" height="18.500308990478516"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(330.9625015258789,134.96992301940918)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00026512145996" height="18.500308990478516"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(522.9484405517578,187.5191421508789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00026512145996" height="18.500308990478516"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(237.11406326293945,202.1441421508789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00026512145996" height="18.500308990478516"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6886" transform="translate(67.1328125,121.39765739440918)"><rect rx="0" ry="0" x="-59.1328125" y="-19.25" width="118.265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-49.1328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">slow+1,fast+2</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6887" transform="translate(237.11406326293945,68.84843826293945)"><polygon points="60.8484375,0 121.696875,-60.8484375 60.8484375,-121.696875 0,-60.8484375" transform="translate(-60.8484375,60.8484375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-38.359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">fast==null?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6889" transform="translate(426.95547103881836,49.59843826293945)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">无环</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6891" transform="translate(426.95547103881836,187.5191421508789)"><polygon points="62.99296875,0 125.9859375,-62.99296875 62.99296875,-125.9859375 0,-62.99296875" transform="translate(-62.99296875,62.99296875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-40.7421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">slow==fast?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6893" transform="translate(581.9484405517578,187.5191421508789)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">有环</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">public class Solution {    public boolean hasCycle(ListNode head) {        ListNode slow = head, fast = head;        while (fast != null &amp;&amp; fast.next != null) {            slow = slow.next; fast = fast.next.next;            if (slow == fast) return true;        }        return false;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="2.-%E4%B8%A4%E6%95%B0%E7%9B%B8%E5%8A%A0" tabindex="-1"><a href="https://leetcode.cn/problems/add-two-numbers/" target="_blank">2. 两数相加</a></h2><blockquote><p>两个链表逆序存两个非负整数（头是低位），返回和的链表。</p></blockquote><p><strong>思路脉络</strong>：模拟加法，从低位（头）逐位相加维护进位。循环条件带 <code>carry != 0</code> 防漏最高位进位。</p><div class="mermaid"><svg id="render2683410231" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 722.4140625px;" viewBox="0 0 722.4140625 89.5"><style>#render2683410231 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2683410231 .error-icon{fill:#552222;}#render2683410231 .error-text{fill:#552222;stroke:#552222;}#render2683410231 .edge-thickness-normal{stroke-width:2px;}#render2683410231 .edge-thickness-thick{stroke-width:3.5px;}#render2683410231 .edge-pattern-solid{stroke-dasharray:0;}#render2683410231 .edge-pattern-dashed{stroke-dasharray:3;}#render2683410231 .edge-pattern-dotted{stroke-dasharray:2;}#render2683410231 .marker{fill:#333333;stroke:#333333;}#render2683410231 .marker.cross{stroke:#333333;}#render2683410231 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2683410231 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2683410231 .cluster-label text{fill:#333;}#render2683410231 .cluster-label span{color:#333;}#render2683410231 .label text,#render2683410231 span{fill:#333;color:#333;}#render2683410231 .node rect,#render2683410231 .node circle,#render2683410231 .node ellipse,#render2683410231 .node polygon,#render2683410231 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2683410231 .node .label{text-align:center;}#render2683410231 .node.clickable{cursor:pointer;}#render2683410231 .arrowheadPath{fill:#333333;}#render2683410231 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2683410231 .flowchart-link{stroke:#333333;fill:none;}#render2683410231 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2683410231 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2683410231 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2683410231 .cluster text{fill:#333;}#render2683410231 .cluster span{color:#333;}#render2683410231 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2683410231 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M108,36.29166666666667L112.16666666666667,34.78472222222223C116.33333333333333,33.27777777777778,124.66666666666667,30.26388888888889,133,28.756944444444446C141.33333333333334,27.25,149.66666666666666,27.25,153.83333333333334,27.25L158,27.25" marker-end="url(#arrowhead5875)" style="fill:none"></path><defs><marker id="arrowhead5875" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M294.546875,27.25L298.7135416666667,27.25C302.8802083333333,27.25,311.2135416666667,27.25,319.546875,27.25C327.8802083333333,27.25,336.2135416666667,27.25,340.3802083333333,27.25L344.546875,27.25" marker-end="url(#arrowhead5876)" style="fill:none"></path><defs><marker id="arrowhead5876" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M580.4140625,27.25L584.5807291666666,27.25C588.7473958333334,27.25,597.0807291666666,27.25,605.4140625,28.936878109452735C613.7473958333334,30.62375621890547,622.0807291666666,33.99751243781095,626.2473958333334,35.68439054726368L630.4140625,37.37126865671642" marker-end="url(#arrowhead5877)" style="fill:none"></path><defs><marker id="arrowhead5877" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M630.4140625,71.37873134328359L626.2473958333334,73.06560945273633C622.0807291666666,74.75248756218906,613.7473958333334,78.12624378109453,585.7584635416666,79.81312189054727C557.76953125,81.5,510.125,81.5,462.48046875,81.5C414.8359375,81.5,367.19140625,81.5,327.8235677083333,81.5C288.4557291666667,81.5,257.3645833333333,81.5,226.2734375,81.5C195.18229166666666,81.5,164.09114583333334,81.5,144.37890625,79.99305555555556C124.66666666666667,78.4861111111111,116.33333333333333,75.47222222222221,112.16666666666667,73.96527777777777L108,72.45833333333333" marker-end="url(#arrowhead5878)" style="fill:none"></path><defs><marker id="arrowhead5878" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6904" transform="translate(58,54.375)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历两链表</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6905" transform="translate(226.2734375,27.25)"><rect rx="0" ry="0" x="-68.2734375" y="-19.25" width="136.546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.2734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sum=carry+l1+l2</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6907" transform="translate(462.48046875,27.25)"><rect rx="0" ry="0" x="-117.93359375" y="-19.25" width="235.8671875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-107.93359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">carry=sum/10,新节点=sum%10</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6909" transform="translate(672.4140625,54.375)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一节点</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {        ListNode dummy = new ListNode(0), cur = dummy;        int carry = 0;        while (l1 != null || l2 != null || carry != 0) {            int sum = carry;            if (l1 != null) { sum += l1.val; l1 = l1.next; }            if (l2 != null) { sum += l2.val; l2 = l2.next; }            carry = sum / 10;            cur.next = new ListNode(sum % 10);            cur = cur.next;        }        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(max(m,n))，空间 O(max(m,n))。</p></blockquote><h2 id="21.-%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/merge-two-sorted-lists/" target="_blank">21. 合并两个有序链表</a></h2><blockquote><p>合并两个升序链表。</p></blockquote><p><strong>思路脉络</strong>：dummy 哨兵，逐个比较小的接上，剩余直接挂。</p><div class="mermaid"><svg id="render3428831465" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="281.140625" style="max-width: 512.6875px;" viewBox="0 0 512.6875 281.140625"><style>#render3428831465 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3428831465 .error-icon{fill:#552222;}#render3428831465 .error-text{fill:#552222;stroke:#552222;}#render3428831465 .edge-thickness-normal{stroke-width:2px;}#render3428831465 .edge-thickness-thick{stroke-width:3.5px;}#render3428831465 .edge-pattern-solid{stroke-dasharray:0;}#render3428831465 .edge-pattern-dashed{stroke-dasharray:3;}#render3428831465 .edge-pattern-dotted{stroke-dasharray:2;}#render3428831465 .marker{fill:#333333;stroke:#333333;}#render3428831465 .marker.cross{stroke:#333333;}#render3428831465 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3428831465 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3428831465 .cluster-label text{fill:#333;}#render3428831465 .cluster-label span{color:#333;}#render3428831465 .label text,#render3428831465 span{fill:#333;color:#333;}#render3428831465 .node rect,#render3428831465 .node circle,#render3428831465 .node ellipse,#render3428831465 .node polygon,#render3428831465 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3428831465 .node .label{text-align:center;}#render3428831465 .node.clickable{cursor:pointer;}#render3428831465 .arrowheadPath{fill:#333333;}#render3428831465 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3428831465 .flowchart-link{stroke:#333333;fill:none;}#render3428831465 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3428831465 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3428831465 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3428831465 .cluster text{fill:#333;}#render3428831465 .cluster span{color:#333;}#render3428831465 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3428831465 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M84.01391702633666,146.140625L94.84493085528055,134.56640625C105.67594468422443,122.9921875,127.33797234211222,99.84375,142.4189861710561,88.35286458333333C157.5,76.86197916666667,166,77.02864583333333,170.25,77.11197916666667L174.5,77.1953125" marker-end="url(#arrowhead5901)" style="fill:none"></path><defs><marker id="arrowhead5901" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M311.890625,77.1953125L317.3072916666667,77.11197916666667C322.7239583333333,77.02864583333333,333.5572916666667,76.86197916666667,344.4739583333333,77.63225508076685C355.390625,78.40253099486704,366.390625,80.10974948973406,371.890625,80.96335873716757L377.390625,81.81696798460108" marker-end="url(#arrowhead5902)" style="fill:none"></path><defs><marker id="arrowhead5902" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M272.96846568680496,116.11747181319504L284.8721589056708,131.70466401099586C296.77585212453664,147.2918562087967,320.5832385622683,178.46624060439834,337.98693178113416,194.90704204963268C355.390625,211.34784349486702,366.390625,213.05506198973407,371.890625,213.90867123716757L377.390625,214.7622804846011" marker-end="url(#arrowhead5903)" style="fill:none"></path><defs><marker id="arrowhead5903" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M420.062954863252,110.9453125L407.4508998860433,122.51953125C394.8388449088347,134.09375,369.6147349544173,157.2421875,340.05346122720863,168.81640625C310.4921875,180.390625,276.59375,180.390625,244.02864583333334,180.390625C211.46354166666666,180.390625,180.23177083333334,180.390625,159.12369791666666,179.39806099397592C138.015625,178.4054969879518,127.03125,176.42036897590364,121.5390625,175.4278049698795L116.046875,174.43524096385542" marker-end="url(#arrowhead5904)" style="fill:none"></path><defs><marker id="arrowhead5904" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M377.43282585470087,243.890625L371.9257923789174,245.55729166666666C366.41875890313395,247.22395833333334,355.404691951567,250.55729166666666,332.9484397257835,252.22395833333334C310.4921875,253.890625,276.59375,253.890625,244.02864583333334,253.890625C211.46354166666666,253.890625,180.23177083333334,253.890625,153.79149746939737,242.34895833333334C127.35122410546138,230.80729166666666,105.70244821092278,207.72395833333334,94.87806026365348,196.18229166666666L84.05367231638418,184.640625" marker-end="url(#arrowhead5905)" style="fill:none"></path><defs><marker id="arrowhead5905" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(344.390625,76.6953125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(344.390625,209.640625)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6923" transform="translate(66,165.390625)"><rect rx="0" ry="0" x="-50.046875" y="-19.25" width="100.09375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.046875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l1,l2都非空</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6924" transform="translate(242.6953125,76.6953125)"><polygon points="68.6953125,0 137.390625,-68.6953125 68.6953125,-137.390625 0,-68.6953125" transform="translate(-68.6953125,68.6953125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-47.078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l1.val≤l2.val?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6926" transform="translate(441.0390625,91.6953125)"><rect rx="0" ry="0" x="-63.6484375" y="-19.25" width="127.296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.6484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">接l1,l1=l1.next</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6928" transform="translate(441.0390625,224.640625)"><rect rx="0" ry="0" x="-63.6484375" y="-19.25" width="127.296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.6484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">接l2,l2=l2.next</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6933" transform="translate(66,253.890625)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">剩余直接接上</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {        ListNode dummy = new ListNode(0), cur = dummy;        while (l1 != null &amp;&amp; l2 != null) {            if (l1.val &lt;= l2.val) { cur.next = l1; l1 = l1.next; }            else { cur.next = l2; l2 = l2.next; }            cur = cur.next;        }        cur.next = l1 != null ? l1 : l2;        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(m+n)，空间 O(1)。</p></blockquote><h2 id="138.-%E9%9A%8F%E6%9C%BA%E9%93%BE%E8%A1%A8%E7%9A%84%E5%A4%8D%E5%88%B6" tabindex="-1"><a href="https://leetcode.cn/problems/copy-list-with-random-pointer/" target="_blank">138. 随机链表的复制</a></h2><blockquote><p>链表节点有 <code>next</code> 和 <code>random</code> 指针，深拷贝。</p></blockquote><p><strong>思路脉络</strong>：哈希法两遍扫描——第一遍建所有新节点存 map，第二遍据旧节点 <code>next</code>/<code>random</code> 查 map 连新节点。</p><div class="mermaid"><svg id="render3592809491" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 765.3203125px;" viewBox="0 0 765.3203125 54.5"><style>#render3592809491 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3592809491 .error-icon{fill:#552222;}#render3592809491 .error-text{fill:#552222;stroke:#552222;}#render3592809491 .edge-thickness-normal{stroke-width:2px;}#render3592809491 .edge-thickness-thick{stroke-width:3.5px;}#render3592809491 .edge-pattern-solid{stroke-dasharray:0;}#render3592809491 .edge-pattern-dashed{stroke-dasharray:3;}#render3592809491 .edge-pattern-dotted{stroke-dasharray:2;}#render3592809491 .marker{fill:#333333;stroke:#333333;}#render3592809491 .marker.cross{stroke:#333333;}#render3592809491 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3592809491 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3592809491 .cluster-label text{fill:#333;}#render3592809491 .cluster-label span{color:#333;}#render3592809491 .label text,#render3592809491 span{fill:#333;color:#333;}#render3592809491 .node rect,#render3592809491 .node circle,#render3592809491 .node ellipse,#render3592809491 .node polygon,#render3592809491 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3592809491 .node .label{text-align:center;}#render3592809491 .node.clickable{cursor:pointer;}#render3592809491 .arrowheadPath{fill:#333333;}#render3592809491 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3592809491 .flowchart-link{stroke:#333333;fill:none;}#render3592809491 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3592809491 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3592809491 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3592809491 .cluster text{fill:#333;}#render3592809491 .cluster span{color:#333;}#render3592809491 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3592809491 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M128.4765625,27.25L132.64322916666666,27.25C136.80989583333334,27.25,145.14322916666666,27.25,153.4765625,27.25C161.80989583333334,27.25,170.14322916666666,27.25,174.30989583333334,27.25L178.4765625,27.25" marker-end="url(#arrowhead5922)" style="fill:none"></path><defs><marker id="arrowhead5922" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M310.4765625,27.25L314.6432291666667,27.25C318.8098958333333,27.25,327.1432291666667,27.25,335.4765625,27.25C343.8098958333333,27.25,352.1432291666667,27.25,356.3098958333333,27.25L360.4765625,27.25" marker-end="url(#arrowhead5923)" style="fill:none"></path><defs><marker id="arrowhead5923" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M544.71875,27.25L548.8854166666666,27.25C553.0520833333334,27.25,561.3854166666666,27.25,569.71875,27.25C578.0520833333334,27.25,586.3854166666666,27.25,590.5520833333334,27.25L594.71875,27.25" marker-end="url(#arrowhead5924)" style="fill:none"></path><defs><marker id="arrowhead5924" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6940" transform="translate(68.23828125,27.25)"><rect rx="0" ry="0" x="-60.23828125" y="-19.25" width="120.4765625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.23828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">第一遍:建map</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6941" transform="translate(244.4765625,27.25)"><rect rx="0" ry="0" x="-66" y="-19.25" width="132" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">旧节点→新节点</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6943" transform="translate(452.59765625,27.25)"><rect rx="0" ry="0" x="-92.12109375" y="-19.25" width="184.2421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-82.12109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">第二遍:连next/random</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6945" transform="translate(676.01953125,27.25)"><rect rx="0" ry="0" x="-81.30078125" y="-19.25" width="162.6015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-71.30078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">查map设新节点指针</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public Node copyRandomList(Node head) {        if (head == null) return null;        Map&lt;Node, Node&gt; map = new HashMap&lt;&gt;();        for (Node p = head; p != null; p = p.next) map.put(p, new Node(p.val));        for (Node p = head; p != null; p = p.next) {            map.get(p).next = map.get(p.next);            map.get(p).random = map.get(p.random);        }        return map.get(head);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><h2 id="92.-%E5%8F%8D%E8%BD%AC%E9%93%BE%E8%A1%A8-ii" tabindex="-1"><a href="https://leetcode.cn/problems/reverse-linked-list-ii/" target="_blank">92. 反转链表 II</a></h2><blockquote><p>反转链表 <code>[left, right]</code> 区间（1起），其余不变。</p></blockquote><p><strong>思路脉络</strong>：先定位 <code>pre</code>（left 前驱）。用头插法反转 <code>[left, right]</code>：每次把 <code>cur</code> 插到 <code>pre</code> 后面，<code>tail</code>（原 left）不断被往后推。dummy 处理 left=1。</p><div class="mermaid"><svg id="render940025797" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 787.609375px;" viewBox="0 0 787.609375 54.5"><style>#render940025797 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render940025797 .error-icon{fill:#552222;}#render940025797 .error-text{fill:#552222;stroke:#552222;}#render940025797 .edge-thickness-normal{stroke-width:2px;}#render940025797 .edge-thickness-thick{stroke-width:3.5px;}#render940025797 .edge-pattern-solid{stroke-dasharray:0;}#render940025797 .edge-pattern-dashed{stroke-dasharray:3;}#render940025797 .edge-pattern-dotted{stroke-dasharray:2;}#render940025797 .marker{fill:#333333;stroke:#333333;}#render940025797 .marker.cross{stroke:#333333;}#render940025797 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render940025797 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render940025797 .cluster-label text{fill:#333;}#render940025797 .cluster-label span{color:#333;}#render940025797 .label text,#render940025797 span{fill:#333;color:#333;}#render940025797 .node rect,#render940025797 .node circle,#render940025797 .node ellipse,#render940025797 .node polygon,#render940025797 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render940025797 .node .label{text-align:center;}#render940025797 .node.clickable{cursor:pointer;}#render940025797 .arrowheadPath{fill:#333333;}#render940025797 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render940025797 .flowchart-link{stroke:#333333;fill:none;}#render940025797 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render940025797 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render940025797 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render940025797 .cluster text{fill:#333;}#render940025797 .cluster span{color:#333;}#render940025797 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render940025797 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M141.5625,27.25L145.72916666666666,27.25C149.89583333333334,27.25,158.22916666666666,27.25,166.5625,27.25C174.89583333333334,27.25,183.22916666666666,27.25,187.39583333333334,27.25L191.5625,27.25" marker-end="url(#arrowhead5941)" style="fill:none"></path><defs><marker id="arrowhead5941" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M362.9140625,27.25L367.0807291666667,27.25C371.2473958333333,27.25,379.5807291666667,27.25,387.9140625,27.25C396.2473958333333,27.25,404.5807291666667,27.25,408.7473958333333,27.25L412.9140625,27.25" marker-end="url(#arrowhead5942)" style="fill:none"></path><defs><marker id="arrowhead5942" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M527.65625,27.25L531.8229166666666,27.25C535.9895833333334,27.25,544.3229166666666,27.25,552.65625,27.25C560.9895833333334,27.25,569.3229166666666,27.25,573.4895833333334,27.25L577.65625,27.25" marker-end="url(#arrowhead5943)" style="fill:none"></path><defs><marker id="arrowhead5943" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6952" transform="translate(74.78125,27.25)"><rect rx="0" ry="0" x="-66.78125" y="-19.25" width="133.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.78125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre走到left前驱</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6953" transform="translate(277.23828125,27.25)"><rect rx="0" ry="0" x="-85.67578125" y="-19.25" width="171.3515625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-75.67578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">头插法反转left..right</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6955" transform="translate(470.28515625,27.25)"><rect rx="0" ry="0" x="-57.37109375" y="-19.25" width="114.7421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47.37109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cur插到pre后</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6957" transform="translate(678.6328125,27.25)"><rect rx="0" ry="0" x="-100.9765625" y="-19.25" width="201.953125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-90.9765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">tail.next=next保持右连接</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode reverseBetween(ListNode head, int left, int right) {        ListNode dummy = new ListNode(0, head), pre = dummy;        for (int i = 1; i &lt; left; i++) pre = pre.next;        ListNode tail = pre.next, cur = tail.next;        for (int i = left; i &lt; right; i++) {            ListNode next = cur.next;            cur.next = pre.next; pre.next = cur;            tail.next = next; cur = next;        }        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>头插法：<code>cur</code> 插到 <code>pre</code> 后，<code>tail</code>（原 left）始终是这段的尾，<code>tail.next = next</code> 维持它与右侧连接。</li></ul><h2 id="25.-k-%E4%B8%AA%E4%B8%80%E7%BB%84%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/reverse-nodes-in-k-group/" target="_blank">25. K 个一组翻转链表</a></h2><blockquote><p>每 <code>k</code> 个一组翻转，不足 <code>k</code> 保持原样。</p></blockquote><p><strong>思路脉络</strong>：外层按 <code>k</code> 分组，每组区间反转后头尾接回主链。<code>reverse(head, tail)</code> 里 <code>prev</code> 从 <code>tail.next</code> 起是省处理的关键。</p><div class="mermaid"><svg id="render3007183474" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="191.8751983642578" style="max-width: 1088.461669921875px;" viewBox="0 0 1088.461669921875 191.8751983642578"><style>#render3007183474 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3007183474 .error-icon{fill:#552222;}#render3007183474 .error-text{fill:#552222;stroke:#552222;}#render3007183474 .edge-thickness-normal{stroke-width:2px;}#render3007183474 .edge-thickness-thick{stroke-width:3.5px;}#render3007183474 .edge-pattern-solid{stroke-dasharray:0;}#render3007183474 .edge-pattern-dashed{stroke-dasharray:3;}#render3007183474 .edge-pattern-dotted{stroke-dasharray:2;}#render3007183474 .marker{fill:#333333;stroke:#333333;}#render3007183474 .marker.cross{stroke:#333333;}#render3007183474 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3007183474 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3007183474 .cluster-label text{fill:#333;}#render3007183474 .cluster-label span{color:#333;}#render3007183474 .label text,#render3007183474 span{fill:#333;color:#333;}#render3007183474 .node rect,#render3007183474 .node circle,#render3007183474 .node ellipse,#render3007183474 .node polygon,#render3007183474 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3007183474 .node .label{text-align:center;}#render3007183474 .node.clickable{cursor:pointer;}#render3007183474 .arrowheadPath{fill:#333333;}#render3007183474 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3007183474 .flowchart-link{stroke:#333333;fill:none;}#render3007183474 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3007183474 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3007183474 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3007183474 .cluster text{fill:#333;}#render3007183474 .cluster span{color:#333;}#render3007183474 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3007183474 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M112.359375,112.50019454956055L116.52604166666667,112.50019454956055C120.69270833333333,112.50019454956055,129.02604166666666,112.50019454956055,137.359375,112.50019454956055C145.69270833333334,112.50019454956055,154.02604166666666,112.50019454956055,158.19270833333334,112.50019454956055L162.359375,112.50019454956055" marker-end="url(#arrowhead5981)" style="fill:none"></path><defs><marker id="arrowhead5981" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M247.64724609639404,93.25019454956055L254.2828613303284,89.62516212463379C260.9184765642627,86.00012969970703,274.18970703213137,78.75006484985352,285.07532252037885,75.20836575826009C295.9609380086263,71.66666666666667,304.4609385172526,71.83333333333333,308.71093877156574,71.91666666666667L312.9609390258789,72" marker-end="url(#arrowhead5982)" style="fill:none"></path><defs><marker id="arrowhead5982" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M390.2231999427216,55.2614827186005L398.42961923530834,50.59290226550041C406.6360385278951,45.92432181240033,423.0488771130686,36.58716090620016,442.63615578065543,31.91858045310008C462.2234344482422,27.25,484.9851531982422,27.25,496.3660125732422,27.25L507.7468719482422,27.25" marker-end="url(#arrowhead5983)" style="fill:none"></path><defs><marker id="arrowhead5983" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M390.22320190763946,88.73851836823943L398.4296208727399,93.24043197353285C406.63603983784037,97.74234557882629,423.04887776804134,106.74617278941314,436.75529673314173,111.24808639470656C450.4617156982422,115.75,461.4617156982422,115.75,466.9617156982422,115.75L472.4617156982422,115.75" marker-end="url(#arrowhead5984)" style="fill:none"></path><defs><marker id="arrowhead5984" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M595.0320281982422,115.75L599.1986948649088,115.75C603.3653615315756,115.75,611.6986948649088,115.75,620.0320281982422,115.75C628.3653615315756,115.75,636.6986948649088,115.75,640.8653615315756,115.75L645.0320281982422,115.75" marker-end="url(#arrowhead5985)" style="fill:none"></path><defs><marker id="arrowhead5985" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-G" style="opacity: 1;" id="L-F-G"><path class="path" d="M874.0554656982422,115.75L878.2221323649088,115.75C882.3887990315756,115.75,890.7221323649088,115.75,904.0135038918547,119.37503242492676C917.3048754188006,123.00006484985352,935.5542851393592,130.25012969970703,944.6789899996384,133.8751621246338L953.8036948599176,137.50019454956055" marker-end="url(#arrowhead5986)" style="fill:none"></path><defs><marker id="arrowhead5986" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-G LE-B" style="opacity: 1;" id="L-G-B"><path class="path" d="M929.0176632788873,176.00019454956055L924.023963682113,177.31269454956055C919.0302640853389,178.62519454956055,909.0428648917905,181.25019454956055,880.7972121700163,182.56269454956055C852.5515594482422,183.87519454956055,806.0476531982422,183.87519454956055,759.5437469482422,183.87519454956055C713.0398406982422,183.87519454956055,666.5359344482422,183.87519454956055,628.9031219482422,183.87519454956055C591.2703094482422,183.87519454956055,562.5085906982422,183.87519454956055,532.4135386149088,183.87519454956055C502.3184865315755,183.87519454956055,470.8901011149089,183.87519454956055,441.8425102233887,183.87519454956055C412.79491933186847,183.87519454956055,386.1281229654948,183.87519454956055,360.7946599324544,183.87519454956055C335.46119689941406,183.87519454956055,311.46106719970703,183.87519454956055,290.3260999034875,175.18769454956055C269.19113260726795,166.50019454956055,250.9213277145359,149.12519454956055,241.78642526816986,140.43769454956055L232.65152282180384,131.75019454956055" marker-end="url(#arrowhead5987)" style="fill:none"></path><defs><marker id="arrowhead5987" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(439.4617156982422,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000015258789062" height="18.500019073486328"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(439.4617156982422,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000015258789062" height="18.500019073486328"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6972" transform="translate(60.1796875,112.50019454956055)"><rect rx="0" ry="0" x="-52.1796875" y="-19.25" width="104.359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.1796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre=dummy</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6973" transform="translate(212.41015625,112.50019454956055)"><rect rx="0" ry="0" x="-50.05078125" y="-19.25" width="100.1015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.05078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">数k个找tail</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6975" transform="translate(359.4613265991211,71.5)"><polygon points="47.000390625,0 94.00078125,-47.000390625 47.000390625,-94.00078125 0,-47.000390625" transform="translate(-47.000390625,47.000390625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-22.97265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">不足k?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6977" transform="translate(533.7468719482422,27.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-6979" transform="translate(533.7468719482422,115.75)"><rect rx="0" ry="0" x="-61.28515625" y="-19.25" width="122.5703125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-51.28515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">反转head..tail</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-6981" transform="translate(759.5437469482422,115.75)"><rect rx="0" ry="0" x="-114.51171875" y="-19.25" width="229.0234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-104.51171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre.next=新头,新尾.next=nxt</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-G-6983" transform="translate(1002.2585906982422,156.75019454956055)"><rect rx="0" ry="0" x="-78.203125" y="-19.25" width="156.40625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-68.203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre=新尾,head=nxt</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode reverseKGroup(ListNode head, int k) {        ListNode dummy = new ListNode(0, head), pre = dummy;        while (head != null) {            ListNode tail = pre;            for (int i = 0; i &lt; k; i++) { tail = tail.next; if (tail == null) return dummy.next; }            ListNode nxt = tail.next;            ListNode[] rev = reverse(head, tail);            head = rev[0]; tail = rev[1];            pre.next = head; tail.next = nxt;            pre = tail; head = nxt;        }        return dummy.next;    }    ListNode[] reverse(ListNode head, ListNode tail) {        ListNode prev = tail.next, cur = head;        while (prev != tail) {            ListNode next = cur.next; cur.next = prev; prev = cur; cur = next;        }        return new ListNode[]{tail, head};    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="19.-%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%AC-n-%E4%B8%AA%E7%BB%93%E7%82%B9" tabindex="-1"><a href="https://leetcode.cn/problems/remove-nth-node-from-end-of-list/" target="_blank">19. 删除链表的倒数第 N 个结点</a></h2><blockquote><p>删除倒数第 N 个，一次遍历。</p></blockquote><p><strong>思路脉络</strong>：<code>fast</code> 先走 <code>n</code> 步，再和 <code>slow</code> 同速，<code>fast.next</code> 到 null 时 <code>slow</code> 在待删前驱。dummy 处理删头。</p><div class="mermaid"><svg id="render379982891" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="171.6296844482422" style="max-width: 943.9031372070312px;" viewBox="0 0 943.9031372070312 171.6296844482422"><style>#render379982891 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render379982891 .error-icon{fill:#552222;}#render379982891 .error-text{fill:#552222;stroke:#552222;}#render379982891 .edge-thickness-normal{stroke-width:2px;}#render379982891 .edge-thickness-thick{stroke-width:3.5px;}#render379982891 .edge-pattern-solid{stroke-dasharray:0;}#render379982891 .edge-pattern-dashed{stroke-dasharray:3;}#render379982891 .edge-pattern-dotted{stroke-dasharray:2;}#render379982891 .marker{fill:#333333;stroke:#333333;}#render379982891 .marker.cross{stroke:#333333;}#render379982891 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render379982891 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render379982891 .cluster-label text{fill:#333;}#render379982891 .cluster-label span{color:#333;}#render379982891 .label text,#render379982891 span{fill:#333;color:#333;}#render379982891 .node rect,#render379982891 .node circle,#render379982891 .node ellipse,#render379982891 .node polygon,#render379982891 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render379982891 .node .label{text-align:center;}#render379982891 .node.clickable{cursor:pointer;}#render379982891 .arrowheadPath{fill:#333333;}#render379982891 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render379982891 .flowchart-link{stroke:#333333;fill:none;}#render379982891 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render379982891 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render379982891 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render379982891 .cluster text{fill:#333;}#render379982891 .cluster span{color:#333;}#render379982891 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render379982891 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M111.8828125,85.8148422241211L116.04947916666667,85.8148422241211C120.21614583333333,85.8148422241211,128.54947916666666,85.8148422241211,136.8828125,85.8148422241211C145.21614583333334,85.8148422241211,153.54947916666666,85.8148422241211,157.71614583333334,85.8148422241211L161.8828125,85.8148422241211" marker-end="url(#arrowhead6010)" style="fill:none"></path><defs><marker id="arrowhead6010" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M281.1015625,85.8148422241211L285.2682291666667,85.8148422241211C289.4348958333333,85.8148422241211,297.7682291666667,85.8148422241211,306.18489608764645,85.89817555745442C314.6015630086263,85.98150889078777,323.10156351725254,86.14817555745442,327.3515637715657,86.23150889078777L331.6015640258788,86.3148422241211" marker-end="url(#arrowhead6011)" style="fill:none"></path><defs><marker id="arrowhead6011" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M487.2312484741211,86.31484222412108L492.6479148864746,86.23150889078777C498.06458129882816,86.14817555745442,508.89791412353514,85.98150889078777,519.8145805358887,85.89817555745442C530.7312469482422,85.8148422241211,541.7312469482422,85.8148422241211,547.2312469482422,85.8148422241211L552.7312469482422,85.8148422241211" marker-end="url(#arrowhead6012)" style="fill:none"></path><defs><marker id="arrowhead6012" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M684.4187469482422,85.8148422241211L688.5854136149088,85.8148422241211C692.7520802815756,85.8148422241211,701.0854136149088,85.8148422241211,709.4187469482422,85.8148422241211C717.7520802815756,85.8148422241211,726.0854136149088,85.8148422241211,730.2520802815756,85.8148422241211L734.4187469482422,85.8148422241211" marker-end="url(#arrowhead6013)" style="fill:none"></path><defs><marker id="arrowhead6013" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(519.7312469482422,85.8148422241211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000436782836914" height="18.500505447387695"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-6994" transform="translate(59.94140625,85.8148422241211)"><rect rx="0" ry="0" x="-51.94140625" y="-19.25" width="103.8828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-41.94140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">fast先走n步</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-6995" transform="translate(221.4921875,85.8148422241211)"><rect rx="0" ry="0" x="-59.609375" y="-19.25" width="119.21875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-49.609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">fast/slow同速</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-6997" transform="translate(408.9164047241211,85.8148422241211)"><polygon points="77.81484375000001,0 155.62968750000002,-77.81484375000001 77.81484375000001,-155.62968750000002 0,-77.81484375000001" transform="translate(-77.81484375000001,77.81484375000001)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-57.2109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">fast.next==null?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-6999" transform="translate(618.5749969482422,85.8148422241211)"><rect rx="0" ry="0" x="-65.84375" y="-19.25" width="131.6875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.84375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">slow在待删前驱</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7001" transform="translate(835.1609344482422,85.8148422241211)"><rect rx="0" ry="0" x="-100.7421875" y="-19.25" width="201.484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-90.7421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">slow.next=slow.next.next</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode removeNthFromEnd(ListNode head, int n) {        ListNode dummy = new ListNode(0, head), fast = dummy, slow = dummy;        for (int i = 0; i &lt; n; i++) fast = fast.next;        while (fast.next != null) { fast = fast.next; slow = slow.next; }        slow.next = slow.next.next;        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="82.-%E5%88%A0%E9%99%A4%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E9%87%8D%E5%A4%8D%E5%85%83%E7%B4%A0-ii" tabindex="-1"><a href="https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/" target="_blank">82. 删除排序链表中的重复元素 II</a></h2><blockquote><p>有序链表，删掉所有有重复的节点（重复的全删，不只留一个）。</p></blockquote><p><strong>思路脉络</strong>：dummy + <code>pre</code>。遇 <code>pre.next.val == pre.next.next.val</code> 则记下该值，<code>pre.next</code> 跳过所有等于该值的节点。没有重复则推进 <code>pre</code>。</p><div class="mermaid"><svg id="render1455937999" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="395.21563720703125" style="max-width: 709.4656372070312px;" viewBox="0 0 709.4656372070312 395.21563720703125"><style>#render1455937999 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1455937999 .error-icon{fill:#552222;}#render1455937999 .error-text{fill:#552222;stroke:#552222;}#render1455937999 .edge-thickness-normal{stroke-width:2px;}#render1455937999 .edge-thickness-thick{stroke-width:3.5px;}#render1455937999 .edge-pattern-solid{stroke-dasharray:0;}#render1455937999 .edge-pattern-dashed{stroke-dasharray:3;}#render1455937999 .edge-pattern-dotted{stroke-dasharray:2;}#render1455937999 .marker{fill:#333333;stroke:#333333;}#render1455937999 .marker.cross{stroke:#333333;}#render1455937999 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1455937999 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1455937999 .cluster-label text{fill:#333;}#render1455937999 .cluster-label span{color:#333;}#render1455937999 .label text,#render1455937999 span{fill:#333;color:#333;}#render1455937999 .node rect,#render1455937999 .node circle,#render1455937999 .node ellipse,#render1455937999 .node polygon,#render1455937999 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1455937999 .node .label{text-align:center;}#render1455937999 .node.clickable{cursor:pointer;}#render1455937999 .arrowheadPath{fill:#333333;}#render1455937999 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1455937999 .flowchart-link{stroke:#333333;fill:none;}#render1455937999 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1455937999 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1455937999 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1455937999 .cluster text{fill:#333;}#render1455937999 .cluster span{color:#333;}#render1455937999 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1455937999 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M69.02080843524227,289.84063720703125L80.41056952936856,265.0414174397786C91.80033062349484,240.24219767252603,114.57985281174741,190.64375813802084,130.21961492312633,165.92787170410156C145.8593770345052,141.21198527018228,154.3593790690104,141.37865193684897,158.609380086263,141.46198527018228L162.8593811035156,141.54531860351562" marker-end="url(#arrowhead6036)" style="fill:none"></path><defs><marker id="arrowhead6036" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M428.94999389648433,141.54531860351562L434.3666636149089,141.46198527018228C439.7833333333333,141.37865193684897,450.6166727701823,141.21198527018228,473.57221058396937,151.9240951538086C496.5277483977565,162.6362050374349,531.6054845884818,184.22709147135416,549.1443526838443,195.0225346883138L566.683220779207,205.81797790527344" marker-end="url(#arrowhead6037)" style="fill:none"></path><defs><marker id="arrowhead6037" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M356.70590055262613,213.78941194737374L374.16325249502705,234.54794949064998C391.62060443742786,255.30648703392626,426.5353083222296,296.82356212047875,457.25958734796376,320.4612954229263C487.98386637369794,344.0990287253738,514.5177205403646,349.85742024371643,527.7846476236979,352.73661600288773L541.0515747070312,355.615811762059" marker-end="url(#arrowhead6038)" style="fill:none"></path><defs><marker id="arrowhead6038" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M566.683220779207,244.31797790527344L549.1443526838443,255.11342112223306C531.6054845884818,265.9088643391927,496.5277483977565,287.499750773112,451.31466053514123,298.2951939900716C406.10157267252606,309.09063720703125,350.7531331380208,309.09063720703125,296.73802693684894,309.09063720703125C242.7229207356771,309.09063720703125,190.04114786783853,309.09063720703125,159.5335947672526,309.09063720703125C129.02604166666666,309.09063720703125,120.69270833333333,309.09063720703125,116.52604166666667,309.09063720703125L112.359375,309.09063720703125" marker-end="url(#arrowhead6039)" style="fill:none"></path><defs><marker id="arrowhead6039" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M541.0515747070312,374.06238647733403L527.7846476236979,375.48376159895025C514.5177205403646,376.9051367205664,487.98386637369794,379.74788696379886,447.042719523112,381.1692620854151C406.10157267252606,382.59063720703125,350.7531331380208,382.59063720703125,296.73802693684894,382.59063720703125C242.7229207356771,382.59063720703125,190.04114786783853,382.59063720703125,154.20593479701452,373.54897054036456C118.37072172619048,364.50730387369794,99.38206845238096,346.42397054036456,89.88774181547619,337.38230387369794L80.39341517857143,328.34063720703125" marker-end="url(#arrowhead6040)" style="fill:none"></path><defs><marker id="arrowhead6040" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(461.45001220703125,141.04531860351562)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00019073486328" height="18.500221252441406"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(461.45001220703125,338.34063720703125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00019073486328" height="18.500221252441406"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7012" transform="translate(60.1796875,309.09063720703125)"><rect rx="0" ry="0" x="-52.1796875" y="-19.25" width="104.359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.1796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre=dummy</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7013" transform="translate(295.4046936035156,141.04531860351562)"><polygon points="133.0453125,0 266.090625,-133.0453125 133.0453125,-266.090625 0,-133.0453125" transform="translate(-133.0453125,133.0453125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-118.578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre.next.val==pre.next.next.val?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7015" transform="translate(597.9578247070312,225.06797790527344)"><rect rx="0" ry="0" x="-103.5078125" y="-19.25" width="207.015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-93.5078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">记dup,跳过所有等于dup的</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7017" transform="translate(597.9578247070312,367.96563720703125)"><rect rx="0" ry="0" x="-56.90625" y="-19.25" width="113.8125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-46.90625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre=pre.next</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode deleteDuplicates(ListNode head) {        ListNode dummy = new ListNode(0, head), pre = dummy;        while (pre.next != null &amp;&amp; pre.next.next != null) {            if (pre.next.val == pre.next.next.val) {                int dup = pre.next.val;                while (pre.next != null &amp;&amp; pre.next.val == dup) pre.next = pre.next.next;            } else pre = pre.next;        }        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>和&quot;保留一个&quot;不同：这里重复的全删。<code>pre.next</code> 直接跳过所有重复值节点，不保留任何一个。</li></ul><h2 id="61.-%E6%97%8B%E8%BD%AC%E9%93%BE%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/rotate-list/" target="_blank">61. 旋转链表</a></h2><blockquote><p>链表右轮转 <code>k</code> 步。</p></blockquote><p><strong>思路脉络</strong>：右轮转 k = 把末尾 k 个挪到前面。数长度 <code>n</code>，<code>k %= n</code>。新尾是倒数第 k+1 个（走 <code>n-k-1</code> 步定位），断开，旧尾接旧头。</p><div class="mermaid"><svg id="render601276236" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 934.8828125px;" viewBox="0 0 934.8828125 54.5"><style>#render601276236 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render601276236 .error-icon{fill:#552222;}#render601276236 .error-text{fill:#552222;stroke:#552222;}#render601276236 .edge-thickness-normal{stroke-width:2px;}#render601276236 .edge-thickness-thick{stroke-width:3.5px;}#render601276236 .edge-pattern-solid{stroke-dasharray:0;}#render601276236 .edge-pattern-dashed{stroke-dasharray:3;}#render601276236 .edge-pattern-dotted{stroke-dasharray:2;}#render601276236 .marker{fill:#333333;stroke:#333333;}#render601276236 .marker.cross{stroke:#333333;}#render601276236 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render601276236 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render601276236 .cluster-label text{fill:#333;}#render601276236 .cluster-label span{color:#333;}#render601276236 .label text,#render601276236 span{fill:#333;color:#333;}#render601276236 .node rect,#render601276236 .node circle,#render601276236 .node ellipse,#render601276236 .node polygon,#render601276236 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render601276236 .node .label{text-align:center;}#render601276236 .node.clickable{cursor:pointer;}#render601276236 .arrowheadPath{fill:#333333;}#render601276236 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render601276236 .flowchart-link{stroke:#333333;fill:none;}#render601276236 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render601276236 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render601276236 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render601276236 .cluster text{fill:#333;}#render601276236 .cluster span{color:#333;}#render601276236 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render601276236 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M125.421875,27.25L129.58854166666666,27.25C133.75520833333334,27.25,142.08854166666666,27.25,150.421875,27.25C158.75520833333334,27.25,167.08854166666666,27.25,171.25520833333334,27.25L175.421875,27.25" marker-end="url(#arrowhead6062)" style="fill:none"></path><defs><marker id="arrowhead6062" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M318.25,27.25L322.4166666666667,27.25C326.5833333333333,27.25,334.9166666666667,27.25,343.25,27.25C351.5833333333333,27.25,359.9166666666667,27.25,364.0833333333333,27.25L368.25,27.25" marker-end="url(#arrowhead6063)" style="fill:none"></path><defs><marker id="arrowhead6063" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M554.59375,27.25L558.7604166666666,27.25C562.9270833333334,27.25,571.2604166666666,27.25,579.59375,27.25C587.9270833333334,27.25,596.2604166666666,27.25,600.4270833333334,27.25L604.59375,27.25" marker-end="url(#arrowhead6064)" style="fill:none"></path><defs><marker id="arrowhead6064" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M751.96875,27.25L756.1354166666666,27.25C760.3020833333334,27.25,768.6354166666666,27.25,776.96875,27.25C785.3020833333334,27.25,793.6354166666666,27.25,797.8020833333334,27.25L801.96875,27.25" marker-end="url(#arrowhead6065)" style="fill:none"></path><defs><marker id="arrowhead6065" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7030" transform="translate(66.7109375,27.25)"><rect rx="0" ry="0" x="-58.7109375" y="-19.25" width="117.421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48.7109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">数长度n,k%=n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7031" transform="translate(246.8359375,27.25)"><rect rx="0" ry="0" x="-71.4140625" y="-19.25" width="142.828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-61.4140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">找新尾:走n-k-1步</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7033" transform="translate(461.421875,27.25)"><rect rx="0" ry="0" x="-93.171875" y="-19.25" width="186.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-83.171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">newHead=newTail.next</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7035" transform="translate(678.28125,27.25)"><rect rx="0" ry="0" x="-73.6875" y="-19.25" width="147.375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-63.6875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">newTail.next=null</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7037" transform="translate(864.42578125,27.25)"><rect rx="0" ry="0" x="-62.45703125" y="-19.25" width="124.9140625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-52.45703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">tail.next=head</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0061-rotate-list.gif" alt="" /></p><pre><code class="language-java">class Solution {    public ListNode rotateRight(ListNode head, int k) {        if (head == null) return null;        ListNode tail = head; int n = 1;        while (tail.next != null) { tail = tail.next; n++; }        k %= n; if (k == 0) return head;        ListNode newTail = head;        for (int i = 0; i &lt; n - k - 1; i++) newTail = newTail.next;        ListNode newHead = newTail.next;        newTail.next = null; tail.next = head;        return newHead;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>k %= n</code> 防超长。<code>k == 0</code> 直接返回。新尾是倒数第 k+1 个，走 <code>n-k-1</code> 步定位。</li></ul><h2 id="86.-%E5%88%86%E9%9A%94%E9%93%BE%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/partition-list/" target="_blank">86. 分隔链表</a></h2><blockquote><p>链表按值 <code>x</code> 分区，小于 x 在前、大于等于在后，保持各段原相对顺序。</p></blockquote><p><strong>思路脉络</strong>：两个 dummy 链表分别收集小段和大段，最后小段尾接大段头。复用原节点空间 O(1)。</p><div class="mermaid"><svg id="render3586463691" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="228.5749969482422" style="max-width: 532.0281372070312px;" viewBox="0 0 532.0281372070312 228.5749969482422"><style>#render3586463691 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3586463691 .error-icon{fill:#552222;}#render3586463691 .error-text{fill:#552222;stroke:#552222;}#render3586463691 .edge-thickness-normal{stroke-width:2px;}#render3586463691 .edge-thickness-thick{stroke-width:3.5px;}#render3586463691 .edge-pattern-solid{stroke-dasharray:0;}#render3586463691 .edge-pattern-dashed{stroke-dasharray:3;}#render3586463691 .edge-pattern-dotted{stroke-dasharray:2;}#render3586463691 .marker{fill:#333333;stroke:#333333;}#render3586463691 .marker.cross{stroke:#333333;}#render3586463691 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3586463691 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3586463691 .cluster-label text{fill:#333;}#render3586463691 .cluster-label span{color:#333;}#render3586463691 .label text,#render3586463691 span{fill:#333;color:#333;}#render3586463691 .node rect,#render3586463691 .node circle,#render3586463691 .node ellipse,#render3586463691 .node polygon,#render3586463691 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3586463691 .node .label{text-align:center;}#render3586463691 .node.clickable{cursor:pointer;}#render3586463691 .arrowheadPath{fill:#333333;}#render3586463691 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3586463691 .flowchart-link{stroke:#333333;fill:none;}#render3586463691 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3586463691 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3586463691 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3586463691 .cluster text{fill:#333;}#render3586463691 .cluster span{color:#333;}#render3586463691 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3586463691 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M151.2578125,99.0304494923156L169.1484375,90.92745765594985C187.0390625,82.82446581958409,222.8203125,66.6184821468526,244.96093775431314,58.598823643820175C267.1015630086263,50.57916514078776,275.6015635172526,50.74583180745443,279.85156377156574,50.82916514078775L284.1015640258789,50.91249847412109" marker-end="url(#arrowhead6088)" style="fill:none"></path><defs><marker id="arrowhead6088" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M368.926560974121,50.912498474121094L374.34322738647455,50.82916514078776C379.75989379882805,50.74583180745443,390.59322662353514,50.57916514078776,401.50989303588864,51.55623247020484C412.4265594482422,52.53329979962192,423.4265594482422,54.65410112512276,428.9265594482422,55.71450178787317L434.4265594482422,56.77490245062358" marker-end="url(#arrowhead6089)" style="fill:none"></path><defs><marker id="arrowhead6089" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M344.0806290108271,75.75843348917287L353.6382840837296,89.31119406568443C363.19593915663216,102.86395464219599,382.31124930243715,129.9694757952191,398.710050208673,144.84121011275587C415.1088511149089,159.7129444302927,428.7911427815755,162.3508919123432,435.6322886149089,163.66986565336848L442.4734344482422,164.98883939439372" marker-end="url(#arrowhead6090)" style="fill:none"></path><defs><marker id="arrowhead6090" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M455.231104757079,84.6624984741211L446.2636805389395,91.85624821980794C437.29625632080007,99.0499979654948,419.36140788452116,113.43749745686848,397.82523392069476,120.63124720255534C376.28905995686847,127.82499694824219,351.1515604654948,127.82499694824219,327.3473943074544,127.82499694824219C303.54322814941406,127.82499694824219,281.07239532470703,127.82499694824219,251.94635391235352,125.87755243765456C222.8203125,123.93010792706691,187.0390625,120.03521890589165,169.1484375,118.08777439530401L151.2578125,116.14032988471638" marker-end="url(#arrowhead6091)" style="fill:none"></path><defs><marker id="arrowhead6091" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M442.4734344482422,185.8930041782467L435.6322886149089,188.46500297324596C428.7911427815755,191.0370017682452,415.1088511149089,196.18099935824367,395.6989555358887,198.75299815324294C376.28905995686847,201.3249969482422,351.1515604654948,201.3249969482422,327.3473943074544,201.3249969482422C303.54322814941406,201.3249969482422,281.07239532470703,201.3249969482422,251.86578474745238,189.78333028157553C222.65917417019773,178.24166361490884,186.71678584039546,155.15833028157553,168.74559167549435,143.61666361490884L150.77439751059322,132.0749969482422" marker-end="url(#arrowhead6092)" style="fill:none"></path><defs><marker id="arrowhead6092" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(401.4265594482422,50.412498474121094)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000328063964844" height="18.50037956237793"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(401.4265594482422,157.0749969482422)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000328063964844" height="18.50037956237793"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7049" transform="translate(120.80078125,112.82499694824219)"><rect rx="0" ry="0" x="-30.45703125" y="-19.25" width="60.9140625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20.45703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历p</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7050" transform="translate(326.0140609741211,50.412498474121094)"><polygon points="42.4125,0 84.825,-42.4125 42.4125,-84.825 0,-42.4125" transform="translate(-42.4125,42.4125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-17.875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">p.val</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7052" transform="translate(479.2273406982422,65.4124984741211)"><rect rx="0" ry="0" x="-44.80078125" y="-19.25" width="89.6015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.80078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">接small链</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7054" transform="translate(479.2273406982422,172.0749969482422)"><rect rx="0" ry="0" x="-36.75390625" y="-19.25" width="73.5078125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-26.75390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">接big链</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7059" transform="translate(120.80078125,201.3249969482422)"><rect rx="0" ry="0" x="-112.80078125" y="-19.25" width="225.6015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-102.80078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">small.next=big,big.next=null</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode partition(ListNode head, int x) {        ListNode small = new ListNode(0), big = new ListNode(0);        ListNode s = small, b = big;        for (ListNode p = head; p != null; p = p.next) {            if (p.val &lt; x) { s.next = p; s = s.next; }            else { b.next = p; b = b.next; }        }        b.next = null; s.next = big.next;        return small.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>b.next = null</code> 必须断开，否则大段末尾可能还连着原链表后续节点成环。</li></ul><h2 id="146.-lru-%E7%BC%93%E5%AD%98" tabindex="-1"><a href="https://leetcode.cn/problems/lru-cache/" target="_blank">146. LRU 缓存</a></h2><blockquote><p><code>get</code>/<code>put</code> 均 O(1) 的 LRU 缓存，容量满淘汰最久未使用。</p></blockquote><p><strong>思路脉络</strong>：哈希存 key→节点 + 双向链表维护访问序（头最近、尾最久）。节点存 key 供淘汰时删哈希。</p><div class="mermaid"><svg id="render3636915031" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="211.36874389648438" style="max-width: 590.578125px;" viewBox="0 0 590.578125 211.36874389648438"><style>#render3636915031 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3636915031 .error-icon{fill:#552222;}#render3636915031 .error-text{fill:#552222;stroke:#552222;}#render3636915031 .edge-thickness-normal{stroke-width:2px;}#render3636915031 .edge-thickness-thick{stroke-width:3.5px;}#render3636915031 .edge-pattern-solid{stroke-dasharray:0;}#render3636915031 .edge-pattern-dashed{stroke-dasharray:3;}#render3636915031 .edge-pattern-dotted{stroke-dasharray:2;}#render3636915031 .marker{fill:#333333;stroke:#333333;}#render3636915031 .marker.cross{stroke:#333333;}#render3636915031 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3636915031 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3636915031 .cluster-label text{fill:#333;}#render3636915031 .cluster-label span{color:#333;}#render3636915031 .label text,#render3636915031 span{fill:#333;color:#333;}#render3636915031 .node rect,#render3636915031 .node circle,#render3636915031 .node ellipse,#render3636915031 .node polygon,#render3636915031 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3636915031 .node .label{text-align:center;}#render3636915031 .node.clickable{cursor:pointer;}#render3636915031 .arrowheadPath{fill:#333333;}#render3636915031 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3636915031 .flowchart-link{stroke:#333333;fill:none;}#render3636915031 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3636915031 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3636915031 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3636915031 .cluster text{fill:#333;}#render3636915031 .cluster span{color:#333;}#render3636915031 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3636915031 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M136.7265625,71.5L146.41796875,71.5C156.109375,71.5,175.4921875,71.5,197.6950520833333,71.58333333333333C219.89791666666665,71.66666666666667,244.92083333333326,71.83333333333333,257.4322916666666,71.91666666666667L269.9437499999999,72" marker-end="url(#arrowhead6108)" style="fill:none"></path><defs><marker id="arrowhead6108" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M345.4050564634594,60.72380646345941L360.9625470528828,55.14483871954951C376.5200376423063,49.565870975639605,407.63501882115315,38.407935487819806,428.6925094105766,32.82896774390991C449.75,27.25,460.75,27.25,466.25,27.25L471.75,27.25" marker-end="url(#arrowhead6109)" style="fill:none"></path><defs><marker id="arrowhead6109" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M345.405054205181,83.27619274306112L360.96254517098413,88.68849395255093C376.52003613678727,94.10079516204075,407.63501806839366,104.92539758102038,430.9216757008635,110.33769879051017C454.2083333333333,115.75,469.6666666666667,115.75,477.3958333333333,115.75L485.125,115.75" marker-end="url(#arrowhead6110)" style="fill:none"></path><defs><marker id="arrowhead6110" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M169.875,184.1187515258789L174.04166666666666,184.1187515258789C178.20833333333334,184.1187515258789,186.54166666666666,184.1187515258789,194.875,184.1187515258789C203.20833333333334,184.1187515258789,211.54166666666666,184.1187515258789,215.70833333333334,184.1187515258789L219.875,184.1187515258789" marker-end="url(#arrowhead6111)" style="fill:none"></path><defs><marker id="arrowhead6111" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(438.75,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000118255615234" height="18.500137329101562"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(438.75,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000118255615234" height="18.500137329101562"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7069" transform="translate(88.9375,71.5)"><rect rx="0" ry="0" x="-47.7890625" y="-19.25" width="95.578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.7890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">get:查map</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7070" transform="translate(312.8125,71.5)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">命中?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7072" transform="translate(527.1640625,27.25)"><rect rx="0" ry="0" x="-55.4140625" y="-19.25" width="110.828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.4140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">moveToHead</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7074" transform="translate(527.1640625,115.75)"><rect rx="0" ry="0" x="-42.0390625" y="-19.25" width="84.078125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.0390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return -1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7075" transform="translate(88.9375,184.1187515258789)"><rect rx="0" ry="0" x="-80.9375" y="-19.25" width="161.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-70.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">put:存在则改值移头</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7077" transform="translate(312.8125,184.1187515258789)"><rect rx="0" ry="0" x="-92.9375" y="-19.25" width="185.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-82.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">不存在则加头,超容删尾</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class LRUCache {    class Node { int key, val; Node prev, next; Node(int k, int v){key=k;val=v;} }    Map&lt;Integer, Node&gt; map = new HashMap&lt;&gt;();    int cap; Node head = new Node(0,0), tail = new Node(0,0);    public LRUCache(int capacity) { cap = capacity; head.next = tail; tail.prev = head; }    public int get(int key) {        if (!map.containsKey(key)) return -1;        Node n = map.get(key); moveToHead(n); return n.val;    }    public void put(int key, int value) {        if (map.containsKey(key)) { Node n = map.get(key); n.val = value; moveToHead(n); }        else {            Node n = new Node(key, value); map.put(key, n); addToHead(n);            if (map.size() &gt; cap) { Node last = tail.prev; removeNode(last); map.remove(last.key); }        }    }    void addToHead(Node n) { n.next = head.next; head.next.prev = n; head.next = n; n.prev = head; }    void removeNode(Node n) { n.prev.next = n.next; n.next.prev = n.prev; }    void moveToHead(Node n) { removeNode(n); addToHead(n); }}</code></pre><blockquote><p>[!info] 复杂度<br /><code>get</code>/<code>put</code> 均 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>节点必须存 <code>key</code>：淘汰尾节点时需要用它删哈希项。dummy 头尾让&quot;头是最近、尾是最久&quot;语义稳定。</li></ul><hr /><h1 id="9.-%E4%BA%8C%E5%8F%89%E6%A0%91" tabindex="-1">9. 二叉树</h1><p>树题的灵魂是<strong>递归三部曲</strong>：边界条件、单层逻辑（假设子树已正确）、返回值传递。还有两类遍历：DFS 递归求路径/祖先、BFS 层序按层处理。</p><h2 id="104.-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%A4%A7%E6%B7%B1%E5%BA%A6" tabindex="-1"><a href="https://leetcode.cn/problems/maximum-depth-of-binary-tree/" target="_blank">104. 二叉树的最大深度</a></h2><blockquote><p>求最大深度。</p></blockquote><p><strong>思路脉络</strong>：后序递归。边界 null 返回 0；单层 = <code>max(左, 右) + 1</code>。</p><div class="mermaid"><svg id="render2197915982" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 345.421875px;" viewBox="0 0 345.421875 143"><style>#render2197915982 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2197915982 .error-icon{fill:#552222;}#render2197915982 .error-text{fill:#552222;stroke:#552222;}#render2197915982 .edge-thickness-normal{stroke-width:2px;}#render2197915982 .edge-thickness-thick{stroke-width:3.5px;}#render2197915982 .edge-pattern-solid{stroke-dasharray:0;}#render2197915982 .edge-pattern-dashed{stroke-dasharray:3;}#render2197915982 .edge-pattern-dotted{stroke-dasharray:2;}#render2197915982 .marker{fill:#333333;stroke:#333333;}#render2197915982 .marker.cross{stroke:#333333;}#render2197915982 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2197915982 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2197915982 .cluster-label text{fill:#333;}#render2197915982 .cluster-label span{color:#333;}#render2197915982 .label text,#render2197915982 span{fill:#333;color:#333;}#render2197915982 .node rect,#render2197915982 .node circle,#render2197915982 .node ellipse,#render2197915982 .node polygon,#render2197915982 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2197915982 .node .label{text-align:center;}#render2197915982 .node.clickable{cursor:pointer;}#render2197915982 .arrowheadPath{fill:#333333;}#render2197915982 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2197915982 .flowchart-link{stroke:#333333;fill:none;}#render2197915982 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2197915982 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2197915982 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2197915982 .cluster text{fill:#333;}#render2197915982 .cluster span{color:#333;}#render2197915982 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2197915982 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M90.13382768361582,52.25L97.16360640301319,48.083333333333336C104.19338512241056,43.916666666666664,118.25294256120527,35.583333333333336,137.9415754472693,31.416666666666668C157.63020833333334,27.25,182.94791666666666,27.25,195.60677083333334,27.25L208.265625,27.25" marker-end="url(#arrowhead6119)" style="fill:none"></path><defs><marker id="arrowhead6119" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-A LE-C" style="opacity: 1;" id="L-A-C"><path class="path" d="M90.13382768361582,90.75L97.16360640301319,94.91666666666667C104.19338512241056,99.08333333333333,118.25294256120527,107.41666666666667,129.4493879472693,111.58333333333333C140.64583333333334,115.75,148.97916666666666,115.75,153.14583333333334,115.75L157.3125,115.75" marker-end="url(#arrowhead6120)" style="fill:none"></path><defs><marker id="arrowhead6120" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7082" transform="translate(57.65625,71.5)"><rect rx="0" ry="0" x="-49.65625" y="-19.25" width="99.3125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.65625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">root==null?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7083" transform="translate(247.3671875,27.25)"><rect rx="0" ry="0" x="-39.1015625" y="-19.25" width="78.203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.1015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return 0</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7085" transform="translate(247.3671875,115.75)"><rect rx="0" ry="0" x="-90.0546875" y="-19.25" width="180.109375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.0546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">max(左深度,右深度)+1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxDepth(TreeNode root) {        return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><h2 id="100.-%E7%9B%B8%E5%90%8C%E7%9A%84%E6%A0%91" tabindex="-1"><a href="https://leetcode.cn/problems/same-tree/" target="_blank">100. 相同的树</a></h2><blockquote><p>两棵树是否结构和值都相同。</p></blockquote><p><strong>思路脉络</strong>：两指针同步递归。都 null 返回 true；一空一非空 false；值不等 false；都非空则递归比左右子。</p><div class="mermaid"><svg id="render3525481644" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="187.25" style="max-width: 536.296875px;" viewBox="0 0 536.296875 187.25"><style>#render3525481644 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3525481644 .error-icon{fill:#552222;}#render3525481644 .error-text{fill:#552222;stroke:#552222;}#render3525481644 .edge-thickness-normal{stroke-width:2px;}#render3525481644 .edge-thickness-thick{stroke-width:3.5px;}#render3525481644 .edge-pattern-solid{stroke-dasharray:0;}#render3525481644 .edge-pattern-dashed{stroke-dasharray:3;}#render3525481644 .edge-pattern-dotted{stroke-dasharray:2;}#render3525481644 .marker{fill:#333333;stroke:#333333;}#render3525481644 .marker.cross{stroke:#333333;}#render3525481644 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3525481644 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3525481644 .cluster-label text{fill:#333;}#render3525481644 .cluster-label span{color:#333;}#render3525481644 .label text,#render3525481644 span{fill:#333;color:#333;}#render3525481644 .node rect,#render3525481644 .node circle,#render3525481644 .node ellipse,#render3525481644 .node polygon,#render3525481644 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3525481644 .node .label{text-align:center;}#render3525481644 .node.clickable{cursor:pointer;}#render3525481644 .arrowheadPath{fill:#333333;}#render3525481644 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3525481644 .flowchart-link{stroke:#333333;fill:none;}#render3525481644 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3525481644 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3525481644 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3525481644 .cluster text{fill:#333;}#render3525481644 .cluster span{color:#333;}#render3525481644 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3525481644 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M68.23843573446328,52.25L73.83150894538606,48.083333333333336C79.42458215630886,43.916666666666664,90.61072857815442,35.583333333333336,100.87307262241056,31.416666666666668C111.13541666666667,27.25,120.47395833333333,27.25,125.14322916666667,27.25L129.8125,27.25" marker-end="url(#arrowhead6136)" style="fill:none"></path><defs><marker id="arrowhead6136" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-A LE-C" style="opacity: 1;" id="L-A-C"><path class="path" d="M68.23843573446328,90.75L73.83150894538606,94.91666666666667C79.42458215630886,99.08333333333333,90.61072857815442,107.41666666666667,100.37046845574389,111.58333333333333C110.13020833333333,115.75,118.46354166666667,115.75,122.63020833333333,115.75L126.796875,115.75" marker-end="url(#arrowhead6137)" style="fill:none"></path><defs><marker id="arrowhead6137" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M217.11961511299435,96.5L225.2116584274953,92.33333333333333C233.3037017419962,88.16666666666667,249.48778837099812,79.83333333333333,273.5440243938324,75.66666666666667C297.6002604166667,71.5,329.5286458333333,71.5,345.4928385416667,71.5L361.45703125,71.5" marker-end="url(#arrowhead6138)" style="fill:none"></path><defs><marker id="arrowhead6138" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M217.11961511299435,135L225.2116584274953,139.16666666666666C233.3037017419962,143.33333333333334,249.48778837099812,151.66666666666666,263.0798316854991,155.83333333333334C276.671875,160,287.671875,160,293.171875,160L298.671875,160" marker-end="url(#arrowhead6139)" style="fill:none"></path><defs><marker id="arrowhead6139" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(265.671875,71.5)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(265.671875,160)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7094" transform="translate(42.3984375,71.5)"><rect rx="0" ry="0" x="-34.3984375" y="-19.25" width="68.796875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24.3984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">都null?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7095" transform="translate(179.734375,27.25)"><rect rx="0" ry="0" x="-49.921875" y="-19.25" width="99.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return true</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7097" transform="translate(179.734375,115.75)"><rect rx="0" ry="0" x="-52.9375" y="-19.25" width="105.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">一空一非空?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7099" transform="translate(413.484375,71.5)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7101" transform="translate(413.484375,160)"><rect rx="0" ry="0" x="-114.8125" y="-19.25" width="229.625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-104.8125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">值等?且左子相同?且右子相同?</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isSameTree(TreeNode p, TreeNode q) {        if (p == null &amp;&amp; q == null) return true;        if (p == null || q == null) return false;        return p.val == q.val &amp;&amp; isSameTree(p.left, q.left) &amp;&amp; isSameTree(p.right, q.right);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>先判&quot;双 null&quot;再判&quot;单 null&quot;，顺序反了会漏判。这是所有&quot;两树对比&quot;题的标准开头。</li></ul><h2 id="226.-%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%91" tabindex="-1"><a href="https://leetcode.cn/problems/invert-binary-tree/" target="_blank">226. 翻转二叉树</a></h2><blockquote><p>镜像翻转整棵树。</p></blockquote><p><strong>思路脉络</strong>：每节点交换左右子，递归处理。只关心一层：交换左右，剩下交给递归。</p><div class="mermaid"><svg id="render965900535" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 533.546875px;" viewBox="0 0 533.546875 54.5"><style>#render965900535 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render965900535 .error-icon{fill:#552222;}#render965900535 .error-text{fill:#552222;stroke:#552222;}#render965900535 .edge-thickness-normal{stroke-width:2px;}#render965900535 .edge-thickness-thick{stroke-width:3.5px;}#render965900535 .edge-pattern-solid{stroke-dasharray:0;}#render965900535 .edge-pattern-dashed{stroke-dasharray:3;}#render965900535 .edge-pattern-dotted{stroke-dasharray:2;}#render965900535 .marker{fill:#333333;stroke:#333333;}#render965900535 .marker.cross{stroke:#333333;}#render965900535 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render965900535 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render965900535 .cluster-label text{fill:#333;}#render965900535 .cluster-label span{color:#333;}#render965900535 .label text,#render965900535 span{fill:#333;color:#333;}#render965900535 .node rect,#render965900535 .node circle,#render965900535 .node ellipse,#render965900535 .node polygon,#render965900535 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render965900535 .node .label{text-align:center;}#render965900535 .node.clickable{cursor:pointer;}#render965900535 .arrowheadPath{fill:#333333;}#render965900535 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render965900535 .flowchart-link{stroke:#333333;fill:none;}#render965900535 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render965900535 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render965900535 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render965900535 .cluster text{fill:#333;}#render965900535 .cluster span{color:#333;}#render965900535 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render965900535 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M108,27.25L112.16666666666667,27.25C116.33333333333333,27.25,124.66666666666667,27.25,133,27.25C141.33333333333334,27.25,149.66666666666666,27.25,153.83333333333334,27.25L158,27.25" marker-end="url(#arrowhead6156)" style="fill:none"></path><defs><marker id="arrowhead6156" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M242,27.25L246.16666666666666,27.25C250.33333333333334,27.25,258.6666666666667,27.25,267,27.25C275.3333333333333,27.25,283.6666666666667,27.25,287.8333333333333,27.25L292,27.25" marker-end="url(#arrowhead6157)" style="fill:none"></path><defs><marker id="arrowhead6157" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M376,27.25L380.1666666666667,27.25C384.3333333333333,27.25,392.6666666666667,27.25,401,27.25C409.3333333333333,27.25,417.6666666666667,27.25,421.8333333333333,27.25L426,27.25" marker-end="url(#arrowhead6158)" style="fill:none"></path><defs><marker id="arrowhead6158" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7108" transform="translate(58,27.25)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">交换左右子</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7109" transform="translate(200,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归左子</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7111" transform="translate(334,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归右子</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7113" transform="translate(475.7734375,27.25)"><rect rx="0" ry="0" x="-49.7734375" y="-19.25" width="99.546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.7734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return root</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public TreeNode invertTree(TreeNode root) {        if (root == null) return null;        TreeNode t = root.left; root.left = root.right; root.right = t;        invertTree(root.left); invertTree(root.right);        return root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><h2 id="101.-%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91" tabindex="-1"><a href="https://leetcode.cn/problems/symmetric-tree/" target="_blank">101. 对称二叉树</a></h2><blockquote><p>判断树是否关于根轴对称。</p></blockquote><p><strong>思路脉络</strong>：两个指针比——左子树的左 vs 右子树的右、左子树的右 vs 右子树的左（镜像侧）。</p><div class="mermaid"><svg id="render1115342503" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="211.36874389648438" style="max-width: 746.1640625px;" viewBox="0 0 746.1640625 211.36874389648438"><style>#render1115342503 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1115342503 .error-icon{fill:#552222;}#render1115342503 .error-text{fill:#552222;stroke:#552222;}#render1115342503 .edge-thickness-normal{stroke-width:2px;}#render1115342503 .edge-thickness-thick{stroke-width:3.5px;}#render1115342503 .edge-pattern-solid{stroke-dasharray:0;}#render1115342503 .edge-pattern-dashed{stroke-dasharray:3;}#render1115342503 .edge-pattern-dotted{stroke-dasharray:2;}#render1115342503 .marker{fill:#333333;stroke:#333333;}#render1115342503 .marker.cross{stroke:#333333;}#render1115342503 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1115342503 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1115342503 .cluster-label text{fill:#333;}#render1115342503 .cluster-label span{color:#333;}#render1115342503 .label text,#render1115342503 span{fill:#333;color:#333;}#render1115342503 .node rect,#render1115342503 .node circle,#render1115342503 .node ellipse,#render1115342503 .node polygon,#render1115342503 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1115342503 .node .label{text-align:center;}#render1115342503 .node.clickable{cursor:pointer;}#render1115342503 .arrowheadPath{fill:#333333;}#render1115342503 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1115342503 .flowchart-link{stroke:#333333;fill:none;}#render1115342503 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1115342503 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1115342503 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1115342503 .cluster text{fill:#333;}#render1115342503 .cluster span{color:#333;}#render1115342503 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1115342503 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M146.609375,83.55937576293945L150.77604166666666,83.55937576293945C154.94270833333334,83.55937576293945,163.27604166666666,83.55937576293945,171.69270858764648,83.64270909627278C180.1093755086263,83.72604242960612,188.60937601725263,83.89270909627278,192.85937627156576,83.97604242960611L197.10937652587893,84.05937576293944" marker-end="url(#arrowhead6181)" style="fill:none"></path><defs><marker id="arrowhead6181" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M273.9166919675194,64.29950675633769L282.6266698809732,58.12458896361474C291.336647794427,51.9496711708918,308.7566036213346,39.599835585445895,326.0254359557682,33.424917792722944C343.29426829020184,27.25,360.41197713216144,27.25,368.9708315531413,27.25L377.5296859741211,27.25" marker-end="url(#arrowhead6182)" style="fill:none"></path><defs><marker id="arrowhead6182" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M273.91669377035737,103.81924601846094L282.6266713833382,109.82749693636394C291.33664899631896,115.83574785426693,308.7566042222806,127.85224969007292,323.0499154229079,133.94383394130924C337.34322662353514,140.03541819254556,348.50989379882805,140.20208485921225,354.09322738647444,140.28541819254556L359.67656097412095,140.3687515258789" marker-end="url(#arrowhead6183)" style="fill:none"></path><defs><marker id="arrowhead6183" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M430.50387152826,124.45856208001759L438.57223669021664,119.65192698766113C446.64060185217335,114.8452918953047,462.77733217608665,105.23202171059181,490.6796817130433,100.42538661823535C518.58203125,95.6187515258789,558.25,95.6187515258789,578.083984375,95.6187515258789L597.91796875,95.6187515258789" marker-end="url(#arrowhead6184)" style="fill:none"></path><defs><marker id="arrowhead6184" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M430.503869596065,156.27893985217725L438.5722350800541,160.91890846446086C446.6406005640433,165.55887707674447,462.77733153202166,174.8388143013117,476.3456970160108,179.4787829135953C489.9140625,184.1187515258789,500.9140625,184.1187515258789,506.4140625,184.1187515258789L511.9140625,184.1187515258789" marker-end="url(#arrowhead6185)" style="fill:none"></path><defs><marker id="arrowhead6185" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(326.1765594482422,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000118255615234" height="18.500137329101562"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(326.1765594482422,139.8687515258789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000118255615234" height="18.500137329101562"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(478.9140625,95.6187515258789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000118255615234" height="18.500137329101562"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(478.9140625,184.1187515258789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000118255615234" height="18.500137329101562"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7124" transform="translate(77.3046875,83.55937576293945)"><rect rx="0" ry="0" x="-69.3046875" y="-19.25" width="138.609375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-59.3046875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">check(left,right)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7125" transform="translate(244.8929672241211,83.55937576293945)"><polygon points="48.28359375,0 96.5671875,-48.28359375 48.28359375,-96.5671875 0,-48.28359375" transform="translate(-48.28359375,48.28359375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-24.3984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">都null?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7127" transform="translate(402.5453109741211,27.25)"><rect rx="0" ry="0" x="-25.015625" y="-19.25" width="50.03125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-15.015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">true</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7129" transform="translate(402.5453109741211,139.8687515258789)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">一空?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7131" transform="translate(625.0390625,95.6187515258789)"><rect rx="0" ry="0" x="-27.12109375" y="-19.25" width="54.2421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-17.12109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7133" transform="translate(625.0390625,184.1187515258789)"><rect rx="0" ry="0" x="-113.125" y="-19.25" width="226.25" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-103.125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">值等?左左vs右右?左右vs右左?</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isSymmetric(TreeNode root) {        return check(root.left, root.right);    }    boolean check(TreeNode a, TreeNode b) {        if (a == null &amp;&amp; b == null) return true;        if (a == null || b == null) return false;        return a.val == b.val &amp;&amp; check(a.left, b.right) &amp;&amp; check(a.right, b.left);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><h2 id="105.-%E4%BB%8E%E5%89%8D%E5%BA%8F%E4%B8%8E%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91" tabindex="-1"><a href="https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/" target="_blank">105. 从前序与中序遍历序列构造二叉树</a></h2><blockquote><p>给前序和中序，构造原树。</p></blockquote><p><strong>思路脉络</strong>：前序第一个是根。中序里根位置分左右子树。哈希 O(1) 定位根。前序用全局 <code>i</code> 顺序取根，先建左子再建右子（前序天然&quot;根左右&quot;）。</p><div class="mermaid"><svg id="render2938204044" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 746.6953125px;" viewBox="0 0 746.6953125 54.5"><style>#render2938204044 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2938204044 .error-icon{fill:#552222;}#render2938204044 .error-text{fill:#552222;stroke:#552222;}#render2938204044 .edge-thickness-normal{stroke-width:2px;}#render2938204044 .edge-thickness-thick{stroke-width:3.5px;}#render2938204044 .edge-pattern-solid{stroke-dasharray:0;}#render2938204044 .edge-pattern-dashed{stroke-dasharray:3;}#render2938204044 .edge-pattern-dotted{stroke-dasharray:2;}#render2938204044 .marker{fill:#333333;stroke:#333333;}#render2938204044 .marker.cross{stroke:#333333;}#render2938204044 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2938204044 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2938204044 .cluster-label text{fill:#333;}#render2938204044 .cluster-label span{color:#333;}#render2938204044 .label text,#render2938204044 span{fill:#333;color:#333;}#render2938204044 .node rect,#render2938204044 .node circle,#render2938204044 .node ellipse,#render2938204044 .node polygon,#render2938204044 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2938204044 .node .label{text-align:center;}#render2938204044 .node.clickable{cursor:pointer;}#render2938204044 .arrowheadPath{fill:#333333;}#render2938204044 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2938204044 .flowchart-link{stroke:#333333;fill:none;}#render2938204044 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2938204044 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2938204044 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2938204044 .cluster text{fill:#333;}#render2938204044 .cluster span{color:#333;}#render2938204044 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2938204044 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M187.6171875,27.25L191.78385416666666,27.25C195.95052083333334,27.25,204.28385416666666,27.25,212.6171875,27.25C220.95052083333334,27.25,229.28385416666666,27.25,233.45052083333334,27.25L237.6171875,27.25" marker-end="url(#arrowhead6202)" style="fill:none"></path><defs><marker id="arrowhead6202" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M366.8984375,27.25L371.0651041666667,27.25C375.2317708333333,27.25,383.5651041666667,27.25,391.8984375,27.25C400.2317708333333,27.25,408.5651041666667,27.25,412.7317708333333,27.25L416.8984375,27.25" marker-end="url(#arrowhead6203)" style="fill:none"></path><defs><marker id="arrowhead6203" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M550.7890625,27.25L554.9557291666666,27.25C559.1223958333334,27.25,567.4557291666666,27.25,575.7890625,27.25C584.1223958333334,27.25,592.4557291666666,27.25,596.6223958333334,27.25L600.7890625,27.25" marker-end="url(#arrowhead6204)" style="fill:none"></path><defs><marker id="arrowhead6204" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7140" transform="translate(97.80859375,27.25)"><rect rx="0" ry="0" x="-89.80859375" y="-19.25" width="179.6171875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-79.80859375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">前序取根preorder[i++]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7141" transform="translate(302.2578125,27.25)"><rect rx="0" ry="0" x="-64.640625" y="-19.25" width="129.28125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-54.640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">中序找根位置m</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7143" transform="translate(483.84375,27.25)"><rect rx="0" ry="0" x="-66.9453125" y="-19.25" width="133.890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.9453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左子:中序l..m-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7145" transform="translate(669.7421875,27.25)"><rect rx="0" ry="0" x="-68.953125" y="-19.25" width="137.90625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">右子:中序m+1..r</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    int[] preorder; int i = 0;    Map&lt;Integer, Integer&gt; idx = new HashMap&lt;&gt;();    public TreeNode buildTree(int[] preorder, int[] inorder) {        this.preorder = preorder;        for (int j = 0; j &lt; inorder.length; j++) idx.put(inorder[j], j);        return build(0, inorder.length - 1);    }    TreeNode build(int l, int r) {        if (l &gt; r) return null;        int val = preorder[i++], m = idx.get(val);        TreeNode root = new TreeNode(val);        root.left = build(l, m - 1); root.right = build(m + 1, r);        return root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>前序 <code>i</code> 全局递增，依赖&quot;先建左子再建右子&quot;的顺序，顺序反了 <code>i</code> 错位。</li></ul><h2 id="106.-%E4%BB%8E%E4%B8%AD%E5%BA%8F%E4%B8%8E%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91" tabindex="-1"><a href="https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/" target="_blank">106. 从中序与后序遍历序列构造二叉树</a></h2><blockquote><p>给中序和后序，构造原树。</p></blockquote><p><strong>思路脉络</strong>：后序最后一个是根。中序定位根分左右。后序用全局 <code>i</code> 从后往前取根，先建右子再建左子（后序天然&quot;左右根&quot;，逆过来是&quot;根右左&quot;）。</p><div class="mermaid"><svg id="render1259678969" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 748.125px;" viewBox="0 0 748.125 54.5"><style>#render1259678969 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1259678969 .error-icon{fill:#552222;}#render1259678969 .error-text{fill:#552222;stroke:#552222;}#render1259678969 .edge-thickness-normal{stroke-width:2px;}#render1259678969 .edge-thickness-thick{stroke-width:3.5px;}#render1259678969 .edge-pattern-solid{stroke-dasharray:0;}#render1259678969 .edge-pattern-dashed{stroke-dasharray:3;}#render1259678969 .edge-pattern-dotted{stroke-dasharray:2;}#render1259678969 .marker{fill:#333333;stroke:#333333;}#render1259678969 .marker.cross{stroke:#333333;}#render1259678969 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1259678969 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1259678969 .cluster-label text{fill:#333;}#render1259678969 .cluster-label span{color:#333;}#render1259678969 .label text,#render1259678969 span{fill:#333;color:#333;}#render1259678969 .node rect,#render1259678969 .node circle,#render1259678969 .node ellipse,#render1259678969 .node polygon,#render1259678969 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1259678969 .node .label{text-align:center;}#render1259678969 .node.clickable{cursor:pointer;}#render1259678969 .arrowheadPath{fill:#333333;}#render1259678969 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1259678969 .flowchart-link{stroke:#333333;fill:none;}#render1259678969 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1259678969 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1259678969 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1259678969 .cluster text{fill:#333;}#render1259678969 .cluster span{color:#333;}#render1259678969 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1259678969 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M189.046875,27.25L193.21354166666666,27.25C197.38020833333334,27.25,205.71354166666666,27.25,214.046875,27.25C222.38020833333334,27.25,230.71354166666666,27.25,234.88020833333334,27.25L239.046875,27.25" marker-end="url(#arrowhead6221)" style="fill:none"></path><defs><marker id="arrowhead6221" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M368.328125,27.25L372.4947916666667,27.25C376.6614583333333,27.25,384.9947916666667,27.25,393.328125,27.25C401.6614583333333,27.25,409.9947916666667,27.25,414.1614583333333,27.25L418.328125,27.25" marker-end="url(#arrowhead6222)" style="fill:none"></path><defs><marker id="arrowhead6222" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M556.234375,27.25L560.4010416666666,27.25C564.5677083333334,27.25,572.9010416666666,27.25,581.234375,27.25C589.5677083333334,27.25,597.9010416666666,27.25,602.0677083333334,27.25L606.234375,27.25" marker-end="url(#arrowhead6223)" style="fill:none"></path><defs><marker id="arrowhead6223" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7152" transform="translate(98.5234375,27.25)"><rect rx="0" ry="0" x="-90.5234375" y="-19.25" width="181.046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.5234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">后序取根postorder[i--]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7153" transform="translate(303.6875,27.25)"><rect rx="0" ry="0" x="-64.640625" y="-19.25" width="129.28125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-54.640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">中序找根位置m</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7155" transform="translate(487.28125,27.25)"><rect rx="0" ry="0" x="-68.953125" y="-19.25" width="137.90625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">右子:中序m+1..r</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7157" transform="translate(673.1796875,27.25)"><rect rx="0" ry="0" x="-66.9453125" y="-19.25" width="133.890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.9453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左子:中序l..m-1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    int[] postorder; int i;    Map&lt;Integer, Integer&gt; idx = new HashMap&lt;&gt;();    public TreeNode buildTree(int[] inorder, int[] postorder) {        this.postorder = postorder; this.i = postorder.length - 1;        for (int j = 0; j &lt; inorder.length; j++) idx.put(inorder[j], j);        return build(0, inorder.length - 1);    }    TreeNode build(int l, int r) {        if (l &gt; r) return null;        int val = postorder[i--], m = idx.get(val);        TreeNode root = new TreeNode(val);        root.right = build(m + 1, r); root.left = build(l, m - 1);   // 先右后左        return root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>后序从后往前取根，必须<strong>先建右子再建左子</strong>（和 105 前序先左后右相反）。顺序反了 <code>i</code> 错位。</li></ul><h2 id="117.-%E5%A1%AB%E5%85%85%E6%AF%8F%E4%B8%AA%E8%8A%82%E7%82%B9%E7%9A%84%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%8F%B3%E4%BE%A7%E8%8A%82%E7%82%B9%E6%8C%87%E9%92%88-ii" tabindex="-1"><a href="https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/" target="_blank">117. 填充每个节点的下一个右侧节点指针 II</a></h2><blockquote><p>每层节点用 <code>next</code> 串成链表，树非完美二叉（节点可能缺子）。</p></blockquote><p><strong>思路脉络</strong>：BFS 层序。每层开始记 <code>size</code>，逐个出队并把 <code>next</code> 指向队首（同层下一节点）。关键是 BFS 按层切分，同层内串 <code>next</code>。</p><div class="mermaid"><svg id="render2772954128" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 582.1484375px;" viewBox="0 0 582.1484375 89.5"><style>#render2772954128 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2772954128 .error-icon{fill:#552222;}#render2772954128 .error-text{fill:#552222;stroke:#552222;}#render2772954128 .edge-thickness-normal{stroke-width:2px;}#render2772954128 .edge-thickness-thick{stroke-width:3.5px;}#render2772954128 .edge-pattern-solid{stroke-dasharray:0;}#render2772954128 .edge-pattern-dashed{stroke-dasharray:3;}#render2772954128 .edge-pattern-dotted{stroke-dasharray:2;}#render2772954128 .marker{fill:#333333;stroke:#333333;}#render2772954128 .marker.cross{stroke:#333333;}#render2772954128 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2772954128 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2772954128 .cluster-label text{fill:#333;}#render2772954128 .cluster-label span{color:#333;}#render2772954128 .label text,#render2772954128 span{fill:#333;color:#333;}#render2772954128 .node rect,#render2772954128 .node circle,#render2772954128 .node ellipse,#render2772954128 .node polygon,#render2772954128 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2772954128 .node .label{text-align:center;}#render2772954128 .node.clickable{cursor:pointer;}#render2772954128 .arrowheadPath{fill:#333333;}#render2772954128 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2772954128 .flowchart-link{stroke:#333333;fill:none;}#render2772954128 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2772954128 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2772954128 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2772954128 .cluster text{fill:#333;}#render2772954128 .cluster span{color:#333;}#render2772954128 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2772954128 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M85.1484375,37.91666666666667L89.31510416666667,36.13888888888889C93.48177083333333,34.361111111111114,101.81510416666667,30.805555555555557,110.1484375,29.027777777777782C118.48177083333333,27.25,126.81510416666667,27.25,130.98177083333334,27.25L135.1484375,27.25" marker-end="url(#arrowhead6246)" style="fill:none"></path><defs><marker id="arrowhead6246" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M230.5078125,27.25L234.67447916666666,27.25C238.84114583333334,27.25,247.17447916666666,27.25,255.5078125,27.25C263.8411458333333,27.25,272.1744791666667,27.25,276.3411458333333,27.25L280.5078125,27.25" marker-end="url(#arrowhead6247)" style="fill:none"></path><defs><marker id="arrowhead6247" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M424.1484375,27.25L428.3151041666667,27.25C432.4817708333333,27.25,440.8151041666667,27.25,449.1484375,28.756944444444446C457.4817708333333,30.26388888888889,465.8151041666667,33.27777777777778,469.9817708333333,34.78472222222223L474.1484375,36.29166666666667" marker-end="url(#arrowhead6248)" style="fill:none"></path><defs><marker id="arrowhead6248" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M474.1484375,72.45833333333333L469.9817708333333,73.96527777777777C465.8151041666667,75.47222222222221,457.4817708333333,78.4861111111111,437.1783854166667,79.99305555555556C416.875,81.5,384.6015625,81.5,352.328125,81.5C320.0546875,81.5,287.78125,81.5,259.53125,81.5C231.28125,81.5,207.0546875,81.5,182.828125,81.5C158.6015625,81.5,134.375,81.5,118.09505208333333,79.72222222222221C101.81510416666667,77.94444444444444,93.48177083333333,74.38888888888889,89.31510416666667,72.6111111111111L85.1484375,70.83333333333333" marker-end="url(#arrowhead6249)" style="fill:none"></path><defs><marker id="arrowhead6249" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7166" transform="translate(46.57421875,54.375)"><rect rx="0" ry="0" x="-38.57421875" y="-19.25" width="77.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-28.57421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">BFS层序</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7167" transform="translate(182.828125,27.25)"><rect rx="0" ry="0" x="-47.6796875" y="-19.25" width="95.359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.6796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">每层记size</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7169" transform="translate(352.328125,27.25)"><rect rx="0" ry="0" x="-71.8203125" y="-19.25" width="143.640625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-61.8203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">prev.next=下一个</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7171" transform="translate(524.1484375,54.375)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左右子入队</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public Node connect(Node root) {        if (root == null) return null;        Queue&lt;Node&gt; q = new ArrayDeque&lt;&gt;(); q.offer(root);        while (!q.isEmpty()) {            int size = q.size();            Node prev = null;            for (int i = 0; i &lt; size; i++) {                Node n = q.poll();                if (prev != null) prev.next = n;                prev = n;                if (n.left != null) q.offer(n.left);                if (n.right != null) q.offer(n.right);            }        }        return root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 进阶</p></blockquote><ul><li>O(1) 空间：利用上一层已串好的 <code>next</code> 链表遍历下一层，不用队列。但 BFS 法最直观，面试首选。</li></ul><h2 id="114.-%E4%BA%8C%E5%8F%89%E6%A0%91%E5%B1%95%E5%BC%80%E4%B8%BA%E9%93%BE%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/" target="_blank">114. 二叉树展开为链表</a></h2><blockquote><p>按前序把树展开成右链（每节点只有右孩子）。</p></blockquote><p><strong>思路脉络</strong>：后序&quot;右→左→中&quot;配全局 <code>prev</code>。处理当前时左右子已展平，<code>root.right = prev</code>、<code>root.left = null</code>、<code>prev = root</code>。逆前序让链表从尾向头接。</p><div class="mermaid"><svg id="render3466586199" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 639.9140625px;" viewBox="0 0 639.9140625 89.5"><style>#render3466586199 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3466586199 .error-icon{fill:#552222;}#render3466586199 .error-text{fill:#552222;stroke:#552222;}#render3466586199 .edge-thickness-normal{stroke-width:2px;}#render3466586199 .edge-thickness-thick{stroke-width:3.5px;}#render3466586199 .edge-pattern-solid{stroke-dasharray:0;}#render3466586199 .edge-pattern-dashed{stroke-dasharray:3;}#render3466586199 .edge-pattern-dotted{stroke-dasharray:2;}#render3466586199 .marker{fill:#333333;stroke:#333333;}#render3466586199 .marker.cross{stroke:#333333;}#render3466586199 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3466586199 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3466586199 .cluster-label text{fill:#333;}#render3466586199 .cluster-label span{color:#333;}#render3466586199 .label text,#render3466586199 span{fill:#333;color:#333;}#render3466586199 .node rect,#render3466586199 .node circle,#render3466586199 .node ellipse,#render3466586199 .node polygon,#render3466586199 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3466586199 .node .label{text-align:center;}#render3466586199 .node.clickable{cursor:pointer;}#render3466586199 .arrowheadPath{fill:#333333;}#render3466586199 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3466586199 .flowchart-link{stroke:#333333;fill:none;}#render3466586199 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3466586199 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3466586199 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3466586199 .cluster text{fill:#333;}#render3466586199 .cluster span{color:#333;}#render3466586199 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3466586199 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M143.60282258064518,35.125L148.14818548387098,33.8125C152.6935483870968,32.5,161.78427419354838,29.875,170.49630376344086,28.5625C179.20833333333334,27.25,187.54166666666666,27.25,191.70833333333334,27.25L195.875,27.25" marker-end="url(#arrowhead6272)" style="fill:none"></path><defs><marker id="arrowhead6272" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M325.46875,27.25L329.6354166666667,27.25C333.8020833333333,27.25,342.1354166666667,27.25,350.46875,27.25C358.8020833333333,27.25,367.1354166666667,27.25,371.3020833333333,27.25L375.46875,27.25" marker-end="url(#arrowhead6273)" style="fill:none"></path><defs><marker id="arrowhead6273" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M492.09375,27.25L496.2604166666667,27.25C500.4270833333333,27.25,508.7604166666667,27.25,517.09375,28.866658285373713C525.4270833333334,30.483316570747423,533.7604166666666,33.716633141494846,537.9270833333334,35.33329142686856L542.09375,36.94994971224227" marker-end="url(#arrowhead6274)" style="fill:none"></path><defs><marker id="arrowhead6274" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M542.09375,71.80005028775773L537.9270833333334,73.41670857313143C533.7604166666666,75.03336685850515,525.4270833333334,78.26668342925258,507.375,79.88334171462628C489.3229166666667,81.5,461.5520833333333,81.5,433.78125,81.5C406.0104166666667,81.5,378.2395833333333,81.5,349.3880208333333,81.5C320.5364583333333,81.5,290.6041666666667,81.5,260.671875,81.5C230.73958333333334,81.5,200.80729166666666,81.5,181.29578293010752,80.1875C161.78427419354838,78.875,152.6935483870968,76.25,148.14818548387098,74.9375L143.60282258064518,73.625" marker-end="url(#arrowhead6275)" style="fill:none"></path><defs><marker id="arrowhead6275" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7182" transform="translate(76.9375,54.375)"><rect rx="0" ry="0" x="-68.9375" y="-19.25" width="137.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">后序:右→左→中</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7183" transform="translate(260.671875,27.25)"><rect rx="0" ry="0" x="-64.796875" y="-19.25" width="129.59375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-54.796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">root.right=prev</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7185" transform="translate(433.78125,27.25)"><rect rx="0" ry="0" x="-58.3125" y="-19.25" width="116.625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48.3125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">root.left=null</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7187" transform="translate(587.00390625,54.375)"><rect rx="0" ry="0" x="-44.91015625" y="-19.25" width="89.8203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.91015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">prev=root</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    TreeNode prev = null;    public void flatten(TreeNode root) {        if (root == null) return;        flatten(root.right); flatten(root.left);        root.right = prev; root.left = null; prev = root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须先 <code>flatten(right)</code> 再 <code>flatten(left)</code>，模拟&quot;前序的逆序&quot;（右左中）。</li></ul><h2 id="112.-%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/path-sum/" target="_blank">112. 路径总和</a></h2><blockquote><p>判断是否有根到叶路径和等于 <code>targetSum</code>。</p></blockquote><p><strong>思路脉络</strong>：递归减值。到叶子时判剩余是否等于叶子值。空树直接 false（没有根到叶路径）。</p><div class="mermaid"><svg id="render2893062482" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 429.5734558105469px;" viewBox="0 0 429.5734558105469 143"><style>#render2893062482 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2893062482 .error-icon{fill:#552222;}#render2893062482 .error-text{fill:#552222;stroke:#552222;}#render2893062482 .edge-thickness-normal{stroke-width:2px;}#render2893062482 .edge-thickness-thick{stroke-width:3.5px;}#render2893062482 .edge-pattern-solid{stroke-dasharray:0;}#render2893062482 .edge-pattern-dashed{stroke-dasharray:3;}#render2893062482 .edge-pattern-dotted{stroke-dasharray:2;}#render2893062482 .marker{fill:#333333;stroke:#333333;}#render2893062482 .marker.cross{stroke:#333333;}#render2893062482 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2893062482 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2893062482 .cluster-label text{fill:#333;}#render2893062482 .cluster-label span{color:#333;}#render2893062482 .label text,#render2893062482 span{fill:#333;color:#333;}#render2893062482 .node rect,#render2893062482 .node circle,#render2893062482 .node ellipse,#render2893062482 .node polygon,#render2893062482 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2893062482 .node .label{text-align:center;}#render2893062482 .node.clickable{cursor:pointer;}#render2893062482 .arrowheadPath{fill:#333333;}#render2893062482 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2893062482 .flowchart-link{stroke:#333333;fill:none;}#render2893062482 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2893062482 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2893062482 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2893062482 .cluster text{fill:#333;}#render2893062482 .cluster span{color:#333;}#render2893062482 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2893062482 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92,71.5L96.16666666666667,71.5C100.33333333333333,71.5,108.66666666666667,71.5,117.0833335876465,71.58333333333333C125.50000050862631,71.66666666666667,134.00000101725263,71.83333333333333,138.25000127156576,71.91666666666667L142.50000152587893,72" marker-end="url(#arrowhead6290)" style="fill:none"></path><defs><marker id="arrowhead6290" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M213.32731208001766,56.08981055413872L221.39567724197434,51.283175461782264C229.46404240393107,46.47654036942581,245.60077272784443,36.86327018471291,260.7648410148011,32.05663509235645C275.9289093017578,27.25,290.1203155517578,27.25,297.2160186767578,27.25L304.3117218017578,27.25" marker-end="url(#arrowhead6291)" style="fill:none"></path><defs><marker id="arrowhead6291" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M213.32731014782274,87.91018832629831L221.39567563181194,92.55015693858194C229.4640411158011,97.19012555086555,245.60077208377947,106.47006277543278,259.16913756776864,111.11003138771639C272.7375030517578,115.75,283.7375030517578,115.75,289.2375030517578,115.75L294.7375030517578,115.75" marker-end="url(#arrowhead6292)" style="fill:none"></path><defs><marker id="arrowhead6292" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(261.7375030517578,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000118255615234" height="18.500137329101562"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(261.7375030517578,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000118255615234" height="18.500137329101562"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7196" transform="translate(50,71.5)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归减值</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7197" transform="translate(185.3687515258789,71.5)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">叶子?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7199" transform="translate(358.1554718017578,27.25)"><rect rx="0" ry="0" x="-53.84375" y="-19.25" width="107.6875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.84375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">val==target?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7201" transform="translate(358.1554718017578,115.75)"><rect rx="0" ry="0" x="-63.41796875" y="-19.25" width="126.8359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.41796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归左右,减val</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean hasPathSum(TreeNode root, int targetSum) {        if (root == null) return false;        if (root.left == null &amp;&amp; root.right == null) return root.val == targetSum;        return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须&quot;根到叶&quot;，叶子定义是左右都空。中间节点（只有一子）不算终止。空树返回 false。</li></ul><h2 id="129.-%E6%B1%82%E6%A0%B9%E8%8A%82%E7%82%B9%E5%88%B0%E5%8F%B6%E8%8A%82%E7%82%B9%E6%95%B0%E5%AD%97%E4%B9%8B%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/sum-root-to-leaf-numbers/" target="_blank">129. 求根节点到叶节点数字之和</a></h2><blockquote><p>每条根到叶路径表示一个数字（如 1→2→3 = 123），求所有路径数字之和。</p></blockquote><p><strong>思路脉络</strong>：DFS 传&quot;当前累计值&quot; <code>cur = cur*10 + node.val</code>。到叶子时把 <code>cur</code> 加进总和。</p><div class="mermaid"><svg id="render3418703631" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 657.1281127929688px;" viewBox="0 0 657.1281127929688 143"><style>#render3418703631 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3418703631 .error-icon{fill:#552222;}#render3418703631 .error-text{fill:#552222;stroke:#552222;}#render3418703631 .edge-thickness-normal{stroke-width:2px;}#render3418703631 .edge-thickness-thick{stroke-width:3.5px;}#render3418703631 .edge-pattern-solid{stroke-dasharray:0;}#render3418703631 .edge-pattern-dashed{stroke-dasharray:3;}#render3418703631 .edge-pattern-dotted{stroke-dasharray:2;}#render3418703631 .marker{fill:#333333;stroke:#333333;}#render3418703631 .marker.cross{stroke:#333333;}#render3418703631 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3418703631 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3418703631 .cluster-label text{fill:#333;}#render3418703631 .cluster-label span{color:#333;}#render3418703631 .label text,#render3418703631 span{fill:#333;color:#333;}#render3418703631 .node rect,#render3418703631 .node circle,#render3418703631 .node ellipse,#render3418703631 .node polygon,#render3418703631 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3418703631 .node .label{text-align:center;}#render3418703631 .node.clickable{cursor:pointer;}#render3418703631 .arrowheadPath{fill:#333333;}#render3418703631 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3418703631 .flowchart-link{stroke:#333333;fill:none;}#render3418703631 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3418703631 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3418703631 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3418703631 .cluster text{fill:#333;}#render3418703631 .cluster span{color:#333;}#render3418703631 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3418703631 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M129.3828125,71.5L133.54947916666666,71.5C137.71614583333334,71.5,146.04947916666666,71.5,154.3828125,71.5C162.71614583333334,71.5,171.04947916666666,71.5,175.21614583333334,71.5L179.3828125,71.5" marker-end="url(#arrowhead6312)" style="fill:none"></path><defs><marker id="arrowhead6312" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M346.390625,71.5L350.5572916666667,71.5C354.7239583333333,71.5,363.0572916666667,71.5,371.47395858764645,71.58333333333333C379.8906255086263,71.66666666666667,388.39062601725254,71.83333333333333,392.6406262715657,71.91666666666667L396.8906265258788,72" marker-end="url(#arrowhead6313)" style="fill:none"></path><defs><marker id="arrowhead6313" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M467.71793708001775,56.089810554138715L475.78630224197445,51.283175461782264C483.85466740393105,46.47654036942581,499.99139772784446,36.86327018471291,514.5460910148012,32.05663509235645C529.1007843017578,27.25,542.0734405517578,27.25,548.5597686767578,27.25L555.0460968017578,27.25" marker-end="url(#arrowhead6314)" style="fill:none"></path><defs><marker id="arrowhead6314" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M467.7179351478228,87.91018832629835L475.7863006318119,92.55015693858195C483.85466611580114,97.19012555086556,499.9913970837795,106.47006277543278,513.5597625677686,111.11003138771639C527.1281280517578,115.75,538.1281280517578,115.75,543.6281280517578,115.75L549.1281280517578,115.75" marker-end="url(#arrowhead6315)" style="fill:none"></path><defs><marker id="arrowhead6315" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(516.1281280517578,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000076293945312" height="18.50008773803711"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(516.1281280517578,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000076293945312" height="18.50008773803711"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7210" transform="translate(68.69140625,71.5)"><rect rx="0" ry="0" x="-60.69140625" y="-19.25" width="121.3828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.69140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">DFS(node,cur)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7211" transform="translate(262.88671875,71.5)"><rect rx="0" ry="0" x="-83.50390625" y="-19.25" width="167.0078125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-73.50390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cur=cur*10+node.val</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7213" transform="translate(439.7593765258789,71.5)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">叶子?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7215" transform="translate(599.1281280517578,27.25)"><rect rx="0" ry="0" x="-44.08203125" y="-19.25" width="88.1640625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.08203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sum+=cur</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7217" transform="translate(599.1281280517578,115.75)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归左右子</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0129-sum-numbers.gif" alt="" /></p><pre><code class="language-java">class Solution {    int sum = 0;    public int sumNumbers(TreeNode root) {        dfs(root, 0); return sum;    }    void dfs(TreeNode node, int cur) {        if (node == null) return;        cur = cur * 10 + node.val;        if (node.left == null &amp;&amp; node.right == null) sum += cur;        dfs(node.left, cur); dfs(node.right, cur);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>cur</code> 是值传递，回溯天然（每层独立副本），不用显式撤销。叶子判断后才加 <code>cur</code>，中间节点不加。</li></ul><h2 id="124.-%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E7%9A%84%E6%9C%80%E5%A4%A7%E8%B7%AF%E5%BE%84%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/binary-tree-maximum-path-sum/" target="_blank">124. 二叉树中的最大路径和</a></h2><blockquote><p>任意路径（可不经过根）的最大和。</p></blockquote><p><strong>思路脉络</strong>：<code>gain(root)</code> 返回从 root 向下单边最大和（负贡献截断为 0）。每节点处以它为最高点的路径和 = <code>左贡献 + 右贡献 + 自身</code>，更新答案。返回给上层只能单边。和热题 100 相同。</p><div class="mermaid"><svg id="render3249617443" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 1105.3515625px;" viewBox="0 0 1105.3515625 54.5"><style>#render3249617443 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3249617443 .error-icon{fill:#552222;}#render3249617443 .error-text{fill:#552222;stroke:#552222;}#render3249617443 .edge-thickness-normal{stroke-width:2px;}#render3249617443 .edge-thickness-thick{stroke-width:3.5px;}#render3249617443 .edge-pattern-solid{stroke-dasharray:0;}#render3249617443 .edge-pattern-dashed{stroke-dasharray:3;}#render3249617443 .edge-pattern-dotted{stroke-dasharray:2;}#render3249617443 .marker{fill:#333333;stroke:#333333;}#render3249617443 .marker.cross{stroke:#333333;}#render3249617443 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3249617443 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3249617443 .cluster-label text{fill:#333;}#render3249617443 .cluster-label span{color:#333;}#render3249617443 .label text,#render3249617443 span{fill:#333;color:#333;}#render3249617443 .node rect,#render3249617443 .node circle,#render3249617443 .node ellipse,#render3249617443 .node polygon,#render3249617443 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3249617443 .node .label{text-align:center;}#render3249617443 .node.clickable{cursor:pointer;}#render3249617443 .arrowheadPath{fill:#333333;}#render3249617443 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3249617443 .flowchart-link{stroke:#333333;fill:none;}#render3249617443 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3249617443 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3249617443 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3249617443 .cluster text{fill:#333;}#render3249617443 .cluster span{color:#333;}#render3249617443 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3249617443 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M99.2265625,27.25L103.39322916666667,27.25C107.55989583333333,27.25,115.89322916666667,27.25,124.2265625,27.25C132.55989583333334,27.25,140.89322916666666,27.25,145.05989583333334,27.25L149.2265625,27.25" marker-end="url(#arrowhead6337)" style="fill:none"></path><defs><marker id="arrowhead6337" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M327.078125,27.25L331.2447916666667,27.25C335.4114583333333,27.25,343.7447916666667,27.25,352.078125,27.25C360.4114583333333,27.25,368.7447916666667,27.25,372.9114583333333,27.25L377.078125,27.25" marker-end="url(#arrowhead6338)" style="fill:none"></path><defs><marker id="arrowhead6338" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M554.9296875,27.25L559.0963541666666,27.25C563.2630208333334,27.25,571.5963541666666,27.25,579.9296875,27.25C588.2630208333334,27.25,596.5963541666666,27.25,600.7630208333334,27.25L604.9296875,27.25" marker-end="url(#arrowhead6339)" style="fill:none"></path><defs><marker id="arrowhead6339" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M833.25,27.25L837.4166666666666,27.25C841.5833333333334,27.25,849.9166666666666,27.25,858.25,27.25C866.5833333333334,27.25,874.9166666666666,27.25,879.0833333333334,27.25L883.25,27.25" marker-end="url(#arrowhead6340)" style="fill:none"></path><defs><marker id="arrowhead6340" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7226" transform="translate(53.61328125,27.25)"><rect rx="0" ry="0" x="-45.61328125" y="-19.25" width="91.2265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-35.61328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">gain(root)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7227" transform="translate(238.15234375,27.25)"><rect rx="0" ry="0" x="-88.92578125" y="-19.25" width="177.8515625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-78.92578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左贡献=max(0,gain左)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7229" transform="translate(466.00390625,27.25)"><rect rx="0" ry="0" x="-88.92578125" y="-19.25" width="177.8515625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-78.92578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">右贡献=max(0,gain右)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7231" transform="translate(719.08984375,27.25)"><rect rx="0" ry="0" x="-114.16015625" y="-19.25" width="228.3203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-104.16015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans=max(ans,左+右+root.val)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7233" transform="translate(990.30078125,27.25)"><rect rx="0" ry="0" x="-107.05078125" y="-19.25" width="214.1015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-97.05078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return max(左,右)+root.val</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    int ans = Integer.MIN_VALUE;    public int maxPathSum(TreeNode root) { gain(root); return ans; }    int gain(TreeNode root) {        if (root == null) return 0;        int l = Math.max(0, gain(root.left)), r = Math.max(0, gain(root.right));        ans = Math.max(ans, l + r + root.val);        return Math.max(l, r) + root.val;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>ans</code> 用&quot;两边和&quot;（完整路径可分叉在当前），<code>return</code> 用&quot;单边&quot;（向上不能分叉）。负贡献 <code>Math.max(0, ...)</code> 截断。</li></ul><h2 id="173.-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E8%BF%AD%E4%BB%A3%E5%99%A8" tabindex="-1"><a href="https://leetcode.cn/problems/binary-search-tree-iterator/" target="_blank">173. 二叉搜索树迭代器</a></h2><blockquote><p>设计按升序遍历 BST 的迭代器，<code>next</code>/<code>hasNext</code> 均 O(1) 均摊。</p></blockquote><p><strong>思路脉络</strong>：用栈模拟中序。构造时一路向左压栈。<code>next</code> 弹栈（当前最小）、把右子树一路向左压栈。均摊 O(1) 因为每节点入栈出栈各一次。</p><div class="mermaid"><svg id="render201349960" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 657.578125px;" viewBox="0 0 657.578125 54.5"><style>#render201349960 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render201349960 .error-icon{fill:#552222;}#render201349960 .error-text{fill:#552222;stroke:#552222;}#render201349960 .edge-thickness-normal{stroke-width:2px;}#render201349960 .edge-thickness-thick{stroke-width:3.5px;}#render201349960 .edge-pattern-solid{stroke-dasharray:0;}#render201349960 .edge-pattern-dashed{stroke-dasharray:3;}#render201349960 .edge-pattern-dotted{stroke-dasharray:2;}#render201349960 .marker{fill:#333333;stroke:#333333;}#render201349960 .marker.cross{stroke:#333333;}#render201349960 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render201349960 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render201349960 .cluster-label text{fill:#333;}#render201349960 .cluster-label span{color:#333;}#render201349960 .label text,#render201349960 span{fill:#333;color:#333;}#render201349960 .node rect,#render201349960 .node circle,#render201349960 .node ellipse,#render201349960 .node polygon,#render201349960 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render201349960 .node .label{text-align:center;}#render201349960 .node.clickable{cursor:pointer;}#render201349960 .arrowheadPath{fill:#333333;}#render201349960 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render201349960 .flowchart-link{stroke:#333333;fill:none;}#render201349960 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render201349960 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render201349960 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render201349960 .cluster text{fill:#333;}#render201349960 .cluster span{color:#333;}#render201349960 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render201349960 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M161.875,27.25L166.04166666666666,27.25C170.20833333333334,27.25,178.54166666666666,27.25,186.875,27.25C195.20833333333334,27.25,203.54166666666666,27.25,207.70833333333334,27.25L211.875,27.25" marker-end="url(#arrowhead6357)" style="fill:none"></path><defs><marker id="arrowhead6357" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M301.578125,27.25L305.7447916666667,27.25C309.9114583333333,27.25,318.2447916666667,27.25,326.578125,27.25C334.9114583333333,27.25,343.2447916666667,27.25,347.4114583333333,27.25L351.578125,27.25" marker-end="url(#arrowhead6358)" style="fill:none"></path><defs><marker id="arrowhead6358" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M499.578125,27.25L503.7447916666667,27.25C507.9114583333333,27.25,516.2447916666666,27.25,524.578125,27.25C532.9114583333334,27.25,541.2447916666666,27.25,545.4114583333334,27.25L549.578125,27.25" marker-end="url(#arrowhead6359)" style="fill:none"></path><defs><marker id="arrowhead6359" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7240" transform="translate(84.9375,27.25)"><rect rx="0" ry="0" x="-76.9375" y="-19.25" width="153.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-66.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">构造:一路向左压栈</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7241" transform="translate(256.7265625,27.25)"><rect rx="0" ry="0" x="-44.8515625" y="-19.25" width="89.703125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.8515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">next:弹栈</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7243" transform="translate(425.578125,27.25)"><rect rx="0" ry="0" x="-74" y="-19.25" width="148" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-64,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">右子一路向左压栈</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7245" transform="translate(599.578125,27.25)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回弹栈值</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class BSTIterator {    Deque&lt;TreeNode&gt; st = new ArrayDeque&lt;&gt;();    public BSTIterator(TreeNode root) { pushLeft(root); }    public int next() { TreeNode n = st.pop(); pushLeft(n.right); return n.val; }    public boolean hasNext() { return !st.isEmpty(); }    void pushLeft(TreeNode n) { while (n != null) { st.push(n); n = n.left; } }}</code></pre><blockquote><p>[!info] 复杂度<br /><code>next</code>/<code>hasNext</code> 均摊 O(1)，空间 O(h)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;迭代中序&quot;通用模板：栈 + <code>pushLeft</code>。惰性遍历，比一次性中序省空间（只存一条左链）。</li></ul><h2 id="222.-%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E8%8A%82%E7%82%B9%E4%B8%AA%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/count-complete-tree-nodes/" target="_blank">222. 完全二叉树的节点个数</a></h2><blockquote><p>完全二叉树节点数，O(log²n)。</p></blockquote><p><strong>思路脉络</strong>：完全二叉树——若左右子树高度相同，左子是满的，节点数 = <code>2^h - 1</code> 用公式；不同则递归数。满子树 O(1) 算，非满递归，整体 O(log²n)。</p><div class="mermaid"><svg id="render3835587632" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 524.6281127929688px;" viewBox="0 0 524.6281127929688 143"><style>#render3835587632 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3835587632 .error-icon{fill:#552222;}#render3835587632 .error-text{fill:#552222;stroke:#552222;}#render3835587632 .edge-thickness-normal{stroke-width:2px;}#render3835587632 .edge-thickness-thick{stroke-width:3.5px;}#render3835587632 .edge-pattern-solid{stroke-dasharray:0;}#render3835587632 .edge-pattern-dashed{stroke-dasharray:3;}#render3835587632 .edge-pattern-dotted{stroke-dasharray:2;}#render3835587632 .marker{fill:#333333;stroke:#333333;}#render3835587632 .marker.cross{stroke:#333333;}#render3835587632 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3835587632 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3835587632 .cluster-label text{fill:#333;}#render3835587632 .cluster-label span{color:#333;}#render3835587632 .label text,#render3835587632 span{fill:#333;color:#333;}#render3835587632 .node rect,#render3835587632 .node circle,#render3835587632 .node ellipse,#render3835587632 .node polygon,#render3835587632 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3835587632 .node .label{text-align:center;}#render3835587632 .node.clickable{cursor:pointer;}#render3835587632 .arrowheadPath{fill:#333333;}#render3835587632 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3835587632 .flowchart-link{stroke:#333333;fill:none;}#render3835587632 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3835587632 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3835587632 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3835587632 .cluster text{fill:#333;}#render3835587632 .cluster span{color:#333;}#render3835587632 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3835587632 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M140,71.5L144.16666666666666,71.5C148.33333333333334,71.5,156.66666666666666,71.5,165.08333358764648,71.58333333333333C173.5000005086263,71.66666666666667,182.00000101725263,71.83333333333333,186.25000127156576,71.91666666666667L190.50000152587893,72" marker-end="url(#arrowhead6374)" style="fill:none"></path><defs><marker id="arrowhead6374" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M271.87970329650165,54.759389270622705L280.16980533904433,50.174491058852254C288.459907381587,45.5895928470818,305.04011146667244,36.4197964235409,318.9552135092151,31.83489821177045C332.8703155517578,27.25,344.1203155517578,27.25,349.7453155517578,27.25L355.3703155517578,27.25" marker-end="url(#arrowhead6375)" style="fill:none"></path><defs><marker id="arrowhead6375" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M271.87970131174853,89.24060966237259L280.1698036850834,93.6588413853105C288.4599060584183,98.0770731082484,305.04011080508803,106.9135365541242,318.8302131784229,111.3317682770621C332.6203155517578,115.75,343.6203155517578,115.75,349.1203155517578,115.75L354.6203155517578,115.75" marker-end="url(#arrowhead6376)" style="fill:none"></path><defs><marker id="arrowhead6376" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(321.6203155517578,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00009536743164" height="18.500110626220703"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(321.6203155517578,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00009536743164" height="18.500110626220703"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7252" transform="translate(74,71.5)"><rect rx="0" ry="0" x="-66" y="-19.25" width="132" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">算左右子树高度</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7253" transform="translate(239.3101577758789,71.5)"><polygon points="49.31015625,0 98.6203125,-49.31015625 49.31015625,-98.6203125 0,-49.31015625" transform="translate(-49.31015625,49.31015625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-25.5390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">lh==rh?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7255" transform="translate(435.6242218017578,27.25)"><rect rx="0" ry="0" x="-80.25390625" y="-19.25" width="160.5078125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-70.25390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左子满,2^lh+递归右</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7257" transform="translate(435.6242218017578,115.75)"><rect rx="0" ry="0" x="-81.00390625" y="-19.25" width="162.0078125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-71.00390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">右子满,2^rh+递归左</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int countNodes(TreeNode root) {        if (root == null) return 0;        int lh = height(root.left), rh = height(root.right);        if (lh == rh) return (1 &lt;&lt; lh) + countNodes(root.right);   // 左满, 数左+递归右        return (1 &lt;&lt; rh) + countNodes(root.left);                   // 右满, 数右+递归左    }    int height(TreeNode n) { int h = 0; while (n != null) { h++; n = n.left; } return h; }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log²n)，空间 O(log n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>height</code> 只走左链（完全二叉树性质：左链高度 = 该子树高度，除非最后一层）。<code>lh == rh</code> 说明左子树满（高度 lh），左子节点 = <code>2^lh - 1</code>，加根 1 = <code>2^lh</code>，再递归右。<code>1 &lt;&lt; lh</code> 是 <code>2^lh</code>。</li></ul><h2 id="236.-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88" tabindex="-1"><a href="https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/" target="_blank">236. 二叉树的最近公共祖先</a></h2><blockquote><p>找两个节点 p、q 的最近公共祖先。</p></blockquote><p><strong>思路脉络</strong>：递归返回祖先情况——命中 p/q 返回自身；左右都非空当前是 LCA；单边非空透传那侧。和热题 100 相同。</p><div class="mermaid"><svg id="render3724386660" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 789.8499755859375px;" viewBox="0 0 789.8499755859375 143"><style>#render3724386660 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3724386660 .error-icon{fill:#552222;}#render3724386660 .error-text{fill:#552222;stroke:#552222;}#render3724386660 .edge-thickness-normal{stroke-width:2px;}#render3724386660 .edge-thickness-thick{stroke-width:3.5px;}#render3724386660 .edge-pattern-solid{stroke-dasharray:0;}#render3724386660 .edge-pattern-dashed{stroke-dasharray:3;}#render3724386660 .edge-pattern-dotted{stroke-dasharray:2;}#render3724386660 .marker{fill:#333333;stroke:#333333;}#render3724386660 .marker.cross{stroke:#333333;}#render3724386660 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3724386660 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3724386660 .cluster-label text{fill:#333;}#render3724386660 .cluster-label span{color:#333;}#render3724386660 .label text,#render3724386660 span{fill:#333;color:#333;}#render3724386660 .node rect,#render3724386660 .node circle,#render3724386660 .node ellipse,#render3724386660 .node polygon,#render3724386660 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3724386660 .node .label{text-align:center;}#render3724386660 .node.clickable{cursor:pointer;}#render3724386660 .arrowheadPath{fill:#333333;}#render3724386660 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3724386660 .flowchart-link{stroke:#333333;fill:none;}#render3724386660 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3724386660 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3724386660 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3724386660 .cluster text{fill:#333;}#render3724386660 .cluster span{color:#333;}#render3724386660 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3724386660 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M141.921875,71.5L146.08854166666666,71.5C150.25520833333334,71.5,158.58854166666666,71.5,166.921875,71.5C175.25520833333334,71.5,183.58854166666666,71.5,187.75520833333334,71.5L191.921875,71.5" marker-end="url(#arrowhead6401)" style="fill:none"></path><defs><marker id="arrowhead6401" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M273.65625,71.5L277.8229166666667,71.5C281.9895833333333,71.5,290.3229166666667,71.5,298.65625,71.5C306.9895833333333,71.5,315.3229166666667,71.5,319.4895833333333,71.5L323.65625,71.5" marker-end="url(#arrowhead6402)" style="fill:none"></path><defs><marker id="arrowhead6402" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M424.46875,71.5L428.6354166666667,71.5C432.8020833333333,71.5,441.1354166666667,71.5,449.55208346048994,71.58333333333333C457.9687502543132,71.66666666666667,466.46875050862633,71.83333333333333,470.71875063578295,71.91666666666667L474.9687507629395,72" marker-end="url(#arrowhead6403)" style="fill:none"></path><defs><marker id="arrowhead6403" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M580.526876599343,52.17687736228244L589.247396911806,48.02239780190203C597.9679172242691,43.86791824152163,615.4089578491951,35.55895912076081,631.5591656616581,31.404479560380405C647.7093734741211,27.25,662.5687484741211,27.25,669.9984359741211,27.25L677.4281234741211,27.25" marker-end="url(#arrowhead6404)" style="fill:none"></path><defs><marker id="arrowhead6404" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M580.5268776427305,91.82312312020888L589.2473977812956,95.8109359335074C597.9679179198607,99.79874874680593,615.408958196991,107.77437437340296,629.629478335556,111.76218718670147C643.8499984741211,115.75,654.8499984741211,115.75,660.3499984741211,115.75L665.8499984741211,115.75" marker-end="url(#arrowhead6405)" style="fill:none"></path><defs><marker id="arrowhead6405" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(632.8499984741211,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000125885009766" height="18.500144958496094"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(632.8499984741211,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000125885009766" height="18.500144958496094"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7268" transform="translate(74.9609375,71.5)"><rect rx="0" ry="0" x="-66.9609375" y="-19.25" width="133.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.9609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">root==null/p/q?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7269" transform="translate(232.7890625,71.5)"><rect rx="0" ry="0" x="-40.8671875" y="-19.25" width="81.734375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.8671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回root</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7271" transform="translate(374.0625,71.5)"><rect rx="0" ry="0" x="-50.40625" y="-19.25" width="100.8125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.40625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归左l,右r</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7273" transform="translate(537.1593742370605,71.5)"><polygon points="62.690625000000004,0 125.38125000000001,-62.690625000000004 62.690625000000004,-125.38125000000001 0,-62.690625000000004" transform="translate(-62.690625000000004,62.690625000000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-40.40625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l和r都非空?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7275" transform="translate(723.8499984741211,27.25)"><rect rx="0" ry="0" x="-46.421875" y="-19.25" width="92.84375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">root是LCA</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7277" transform="translate(723.8499984741211,115.75)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回非空那侧</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {        if (root == null || root == p || root == q) return root;        TreeNode l = lowestCommonAncestor(root.left, p, q);        TreeNode r = lowestCommonAncestor(root.right, p, q);        if (l != null &amp;&amp; r != null) return root;        return l != null ? l : r;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><h2 id="199.-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%8F%B3%E8%A7%86%E5%9B%BE" tabindex="-1"><a href="https://leetcode.cn/problems/binary-tree-right-side-view/" target="_blank">199. 二叉树的右视图</a></h2><blockquote><p>从右侧看树，从上到下返回每层最右节点。</p></blockquote><p><strong>思路脉络</strong>：层序遍历，每层最后一个节点就是右视图。复用 BFS 模板，记录 <code>i == size-1</code> 的节点。</p><div class="mermaid"><svg id="render3027419459" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 558.1171875px;" viewBox="0 0 558.1171875 89.5"><style>#render3027419459 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3027419459 .error-icon{fill:#552222;}#render3027419459 .error-text{fill:#552222;stroke:#552222;}#render3027419459 .edge-thickness-normal{stroke-width:2px;}#render3027419459 .edge-thickness-thick{stroke-width:3.5px;}#render3027419459 .edge-pattern-solid{stroke-dasharray:0;}#render3027419459 .edge-pattern-dashed{stroke-dasharray:3;}#render3027419459 .edge-pattern-dotted{stroke-dasharray:2;}#render3027419459 .marker{fill:#333333;stroke:#333333;}#render3027419459 .marker.cross{stroke:#333333;}#render3027419459 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3027419459 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3027419459 .cluster-label text{fill:#333;}#render3027419459 .cluster-label span{color:#333;}#render3027419459 .label text,#render3027419459 span{fill:#333;color:#333;}#render3027419459 .node rect,#render3027419459 .node circle,#render3027419459 .node ellipse,#render3027419459 .node polygon,#render3027419459 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3027419459 .node .label{text-align:center;}#render3027419459 .node.clickable{cursor:pointer;}#render3027419459 .arrowheadPath{fill:#333333;}#render3027419459 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3027419459 .flowchart-link{stroke:#333333;fill:none;}#render3027419459 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3027419459 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3027419459 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3027419459 .cluster text{fill:#333;}#render3027419459 .cluster span{color:#333;}#render3027419459 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3027419459 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M85.1484375,37.91666666666667L89.31510416666667,36.13888888888889C93.48177083333333,34.361111111111114,101.81510416666667,30.805555555555557,110.1484375,29.027777777777782C118.48177083333333,27.25,126.81510416666667,27.25,130.98177083333334,27.25L135.1484375,27.25" marker-end="url(#arrowhead6428)" style="fill:none"></path><defs><marker id="arrowhead6428" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M251.1484375,27.25L255.31510416666666,27.25C259.4817708333333,27.25,267.8151041666667,27.25,276.1484375,27.25C284.4817708333333,27.25,292.8151041666667,27.25,296.9817708333333,27.25L301.1484375,27.25" marker-end="url(#arrowhead6429)" style="fill:none"></path><defs><marker id="arrowhead6429" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M432.1171875,27.25L436.2838541666667,27.25C440.4505208333333,27.25,448.7838541666667,27.25,457.1171875,29.165607344632765C465.4505208333333,31.081214689265536,473.7838541666667,34.91242937853107,477.9505208333333,36.82803672316384L482.1171875,38.74364406779661" marker-end="url(#arrowhead6430)" style="fill:none"></path><defs><marker id="arrowhead6430" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M482.1171875,70.00635593220339L477.9505208333333,71.92196327683617C473.7838541666667,73.83757062146893,465.4505208333333,77.66878531073446,446.203125,79.58439265536724C426.9557291666667,81.5,396.7942708333333,81.5,366.6328125,81.5C336.4713541666667,81.5,306.3098958333333,81.5,277.3958333333333,81.5C248.48177083333334,81.5,220.81510416666666,81.5,193.1484375,81.5C165.48177083333334,81.5,137.81510416666666,81.5,119.81510416666667,79.72222222222221C101.81510416666667,77.94444444444444,93.48177083333333,74.38888888888889,89.31510416666667,72.6111111111111L85.1484375,70.83333333333333" marker-end="url(#arrowhead6431)" style="fill:none"></path><defs><marker id="arrowhead6431" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7286" transform="translate(46.57421875,54.375)"><rect rx="0" ry="0" x="-38.57421875" y="-19.25" width="77.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-28.57421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">BFS层序</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7287" transform="translate(193.1484375,27.25)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">每层最后一个</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7289" transform="translate(366.6328125,27.25)"><rect rx="0" ry="0" x="-65.484375" y="-19.25" width="130.96875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i==size-1则记录</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7291" transform="translate(516.1171875,54.375)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一层</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;Integer&gt; rightSideView(TreeNode root) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Queue&lt;TreeNode&gt; q = new ArrayDeque&lt;&gt;(); q.offer(root);        while (!q.isEmpty()) {            int size = q.size();            for (int i = 0; i &lt; size; i++) {                TreeNode n = q.poll();                if (i == size - 1) res.add(n.val);                if (n.left != null) q.offer(n.left);                if (n.right != null) q.offer(n.right);            }        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><h2 id="637.-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%B9%B3%E5%9D%87%E5%80%BC" tabindex="-1"><a href="https://leetcode.cn/problems/average-of-levels-in-binary-tree/" target="_blank">637. 二叉树的层平均值</a></h2><blockquote><p>每层节点值的平均值。</p></blockquote><p><strong>思路脉络</strong>：层序 BFS，每层累加求和再除以节点数。注意 <code>double</code> 防溢出。</p><div class="mermaid"><svg id="render2229280186" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 573.1796875px;" viewBox="0 0 573.1796875 89.5"><style>#render2229280186 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2229280186 .error-icon{fill:#552222;}#render2229280186 .error-text{fill:#552222;stroke:#552222;}#render2229280186 .edge-thickness-normal{stroke-width:2px;}#render2229280186 .edge-thickness-thick{stroke-width:3.5px;}#render2229280186 .edge-pattern-solid{stroke-dasharray:0;}#render2229280186 .edge-pattern-dashed{stroke-dasharray:3;}#render2229280186 .edge-pattern-dotted{stroke-dasharray:2;}#render2229280186 .marker{fill:#333333;stroke:#333333;}#render2229280186 .marker.cross{stroke:#333333;}#render2229280186 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2229280186 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2229280186 .cluster-label text{fill:#333;}#render2229280186 .cluster-label span{color:#333;}#render2229280186 .label text,#render2229280186 span{fill:#333;color:#333;}#render2229280186 .node rect,#render2229280186 .node circle,#render2229280186 .node ellipse,#render2229280186 .node polygon,#render2229280186 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2229280186 .node .label{text-align:center;}#render2229280186 .node.clickable{cursor:pointer;}#render2229280186 .arrowheadPath{fill:#333333;}#render2229280186 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2229280186 .flowchart-link{stroke:#333333;fill:none;}#render2229280186 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2229280186 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2229280186 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2229280186 .cluster text{fill:#333;}#render2229280186 .cluster span{color:#333;}#render2229280186 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2229280186 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M85.1484375,37.91666666666667L89.31510416666667,36.13888888888889C93.48177083333333,34.361111111111114,101.81510416666667,30.805555555555557,110.1484375,29.027777777777782C118.48177083333333,27.25,126.81510416666667,27.25,130.98177083333334,27.25L135.1484375,27.25" marker-end="url(#arrowhead6454)" style="fill:none"></path><defs><marker id="arrowhead6454" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M247.6484375,27.25L251.81510416666666,27.25C255.98177083333334,27.25,264.3151041666667,27.25,272.6484375,27.25C280.9817708333333,27.25,289.3151041666667,27.25,293.4817708333333,27.25L297.6484375,27.25" marker-end="url(#arrowhead6455)" style="fill:none"></path><defs><marker id="arrowhead6455" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M447.1796875,27.25L451.3463541666667,27.25C455.5130208333333,27.25,463.8463541666667,27.25,472.1796875,29.165607344632765C480.5130208333333,31.081214689265536,488.8463541666667,34.91242937853107,493.0130208333333,36.82803672316384L497.1796875,38.74364406779661" marker-end="url(#arrowhead6456)" style="fill:none"></path><defs><marker id="arrowhead6456" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M497.1796875,70.00635593220339L493.0130208333333,71.92196327683617C488.8463541666667,73.83757062146893,480.5130208333333,77.66878531073446,459.71875,79.58439265536724C438.9244791666667,81.5,405.6692708333333,81.5,372.4140625,81.5C339.1588541666667,81.5,305.9036458333333,81.5,275.734375,81.5C245.56510416666666,81.5,218.48177083333334,81.5,191.3984375,81.5C164.31510416666666,81.5,137.23177083333334,81.5,119.5234375,79.72222222222221C101.81510416666667,77.94444444444444,93.48177083333333,74.38888888888889,89.31510416666667,72.6111111111111L85.1484375,70.83333333333333" marker-end="url(#arrowhead6457)" style="fill:none"></path><defs><marker id="arrowhead6457" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7302" transform="translate(46.57421875,54.375)"><rect rx="0" ry="0" x="-38.57421875" y="-19.25" width="77.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-28.57421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">BFS层序</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7303" transform="translate(191.3984375,27.25)"><rect rx="0" ry="0" x="-56.25" y="-19.25" width="112.5" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-46.25,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">每层累加sum</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7305" transform="translate(372.4140625,27.25)"><rect rx="0" ry="0" x="-74.765625" y="-19.25" width="149.53125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-64.765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">res.add(sum/size)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7307" transform="translate(531.1796875,54.375)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一层</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;Double&gt; averageOfLevels(TreeNode root) {        List&lt;Double&gt; res = new ArrayList&lt;&gt;();        Queue&lt;TreeNode&gt; q = new ArrayDeque&lt;&gt;(); q.offer(root);        while (!q.isEmpty()) {            int size = q.size(); double sum = 0;            for (int i = 0; i &lt; size; i++) {                TreeNode n = q.poll(); sum += n.val;                if (n.left != null) q.offer(n.left);                if (n.right != null) q.offer(n.right);            }            res.add(sum / size);        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>sum</code> 用 <code>double</code>，否则大数相加溢出。<code>sum / size</code> 是 double 除法。</li></ul><h2 id="102.-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86" tabindex="-1"><a href="https://leetcode.cn/problems/binary-tree-level-order-traversal/" target="_blank">102. 二叉树的层序遍历</a></h2><blockquote><p>自顶向下一层一层返回节点值。</p></blockquote><p><strong>思路脉络</strong>：BFS 模板，每层开始记 <code>size</code>，按 size 循环切开层。</p><div class="mermaid"><svg id="render3921409671" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 571.7421875px;" viewBox="0 0 571.7421875 89.5"><style>#render3921409671 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3921409671 .error-icon{fill:#552222;}#render3921409671 .error-text{fill:#552222;stroke:#552222;}#render3921409671 .edge-thickness-normal{stroke-width:2px;}#render3921409671 .edge-thickness-thick{stroke-width:3.5px;}#render3921409671 .edge-pattern-solid{stroke-dasharray:0;}#render3921409671 .edge-pattern-dashed{stroke-dasharray:3;}#render3921409671 .edge-pattern-dotted{stroke-dasharray:2;}#render3921409671 .marker{fill:#333333;stroke:#333333;}#render3921409671 .marker.cross{stroke:#333333;}#render3921409671 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3921409671 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3921409671 .cluster-label text{fill:#333;}#render3921409671 .cluster-label span{color:#333;}#render3921409671 .label text,#render3921409671 span{fill:#333;color:#333;}#render3921409671 .node rect,#render3921409671 .node circle,#render3921409671 .node ellipse,#render3921409671 .node polygon,#render3921409671 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3921409671 .node .label{text-align:center;}#render3921409671 .node.clickable{cursor:pointer;}#render3921409671 .arrowheadPath{fill:#333333;}#render3921409671 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3921409671 .flowchart-link{stroke:#333333;fill:none;}#render3921409671 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3921409671 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3921409671 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3921409671 .cluster text{fill:#333;}#render3921409671 .cluster span{color:#333;}#render3921409671 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3921409671 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M102.3828125,36.64343109139116L106.54947916666667,35.077859242825966C110.71614583333333,33.512287394260774,119.04947916666667,30.381143697130387,127.3828125,28.815571848565195C135.71614583333334,27.25,144.04947916666666,27.25,148.21614583333334,27.25L152.3828125,27.25" marker-end="url(#arrowhead6480)" style="fill:none"></path><defs><marker id="arrowhead6480" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M279.7421875,27.25L283.9088541666667,27.25C288.0755208333333,27.25,296.4088541666667,27.25,304.7421875,27.25C313.0755208333333,27.25,321.4088541666667,27.25,325.5755208333333,27.25L329.7421875,27.25" marker-end="url(#arrowhead6481)" style="fill:none"></path><defs><marker id="arrowhead6481" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M413.7421875,27.25L417.9088541666667,27.25C422.0755208333333,27.25,430.4088541666667,27.25,438.7421875,28.756944444444446C447.0755208333333,30.26388888888889,455.4088541666667,33.27777777777778,459.5755208333333,34.78472222222223L463.7421875,36.29166666666667" marker-end="url(#arrowhead6482)" style="fill:none"></path><defs><marker id="arrowhead6482" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M463.7421875,72.45833333333333L459.5755208333333,73.96527777777777C455.4088541666667,75.47222222222221,447.0755208333333,78.4861111111111,431.7421875,79.99305555555556C416.4088541666667,81.5,394.0755208333333,81.5,371.7421875,81.5C349.4088541666667,81.5,327.0755208333333,81.5,301.12890625,81.5C275.1822916666667,81.5,245.62239583333334,81.5,216.0625,81.5C186.50260416666666,81.5,156.94270833333334,81.5,137.99609375,79.9344281514348C119.04947916666667,78.36885630286962,110.71614583333333,75.23771260573922,106.54947916666667,73.67214075717403L102.3828125,72.10656890860884" marker-end="url(#arrowhead6483)" style="fill:none"></path><defs><marker id="arrowhead6483" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7318" transform="translate(55.19140625,54.375)"><rect rx="0" ry="0" x="-47.19140625" y="-19.25" width="94.3828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.19140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">BFS,记size</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7319" transform="translate(216.0625,27.25)"><rect rx="0" ry="0" x="-63.6796875" y="-19.25" width="127.359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.6796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">按size循环出队</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7321" transform="translate(371.7421875,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">收集本层</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7323" transform="translate(513.7421875,54.375)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左右子入队</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; levelOrder(TreeNode root) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Queue&lt;TreeNode&gt; q = new ArrayDeque&lt;&gt;(); q.offer(root);        while (!q.isEmpty()) {            int size = q.size(); List&lt;Integer&gt; level = new ArrayList&lt;&gt;();            for (int i = 0; i &lt; size; i++) {                TreeNode n = q.poll(); level.add(n.val);                if (n.left != null) q.offer(n.left);                if (n.right != null) q.offer(n.right);            }            res.add(level);        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><h2 id="103.-%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%94%AF%E9%BD%BF%E5%BD%A2%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86" tabindex="-1"><a href="https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/" target="_blank">103. 二叉树的锯齿形层序遍历</a></h2><blockquote><p>层序，但奇数层从左到右、偶数层从右到左。</p></blockquote><p><strong>思路脉络</strong>：BFS 基础上加方向标志。偶数层（0起）正向收、奇数层用 <code>add(0, val)</code> 反向收（或最后 <code>reverse</code>）。</p><div class="mermaid"><svg id="render799144806" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="201.36834716796875" style="max-width: 619.9156494140625px;" viewBox="0 0 619.9156494140625 201.36834716796875"><style>#render799144806 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render799144806 .error-icon{fill:#552222;}#render799144806 .error-text{fill:#552222;stroke:#552222;}#render799144806 .edge-thickness-normal{stroke-width:2px;}#render799144806 .edge-thickness-thick{stroke-width:3.5px;}#render799144806 .edge-pattern-solid{stroke-dasharray:0;}#render799144806 .edge-pattern-dashed{stroke-dasharray:3;}#render799144806 .edge-pattern-dotted{stroke-dasharray:2;}#render799144806 .marker{fill:#333333;stroke:#333333;}#render799144806 .marker.cross{stroke:#333333;}#render799144806 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render799144806 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render799144806 .cluster-label text{fill:#333;}#render799144806 .cluster-label span{color:#333;}#render799144806 .label text,#render799144806 span{fill:#333;color:#333;}#render799144806 .node rect,#render799144806 .node circle,#render799144806 .node ellipse,#render799144806 .node polygon,#render799144806 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render799144806 .node .label{text-align:center;}#render799144806 .node.clickable{cursor:pointer;}#render799144806 .arrowheadPath{fill:#333333;}#render799144806 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render799144806 .flowchart-link{stroke:#333333;fill:none;}#render799144806 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render799144806 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render799144806 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render799144806 .cluster text{fill:#333;}#render799144806 .cluster span{color:#333;}#render799144806 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render799144806 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M97.49335822843952,102.74335479736328L105.91503810703294,97.6742795308431C114.33671798562635,92.60520426432292,131.18007774281318,82.46705373128255,143.8517581300329,77.4813117980957C156.52343851725263,72.49556986490886,165.02343953450523,72.66223653157552,169.27344004313153,72.74556986490886L173.52344055175783,72.82890319824219" marker-end="url(#arrowhead6510)" style="fill:none"></path><defs><marker id="arrowhead6510" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M282.07546821101965,52.723124461019694L290.84309749193045,48.61575425055677C299.61072677284125,44.50838404009386,317.1459853346628,36.29364361916802,334.08028128224026,32.186273408705105C351.0145772298177,28.078903198242188,367.34791056315106,28.078903198242188,375.5145772298177,28.078903198242188L383.6812438964844,28.078903198242188" marker-end="url(#arrowhead6511)" style="fill:none"></path><defs><marker id="arrowhead6511" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M282.0754724069024,92.9346838430975L290.84310098849943,96.87538706895494C299.6107295700964,100.8160902948124,317.1459867332904,108.69749674652729,331.4136153148874,112.63819997238473C345.6812438964844,116.57890319824219,356.6812438964844,116.57890319824219,362.1812438964844,116.57890319824219L367.6812438964844,116.57890319824219" marker-end="url(#arrowhead6512)" style="fill:none"></path><defs><marker id="arrowhead6512" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M461.9156188964844,28.078903198242188L468.7489522298177,28.078903198242188C475.58228556315106,28.078903198242188,489.2489522298177,28.078903198242188,504.82004450853714,39.62056986490885C520.3911367872565,51.16223653157552,537.8666546780286,74.24556986490886,546.6044136234146,85.78723653157552L555.3421725688007,97.32890319824219" marker-end="url(#arrowhead6513)" style="fill:none"></path><defs><marker id="arrowhead6513" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M477.9156188964844,116.57890319824219L482.08228556315106,116.57890319824219C486.2489522298177,116.57890319824219,494.58228556315106,116.57890319824219,502.9156188964844,116.57890319824219C511.2489522298177,116.57890319824219,519.582285563151,116.57890319824219,523.7489522298177,116.57890319824219L527.9156188964844,116.57890319824219" marker-end="url(#arrowhead6514)" style="fill:none"></path><defs><marker id="arrowhead6514" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M553.1196922015897,135.8289031982422L544.7523466507388,145.4188117980957C536.385001099888,155.00872039794922,519.6503099981861,174.18853759765625,497.93009986400193,183.77844619750977C476.2098897298177,193.36835479736328,449.50416056315106,193.36835479736328,421.46509806315106,193.36835479736328C393.42603556315106,193.36835479736328,364.0536397298177,193.36835479736328,333.14595794677734,193.36835479736328C302.238276163737,193.36835479736328,269.79530843098956,193.36835479736328,238.68567403157553,193.36835479736328C207.57603963216147,193.36835479736328,177.79973856608072,193.36835479736328,152.8685679750281,184.68085479736328C127.93739738397547,175.99335479736328,107.85135726795096,158.61835479736328,97.8083372099387,149.93085479736328L87.76531715192644,141.24335479736328" marker-end="url(#arrowhead6515)" style="fill:none"></path><defs><marker id="arrowhead6515" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(334.6812438964844,28.078903198242188)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000093460083008" height="18.50010871887207"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(334.6812438964844,116.57890319824219)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000093460083008" height="18.50010871887207"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7338" transform="translate(65.51171875,121.99335479736328)"><rect rx="0" ry="0" x="-57.51171875" y="-19.25" width="115.0234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47.51171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">BFS,方向标志</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7339" transform="translate(237.3523406982422,72.32890319824219)"><polygon points="64.32890625,0 128.6578125,-64.32890625 64.32890625,-128.6578125 0,-64.32890625" transform="translate(-64.32890625,64.32890625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-42.2265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">leftToRight?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7341" transform="translate(422.7984313964844,28.078903198242188)"><rect rx="0" ry="0" x="-39.1171875" y="-19.25" width="78.234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.1171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">add正序</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7343" transform="translate(422.7984313964844,116.57890319824219)"><rect rx="0" ry="0" x="-55.1171875" y="-19.25" width="110.234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.1171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">addFirst逆序</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7345" transform="translate(569.9156188964844,116.57890319824219)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">翻转方向</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; zigzagLevelOrder(TreeNode root) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Queue&lt;TreeNode&gt; q = new ArrayDeque&lt;&gt;(); q.offer(root);        boolean leftToRight = true;        while (!q.isEmpty()) {            int size = q.size(); LinkedList&lt;Integer&gt; level = new LinkedList&lt;&gt;();            for (int i = 0; i &lt; size; i++) {                TreeNode n = q.poll();                if (leftToRight) level.add(n.val); else level.addFirst(n.val);                if (n.left != null) q.offer(n.left);                if (n.right != null) q.offer(n.right);            }            res.add(level); leftToRight = !leftToRight;        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>锯齿层序用 <code>LinkedList.addFirst</code> 在反向层插头，比&quot;正向收集再 reverse&quot;更简洁。BFS 层序模板是 [[#199. 二叉树的右视图]]、[[#102. 二叉树的层序遍历]]、[[#637. 二叉树的层平均值]] 的共同基底。</li></ul><hr /><h1 id="10.-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91" tabindex="-1">10. 二叉搜索树</h1><p>BST 的核心性质：<strong>中序遍历是升序</strong>。这给三类题统一解法：最小差/第 K 小/验证合法性，都靠中序。</p><h2 id="530.-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E6%9C%80%E5%B0%8F%E7%BB%9D%E5%AF%B9%E5%B7%AE" tabindex="-1"><a href="https://leetcode.cn/problems/minimum-absolute-difference-in-bst/" target="_blank">530. 二叉搜索树的最小绝对差</a></h2><blockquote><p>BST 任意两节点值最小绝对差。</p></blockquote><p><strong>思路脉络</strong>：BST 中序升序，最小差必在相邻中序节点间。中序遍历维护 <code>prev</code>，每步算 <code>cur - prev</code> 取最小。</p><div class="mermaid"><svg id="render3955129552" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 596.96875px;" viewBox="0 0 596.96875 89.5"><style>#render3955129552 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3955129552 .error-icon{fill:#552222;}#render3955129552 .error-text{fill:#552222;stroke:#552222;}#render3955129552 .edge-thickness-normal{stroke-width:2px;}#render3955129552 .edge-thickness-thick{stroke-width:3.5px;}#render3955129552 .edge-pattern-solid{stroke-dasharray:0;}#render3955129552 .edge-pattern-dashed{stroke-dasharray:3;}#render3955129552 .edge-pattern-dotted{stroke-dasharray:2;}#render3955129552 .marker{fill:#333333;stroke:#333333;}#render3955129552 .marker.cross{stroke:#333333;}#render3955129552 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3955129552 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3955129552 .cluster-label text{fill:#333;}#render3955129552 .cluster-label span{color:#333;}#render3955129552 .label text,#render3955129552 span{fill:#333;color:#333;}#render3955129552 .node rect,#render3955129552 .node circle,#render3955129552 .node ellipse,#render3955129552 .node polygon,#render3955129552 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3955129552 .node .label{text-align:center;}#render3955129552 .node.clickable{cursor:pointer;}#render3955129552 .arrowheadPath{fill:#333333;}#render3955129552 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3955129552 .flowchart-link{stroke:#333333;fill:none;}#render3955129552 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3955129552 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3955129552 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3955129552 .cluster text{fill:#333;}#render3955129552 .cluster span{color:#333;}#render3955129552 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3955129552 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92,37.37126865671642L96.16666666666667,35.68439054726368C100.33333333333333,33.99751243781095,108.66666666666667,30.62375621890547,117,28.936878109452735C125.33333333333333,27.25,133.66666666666666,27.25,137.83333333333334,27.25L142,27.25" marker-end="url(#arrowhead6538)" style="fill:none"></path><defs><marker id="arrowhead6538" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M225.6953125,27.25L229.86197916666666,27.25C234.02864583333334,27.25,242.36197916666666,27.25,250.6953125,27.25C259.0286458333333,27.25,267.3619791666667,27.25,271.5286458333333,27.25L275.6953125,27.25" marker-end="url(#arrowhead6539)" style="fill:none"></path><defs><marker id="arrowhead6539" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M456,27.25L460.1666666666667,27.25C464.3333333333333,27.25,472.6666666666667,27.25,481,28.94996083039561C489.3333333333333,30.649921660791225,497.6666666666667,34.04984332158245,501.8333333333333,35.74980415197806L506,37.44976498237368" marker-end="url(#arrowhead6540)" style="fill:none"></path><defs><marker id="arrowhead6540" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M506,71.30023501762632L501.8333333333333,73.00019584802193C497.6666666666667,74.70015667841754,489.3333333333333,78.10007833920878,465.974609375,79.80003916960439C442.6158854166667,81.5,404.2317708333333,81.5,365.84765625,81.5C327.4635416666667,81.5,289.0794270833333,81.5,258.74609375,81.5C228.41276041666666,81.5,206.13020833333334,81.5,183.84765625,81.5C161.56510416666666,81.5,139.28255208333334,81.5,123.974609375,79.81312189054727C108.66666666666667,78.12624378109453,100.33333333333333,74.75248756218906,96.16666666666667,73.06560945273633L92,71.37873134328359" marker-end="url(#arrowhead6541)" style="fill:none"></path><defs><marker id="arrowhead6541" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7358" transform="translate(50,54.375)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">中序遍历</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7359" transform="translate(183.84765625,27.25)"><rect rx="0" ry="0" x="-41.84765625" y="-19.25" width="83.6953125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.84765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">维护prev</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7361" transform="translate(365.84765625,27.25)"><rect rx="0" ry="0" x="-90.15234375" y="-19.25" width="180.3046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.15234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans=min(ans,cur-prev)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7363" transform="translate(547.484375,54.375)"><rect rx="0" ry="0" x="-41.484375" y="-19.25" width="82.96875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">prev=cur</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    Integer prev = null; int ans = Integer.MAX_VALUE;    public int getMinimumDifference(TreeNode root) {        dfs(root); return ans;    }    void dfs(TreeNode n) {        if (n == null) return;        dfs(n.left);        if (prev != null) ans = Math.min(ans, n.val - prev);        prev = n.val;        dfs(n.right);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>BST 中序后相邻即&quot;最接近&quot;，最小差必在中序相邻。<code>prev</code> 用 <code>Integer</code> 处理首节点无前驱。</li></ul><h2 id="230.-%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%AC%AC-k-%E5%B0%8F%E7%9A%84%E5%85%83%E7%B4%A0" tabindex="-1"><a href="https://leetcode.cn/problems/kth-smallest-element-in-a-bst/" target="_blank">230. 二叉搜索树中第 K 小的元素</a></h2><blockquote><p>BST 找第 K 小。</p></blockquote><p><strong>思路脉络</strong>：BST 中序升序，第 K 小即中序第 K 个。递归中序，计数器数到第 K 个即停。和热题 100 相同。</p><div class="mermaid"><svg id="render1785034979" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="114.43046569824219" style="max-width: 481.31329345703125px;" viewBox="0 0 481.31329345703125 114.43046569824219"><style>#render1785034979 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1785034979 .error-icon{fill:#552222;}#render1785034979 .error-text{fill:#552222;stroke:#552222;}#render1785034979 .edge-thickness-normal{stroke-width:2px;}#render1785034979 .edge-thickness-thick{stroke-width:3.5px;}#render1785034979 .edge-pattern-solid{stroke-dasharray:0;}#render1785034979 .edge-pattern-dashed{stroke-dasharray:3;}#render1785034979 .edge-pattern-dotted{stroke-dasharray:2;}#render1785034979 .marker{fill:#333333;stroke:#333333;}#render1785034979 .marker.cross{stroke:#333333;}#render1785034979 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1785034979 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1785034979 .cluster-label text{fill:#333;}#render1785034979 .cluster-label span{color:#333;}#render1785034979 .label text,#render1785034979 span{fill:#333;color:#333;}#render1785034979 .node rect,#render1785034979 .node circle,#render1785034979 .node ellipse,#render1785034979 .node polygon,#render1785034979 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1785034979 .node .label{text-align:center;}#render1785034979 .node.clickable{cursor:pointer;}#render1785034979 .arrowheadPath{fill:#333333;}#render1785034979 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1785034979 .flowchart-link{stroke:#333333;fill:none;}#render1785034979 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1785034979 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1785034979 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1785034979 .cluster text{fill:#333;}#render1785034979 .cluster span{color:#333;}#render1785034979 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1785034979 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92,49.025232849121096L97.5,47.9527328491211C103,46.88023284912109,114,44.7352328491211,126.32209688695934,44.94480288314157C138.6441937739187,45.15437291716204,152.28838754783737,47.718512985202985,159.11048443479672,49.00058301922346L165.93258132175606,50.28265305324394" marker-end="url(#arrowhead6557)" style="fill:none"></path><defs><marker id="arrowhead6557" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M256.9304672241211,57.7152328491211L262.3471336364746,57.63189951578777C267.76380004882816,57.54856618245443,278.59713287353514,57.38189951578776,289.51379928588864,57.29856618245443C300.4304656982422,57.215232849121094,311.4304656982422,57.215232849121094,316.9304656982422,57.215232849121094L322.4304656982422,57.215232849121094" marker-end="url(#arrowhead6558)" style="fill:none"></path><defs><marker id="arrowhead6558" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-A" style="opacity: 1;" id="L-B-A"><path class="path" d="M165.9325787308807,65.14781310588064L159.11048227573392,66.26321639642072C152.28838582058714,67.37861968696079,138.64419291029355,69.60942626804093,126.32209645514678,69.65232955858102C114,69.6952328491211,103,67.55023284912109,97.5,66.4777328491211L92,65.40523284912109" marker-end="url(#arrowhead6559)" style="fill:none"></path><defs><marker id="arrowhead6559" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(289.4304656982422,57.215232849121094)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000106811523438" height="18.500125885009766"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(125,71.8402328491211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000106811523438" height="18.500125885009766"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7372" transform="translate(50,57.215232849121094)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">中序遍历</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7373" transform="translate(207.2152328491211,57.215232849121094)"><polygon points="49.215234375,0 98.43046875,-49.215234375 49.215234375,-98.43046875 0,-49.215234375" transform="translate(-49.215234375,49.215234375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-25.43359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">--k==0?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7375" transform="translate(397.8718719482422,57.215232849121094)"><rect rx="0" ry="0" x="-75.44140625" y="-19.25" width="150.8828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-65.44140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans=当前值,return</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    int k, ans;    public int kthSmallest(TreeNode root, int k) {        this.k = k; dfs(root); return ans;    }    void dfs(TreeNode n) {        if (n == null || k == 0) return;        dfs(n.left);        if (--k == 0) { ans = n.val; return; }        dfs(n.right);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(h+k)，空间 O(h)。</p></blockquote><h2 id="98.-%E9%AA%8C%E8%AF%81%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91" tabindex="-1"><a href="https://leetcode.cn/problems/validate-binary-search-tree/" target="_blank">98. 验证二叉搜索树</a></h2><blockquote><p>判断是否合法 BST。</p></blockquote><p><strong>思路脉络</strong>：递归传上下界 <code>(lo, hi)</code>，每节点须在 <code>(lo, hi)</code> 内，进左收紧上界、进右收紧下界。用 <code>long</code> 防整型极值越界。和热题 100 相同。</p><div class="mermaid"><svg id="render1867662080" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="161.4132843017578" style="max-width: 612.296142578125px;" viewBox="0 0 612.296142578125 161.4132843017578"><style>#render1867662080 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1867662080 .error-icon{fill:#552222;}#render1867662080 .error-text{fill:#552222;stroke:#552222;}#render1867662080 .edge-thickness-normal{stroke-width:2px;}#render1867662080 .edge-thickness-thick{stroke-width:3.5px;}#render1867662080 .edge-pattern-solid{stroke-dasharray:0;}#render1867662080 .edge-pattern-dashed{stroke-dasharray:3;}#render1867662080 .edge-pattern-dotted{stroke-dasharray:2;}#render1867662080 .marker{fill:#333333;stroke:#333333;}#render1867662080 .marker.cross{stroke:#333333;}#render1867662080 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1867662080 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1867662080 .cluster-label text{fill:#333;}#render1867662080 .cluster-label span{color:#333;}#render1867662080 .label text,#render1867662080 span{fill:#333;color:#333;}#render1867662080 .node rect,#render1867662080 .node circle,#render1867662080 .node ellipse,#render1867662080 .node polygon,#render1867662080 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1867662080 .node .label{text-align:center;}#render1867662080 .node.clickable{cursor:pointer;}#render1867662080 .arrowheadPath{fill:#333333;}#render1867662080 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1867662080 .flowchart-link{stroke:#333333;fill:none;}#render1867662080 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1867662080 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1867662080 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1867662080 .cluster text{fill:#333;}#render1867662080 .cluster span{color:#333;}#render1867662080 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1867662080 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M149.2265625,80.7066421508789L153.39322916666666,80.7066421508789C157.55989583333334,80.7066421508789,165.89322916666666,80.7066421508789,174.30989608764648,80.78997548421223C182.72656300862627,80.87330881754558,191.22656351725257,81.03997548421223,195.4765637715657,81.12330881754558L199.72656402587887,81.2066421508789" marker-end="url(#arrowhead6574)" style="fill:none"></path><defs><marker id="arrowhead6574" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M323.6851847719501,59.751981646950085L332.67762844358475,55.86942506427155C341.6700721152194,51.98686848159303,359.6549594584886,44.221755316235964,381.61420000512317,40.33919873355743C403.5734405517578,36.456642150878906,429.5070343017578,36.456642150878906,442.4738311767578,36.456642150878906L455.4406280517578,36.456642150878906" marker-end="url(#arrowhead6575)" style="fill:none"></path><defs><marker id="arrowhead6575" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M323.68518262072115,102.66130175427885L332.67762665089396,106.37719182037887C341.67007068106676,110.09308188647886,359.65495874141226,117.52486201867889,374.14740277158506,121.24075208477889C388.6398468017578,124.9566421508789,399.6398468017578,124.9566421508789,405.1398468017578,124.9566421508789L410.6398468017578,124.9566421508789" marker-end="url(#arrowhead6576)" style="fill:none"></path><defs><marker id="arrowhead6576" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(377.6398468017578,36.456642150878906)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00069808959961" height="18.50080680847168"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(377.6398468017578,124.9566421508789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00069808959961" height="18.50080680847168"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7384" transform="translate(78.61328125,80.7066421508789)"><rect rx="0" ry="0" x="-70.61328125" y="-19.25" width="141.2265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-60.61328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">check(root,lo,hi)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7385" transform="translate(271.9332046508789,80.7066421508789)"><polygon points="72.706640625,0 145.41328125,-72.706640625 72.706640625,-145.41328125 0,-72.706640625" transform="translate(-72.706640625,72.706640625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-51.53515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">val在(lo,hi)内?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7387" transform="translate(507.4679718017578,36.456642150878906)"><rect rx="0" ry="0" x="-52.02734375" y="-19.25" width="104.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return false</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7389" transform="translate(507.4679718017578,124.9566421508789)"><rect rx="0" ry="0" x="-96.828125" y="-19.25" width="193.65625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-86.828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左子(lo,val),右子(val,hi)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isValidBST(TreeNode root) {        return check(root, Long.MIN_VALUE, Long.MAX_VALUE);    }    boolean check(TreeNode n, long lo, long hi) {        if (n == null) return true;        if (n.val &lt;= lo || n.val &gt;= hi) return false;        return check(n.left, lo, n.val) &amp;&amp; check(n.right, n.val, hi);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>BST 要求&quot;整棵左子树 &lt; 根&quot;，不是只比直接子节点。上下界法体现全局约束。<code>long</code> 处理 <code>Integer.MIN_VALUE</code> 节点。</li></ul><hr /><h1 id="11.-%E5%9B%BE" tabindex="-1">11. 图</h1><p>图的四把刀：DFS 染色（连通/感染）、BFS 层序（最短步数）、拓扑排序（依赖关系）、并查集（连通分量）。难点在建图（邻接表、隐式图）。</p><h2 id="200.-%E5%B2%9B%E5%B1%BF%E6%95%B0%E9%87%8F" tabindex="-1"><a href="https://leetcode.cn/problems/number-of-islands/" target="_blank">200. 岛屿数量</a></h2><blockquote><p><code>1</code> 是陆地，数岛屿数。</p></blockquote><p><strong>思路脉络</strong>：遇 <code>1</code> 是新岛，DFS 把整片连通 <code>1</code> 染成 <code>0</code>（原地标记），计数加一。和热题 100 相同。</p><div class="mermaid"><svg id="render764632251" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="135.4875030517578" style="max-width: 562.0265502929688px;" viewBox="0 0 562.0265502929688 135.4875030517578"><style>#render764632251 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render764632251 .error-icon{fill:#552222;}#render764632251 .error-text{fill:#552222;stroke:#552222;}#render764632251 .edge-thickness-normal{stroke-width:2px;}#render764632251 .edge-thickness-thick{stroke-width:3.5px;}#render764632251 .edge-pattern-solid{stroke-dasharray:0;}#render764632251 .edge-pattern-dashed{stroke-dasharray:3;}#render764632251 .edge-pattern-dotted{stroke-dasharray:2;}#render764632251 .marker{fill:#333333;stroke:#333333;}#render764632251 .marker.cross{stroke:#333333;}#render764632251 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render764632251 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render764632251 .cluster-label text{fill:#333;}#render764632251 .cluster-label span{color:#333;}#render764632251 .label text,#render764632251 span{fill:#333;color:#333;}#render764632251 .node rect,#render764632251 .node circle,#render764632251 .node ellipse,#render764632251 .node polygon,#render764632251 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render764632251 .node .label{text-align:center;}#render764632251 .node.clickable{cursor:pointer;}#render764632251 .arrowheadPath{fill:#333333;}#render764632251 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render764632251 .flowchart-link{stroke:#333333;fill:none;}#render764632251 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render764632251 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render764632251 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render764632251 .cluster text{fill:#333;}#render764632251 .cluster span{color:#333;}#render764632251 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render764632251 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M84.17995765048741,54.99062728881836L90.2710584587395,51.76198132832845C96.36215926699161,48.53333536783854,108.5443608834958,42.076043446858726,121.36456731478755,40.222458275682676C134.1847737460793,38.36887310450664,147.6429849921586,41.11899468313437,154.37209061519826,42.49405547244823L161.10119623823792,43.8691162617621" marker-end="url(#arrowhead6603)" style="fill:none"></path><defs><marker id="arrowhead6603" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M238.7140609741211,50.743751525878906L244.1307284037272,50.66041819254557C249.54739583333335,50.57708485921224,260.38073069254557,50.41041819254557,271.2973981221517,50.32708485921224C282.2140655517578,50.243751525878906,293.2140655517578,50.243751525878906,298.7140655517578,50.243751525878906L304.2140655517578,50.243751525878906" marker-end="url(#arrowhead6604)" style="fill:none"></path><defs><marker id="arrowhead6604" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M443.6203155517578,50.243751525878906L447.7869822184245,50.243751525878906C451.9536488850911,50.243751525878906,460.2869822184245,50.243751525878906,469.06842595163374,53.47239748636881C477.849869684843,56.701043446858726,487.0794238179281,63.15833536783854,491.6942008844707,66.38698132832845L496.3089779510133,69.61562728881836" marker-end="url(#arrowhead6605)" style="fill:none"></path><defs><marker id="arrowhead6605" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-A" style="opacity: 1;" id="L-B-A"><path class="path" d="M164.44131224145062,60.95849974145061L157.1555206178755,63.1721876660119C149.8697289943004,65.38587559057319,135.29814574715022,69.81325143969578,122.51235412357511,72.02693936425707C109.7265625,74.24062728881836,98.7265625,74.24062728881836,93.2265625,74.24062728881836L87.7265625,74.24062728881836" marker-end="url(#arrowhead6606)" style="fill:none"></path><defs><marker id="arrowhead6606" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M496.3089779510133,108.11562728881836L491.6942008844707,111.34427324930827C487.0794238179281,114.57291920979817,477.849869684843,121.030211130778,457.45123845163374,124.2588570912679C437.0526072184245,127.48750305175781,405.4848988850911,127.48750305175781,372.5838572184245,127.48750305175781C339.6828155517578,127.48750305175781,305.4484405517578,127.48750305175781,275.79062779744464,127.48750305175781C246.1328150431315,127.48750305175781,221.05156453450522,127.48750305175781,195.9703140258789,127.48750305175781C170.8890635172526,127.48750305175781,145.8078130086263,127.48750305175781,125.51360633179371,121.8213570912679C105.21939965496114,116.155211130778,89.71223680992227,104.82291920979817,81.95865538740283,99.15677324930827L74.2050739648834,93.49062728881836" marker-end="url(#arrowhead6607)" style="fill:none"></path><defs><marker id="arrowhead6607" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(271.2140655517578,50.243751525878906)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000370025634766" height="18.50042724609375"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(120.7265625,74.24062728881836)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000370025634766" height="18.50042724609375"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7400" transform="translate(47.86328125,74.24062728881836)"><rect rx="0" ry="0" x="-39.86328125" y="-19.25" width="79.7265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.86328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历grid</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7401" transform="translate(195.9703140258789,50.243751525878906)"><polygon points="42.24375,0 84.4875,-42.24375 42.24375,-84.4875 0,-42.24375" transform="translate(-42.24375,42.24375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-17.6875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">是'1'?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7403" transform="translate(373.9171905517578,50.243751525878906)"><rect rx="0" ry="0" x="-69.703125" y="-19.25" width="139.40625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-59.703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">DFS感染整岛为'0'</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7405" transform="translate(523.8234405517578,88.86562728881836)"><rect rx="0" ry="0" x="-30.203125" y="-19.25" width="60.40625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20.203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int numIslands(char[][] grid) {        int m = grid.length, n = grid[0].length, ans = 0;        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++)                if (grid[i][j] == &#39;1&#39;) { dfs(grid, i, j); ans++; }        return ans;    }    void dfs(char[][] g, int i, int j) {        if (i &lt; 0 || i &gt;= g.length || j &lt; 0 || j &gt;= g[0].length || g[i][j] != &#39;1&#39;) return;        g[i][j] = &#39;0&#39;;        dfs(g, i+1, j); dfs(g, i-1, j); dfs(g, i, j+1); dfs(g, i, j-1);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(mn)。</p></blockquote><h2 id="130.-%E8%A2%AB%E5%9B%B4%E7%BB%95%E7%9A%84%E5%8C%BA%E5%9F%9F" tabindex="-1"><a href="https://leetcode.cn/problems/surrounded-regions/" target="_blank">130. 被围绕的区域</a></h2><blockquote><p>矩阵 <code>O</code>/<code>X</code>，把被 <code>X</code> 围住的 <code>O</code> 翻成 <code>X</code>（与边界相连的 <code>O</code> 不翻）。</p></blockquote><p><strong>思路脉络</strong>：反向思考——从边界所有 <code>O</code> 出发 DFS/BFS 标记&quot;不翻&quot;，剩下的 <code>O</code> 都是被围的，翻成 <code>X</code>。</p><div class="mermaid"><svg id="render3682508044" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 652.3125px;" viewBox="0 0 652.3125 54.5"><style>#render3682508044 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3682508044 .error-icon{fill:#552222;}#render3682508044 .error-text{fill:#552222;stroke:#552222;}#render3682508044 .edge-thickness-normal{stroke-width:2px;}#render3682508044 .edge-thickness-thick{stroke-width:3.5px;}#render3682508044 .edge-pattern-solid{stroke-dasharray:0;}#render3682508044 .edge-pattern-dashed{stroke-dasharray:3;}#render3682508044 .edge-pattern-dotted{stroke-dasharray:2;}#render3682508044 .marker{fill:#333333;stroke:#333333;}#render3682508044 .marker.cross{stroke:#333333;}#render3682508044 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3682508044 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3682508044 .cluster-label text{fill:#333;}#render3682508044 .cluster-label span{color:#333;}#render3682508044 .label text,#render3682508044 span{fill:#333;color:#333;}#render3682508044 .node rect,#render3682508044 .node circle,#render3682508044 .node ellipse,#render3682508044 .node polygon,#render3682508044 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3682508044 .node .label{text-align:center;}#render3682508044 .node.clickable{cursor:pointer;}#render3682508044 .arrowheadPath{fill:#333333;}#render3682508044 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3682508044 .flowchart-link{stroke:#333333;fill:none;}#render3682508044 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3682508044 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3682508044 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3682508044 .cluster text{fill:#333;}#render3682508044 .cluster span{color:#333;}#render3682508044 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3682508044 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M133.796875,27.25L137.96354166666666,27.25C142.13020833333334,27.25,150.46354166666666,27.25,158.796875,27.25C167.13020833333334,27.25,175.46354166666666,27.25,179.63020833333334,27.25L183.796875,27.25" marker-end="url(#arrowhead6624)" style="fill:none"></path><defs><marker id="arrowhead6624" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M310.09375,27.25L314.2604166666667,27.25C318.4270833333333,27.25,326.7604166666667,27.25,335.09375,27.25C343.4270833333333,27.25,351.7604166666667,27.25,355.9270833333333,27.25L360.09375,27.25" marker-end="url(#arrowhead6625)" style="fill:none"></path><defs><marker id="arrowhead6625" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M428.09375,27.25L432.2604166666667,27.25C436.4270833333333,27.25,444.7604166666667,27.25,453.09375,27.25C461.4270833333333,27.25,469.7604166666667,27.25,473.9270833333333,27.25L478.09375,27.25" marker-end="url(#arrowhead6626)" style="fill:none"></path><defs><marker id="arrowhead6626" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7416" transform="translate(70.8984375,27.25)"><rect rx="0" ry="0" x="-62.8984375" y="-19.25" width="125.796875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-52.8984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">边界'O'出发DFS</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7417" transform="translate(246.9453125,27.25)"><rect rx="0" ry="0" x="-63.1484375" y="-19.25" width="126.296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.1484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">标记为'A'(不翻)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7419" transform="translate(394.09375,27.25)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历完</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7421" transform="translate(561.203125,27.25)"><rect rx="0" ry="0" x="-83.109375" y="-19.25" width="166.21875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-73.109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">剩余'O'翻'X','A'还原'O'</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public void solve(char[][] board) {        int m = board.length, n = board[0].length;        for (int i = 0; i &lt; m; i++) {            dfs(board, i, 0); dfs(board, i, n - 1);           // 左右边界        }        for (int j = 0; j &lt; n; j++) {            dfs(board, 0, j); dfs(board, m - 1, j);           // 上下边界        }        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++) {                if (board[i][j] == &#39;O&#39;) board[i][j] = &#39;X&#39;;    // 剩下的 O 是被围的                else if (board[i][j] == &#39;A&#39;) board[i][j] = &#39;O&#39;; // 标记的还原            }    }    void dfs(char[][] b, int i, int j) {        if (i &lt; 0 || i &gt;= b.length || j &lt; 0 || j &gt;= b[0].length || b[i][j] != &#39;O&#39;) return;        b[i][j] = &#39;A&#39;;        dfs(b, i+1, j); dfs(b, i-1, j); dfs(b, i, j+1); dfs(b, i, j-1);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(mn)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;与边界连通的保留&quot;类问题：从边界反向标记，再处理内部。比&quot;正向判断是否被围&quot;简单得多。</li></ul><h2 id="133.-%E5%85%8B%E9%9A%86%E5%9B%BE" tabindex="-1"><a href="https://leetcode.cn/problems/clone-graph/" target="_blank">133. 克隆图</a></h2><blockquote><p>无向连通图，深拷贝。</p></blockquote><p><strong>思路脉络</strong>：BFS/DFS + 哈希记&quot;原节点→新节点&quot;。访问前查哈希，已建则返回新节点，未建则建后递归邻居。和随机链表复制同源。</p><div class="mermaid"><svg id="render3442754842" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 641.921875px;" viewBox="0 0 641.921875 143"><style>#render3442754842 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3442754842 .error-icon{fill:#552222;}#render3442754842 .error-text{fill:#552222;stroke:#552222;}#render3442754842 .edge-thickness-normal{stroke-width:2px;}#render3442754842 .edge-thickness-thick{stroke-width:3.5px;}#render3442754842 .edge-pattern-solid{stroke-dasharray:0;}#render3442754842 .edge-pattern-dashed{stroke-dasharray:3;}#render3442754842 .edge-pattern-dotted{stroke-dasharray:2;}#render3442754842 .marker{fill:#333333;stroke:#333333;}#render3442754842 .marker.cross{stroke:#333333;}#render3442754842 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3442754842 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3442754842 .cluster-label text{fill:#333;}#render3442754842 .cluster-label span{color:#333;}#render3442754842 .label text,#render3442754842 span{fill:#333;color:#333;}#render3442754842 .node rect,#render3442754842 .node circle,#render3442754842 .node ellipse,#render3442754842 .node polygon,#render3442754842 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3442754842 .node .label{text-align:center;}#render3442754842 .node.clickable{cursor:pointer;}#render3442754842 .arrowheadPath{fill:#333333;}#render3442754842 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3442754842 .flowchart-link{stroke:#333333;fill:none;}#render3442754842 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3442754842 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3442754842 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3442754842 .cluster text{fill:#333;}#render3442754842 .cluster span{color:#333;}#render3442754842 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3442754842 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M95.96923552259886,52.25L103.38191501883239,48.083333333333336C110.79459451506591,43.916666666666664,125.61995350753295,35.583333333333336,141.57234654543313,31.416666666666668C157.52473958333334,27.25,174.60416666666666,27.25,183.14388020833334,27.25L191.68359375,27.25" marker-end="url(#arrowhead6644)" style="fill:none"></path><defs><marker id="arrowhead6644" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-A LE-C" style="opacity: 1;" id="L-A-C"><path class="path" d="M95.96923552259886,90.75L103.38191501883239,94.91666666666667C110.79459451506591,99.08333333333333,125.61995350753295,107.41666666666667,137.19929967043313,111.58333333333333C148.77864583333334,115.75,157.11197916666666,115.75,161.27864583333334,115.75L165.4453125,115.75" marker-end="url(#arrowhead6645)" style="fill:none"></path><defs><marker id="arrowhead6645" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M317.921875,115.75L322.0885416666667,115.75C326.2552083333333,115.75,334.5885416666667,115.75,342.921875,115.75C351.2552083333333,115.75,359.5885416666667,115.75,363.7552083333333,115.75L367.921875,115.75" marker-end="url(#arrowhead6646)" style="fill:none"></path><defs><marker id="arrowhead6646" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M483.921875,115.75L488.0885416666667,115.75C492.2552083333333,115.75,500.5885416666667,115.75,508.921875,115.75C517.2552083333334,115.75,525.5885416666666,115.75,529.7552083333334,115.75L533.921875,115.75" marker-end="url(#arrowhead6647)" style="fill:none"></path><defs><marker id="arrowhead6647" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7430" transform="translate(61.72265625,71.5)"><rect rx="0" ry="0" x="-53.72265625" y="-19.25" width="107.4453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.72265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">map含node?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7431" transform="translate(241.68359375,27.25)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回新节点</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7433" transform="translate(241.68359375,115.75)"><rect rx="0" ry="0" x="-76.23828125" y="-19.25" width="152.4765625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-66.23828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">创建新节点,map记</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7435" transform="translate(425.921875,115.75)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归所有邻居</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7437" transform="translate(583.921875,115.75)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回新节点</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    Map&lt;Node, Node&gt; map = new HashMap&lt;&gt;();    public Node cloneGraph(Node node) {        if (node == null) return null;        if (map.containsKey(node)) return map.get(node);        Node clone = new Node(node.val);        map.put(node, clone);        for (Node nb : node.neighbors) clone.neighbors.add(cloneGraph(nb));        return clone;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><h2 id="399.-%E9%99%A4%E6%B3%95%E6%B1%82%E5%80%BC" tabindex="-1"><a href="https://leetcode.cn/problems/evaluate-division/" target="_blank">399. 除法求值</a></h2><blockquote><p>给一组 <code>a/b = k</code> 等式，查询若干 <code>c/d</code> 的值。</p></blockquote><p><strong>思路脉络</strong>：把等式建成带权有向图——<code>a→b</code> 权 k、<code>b→a</code> 权 1/k。每次查询从 c 到 d 做 BFS/DFS，路径权乘积即答案；不可达返回 -1。</p><div class="mermaid"><svg id="render196537019" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 742.734375px;" viewBox="0 0 742.734375 54.5"><style>#render196537019 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render196537019 .error-icon{fill:#552222;}#render196537019 .error-text{fill:#552222;stroke:#552222;}#render196537019 .edge-thickness-normal{stroke-width:2px;}#render196537019 .edge-thickness-thick{stroke-width:3.5px;}#render196537019 .edge-pattern-solid{stroke-dasharray:0;}#render196537019 .edge-pattern-dashed{stroke-dasharray:3;}#render196537019 .edge-pattern-dotted{stroke-dasharray:2;}#render196537019 .marker{fill:#333333;stroke:#333333;}#render196537019 .marker.cross{stroke:#333333;}#render196537019 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render196537019 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render196537019 .cluster-label text{fill:#333;}#render196537019 .cluster-label span{color:#333;}#render196537019 .label text,#render196537019 span{fill:#333;color:#333;}#render196537019 .node rect,#render196537019 .node circle,#render196537019 .node ellipse,#render196537019 .node polygon,#render196537019 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render196537019 .node .label{text-align:center;}#render196537019 .node.clickable{cursor:pointer;}#render196537019 .arrowheadPath{fill:#333333;}#render196537019 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render196537019 .flowchart-link{stroke:#333333;fill:none;}#render196537019 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render196537019 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render196537019 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render196537019 .cluster text{fill:#333;}#render196537019 .cluster span{color:#333;}#render196537019 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render196537019 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M214.21875,27.25L218.38541666666666,27.25C222.55208333333334,27.25,230.88541666666666,27.25,239.21875,27.25C247.55208333333334,27.25,255.88541666666666,27.25,260.0520833333333,27.25L264.21875,27.25" marker-end="url(#arrowhead6664)" style="fill:none"></path><defs><marker id="arrowhead6664" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M396.078125,27.25L400.2447916666667,27.25C404.4114583333333,27.25,412.7447916666667,27.25,421.078125,27.25C429.4114583333333,27.25,437.7447916666667,27.25,441.9114583333333,27.25L446.078125,27.25" marker-end="url(#arrowhead6665)" style="fill:none"></path><defs><marker id="arrowhead6665" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M586.46875,27.25L590.6354166666666,27.25C594.8020833333334,27.25,603.1354166666666,27.25,611.46875,27.25C619.8020833333334,27.25,628.1354166666666,27.25,632.3020833333334,27.25L636.46875,27.25" marker-end="url(#arrowhead6666)" style="fill:none"></path><defs><marker id="arrowhead6666" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7444" transform="translate(111.109375,27.25)"><rect rx="0" ry="0" x="-103.109375" y="-19.25" width="206.21875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-93.109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">建带权图a→b=k,b→a=1/k</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7445" transform="translate(330.1484375,27.25)"><rect rx="0" ry="0" x="-65.9296875" y="-19.25" width="131.859375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.9296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">查询:BFS从c到d</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7447" transform="translate(516.2734375,27.25)"><rect rx="0" ry="0" x="-70.1953125" y="-19.25" width="140.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-60.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">路径权乘积=答案</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7449" transform="translate(685.6015625,27.25)"><rect rx="0" ry="0" x="-49.1328125" y="-19.25" width="98.265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.1328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">不可达→-1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public double[] calcEquation(List&lt;List&lt;String&gt;&gt; eq, double[] val, List&lt;List&lt;String&gt;&gt; q) {        Map&lt;String, Map&lt;String, Double&gt;&gt; g = new HashMap&lt;&gt;();        for (int i = 0; i &lt; eq.size(); i++) {            String a = eq.get(i).get(0), b = eq.get(i).get(1);            g.computeIfAbsent(a, k -&gt; new HashMap&lt;&gt;()).put(b, val[i]);            g.computeIfAbsent(b, k -&gt; new HashMap&lt;&gt;()).put(a, 1.0 / val[i]);        }        double[] res = new double[q.size()];        for (int i = 0; i &lt; q.size(); i++) {            res[i] = bfs(g, q.get(i).get(0), q.get(i).get(1), new HashSet&lt;&gt;());        }        return res;    }    double bfs(Map&lt;String, Map&lt;String, Double&gt;&gt; g, String s, String t, Set&lt;String&gt; seen) {        if (!g.containsKey(s) || !g.containsKey(t)) return -1.0;        if (s.equals(t)) return 1.0;        seen.add(s);        for (Map.Entry&lt;String, Double&gt; e : g.get(s).entrySet()) {            if (!seen.contains(e.getKey())) {                double sub = bfs(g, e.getKey(), t, seen);                if (sub != -1.0) return e.getValue() * sub;            }        }        return -1.0;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(查询数 × 节点数)，空间 O(等式数)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;变量间传递关系&quot;建模成带权图，路径乘积即传递结果。并查集也能做（带权并查集），BFS 更直观。</li></ul><h2 id="207.-%E8%AF%BE%E7%A8%8B%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/course-schedule/" target="_blank">207. 课程表</a></h2><blockquote><p><code>n</code> 门课，先修关系，判断能否修完。</p></blockquote><p><strong>思路脉络</strong>：拓扑排序 = 判环。建入度表，入度 0 的入队，出队减后继入度，新 0 入队。处理数等于 n 则无环。和热题 100 相同。</p><div class="mermaid"><svg id="render2545177345" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="140.1085968017578" style="max-width: 734.1242065429688px;" viewBox="0 0 734.1242065429688 140.1085968017578"><style>#render2545177345 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2545177345 .error-icon{fill:#552222;}#render2545177345 .error-text{fill:#552222;stroke:#552222;}#render2545177345 .edge-thickness-normal{stroke-width:2px;}#render2545177345 .edge-thickness-thick{stroke-width:3.5px;}#render2545177345 .edge-pattern-solid{stroke-dasharray:0;}#render2545177345 .edge-pattern-dashed{stroke-dasharray:3;}#render2545177345 .edge-pattern-dotted{stroke-dasharray:2;}#render2545177345 .marker{fill:#333333;stroke:#333333;}#render2545177345 .marker.cross{stroke:#333333;}#render2545177345 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2545177345 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2545177345 .cluster-label text{fill:#333;}#render2545177345 .cluster-label span{color:#333;}#render2545177345 .label text,#render2545177345 span{fill:#333;color:#333;}#render2545177345 .node rect,#render2545177345 .node circle,#render2545177345 .node ellipse,#render2545177345 .node polygon,#render2545177345 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2545177345 .node .label{text-align:center;}#render2545177345 .node.clickable{cursor:pointer;}#render2545177345 .arrowheadPath{fill:#333333;}#render2545177345 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2545177345 .flowchart-link{stroke:#333333;fill:none;}#render2545177345 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2545177345 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2545177345 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2545177345 .cluster text{fill:#333;}#render2545177345 .cluster span{color:#333;}#render2545177345 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2545177345 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92,70.0542984008789L96.16666666666667,70.0542984008789C100.33333333333333,70.0542984008789,108.66666666666667,70.0542984008789,117,70.0542984008789C125.33333333333333,70.0542984008789,133.66666666666666,70.0542984008789,137.83333333333334,70.0542984008789L142,70.0542984008789" marker-end="url(#arrowhead6692)" style="fill:none"></path><defs><marker id="arrowhead6692" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M234.390625,70.0542984008789L238.55729166666666,70.0542984008789C242.72395833333334,70.0542984008789,251.05729166666666,70.0542984008789,259.390625,70.0542984008789C267.7239583333333,70.0542984008789,276.0572916666667,70.0542984008789,280.2239583333333,70.0542984008789L284.390625,70.0542984008789" marker-end="url(#arrowhead6693)" style="fill:none"></path><defs><marker id="arrowhead6693" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M418.015625,60.26461461878748L423.515625,59.458728582469384C429.015625,58.652842546151284,440.015625,57.0410704735151,452.4780456395426,57.37693021000798C464.94046627908506,57.712789946500855,478.8653075581701,59.99628149212279,485.8277281977127,61.13802726493376L492.79014883725523,62.27977303774473" marker-end="url(#arrowhead6694)" style="fill:none"></path><defs><marker id="arrowhead6694" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-G" style="opacity: 1;" id="L-D-G"><path class="path" d="M608.624217224121,70.5542984008789L614.0408846537272,70.47096506754558C619.4575520833333,70.38763173421223,630.2908869425455,70.22096506754558,641.2075543721517,70.13763173421223C652.1242218017578,70.0542984008789,663.1242218017578,70.0542984008789,668.6242218017578,70.0542984008789L674.1242218017578,70.0542984008789" marker-end="url(#arrowhead6695)" style="fill:none"></path><defs><marker id="arrowhead6695" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-C" style="opacity: 1;" id="L-D-C"><path class="path" d="M492.7901514820816,78.8288233570816L485.8277304017347,79.80390253104783C478.86530932138777,80.77898170501403,464.9404671606939,82.72914005294648,452.4780460803469,82.89833319059458C440.015625,83.0675263282427,429.015625,81.45575425560652,423.515625,80.64986821928842L418.015625,79.84398218297032" marker-end="url(#arrowhead6696)" style="fill:none"></path><defs><marker id="arrowhead6696" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(641.1242218017578,70.0542984008789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000802993774414" height="18.50092887878418"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(451.015625,84.6792984008789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000802993774414" height="18.50092887878418"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7460" transform="translate(50,70.0542984008789)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">建入度表</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7461" transform="translate(188.1953125,70.0542984008789)"><rect rx="0" ry="0" x="-46.1953125" y="-19.25" width="92.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">入度0入队</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7463" transform="translate(351.203125,70.0542984008789)"><rect rx="0" ry="0" x="-66.8125" y="-19.25" width="133.625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.8125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">出队,后继入度--</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7465" transform="translate(546.0699234008789,70.0542984008789)"><polygon points="62.054296875,0 124.10859375,-62.054296875 62.054296875,-124.10859375 0,-62.054296875" transform="translate(-62.054296875,62.054296875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-39.69921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">处理数==n?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-G-7467" transform="translate(700.1242218017578,70.0542984008789)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">无环</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean canFinish(int n, int[][] pre) {        List&lt;List&lt;Integer&gt;&gt; g = new ArrayList&lt;&gt;();        int[] indeg = new int[n];        for (int i = 0; i &lt; n; i++) g.add(new ArrayList&lt;&gt;());        for (int[] p : pre) { g.get(p[1]).add(p[0]); indeg[p[0]]++; }        Queue&lt;Integer&gt; q = new ArrayDeque&lt;&gt;();        for (int i = 0; i &lt; n; i++) if (indeg[i] == 0) q.offer(i);        int cnt = 0;        while (!q.isEmpty()) { int c = q.poll(); cnt++; for (int nx : g.get(c)) if (--indeg[nx] == 0) q.offer(nx); }        return cnt == n;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(V+E)，空间 O(V+E)。</p></blockquote><h2 id="210.-%E8%AF%BE%E7%A8%8B%E8%A1%A8-ii" tabindex="-1"><a href="https://leetcode.cn/problems/course-schedule-ii/" target="_blank">210. 课程表 II</a></h2><blockquote><p>返回一种可行的修课顺序，不可行返回空。</p></blockquote><p><strong>思路脉络</strong>：和 207 完全相同，只是出队顺序就是拓扑序，收集进结果数组。最后判 <code>cnt == n</code>。</p><div class="mermaid"><svg id="render1190392463" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="117.12577819824219" style="max-width: 879.5015869140625px;" viewBox="0 0 879.5015869140625 117.12577819824219"><style>#render1190392463 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1190392463 .error-icon{fill:#552222;}#render1190392463 .error-text{fill:#552222;stroke:#552222;}#render1190392463 .edge-thickness-normal{stroke-width:2px;}#render1190392463 .edge-thickness-thick{stroke-width:3.5px;}#render1190392463 .edge-pattern-solid{stroke-dasharray:0;}#render1190392463 .edge-pattern-dashed{stroke-dasharray:3;}#render1190392463 .edge-pattern-dotted{stroke-dasharray:2;}#render1190392463 .marker{fill:#333333;stroke:#333333;}#render1190392463 .marker.cross{stroke:#333333;}#render1190392463 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1190392463 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1190392463 .cluster-label text{fill:#333;}#render1190392463 .cluster-label span{color:#333;}#render1190392463 .label text,#render1190392463 span{fill:#333;color:#333;}#render1190392463 .node rect,#render1190392463 .node circle,#render1190392463 .node ellipse,#render1190392463 .node polygon,#render1190392463 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1190392463 .node .label{text-align:center;}#render1190392463 .node.clickable{cursor:pointer;}#render1190392463 .arrowheadPath{fill:#333333;}#render1190392463 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1190392463 .flowchart-link{stroke:#333333;fill:none;}#render1190392463 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1190392463 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1190392463 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1190392463 .cluster text{fill:#333;}#render1190392463 .cluster span{color:#333;}#render1190392463 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1190392463 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92,59L96.16666666666667,59C100.33333333333333,59,108.66666666666667,59,117,59C125.33333333333333,59,133.66666666666666,59,137.83333333333334,59L142,59" marker-end="url(#arrowhead6729)" style="fill:none"></path><defs><marker id="arrowhead6729" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M234.390625,59L238.55729166666666,59C242.72395833333334,59,251.05729166666666,59,259.390625,59C267.7239583333333,59,276.0572916666667,59,280.2239583333333,59L284.390625,59" marker-end="url(#arrowhead6730)" style="fill:none"></path><defs><marker id="arrowhead6730" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M384.2176427165354,39.75L389.3167855971128,37.666666666666664C394.41592847769033,35.583333333333336,404.61421423884514,31.416666666666668,413.8800237860892,29.333333333333332C423.1458333333333,27.25,431.4791666666667,27.25,435.6458333333333,27.25L439.8125,27.25" marker-end="url(#arrowhead6731)" style="fill:none"></path><defs><marker id="arrowhead6731" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M581.828125,27.25L585.9947916666666,27.25C590.1614583333334,27.25,598.4947916666666,27.25,609.3933014157411,30.143156790435654C620.2918111648155,33.03631358087131,633.755497329631,38.822627161742616,640.4873404120387,41.71578395217827L647.2191834944465,44.608940742613925" marker-end="url(#arrowhead6732)" style="fill:none"></path><defs><marker id="arrowhead6732" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M732.5796867370606,59.5L737.9963537851969,59.416666666666664C743.4130208333332,59.333333333333336,754.2463549296061,59.166666666666664,765.1630219777426,59.083333333333336C776.0796890258789,59,787.0796890258789,59,792.5796890258789,59L798.0796890258789,59" marker-end="url(#arrowhead6733)" style="fill:none"></path><defs><marker id="arrowhead6733" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-C" style="opacity: 1;" id="L-E-C"><path class="path" d="M647.2191845670267,74.39105880408734L640.4873413058556,77.11754900340611C633.7554980446845,79.84403920272489,620.2918115223423,85.29701960136244,597.5586661778378,88.02350980068123C574.8255208333334,90.75,542.8229166666666,90.75,510.8203125,90.75C478.8177083333333,90.75,446.8151041666667,90.75,425.7146592027559,88.66666666666667C404.61421423884514,86.58333333333333,394.41592847769033,82.41666666666667,389.3167855971128,80.33333333333333L384.2176427165354,78.25" marker-end="url(#arrowhead6734)" style="fill:none"></path><defs><marker id="arrowhead6734" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(765.0796890258789,59)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000106811523438" height="18.500123977661133"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(510.8203125,90.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000106811523438" height="18.500123977661133"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7482" transform="translate(50,59)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">建入度表</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7483" transform="translate(188.1953125,59)"><rect rx="0" ry="0" x="-46.1953125" y="-19.25" width="92.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">入度0入队</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7485" transform="translate(337.1015625,59)"><rect rx="0" ry="0" x="-52.7109375" y="-19.25" width="105.421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.7109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">出队记入res</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7487" transform="translate(510.8203125,27.25)"><rect rx="0" ry="0" x="-71.0078125" y="-19.25" width="142.015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-61.0078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">后继入度--,0入队</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7489" transform="translate(681.9539070129395,59)"><polygon points="50.12578125,0 100.2515625,-50.12578125 50.12578125,-100.2515625 0,-50.12578125" transform="translate(-50.12578125,50.12578125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-26.4453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">idx==n?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7491" transform="translate(834.7906265258789,59)"><rect rx="0" ry="0" x="-36.7109375" y="-19.25" width="73.421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-26.7109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回res</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int[] findOrder(int n, int[][] pre) {        List&lt;List&lt;Integer&gt;&gt; g = new ArrayList&lt;&gt;();        int[] indeg = new int[n];        for (int i = 0; i &lt; n; i++) g.add(new ArrayList&lt;&gt;());        for (int[] p : pre) { g.get(p[1]).add(p[0]); indeg[p[0]]++; }        Queue&lt;Integer&gt; q = new ArrayDeque&lt;&gt;();        for (int i = 0; i &lt; n; i++) if (indeg[i] == 0) q.offer(i);        int[] res = new int[n]; int idx = 0;        while (!q.isEmpty()) { int c = q.poll(); res[idx++] = c; for (int nx : g.get(c)) if (--indeg[nx] == 0) q.offer(nx); }        return idx == n ? res : new int[0];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(V+E)，空间 O(V+E)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>边方向：<code>[a,b]</code> 表示&quot;先 b 后 a&quot;，所以 <code>b→a</code>（a 依赖 b）。建反了入度语义错。</li></ul><h2 id="909.-%E8%9B%87%E6%A2%AF%E6%A3%8B" tabindex="-1"><a href="https://leetcode.cn/problems/snakes-and-ladders/" target="_blank">909. 蛇梯棋</a></h2><blockquote><p><code>n×n</code> 棋盘有蛇梯，求从 (1,1) 到终点的最少掷骰次数。</p></blockquote><p><strong>思路脉络</strong>：BFS 最短步数。每个位置 <code>i</code> 模拟掷 1-6，遇到蛇梯跳转（注意 Boustrophedon 编号——奇数行从左到右、偶数行从右到左，坐标映射是难点）。</p><div class="mermaid"><svg id="render2794622713" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="199.76718139648438" style="max-width: 652.6187744140625px;" viewBox="0 0 652.6187744140625 199.76718139648438"><style>#render2794622713 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2794622713 .error-icon{fill:#552222;}#render2794622713 .error-text{fill:#552222;stroke:#552222;}#render2794622713 .edge-thickness-normal{stroke-width:2px;}#render2794622713 .edge-thickness-thick{stroke-width:3.5px;}#render2794622713 .edge-pattern-solid{stroke-dasharray:0;}#render2794622713 .edge-pattern-dashed{stroke-dasharray:3;}#render2794622713 .edge-pattern-dotted{stroke-dasharray:2;}#render2794622713 .marker{fill:#333333;stroke:#333333;}#render2794622713 .marker.cross{stroke:#333333;}#render2794622713 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2794622713 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2794622713 .cluster-label text{fill:#333;}#render2794622713 .cluster-label span{color:#333;}#render2794622713 .label text,#render2794622713 span{fill:#333;color:#333;}#render2794622713 .node rect,#render2794622713 .node circle,#render2794622713 .node ellipse,#render2794622713 .node polygon,#render2794622713 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2794622713 .node .label{text-align:center;}#render2794622713 .node.clickable{cursor:pointer;}#render2794622713 .arrowheadPath{fill:#333333;}#render2794622713 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2794622713 .flowchart-link{stroke:#333333;fill:none;}#render2794622713 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2794622713 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2794622713 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2794622713 .cluster text{fill:#333;}#render2794622713 .cluster span{color:#333;}#render2794622713 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2794622713 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M117.3203125,122.45038604736328L121.48697916666667,122.45038604736328C125.65364583333333,122.45038604736328,133.98697916666666,122.45038604736328,142.3203125,122.45038604736328C150.65364583333334,122.45038604736328,158.98697916666666,122.45038604736328,163.15364583333334,122.45038604736328L167.3203125,122.45038604736328" marker-end="url(#arrowhead6759)" style="fill:none"></path><defs><marker id="arrowhead6759" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M276.6365274159985,103.20038604736328L288.00569992999874,97.2934201558431C299.374872443999,91.38645426432292,322.11321747199946,79.57252248128255,340.5517666865047,74.70034706455034C358.99031590101,69.82817164781815,373.1290693020199,71.89775259739412,380.1984460025249,72.9325430721821L387.2678227030299,73.96733354697008" marker-end="url(#arrowhead6760)" style="fill:none"></path><defs><marker id="arrowhead6760" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M527.1187469482421,82.88359069824219L532.5354131062825,82.80025736490886C537.9520792643228,82.71692403157552,548.7854115804035,82.55025736490886,560.5743495824198,88.3738899230957C572.363287584436,94.19752248128255,585.1078312723876,106.01145426432292,591.4801031163634,111.9184201558431L597.8523749603391,117.82538604736328" marker-end="url(#arrowhead6761)" style="fill:none"></path><defs><marker id="arrowhead6761" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-B" style="opacity: 1;" id="L-C-B"><path class="path" d="M398.5638648073891,103.09589605738918L389.61181442282424,106.32164438905153C380.6597640382594,109.54739272071389,362.7556632691297,115.99888938403858,348.30361288456487,119.22463771570092C333.8515625,122.45038604736328,322.8515625,122.45038604736328,317.3515625,122.45038604736328L311.8515625,122.45038604736328" marker-end="url(#arrowhead6762)" style="fill:none"></path><defs><marker id="arrowhead6762" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M597.8523749603391,156.32538604736328L591.4801031163634,162.23235193888345C585.1078312723876,168.13931783040366,572.363287584436,179.953249613444,548.0937506240865,185.8602155049642C523.8242136637369,191.76718139648438,488.02968343098956,191.76718139648438,452.2351531982422,191.76718139648438C416.4406229654948,191.76718139648438,380.6460927327474,191.76718139648438,350.0767845287212,183.4227155049642C319.507476324695,175.078249613444,294.16339014938995,158.38931783040366,281.49134706173743,150.04485193888345L268.8193039740849,141.70038604736328" marker-end="url(#arrowhead6763)" style="fill:none"></path><defs><marker id="arrowhead6763" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(559.6187438964844,82.38359069824219)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000125885009766" height="18.500144958496094"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(344.8515625,122.45038604736328)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000125885009766" height="18.500144958496094"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7504" transform="translate(62.66015625,122.45038604736328)"><rect rx="0" ry="0" x="-54.66015625" y="-19.25" width="109.3203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-44.66015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">BFS,dist数组</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7505" transform="translate(239.5859375,122.45038604736328)"><rect rx="0" ry="0" x="-72.265625" y="-19.25" width="144.53125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-62.265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">掷1-6,遇蛇梯跳转</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7507" transform="translate(452.2351531982422,82.38359069824219)"><polygon points="74.38359375,0 148.7671875,-74.38359375 74.38359375,-148.7671875 0,-74.38359375" transform="translate(-74.38359375,74.38359375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-53.3984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dist[next]==-1?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7509" transform="translate(618.6187438964844,137.07538604736328)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">入队</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int snakesAndLadders(int[][] board) {        int n = board.length, target = n * n;        int[] dist = new int[target + 1]; Arrays.fill(dist, -1); dist[1] = 0;        Queue&lt;Integer&gt; q = new ArrayDeque&lt;&gt;(); q.offer(1);        while (!q.isEmpty()) {            int cur = q.poll();            for (int move = 1; move &lt;= 6 &amp;&amp; cur + move &lt;= target; move++) {                int next = cur + move;                int[] rc = pos(next, n);                int r = rc[0], c = rc[1];                if (board[r][c] != -1) next = board[r][c];                if (dist[next] == -1) { dist[next] = dist[cur] + 1; q.offer(next); }            }        }        return dist[target];    }    int[] pos(int num, int n) {        int r = (num - 1) / n, c = (num - 1) % n;        // 从底起算的行列        if (r % 2 == 1) c = n - 1 - c;                    // 偶数行(从底0起)反向        return new int[]{n - 1 - r, c};                   // 转成从顶起的行    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(n²)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>坐标映射 <code>pos</code>：从底起第 r 行，r 偶数正向、奇数反向；最终行号 = <code>n-1-r</code>。这块最容易写错，建议画图验证。</li></ul><h2 id="433.-%E6%9C%80%E5%B0%8F%E5%9F%BA%E5%9B%A0%E5%8F%98%E5%8C%96" tabindex="-1"><a href="https://leetcode.cn/problems/minimum-genetic-mutation/" target="_blank">433. 最小基因变化</a></h2><blockquote><p>起始基因串到目标串，每次变一个字符且中间串须在银行库里，求最少步数。</p></blockquote><p><strong>思路脉络</strong>：BFS 最短路。每个状态生成所有&quot;变一位&quot;的邻居，邻居在银行库里且未访问则入队。首次到达目标即最少步数。</p><div class="mermaid"><svg id="render1633239858" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="112.78515625" style="max-width: 872.01953125px;" viewBox="0 0 872.01953125 112.78515625"><style>#render1633239858 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1633239858 .error-icon{fill:#552222;}#render1633239858 .error-text{fill:#552222;stroke:#552222;}#render1633239858 .edge-thickness-normal{stroke-width:2px;}#render1633239858 .edge-thickness-thick{stroke-width:3.5px;}#render1633239858 .edge-pattern-solid{stroke-dasharray:0;}#render1633239858 .edge-pattern-dashed{stroke-dasharray:3;}#render1633239858 .edge-pattern-dotted{stroke-dasharray:2;}#render1633239858 .marker{fill:#333333;stroke:#333333;}#render1633239858 .marker.cross{stroke:#333333;}#render1633239858 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1633239858 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1633239858 .cluster-label text{fill:#333;}#render1633239858 .cluster-label span{color:#333;}#render1633239858 .label text,#render1633239858 span{fill:#333;color:#333;}#render1633239858 .node rect,#render1633239858 .node circle,#render1633239858 .node ellipse,#render1633239858 .node polygon,#render1633239858 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1633239858 .node .label{text-align:center;}#render1633239858 .node.clickable{cursor:pointer;}#render1633239858 .arrowheadPath{fill:#333333;}#render1633239858 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1633239858 .flowchart-link{stroke:#333333;fill:none;}#render1633239858 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1633239858 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1633239858 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1633239858 .cluster text{fill:#333;}#render1633239858 .cluster span{color:#333;}#render1633239858 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1633239858 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M165.76852318548387,37.142578125L170.94121723790326,35.830078125C176.1139112903226,34.517578125,186.4592993951613,31.892578125,195.79866011424733,30.580078125C205.13802083333334,29.267578125,213.47135416666666,29.267578125,217.63802083333334,29.267578125L221.8046875,29.267578125" marker-end="url(#arrowhead6793)" style="fill:none"></path><defs><marker id="arrowhead6793" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M387.9375,29.267578125L393.4375,29.267578125C398.9375,29.267578125,409.9375,29.267578125,420.9375,29.267578125C431.9375,29.267578125,442.9375,29.267578125,448.4375,29.267578125L453.9375,29.267578125" marker-end="url(#arrowhead6794)" style="fill:none"></path><defs><marker id="arrowhead6794" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M544.484375,29.267578125L548.6510416666666,29.267578125C552.8177083333334,29.267578125,561.1510416666666,29.267578125,571.7441911371806,31.695261987819393C582.3373406076945,34.122945850638786,595.1903062153891,38.978313576277564,601.6167890192363,41.40599743909696L608.0432718230836,43.83368130191635" marker-end="url(#arrowhead6795)" style="fill:none"></path><defs><marker id="arrowhead6795" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M691.76953125,56.892578125L697.1861979166666,56.809244791666664C702.6028645833334,56.725911458333336,713.4361979166666,56.559244791666664,724.3528645833334,56.475911458333336C735.26953125,56.392578125,746.26953125,56.392578125,751.76953125,56.392578125L757.26953125,56.392578125" marker-end="url(#arrowhead6796)" style="fill:none"></path><defs><marker id="arrowhead6796" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M608.0432718230836,69.95147494808364L601.6167890192363,72.21249214423636C595.1903062153891,74.4735093403891,582.3373406076945,78.99554373269454,564.1986182205139,81.25656092884726C546.0598958333334,83.517578125,522.6354166666666,83.517578125,497.8776041666667,83.517578125C473.1197916666667,83.517578125,447.0286458333333,83.517578125,414.638671875,83.517578125C382.2486979166667,83.517578125,343.5598958333333,83.517578125,306.2044270833333,83.517578125C268.8489583333333,83.517578125,232.82682291666666,83.517578125,209.64306115591398,82.205078125C186.4592993951613,80.892578125,176.1139112903226,78.267578125,170.9412172379032,76.955078125L165.76852318548387,75.642578125" marker-end="url(#arrowhead6797)" style="fill:none"></path><defs><marker id="arrowhead6797" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(724.26953125,56.392578125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00055503845215" height="18.500640869140625"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(420.9375,83.517578125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00055503845215" height="18.500640869140625"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7524" transform="translate(89.90234375,56.392578125)"><rect rx="0" ry="0" x="-81.90234375" y="-19.25" width="163.8046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-71.90234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">BFS,枚举每位×4字符</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7525" transform="translate(304.87109375,29.267578125)"><rect rx="0" ry="0" x="-83.06640625" y="-19.25" width="166.1328125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-73.06640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">邻居在bank且未访问</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7527" transform="translate(499.2109375,29.267578125)"><rect rx="0" ry="0" x="-45.2734375" y="-19.25" width="90.546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-35.2734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">入队,seen</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7529" transform="translate(642.876953125,56.392578125)"><polygon points="48.392578125,0 96.78515625,-48.392578125 48.392578125,-96.78515625 0,-48.392578125" transform="translate(-48.392578125,48.392578125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-24.51953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">==end?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7531" transform="translate(810.64453125,56.392578125)"><rect rx="0" ry="0" x="-53.375" y="-19.25" width="106.75" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return steps</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int minMutation(String start, String end, String[] bank) {        Set&lt;String&gt; set = new HashSet&lt;&gt;(Arrays.asList(bank));        if (!set.contains(end)) return -1;        char[] genes = {&#39;A&#39;,&#39;C&#39;,&#39;G&#39;,&#39;T&#39;};        Queue&lt;String&gt; q = new ArrayDeque&lt;&gt;(); q.offer(start);        Set&lt;String&gt; seen = new HashSet&lt;&gt;(); seen.add(start);        int steps = 0;        while (!q.isEmpty()) {            int size = q.size();            for (int i = 0; i &lt; size; i++) {                String cur = q.poll();                if (cur.equals(end)) return steps;                char[] cs = cur.toCharArray();                for (int j = 0; j &lt; cs.length; j++) {                    char old = cs[j];                    for (char g : genes) {                        cs[j] = g;                        String nb = new String(cs);                        if (set.contains(nb) &amp;&amp; !seen.contains(nb)) { seen.add(nb); q.offer(nb); }                    }                    cs[j] = old;                }            }            steps++;        }        return -1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(N × L × 4)（N 银行大小、L 串长），空间 O(N)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;变换一步可达&quot;类问题用 BFS：枚举所有一步邻居（本题枚举每位 × 4 字符），邻居在合法集内则入队。和单词接龙同模板。</li></ul><h2 id="127.-%E5%8D%95%E8%AF%8D%E6%8E%A5%E9%BE%99" tabindex="-1"><a href="https://leetcode.cn/problems/word-ladder/" target="_blank">127. 单词接龙</a></h2><blockquote><p><code>beginWord</code> 到 <code>endWord</code>，每次变一个字母且中间词在词典里，求最短转换序列长度。</p></blockquote><p><strong>思路脉络</strong>：和最小基因变化完全同模板——BFS，枚举每位 × 26 字母邻居，邻居在词典且未访问则入队。双向 BFS 可优化（从两端相向扩展，搜更窄的一侧），但单向 BFS 简洁。</p><div class="mermaid"><svg id="render356627214" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="112.78515625" style="max-width: 890.80078125px;" viewBox="0 0 890.80078125 112.78515625"><style>#render356627214 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render356627214 .error-icon{fill:#552222;}#render356627214 .error-text{fill:#552222;stroke:#552222;}#render356627214 .edge-thickness-normal{stroke-width:2px;}#render356627214 .edge-thickness-thick{stroke-width:3.5px;}#render356627214 .edge-pattern-solid{stroke-dasharray:0;}#render356627214 .edge-pattern-dashed{stroke-dasharray:3;}#render356627214 .edge-pattern-dotted{stroke-dasharray:2;}#render356627214 .marker{fill:#333333;stroke:#333333;}#render356627214 .marker.cross{stroke:#333333;}#render356627214 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render356627214 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render356627214 .cluster-label text{fill:#333;}#render356627214 .cluster-label span{color:#333;}#render356627214 .label text,#render356627214 span{fill:#333;color:#333;}#render356627214 .node rect,#render356627214 .node circle,#render356627214 .node ellipse,#render356627214 .node polygon,#render356627214 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render356627214 .node .label{text-align:center;}#render356627214 .node.clickable{cursor:pointer;}#render356627214 .arrowheadPath{fill:#333333;}#render356627214 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render356627214 .flowchart-link{stroke:#333333;fill:none;}#render356627214 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render356627214 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render356627214 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render356627214 .cluster text{fill:#333;}#render356627214 .cluster span{color:#333;}#render356627214 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render356627214 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M172.94115423387098,37.142578125L178.31684727822582,35.830078125C183.69254032258064,34.517578125,194.44392641129033,31.892578125,203.98628612231184,30.580078125C213.52864583333334,29.267578125,221.86197916666666,29.267578125,226.02864583333334,29.267578125L230.1953125,29.267578125" marker-end="url(#arrowhead6827)" style="fill:none"></path><defs><marker id="arrowhead6827" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M389.9375,29.267578125L395.4375,29.267578125C400.9375,29.267578125,411.9375,29.267578125,422.9375,29.267578125C433.9375,29.267578125,444.9375,29.267578125,450.4375,29.267578125L455.9375,29.267578125" marker-end="url(#arrowhead6828)" style="fill:none"></path><defs><marker id="arrowhead6828" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M546.484375,29.267578125L550.6510416666666,29.267578125C554.8177083333334,29.267578125,563.1510416666666,29.267578125,573.7441911371806,31.695261987819393C584.3373406076945,34.122945850638786,597.1903062153891,38.978313576277564,603.6167890192363,41.40599743909696L610.0432718230836,43.83368130191635" marker-end="url(#arrowhead6829)" style="fill:none"></path><defs><marker id="arrowhead6829" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M693.76953125,56.892578125L699.1861979166666,56.809244791666664C704.6028645833334,56.725911458333336,715.4361979166666,56.559244791666664,726.3528645833334,56.475911458333336C737.26953125,56.392578125,748.26953125,56.392578125,753.76953125,56.392578125L759.26953125,56.392578125" marker-end="url(#arrowhead6830)" style="fill:none"></path><defs><marker id="arrowhead6830" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M610.0432718230836,69.95147494808364L603.6167890192363,72.21249214423636C597.1903062153891,74.4735093403891,584.3373406076945,78.99554373269454,566.1986182205139,81.25656092884726C548.0598958333334,83.517578125,524.6354166666666,83.517578125,499.8776041666667,83.517578125C475.1197916666667,83.517578125,449.0286458333333,83.517578125,417.1712239583333,83.517578125C385.3138020833333,83.517578125,347.6901041666667,83.517578125,311.3997395833333,83.517578125C275.109375,83.517578125,240.15234375,83.517578125,217.29813508064515,82.205078125C194.44392641129033,80.892578125,183.69254032258064,78.267578125,178.31684727822582,76.955078125L172.94115423387098,75.642578125" marker-end="url(#arrowhead6831)" style="fill:none"></path><defs><marker id="arrowhead6831" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(726.26953125,56.392578125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00055503845215" height="18.500640869140625"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(422.9375,83.517578125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00055503845215" height="18.500640869140625"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7544" transform="translate(94.09765625,56.392578125)"><rect rx="0" ry="0" x="-86.09765625" y="-19.25" width="172.1953125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-76.09765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">BFS,枚举每位×26字母</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7545" transform="translate(310.06640625,29.267578125)"><rect rx="0" ry="0" x="-79.87109375" y="-19.25" width="159.7421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-69.87109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">邻居在dict且未访问</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7547" transform="translate(501.2109375,29.267578125)"><rect rx="0" ry="0" x="-45.2734375" y="-19.25" width="90.546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-35.2734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">入队,seen</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7549" transform="translate(644.876953125,56.392578125)"><polygon points="48.392578125,0 96.78515625,-48.392578125 48.392578125,-96.78515625 0,-48.392578125" transform="translate(-48.392578125,48.392578125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-24.51953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">==end?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7551" transform="translate(821.03515625,56.392578125)"><rect rx="0" ry="0" x="-61.765625" y="-19.25" width="123.53125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-51.765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return steps+1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int ladderLength(String begin, String end, List&lt;String&gt; wordList) {        Set&lt;String&gt; dict = new HashSet&lt;&gt;(wordList);        if (!dict.contains(end)) return 0;        Queue&lt;String&gt; q = new ArrayDeque&lt;&gt;(); q.offer(begin);        Set&lt;String&gt; seen = new HashSet&lt;&gt;(); seen.add(begin);        int steps = 1;        while (!q.isEmpty()) {            int size = q.size();            for (int i = 0; i &lt; size; i++) {                char[] cs = q.poll().toCharArray();                for (int j = 0; j &lt; cs.length; j++) {                    char old = cs[j];                    for (char c = &#39;a&#39;; c &lt;= &#39;z&#39;; c++) {                        cs[j] = c;                        String nb = new String(cs);                        if (nb.equals(end)) return steps + 1;                        if (dict.contains(nb) &amp;&amp; !seen.contains(nb)) { seen.add(nb); q.offer(nb); }                    }                    cs[j] = old;                }            }            steps++;        }        return 0;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(N × L × 26)，空间 O(N)。</p></blockquote><blockquote><p>[!tip] 进阶</p></blockquote><ul><li>双向 BFS：从 begin 和 end 各自 BFS，每步扩节点少的一侧，相遇即最短。对大词典快很多，但实现稍复杂。</li></ul><hr /><h1 id="12.-%E5%AD%97%E5%85%B8%E6%A0%91" tabindex="-1">12. 字典树</h1><p>Trie 把&quot;前缀匹配&quot;从 O(总词数 × 词长) 降到 O(前缀长)。共享前缀的词共用路径。难点在节点结构（children + isEnd）和带通配符的搜索。</p><h2 id="208.-%E5%AE%9E%E7%8E%B0-trie-(%E5%89%8D%E7%BC%80%E6%A0%91)" tabindex="-1"><a href="https://leetcode.cn/problems/implement-trie-prefix-tree/" target="_blank">208. 实现 Trie (前缀树)</a></h2><blockquote><p><code>insert</code>/<code>search</code>/<code>startsWith</code>。</p></blockquote><p><strong>思路脉络</strong>：26 叉树，每节点 <code>children[26]</code> + <code>isEnd</code>。<code>search</code> 要终点 <code>isEnd</code>，<code>startsWith</code> 只要求路径存在。和热题 100 相同。</p><div class="mermaid"><svg id="render2713631943" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="205.6374969482422" style="max-width: 597.8125px;" viewBox="0 0 597.8125 205.6374969482422"><style>#render2713631943 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2713631943 .error-icon{fill:#552222;}#render2713631943 .error-text{fill:#552222;stroke:#552222;}#render2713631943 .edge-thickness-normal{stroke-width:2px;}#render2713631943 .edge-thickness-thick{stroke-width:3.5px;}#render2713631943 .edge-pattern-solid{stroke-dasharray:0;}#render2713631943 .edge-pattern-dashed{stroke-dasharray:3;}#render2713631943 .edge-pattern-dotted{stroke-dasharray:2;}#render2713631943 .marker{fill:#333333;stroke:#333333;}#render2713631943 .marker.cross{stroke:#333333;}#render2713631943 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2713631943 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2713631943 .cluster-label text{fill:#333;}#render2713631943 .cluster-label span{color:#333;}#render2713631943 .label text,#render2713631943 span{fill:#333;color:#333;}#render2713631943 .node rect,#render2713631943 .node circle,#render2713631943 .node ellipse,#render2713631943 .node polygon,#render2713631943 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2713631943 .node .label{text-align:center;}#render2713631943 .node.clickable{cursor:pointer;}#render2713631943 .arrowheadPath{fill:#333333;}#render2713631943 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2713631943 .flowchart-link{stroke:#333333;fill:none;}#render2713631943 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2713631943 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2713631943 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2713631943 .cluster text{fill:#333;}#render2713631943 .cluster span{color:#333;}#render2713631943 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2713631943 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92,103.85312271118164L96.16666666666667,103.85312271118164C100.33333333333333,103.85312271118164,108.66666666666667,103.85312271118164,117,103.85312271118164C125.33333333333333,103.85312271118164,133.66666666666666,103.85312271118164,137.83333333333334,103.85312271118164L142,103.85312271118164" marker-end="url(#arrowhead6853)" style="fill:none"></path><defs><marker id="arrowhead6853" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M236.20788967006712,84.60312271118164L247.09772055838926,77.82656033833821C257.9875514467114,71.0499979654948,279.76721322335567,57.496873219807945,297.8138319180211,52.011700632580904C315.86045061268646,46.52652804535387,330.174026225373,49.10930761658665,337.3308140317162,50.400697402203036L344.48760183805945,51.692087187819425" marker-end="url(#arrowhead6854)" style="fill:none"></path><defs><marker id="arrowhead6854" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M438.2484375000001,59.0687484741211L444.0091145833334,58.98541514078777C449.7697916666668,58.90208180745443,461.29114583333336,58.73541514078776,472.55182291666665,58.65208180745443C483.8125,58.568748474121094,494.8125,58.568748474121094,500.3125,58.568748474121094L505.8125,58.568748474121094" marker-end="url(#arrowhead6855)" style="fill:none"></path><defs><marker id="arrowhead6855" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-B" style="opacity: 1;" id="L-C-B"><path class="path" d="M354.60271422370573,76.56052519782683L345.7600743530881,81.10929145005264C336.91743448247047,85.65805770227844,319.23215474123526,94.75559020673005,304.88951487061763,99.30435645895584C290.546875,103.85312271118164,279.546875,103.85312271118164,274.046875,103.85312271118164L268.546875,103.85312271118164" marker-end="url(#arrowhead6856)" style="fill:none"></path><defs><marker id="arrowhead6856" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-E" style="opacity: 1;" id="L-B-E"><path class="path" d="M230.1379874589303,123.10312271118164L242.03946871577523,132.31718508402506C253.9409499726202,141.5312474568685,277.7439124863101,159.95937220255533,295.145393743155,169.17343457539877C312.546875,178.3874969482422,323.546875,178.3874969482422,329.046875,178.3874969482422L334.546875,178.3874969482422" marker-end="url(#arrowhead6857)" style="fill:none"></path><defs><marker id="arrowhead6857" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(472.8125,58.568748474121094)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000364303588867" height="18.50042152404785"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(301.546875,103.85312271118164)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000364303588867" height="18.50042152404785"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7564" transform="translate(50,103.85312271118164)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历字符</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7565" transform="translate(205.2734375,103.85312271118164)"><rect rx="0" ry="0" x="-63.2734375" y="-19.25" width="126.546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.2734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">沿children下走</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7567" transform="translate(387.1796875,58.568748474121094)"><polygon points="50.56875,0 101.1375,-50.56875 50.56875,-101.1375 0,-50.56875" transform="translate(-50.56875,50.56875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-26.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">不存在?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7569" transform="translate(547.8125,58.568748474121094)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">新建节点</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7573" transform="translate(387.1796875,178.3874969482422)"><rect rx="0" ry="0" x="-52.6328125" y="-19.25" width="105.265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.6328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">末尾置isEnd</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Trie {    private Trie[] children = new Trie[26];    private boolean isEnd;    public void insert(String word) {        Trie node = this;        for (char c : word.toCharArray()) {            int i = c - &#39;a&#39;;            if (node.children[i] == null) node.children[i] = new Trie();            node = node.children[i];        }        node.isEnd = true;    }    public boolean search(String word) { Trie n = walk(word); return n != null &amp;&amp; n.isEnd; }    public boolean startsWith(String prefix) { return walk(prefix) != null; }    private Trie walk(String s) {        Trie node = this;        for (char c : s.toCharArray()) {            node = node.children[c - &#39;a&#39;];            if (node == null) return null;        }        return node;    }}</code></pre><blockquote><p>[!info] 复杂度<br />各操作 O(L)，空间 O(总字符数)。</p></blockquote><h2 id="211.-%E6%B7%BB%E5%8A%A0%E4%B8%8E%E6%90%9C%E7%B4%A2%E5%8D%95%E8%AF%8D---%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E8%AE%BE%E8%AE%A1" tabindex="-1"><a href="https://leetcode.cn/problems/design-add-and-search-words-data-structure/" target="_blank">211. 添加与搜索单词 - 数据结构设计</a></h2><blockquote><p><code>addWord</code> + <code>search</code>，search 支持 <code>.</code> 通配任意字母。</p></blockquote><p><strong>思路脉络</strong>：Trie 基础上，搜索遇 <code>.</code> 时递归所有非空 children。<code>search</code> 改成递归（带位置 i）。</p><div class="mermaid"><svg id="render849030952" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="234.17031860351562" style="max-width: 764.4812622070312px;" viewBox="0 0 764.4812622070312 234.17031860351562"><style>#render849030952 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render849030952 .error-icon{fill:#552222;}#render849030952 .error-text{fill:#552222;stroke:#552222;}#render849030952 .edge-thickness-normal{stroke-width:2px;}#render849030952 .edge-thickness-thick{stroke-width:3.5px;}#render849030952 .edge-pattern-solid{stroke-dasharray:0;}#render849030952 .edge-pattern-dashed{stroke-dasharray:3;}#render849030952 .edge-pattern-dotted{stroke-dasharray:2;}#render849030952 .marker{fill:#333333;stroke:#333333;}#render849030952 .marker.cross{stroke:#333333;}#render849030952 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render849030952 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render849030952 .cluster-label text{fill:#333;}#render849030952 .cluster-label span{color:#333;}#render849030952 .label text,#render849030952 span{fill:#333;color:#333;}#render849030952 .node rect,#render849030952 .node circle,#render849030952 .node ellipse,#render849030952 .node polygon,#render849030952 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render849030952 .node .label{text-align:center;}#render849030952 .node.clickable{cursor:pointer;}#render849030952 .arrowheadPath{fill:#333333;}#render849030952 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render849030952 .flowchart-link{stroke:#333333;fill:none;}#render849030952 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render849030952 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render849030952 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render849030952 .cluster text{fill:#333;}#render849030952 .cluster span{color:#333;}#render849030952 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render849030952 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M173.1484375,94.2925796508789L177.31510416666666,94.2925796508789C181.48177083333334,94.2925796508789,189.81510416666666,94.2925796508789,198.2317709604899,94.37591298421223C206.64843775431314,94.45924631754558,215.1484380086263,94.62591298421223,219.39843813578287,94.70924631754559L223.64843826293946,94.79257965087892" marker-end="url(#arrowhead6880)" style="fill:none"></path><defs><marker id="arrowhead6880" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M298.85671020320785,72.88522811702614L307.92460199836006,65.27935676418845C316.99249379351227,57.67348541135076,335.1282773838167,42.461742705675384,351.57885197926186,34.85587135283769C368.02942657470703,27.25,382.79479217529297,27.25,390.17747497558594,27.25L397.5601577758789,27.25" marker-end="url(#arrowhead6881)" style="fill:none"></path><defs><marker id="arrowhead6881" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M298.856711040671,116.69993187314749L307.924602696246,124.13913644458255C316.992494351821,131.5783410160176,335.128277662971,146.4567501588877,349.7795031605057,153.9792880636561C364.4307286580403,161.50182596842447,375.5973963419596,161.66849263509116,381.1807301839192,161.75182596842447L386.76406402587884,161.8351593017578" marker-end="url(#arrowhead6882)" style="fill:none"></path><defs><marker id="arrowhead6882" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M496.24257269385834,141.64335546973712L505.0245405078214,137.5503227750739C513.8065083217845,133.45729008041067,531.3704439497106,125.27122469108424,545.6524117636736,121.17819199642103C559.9343795776367,117.08515930175781,570.9343795776367,117.08515930175781,576.4343795776367,117.08515930175781L581.9343795776367,117.08515930175781" marker-end="url(#arrowhead6883)" style="fill:none"></path><defs><marker id="arrowhead6883" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M496.2425684911785,182.0269612329426L505.0245370055882,185.9533275777451C513.8065055199978,189.87969392254766,531.3704425488173,197.73242661215272,550.2930360632271,201.65879295695527C569.2156295776367,205.5851593017578,589.4968795776367,205.5851593017578,599.6375045776367,205.5851593017578L609.7781295776367,205.5851593017578" marker-end="url(#arrowhead6884)" style="fill:none"></path><defs><marker id="arrowhead6884" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(353.2640609741211,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00042724609375" height="18.5004940032959"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(353.2640609741211,161.3351593017578)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00042724609375" height="18.5004940032959"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(548.9343795776367,117.08515930175781)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00042724609375" height="18.5004940032959"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(548.9343795776367,205.5851593017578)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00042724609375" height="18.5004940032959"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7584" transform="translate(90.57421875,94.2925796508789)"><rect rx="0" ry="0" x="-82.57421875" y="-19.25" width="165.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-72.57421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">search(word,i,node)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7585" transform="translate(271.70624923706055,94.2925796508789)"><polygon points="48.557812500000004,0 97.11562500000001,-48.557812500000004 48.557812500000004,-97.11562500000001 0,-48.557812500000004" transform="translate(-48.557812500000004,48.557812500000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-24.703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i==len?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7587" transform="translate(451.0992202758789,27.25)"><rect rx="0" ry="0" x="-53.5390625" y="-19.25" width="107.078125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return isEnd</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7589" transform="translate(451.0992202758789,161.3351593017578)"><polygon points="64.83515625,0 129.6703125,-64.83515625 64.83515625,-129.6703125 0,-64.83515625" transform="translate(-64.83515625,64.83515625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-42.7890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">word[i]=='.'?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7591" transform="translate(669.2078170776367,117.08515930175781)"><rect rx="0" ry="0" x="-87.2734375" y="-19.25" width="174.546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-77.2734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归所有非空children</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7593" transform="translate(669.2078170776367,205.5851593017578)"><rect rx="0" ry="0" x="-59.4296875" y="-19.25" width="118.859375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-49.4296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归对应child</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class WordDictionary {    private WordDictionary[] children = new WordDictionary[26];    private boolean isEnd;    public void addWord(String word) {        WordDictionary node = this;        for (char c : word.toCharArray()) {            int i = c - &#39;a&#39;;            if (node.children[i] == null) node.children[i] = new WordDictionary();            node = node.children[i];        }        node.isEnd = true;    }    public boolean search(String word) { return dfs(word, 0, this); }    boolean dfs(String word, int i, WordDictionary node) {        if (i == word.length()) return node.isEnd;        char c = word.charAt(i);        if (c == &#39;.&#39;) {            for (WordDictionary ch : node.children) if (ch != null &amp;&amp; dfs(word, i + 1, ch)) return true;            return false;        }        return node.children[c - &#39;a&#39;] != null &amp;&amp; dfs(word, i + 1, node.children[c - &#39;a&#39;]);    }}</code></pre><blockquote><p>[!info] 复杂度<br /><code>addWord</code> O(L)，<code>search</code> 最坏 O(26^L)（全通配）。</p></blockquote><h2 id="212.-%E5%8D%95%E8%AF%8D%E6%90%9C%E7%B4%A2-ii" tabindex="-1"><a href="https://leetcode.cn/problems/word-search-ii/" target="_blank">212. 单词搜索 II</a></h2><blockquote><p>字符网格里找所有词典里的词。</p></blockquote><p><strong>思路脉络</strong>：Trie + 回溯。把词典建 Trie，DFS 网格每个起点，沿 Trie 下走，遇 <code>isEnd</code> 收集，越界/字符不在 Trie/已访问则回溯。用 Trie 剪枝：只走 Trie 里有对应前缀的方向。</p><div class="mermaid"><svg id="render1889194941" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 901.4765625px;" viewBox="0 0 901.4765625 54.5"><style>#render1889194941 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1889194941 .error-icon{fill:#552222;}#render1889194941 .error-text{fill:#552222;stroke:#552222;}#render1889194941 .edge-thickness-normal{stroke-width:2px;}#render1889194941 .edge-thickness-thick{stroke-width:3.5px;}#render1889194941 .edge-pattern-solid{stroke-dasharray:0;}#render1889194941 .edge-pattern-dashed{stroke-dasharray:3;}#render1889194941 .edge-pattern-dotted{stroke-dasharray:2;}#render1889194941 .marker{fill:#333333;stroke:#333333;}#render1889194941 .marker.cross{stroke:#333333;}#render1889194941 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1889194941 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1889194941 .cluster-label text{fill:#333;}#render1889194941 .cluster-label span{color:#333;}#render1889194941 .label text,#render1889194941 span{fill:#333;color:#333;}#render1889194941 .node rect,#render1889194941 .node circle,#render1889194941 .node ellipse,#render1889194941 .node polygon,#render1889194941 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1889194941 .node .label{text-align:center;}#render1889194941 .node.clickable{cursor:pointer;}#render1889194941 .arrowheadPath{fill:#333333;}#render1889194941 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1889194941 .flowchart-link{stroke:#333333;fill:none;}#render1889194941 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1889194941 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1889194941 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1889194941 .cluster text{fill:#333;}#render1889194941 .cluster span{color:#333;}#render1889194941 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1889194941 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M103.0390625,27.25L107.20572916666667,27.25C111.37239583333333,27.25,119.70572916666667,27.25,128.0390625,27.25C136.37239583333334,27.25,144.70572916666666,27.25,148.87239583333334,27.25L153.0390625,27.25" marker-end="url(#arrowhead6906)" style="fill:none"></path><defs><marker id="arrowhead6906" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M294.9453125,27.25L299.1119791666667,27.25C303.2786458333333,27.25,311.6119791666667,27.25,319.9453125,27.25C328.2786458333333,27.25,336.6119791666667,27.25,340.7786458333333,27.25L344.9453125,27.25" marker-end="url(#arrowhead6907)" style="fill:none"></path><defs><marker id="arrowhead6907" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M439.984375,27.25L444.1510416666667,27.25C448.3177083333333,27.25,456.6510416666667,27.25,464.984375,27.25C473.3177083333333,27.25,481.6510416666667,27.25,485.8177083333333,27.25L489.984375,27.25" marker-end="url(#arrowhead6908)" style="fill:none"></path><defs><marker id="arrowhead6908" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M676.046875,27.25L680.2135416666666,27.25C684.3802083333334,27.25,692.7135416666666,27.25,701.046875,27.25C709.3802083333334,27.25,717.7135416666666,27.25,721.8802083333334,27.25L726.046875,27.25" marker-end="url(#arrowhead6909)" style="fill:none"></path><defs><marker id="arrowhead6909" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7602" transform="translate(55.51953125,27.25)"><rect rx="0" ry="0" x="-47.51953125" y="-19.25" width="95.0390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.51953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">词典建Trie</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7603" transform="translate(223.9921875,27.25)"><rect rx="0" ry="0" x="-70.953125" y="-19.25" width="141.90625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-60.953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">DFS网格每个起点</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7605" transform="translate(392.46484375,27.25)"><rect rx="0" ry="0" x="-47.51953125" y="-19.25" width="95.0390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.51953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">沿Trie下走</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7607" transform="translate(583.015625,27.25)"><rect rx="0" ry="0" x="-93.03125" y="-19.25" width="186.0625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-83.03125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遇isEnd收集,置null去重</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7609" transform="translate(809.76171875,27.25)"><rect rx="0" ry="0" x="-83.71484375" y="-19.25" width="167.4296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-73.71484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">越界/不在Trie→回溯</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    class Trie { Trie[] ch = new Trie[26]; String word; }    int[] dx = {-1,1,0,0}, dy = {0,0,-1,1};    public List&lt;String&gt; findWords(char[][] board, String[] words) {        Trie root = new Trie();        for (String w : words) {            Trie node = root;            for (char c : w.toCharArray()) { int i = c - &#39;a&#39;; if (node.ch[i] == null) node.ch[i] = new Trie(); node = node.ch[i]; }            node.word = w;        }        List&lt;String&gt; res = new ArrayList&lt;&gt;();        for (int i = 0; i &lt; board.length; i++)            for (int j = 0; j &lt; board[0].length; j++)                dfs(board, i, j, root, res);        return res;    }    void dfs(char[][] b, int i, int j, Trie node, List&lt;String&gt; res) {        char c = b[i][j];        if (c == &#39;#&#39; || node.ch[c - &#39;a&#39;] == null) return;        node = node.ch[c - &#39;a&#39;];        if (node.word != null) { res.add(node.word); node.word = null; }  // 收集并去重        b[i][j] = &#39;#&#39;;        for (int k = 0; k &lt; 4; k++) {            int x = i + dx[k], y = j + dy[k];            if (x &gt;= 0 &amp;&amp; x &lt; b.length &amp;&amp; y &gt;= 0 &amp;&amp; y &lt; b[0].length) dfs(b, x, y, node, res);        }        b[i][j] = c;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn × 4^L)（L 最长词），空间 O(词典总字符)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>收集后置 <code>node.word = null</code> 防同一词重复收集。<code>b[i][j]='#'</code> 标记访问、回溯恢复，Trie 剪枝只走有前缀的方向。</li></ul><hr /><h1 id="13.-%E5%9B%9E%E6%BA%AF" tabindex="-1">13. 回溯</h1><p>回溯是&quot;带撤销的 DFS&quot;。统一框架：做选择 → 递归 → 撤销选择。区分排列（用 <code>used</code>）/组合（用 <code>start</code>），收集在叶子或每个节点视题而定。</p><h2 id="17.-%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88" tabindex="-1"><a href="https://leetcode.cn/problems/letter-combinations-of-a-phone-number/" target="_blank">17. 电话号码的字母组合</a></h2><blockquote><p>数字串映射字母组合。</p></blockquote><p><strong>思路脉络</strong>：每个数字对应一组字母，笛卡尔积。回溯按数字位置推进，每层选一个字母。和热题 100 相同。</p><div class="mermaid"><svg id="render3684628131" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 719.7734375px;" viewBox="0 0 719.7734375 89.5"><style>#render3684628131 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3684628131 .error-icon{fill:#552222;}#render3684628131 .error-text{fill:#552222;stroke:#552222;}#render3684628131 .edge-thickness-normal{stroke-width:2px;}#render3684628131 .edge-thickness-thick{stroke-width:3.5px;}#render3684628131 .edge-pattern-solid{stroke-dasharray:0;}#render3684628131 .edge-pattern-dashed{stroke-dasharray:3;}#render3684628131 .edge-pattern-dotted{stroke-dasharray:2;}#render3684628131 .marker{fill:#333333;stroke:#333333;}#render3684628131 .marker.cross{stroke:#333333;}#render3684628131 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3684628131 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3684628131 .cluster-label text{fill:#333;}#render3684628131 .cluster-label span{color:#333;}#render3684628131 .label text,#render3684628131 span{fill:#333;color:#333;}#render3684628131 .node rect,#render3684628131 .node circle,#render3684628131 .node ellipse,#render3684628131 .node polygon,#render3684628131 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3684628131 .node .label{text-align:center;}#render3684628131 .node.clickable{cursor:pointer;}#render3684628131 .arrowheadPath{fill:#333333;}#render3684628131 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3684628131 .flowchart-link{stroke:#333333;fill:none;}#render3684628131 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3684628131 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3684628131 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3684628131 .cluster text{fill:#333;}#render3684628131 .cluster span{color:#333;}#render3684628131 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3684628131 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M142.48084677419354,35.125L146.99445564516128,33.8125C151.50806451612902,32.5,160.5352822580645,29.875,169.2155577956989,28.5625C177.89583333333334,27.25,186.22916666666666,27.25,190.39583333333334,27.25L194.5625,27.25" marker-end="url(#arrowhead6932)" style="fill:none"></path><defs><marker id="arrowhead6932" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M294.5625,27.25L298.7291666666667,27.25C302.8958333333333,27.25,311.2291666666667,27.25,319.5625,27.25C327.8958333333333,27.25,336.2291666666667,27.25,340.3958333333333,27.25L344.5625,27.25" marker-end="url(#arrowhead6933)" style="fill:none"></path><defs><marker id="arrowhead6933" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M497.6640625,27.25L501.8307291666667,27.25C505.9973958333333,27.25,514.3307291666666,27.25,523.6774613575269,28.5625C533.0241935483872,29.875,543.3843245967743,32.5,548.5643901209678,33.8125L553.7444556451613,35.125" marker-end="url(#arrowhead6934)" style="fill:none"></path><defs><marker id="arrowhead6934" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M553.7444556451613,73.625L548.5643901209678,74.9375C543.3843245967743,76.25,533.0241935483872,78.875,510.9189978158602,80.1875C488.8138020833333,81.5,454.9635416666667,81.5,421.11328125,81.5C387.2630208333333,81.5,353.4127604166667,81.5,323.9876302083333,81.5C294.5625,81.5,269.5625,81.5,244.5625,81.5C219.5625,81.5,194.5625,81.5,177.54889112903228,80.1875C160.5352822580645,78.875,151.50806451612902,76.25,146.99445564516128,74.9375L142.48084677419354,73.625" marker-end="url(#arrowhead6935)" style="fill:none"></path><defs><marker id="arrowhead6935" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7618" transform="translate(76.28125,54.375)"><rect rx="0" ry="0" x="-68.28125" y="-19.25" width="136.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">按数字位置i推进</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7619" transform="translate(244.5625,27.25)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">选一个字母</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7621" transform="translate(421.11328125,27.25)"><rect rx="0" ry="0" x="-76.55078125" y="-19.25" width="153.1015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-66.55078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sb.append,递归i+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7623" transform="translate(629.71875,54.375)"><rect rx="0" ry="0" x="-82.0546875" y="-19.25" width="164.109375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-72.0546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sb.deleteChar(撤销)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    String[] map = {&quot;&quot;,&quot;&quot;,&quot;abc&quot;,&quot;def&quot;,&quot;ghi&quot;,&quot;jkl&quot;,&quot;mno&quot;,&quot;pqrs&quot;,&quot;tuv&quot;,&quot;wxyz&quot;};    List&lt;String&gt; res = new ArrayList&lt;&gt;();    public List&lt;String&gt; letterCombinations(String digits) {        if (digits.isEmpty()) return res;        backtrack(digits, 0, new StringBuilder()); return res;    }    void backtrack(String d, int i, StringBuilder sb) {        if (i == d.length()) { res.add(sb.toString()); return; }        for (char c : map[d.charAt(i) - &#39;0&#39;].toCharArray()) {            sb.append(c); backtrack(d, i + 1, sb); sb.deleteCharAt(sb.length() - 1);        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(4^n)，空间 O(n)。</p></blockquote><h2 id="77.-%E7%BB%84%E5%90%88" tabindex="-1"><a href="https://leetcode.cn/problems/combinations/" target="_blank">77. 组合</a></h2><blockquote><p>返回 <code>1..n</code> 中所有 <code>k</code> 个数的组合。</p></blockquote><p><strong>思路脉络</strong>：组合用 <code>start</code> 防重复（只往后选）。每层从 <code>start</code> 到 <code>n</code>，选了就递归 <code>i+1</code>，长度到 <code>k</code> 收集。</p><div class="mermaid"><svg id="render1896175208" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 613.6796875px;" viewBox="0 0 613.6796875 89.5"><style>#render1896175208 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1896175208 .error-icon{fill:#552222;}#render1896175208 .error-text{fill:#552222;stroke:#552222;}#render1896175208 .edge-thickness-normal{stroke-width:2px;}#render1896175208 .edge-thickness-thick{stroke-width:3.5px;}#render1896175208 .edge-pattern-solid{stroke-dasharray:0;}#render1896175208 .edge-pattern-dashed{stroke-dasharray:3;}#render1896175208 .edge-pattern-dotted{stroke-dasharray:2;}#render1896175208 .marker{fill:#333333;stroke:#333333;}#render1896175208 .marker.cross{stroke:#333333;}#render1896175208 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1896175208 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1896175208 .cluster-label text{fill:#333;}#render1896175208 .cluster-label span{color:#333;}#render1896175208 .label text,#render1896175208 span{fill:#333;color:#333;}#render1896175208 .node rect,#render1896175208 .node circle,#render1896175208 .node ellipse,#render1896175208 .node polygon,#render1896175208 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1896175208 .node .label{text-align:center;}#render1896175208 .node.clickable{cursor:pointer;}#render1896175208 .arrowheadPath{fill:#333333;}#render1896175208 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1896175208 .flowchart-link{stroke:#333333;fill:none;}#render1896175208 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1896175208 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1896175208 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1896175208 .cluster text{fill:#333;}#render1896175208 .cluster span{color:#333;}#render1896175208 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1896175208 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M114.3515625,35.924361664917804L118.51822916666667,34.47863472076484C122.68489583333333,33.03290777661187,131.01822916666666,30.141453888305932,139.3515625,28.695726944152966C147.68489583333334,27.25,156.01822916666666,27.25,160.18489583333334,27.25L164.3515625,27.25" marker-end="url(#arrowhead6958)" style="fill:none"></path><defs><marker id="arrowhead6958" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M265.1796875,27.25L269.3463541666667,27.25C273.5130208333333,27.25,281.8463541666667,27.25,290.1796875,27.25C298.5130208333333,27.25,306.8463541666667,27.25,311.0130208333333,27.25L315.1796875,27.25" marker-end="url(#arrowhead6959)" style="fill:none"></path><defs><marker id="arrowhead6959" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M400.2734375,27.25L404.4401041666667,27.25C408.6067708333333,27.25,416.9401041666667,27.25,426.0762768817204,28.5625C435.21244959677415,29.875,445.15146169354836,32.5,450.12096774193543,33.8125L455.09047379032256,35.125" marker-end="url(#arrowhead6960)" style="fill:none"></path><defs><marker id="arrowhead6960" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M455.09047379032256,73.625L450.12096774193543,74.9375C445.15146169354836,76.25,435.21244959677415,78.875,418.9851310483871,80.1875C402.7578125,81.5,380.2421875,81.5,357.7265625,81.5C335.2109375,81.5,312.6953125,81.5,288.8684895833333,81.5C265.0416666666667,81.5,239.90364583333334,81.5,214.765625,81.5C189.62760416666666,81.5,164.48958333333334,81.5,147.75390625,80.05427305584703C131.01822916666666,78.60854611169407,122.68489583333333,75.71709222338814,118.51822916666667,74.27136527923517L114.3515625,72.8256383350822" marker-end="url(#arrowhead6961)" style="fill:none"></path><defs><marker id="arrowhead6961" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7634" transform="translate(61.17578125,54.375)"><rect rx="0" ry="0" x="-53.17578125" y="-19.25" width="106.3515625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.17578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">从start枚举i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7635" transform="translate(214.765625,27.25)"><rect rx="0" ry="0" x="-50.4140625" y="-19.25" width="100.828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.4140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">path.add(i)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7637" transform="translate(357.7265625,27.25)"><rect rx="0" ry="0" x="-42.546875" y="-19.25" width="85.09375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归(i+1)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7639" transform="translate(527.9765625,54.375)"><rect rx="0" ry="0" x="-77.703125" y="-19.25" width="155.40625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-67.703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">path.remove(撤销)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; combine(int n, int k) {        backtrack(n, k, 1, new ArrayList&lt;&gt;()); return res;    }    void backtrack(int n, int k, int start, List&lt;Integer&gt; path) {        if (path.size() == k) { res.add(new ArrayList&lt;&gt;(path)); return; }        for (int i = start; i &lt;= n; i++) {            path.add(i); backtrack(n, k, i + 1, path); path.remove(path.size() - 1);        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(C(n,k))，空间 O(k)。</p></blockquote><blockquote><p>[!tip] 剪枝</p></blockquote><ul><li><code>i &lt;= n - (k - path.size()) + 1</code>：剩余位置不够凑满 k 个时停，省掉无效分支。</li></ul><h2 id="46.-%E5%85%A8%E6%8E%92%E5%88%97" tabindex="-1"><a href="https://leetcode.cn/problems/permutations/" target="_blank">46. 全排列</a></h2><blockquote><p>无重复数组所有全排列。</p></blockquote><p><strong>思路脉络</strong>：排列用 <code>used</code> 标记，每层从头扫跳过已选。长度满收集。和热题 100 相同。</p><div class="mermaid"><svg id="render3648351293" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 743.7421875px;" viewBox="0 0 743.7421875 89.5"><style>#render3648351293 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3648351293 .error-icon{fill:#552222;}#render3648351293 .error-text{fill:#552222;stroke:#552222;}#render3648351293 .edge-thickness-normal{stroke-width:2px;}#render3648351293 .edge-thickness-thick{stroke-width:3.5px;}#render3648351293 .edge-pattern-solid{stroke-dasharray:0;}#render3648351293 .edge-pattern-dashed{stroke-dasharray:3;}#render3648351293 .edge-pattern-dotted{stroke-dasharray:2;}#render3648351293 .marker{fill:#333333;stroke:#333333;}#render3648351293 .marker.cross{stroke:#333333;}#render3648351293 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3648351293 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3648351293 .cluster-label text{fill:#333;}#render3648351293 .cluster-label span{color:#333;}#render3648351293 .label text,#render3648351293 span{fill:#333;color:#333;}#render3648351293 .node rect,#render3648351293 .node circle,#render3648351293 .node ellipse,#render3648351293 .node polygon,#render3648351293 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3648351293 .node .label{text-align:center;}#render3648351293 .node.clickable{cursor:pointer;}#render3648351293 .arrowheadPath{fill:#333333;}#render3648351293 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3648351293 .flowchart-link{stroke:#333333;fill:none;}#render3648351293 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3648351293 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3648351293 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3648351293 .cluster text{fill:#333;}#render3648351293 .cluster span{color:#333;}#render3648351293 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3648351293 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M134.56023185483872,35.125L138.84967237903228,33.8125C143.13911290322582,32.5,151.7179939516129,29.875,160.17410114247312,28.5625C168.63020833333334,27.25,176.96354166666666,27.25,181.13020833333334,27.25L185.296875,27.25" marker-end="url(#arrowhead6984)" style="fill:none"></path><defs><marker id="arrowhead6984" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M346.96875,27.25L351.1354166666667,27.25C355.3020833333333,27.25,363.6354166666667,27.25,371.96875,27.25C380.3020833333333,27.25,388.6354166666667,27.25,392.8020833333333,27.25L396.96875,27.25" marker-end="url(#arrowhead6985)" style="fill:none"></path><defs><marker id="arrowhead6985" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M448.96875,27.25L453.1354166666667,27.25C457.3020833333333,27.25,465.6354166666667,27.25,476.7401503696237,28.5625C487.84488407258067,29.875,501.7210181451613,32.5,508.65908518145164,33.8125L515.597152217742,35.125" marker-end="url(#arrowhead6986)" style="fill:none"></path><defs><marker id="arrowhead6986" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M515.597152217742,73.625L508.65908518145164,74.9375C501.7210181451613,76.25,487.84488407258067,78.875,472.4068170362903,80.1875C456.96875,81.5,439.96875,81.5,422.96875,81.5C405.96875,81.5,388.96875,81.5,362.8294270833333,81.5C336.6901041666667,81.5,301.4114583333333,81.5,266.1328125,81.5C230.85416666666666,81.5,195.57552083333334,81.5,173.64675739247312,80.1875C151.7179939516129,78.875,143.13911290322582,76.25,138.84967237903228,74.9375L134.56023185483872,73.625" marker-end="url(#arrowhead6987)" style="fill:none"></path><defs><marker id="arrowhead6987" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7650" transform="translate(71.6484375,54.375)"><rect rx="0" ry="0" x="-63.6484375" y="-19.25" width="127.296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.6484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历i,跳过used</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7651" transform="translate(266.1328125,27.25)"><rect rx="0" ry="0" x="-80.8359375" y="-19.25" width="161.671875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-70.8359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">path.add,used=true</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7653" transform="translate(422.96875,27.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7655" transform="translate(617.35546875,54.375)"><rect rx="0" ry="0" x="-118.38671875" y="-19.25" width="236.7734375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-108.38671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">path.remove,used=false(撤销)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; permute(int[] nums) {        backtrack(nums, new ArrayList&lt;&gt;(), new boolean[nums.length]); return res;    }    void backtrack(int[] nums, List&lt;Integer&gt; path, boolean[] used) {        if (path.size() == nums.length) { res.add(new ArrayList&lt;&gt;(path)); return; }        for (int i = 0; i &lt; nums.length; i++) {            if (used[i]) continue;            path.add(nums[i]); used[i] = true;            backtrack(nums, path, used);            path.remove(path.size() - 1); used[i] = false;        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n·n!)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>收集时 <code>new ArrayList&lt;&gt;(path)</code> 复制，不能直接 <code>add(path)</code>——path 是同一引用，回溯后被清空。</li></ul><h2 id="39.-%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/combination-sum/" target="_blank">39. 组合总和</a></h2><blockquote><p>无重复正整数，找所有和为 <code>target</code> 的组合，元素可无限用。</p></blockquote><p><strong>思路脉络</strong>：元素可重复用 → 递归传 <code>i</code>（不是 <code>i+1</code>）。排序后剪枝：<code>候选 &gt; remain</code> 就 break。和热题 100 相同。</p><pre>Error: Parse error on line 2:...&gt; B["remain-c[i]≥0?"}    B -- 是 --&gt; C["-----------------------^Expecting 'SPACE', 'GRAPH', 'DIR', 'subgraph', 'SQE', 'end', 'AMP', 'ALPHA', 'COLON', 'TAGEND', 'START_LINK', 'STYLE', 'LINKSTYLE', 'CLASSDEF', 'CLASS', 'CLICK', 'DOWN', 'UP', 'DEFAULT', 'NUM', 'COMMA', 'MINUS', 'BRKT', 'DOT', 'PCT', 'TAGSTART', 'PUNCTUATION', 'UNICODE_TEXT', 'PLUS', 'EQUALS', 'MULT', 'UNDERSCORE', got 'DIAMOND_STOP'</pre><pre><code class="language-java">class Solution {    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; combinationSum(int[] c, int target) {        Arrays.sort(c); backtrack(c, target, 0, new ArrayList&lt;&gt;()); return res;    }    void backtrack(int[] c, int remain, int start, List&lt;Integer&gt; path) {        if (remain == 0) { res.add(new ArrayList&lt;&gt;(path)); return; }        for (int i = start; i &lt; c.length; i++) {            if (c[i] &gt; remain) break;            path.add(c[i]); backtrack(c, remain - c[i], i, path); path.remove(path.size() - 1);        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(组合数)，空间 O(target/min)。</p></blockquote><h2 id="52.-n-%E7%9A%87%E5%90%8E-ii" tabindex="-1"><a href="https://leetcode.cn/problems/n-queens-ii/" target="_blank">52. N 皇后 II</a></h2><blockquote><p><code>n×n</code> 棋盘放 <code>n</code> 个皇后不互相攻击，返回摆法<strong>总数</strong>（不要求列出每种）。</p></blockquote><p><strong>思路脉络</strong>：逐行回溯，每行选一列。冲突检查只看上方（同行只一个、下方没放）：同列、主对角(row-col)、副对角(row+col)。用三个 Set 记已占列/对角，<code>valid</code> 降到 O(1)。</p><div class="mermaid"><svg id="render773961445" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="191.3156280517578" style="max-width: 760.3156127929688px;" viewBox="0 0 760.3156127929688 191.3156280517578"><style>#render773961445 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render773961445 .error-icon{fill:#552222;}#render773961445 .error-text{fill:#552222;stroke:#552222;}#render773961445 .edge-thickness-normal{stroke-width:2px;}#render773961445 .edge-thickness-thick{stroke-width:3.5px;}#render773961445 .edge-pattern-solid{stroke-dasharray:0;}#render773961445 .edge-pattern-dashed{stroke-dasharray:3;}#render773961445 .edge-pattern-dotted{stroke-dasharray:2;}#render773961445 .marker{fill:#333333;stroke:#333333;}#render773961445 .marker.cross{stroke:#333333;}#render773961445 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render773961445 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render773961445 .cluster-label text{fill:#333;}#render773961445 .cluster-label span{color:#333;}#render773961445 .label text,#render773961445 span{fill:#333;color:#333;}#render773961445 .node rect,#render773961445 .node circle,#render773961445 .node ellipse,#render773961445 .node polygon,#render773961445 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render773961445 .node .label{text-align:center;}#render773961445 .node.clickable{cursor:pointer;}#render773961445 .arrowheadPath{fill:#333333;}#render773961445 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render773961445 .flowchart-link{stroke:#333333;fill:none;}#render773961445 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render773961445 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render773961445 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render773961445 .cluster text{fill:#333;}#render773961445 .cluster span{color:#333;}#render773961445 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render773961445 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M86.7109375,116.11172103881836L90.87760416666667,116.11172103881836C95.04427083333333,116.11172103881836,103.37760416666667,116.11172103881836,111.7109375,116.11172103881836C120.04427083333333,116.11172103881836,128.37760416666666,116.11172103881836,132.54427083333334,116.11172103881836L136.7109375,116.11172103881836" marker-end="url(#arrowhead7019)" style="fill:none"></path><defs><marker id="arrowhead7019" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M200.65183777364294,96.86172103881836L208.14866689470242,91.30690320332845C215.64549601576195,85.75208536783855,230.63915425788096,74.64244969685872,245.1712233156949,70.1565583369679C259.7032923735088,65.67066697707709,273.77377224701763,67.80851992827529,280.80901218377204,68.87744640387439L287.84425212052645,69.94637287947349" marker-end="url(#arrowhead7020)" style="fill:none"></path><defs><marker id="arrowhead7020" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-B" style="opacity: 1;" id="L-C-B"><path class="path" d="M298.0027071308162,97.52770713081625L289.27439135901346,100.62504278214993C280.5460755872108,103.72237843348363,263.0894440436054,109.91704973615099,248.8611282718027,113.01438538748467C234.6328125,116.11172103881836,223.6328125,116.11172103881836,218.1328125,116.11172103881836L212.6328125,116.11172103881836" marker-end="url(#arrowhead7021)" style="fill:none"></path><defs><marker id="arrowhead7021" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M419.4484359741211,78.6578140258789L424.86510340372723,78.57448069254558C430.2817708333334,78.49114735921223,441.11510569254557,78.32448069254558,452.03177312215166,78.24114735921223C462.9484405517578,78.1578140258789,473.9484405517578,78.1578140258789,479.4484405517578,78.1578140258789L484.9484405517578,78.1578140258789" marker-end="url(#arrowhead7022)" style="fill:none"></path><defs><marker id="arrowhead7022" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M618.3156280517578,78.1578140258789L622.4822947184244,78.1578140258789C626.6489613850912,78.1578140258789,634.9822947184244,78.1578140258789,646.2273282839359,83.71263186136882C657.4723618494472,89.26744969685872,671.6290956471365,100.37708536783855,678.7074625459812,105.93190320332845L685.7858294448259,111.48672103881836" marker-end="url(#arrowhead7023)" style="fill:none"></path><defs><marker id="arrowhead7023" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-B" style="opacity: 1;" id="L-E-B"><path class="path" d="M685.7858294448259,149.98672103881836L678.7074625459812,155.54153887430826C671.6290956471365,161.0963567097982,657.4723618494472,172.20599238077799,635.1133959922691,177.7608102162679C612.7544301350912,183.3156280517578,582.1932322184244,183.3156280517578,550.2987009684244,183.3156280517578C518.4041697184244,183.3156280517578,485.1763051350911,183.3156280517578,451.3694038391113,183.3156280517578C417.56250254313153,183.3156280517578,383.1765645345052,183.3156280517578,348.7906265258789,183.3156280517578C314.4046885172526,183.3156280517578,280.0187505086263,183.3156280517578,254.3866536081458,175.3233102162679C228.75455670766527,167.33099238077799,211.87630091533057,151.3463567097982,203.4371730191632,143.35403887430826L194.99804512299585,135.36172103881836" marker-end="url(#arrowhead7024)" style="fill:none"></path><defs><marker id="arrowhead7024" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(245.6328125,116.11172103881836)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000259399414062" height="18.500301361083984"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(451.9484405517578,78.1578140258789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000259399414062" height="18.500301361083984"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7672" transform="translate(47.35546875,116.11172103881836)"><rect rx="0" ry="0" x="-39.35546875" y="-19.25" width="78.7109375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.35546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">逐行row</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7673" transform="translate(174.671875,116.11172103881836)"><rect rx="0" ry="0" x="-37.9609375" y="-19.25" width="75.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27.9609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">枚举列c</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7675" transform="translate(348.7906265258789,78.1578140258789)"><polygon points="70.1578125,0 140.315625,-70.1578125 70.1578125,-140.315625 0,-70.1578125" transform="translate(-70.1578125,70.1578125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-48.703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">col/diag冲突?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7679" transform="translate(551.6320343017578,78.1578140258789)"><rect rx="0" ry="0" x="-66.68359375" y="-19.25" width="133.3671875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.68359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">标记,递归row+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7681" transform="translate(710.3156280517578,130.73672103881836)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">撤销标记</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    int ans = 0;    Set&lt;Integer&gt; col = new HashSet&lt;&gt;(), diag1 = new HashSet&lt;&gt;(), diag2 = new HashSet&lt;&gt;();    public int totalNQueens(int n) { backtrack(n, 0); return ans; }    void backtrack(int n, int row) {        if (row == n) { ans++; return; }        for (int c = 0; c &lt; n; c++) {            int d1 = row - c, d2 = row + c;            if (col.contains(c) || diag1.contains(d1) || diag2.contains(d2)) continue;            col.add(c); diag1.add(d1); diag2.add(d2);            backtrack(n, row + 1);            col.remove(c); diag1.remove(d1); diag2.remove(d2);        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n!)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>对角线用 <code>row-col</code>、<code>row+col</code> 作 key 是经典。和 N 皇后 I 完全同模板，只是不收集棋盘只计数。</li></ul><h2 id="22.-%E6%8B%AC%E5%8F%B7%E7%94%9F%E6%88%90" tabindex="-1"><a href="https://leetcode.cn/problems/generate-parentheses/" target="_blank">22. 括号生成</a></h2><blockquote><p>生成 <code>n</code> 对括号的所有合法组合。</p></blockquote><p><strong>思路脉络</strong>：跟踪 <code>open</code>/<code>close</code> 计数。<code>open &lt; n</code> 可放左括号，<code>close &lt; open</code> 可放右括号（右括号须匹配已放左括号）。和热题 100 相同。</p><div class="mermaid"><svg id="render3596999281" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="231.5" style="max-width: 737.05859375px;" viewBox="0 0 737.05859375 231.5"><style>#render3596999281 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3596999281 .error-icon{fill:#552222;}#render3596999281 .error-text{fill:#552222;stroke:#552222;}#render3596999281 .edge-thickness-normal{stroke-width:2px;}#render3596999281 .edge-thickness-thick{stroke-width:3.5px;}#render3596999281 .edge-pattern-solid{stroke-dasharray:0;}#render3596999281 .edge-pattern-dashed{stroke-dasharray:3;}#render3596999281 .edge-pattern-dotted{stroke-dasharray:2;}#render3596999281 .marker{fill:#333333;stroke:#333333;}#render3596999281 .marker.cross{stroke:#333333;}#render3596999281 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3596999281 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3596999281 .cluster-label text{fill:#333;}#render3596999281 .cluster-label span{color:#333;}#render3596999281 .label text,#render3596999281 span{fill:#333;color:#333;}#render3596999281 .node rect,#render3596999281 .node circle,#render3596999281 .node ellipse,#render3596999281 .node polygon,#render3596999281 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3596999281 .node .label{text-align:center;}#render3596999281 .node.clickable{cursor:pointer;}#render3596999281 .arrowheadPath{fill:#333333;}#render3596999281 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3596999281 .flowchart-link{stroke:#333333;fill:none;}#render3596999281 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3596999281 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3596999281 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3596999281 .cluster text{fill:#333;}#render3596999281 .cluster span{color:#333;}#render3596999281 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3596999281 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M126.5390625,115.75L130.70572916666666,115.75C134.87239583333334,115.75,143.20572916666666,115.75,151.62239608764648,115.83333333333333C160.0390630086263,115.91666666666667,168.5390635172526,116.08333333333333,172.78906377156576,116.16666666666667L177.0390640258789,116.25000000000001" marker-end="url(#arrowhead7054)" style="fill:none"></path><defs><marker id="arrowhead7054" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M238.4024885087742,93.49155253465308L247.61222949868554,82.45129377887757C256.8219704885969,71.41103502310206,275.24145246841954,49.33051751155102,297.07092027514403,38.29025875577551C318.90038808186847,27.25,344.1398417154948,27.25,369.3792953491211,27.25C394.6187489827474,27.25,419.8582026163737,27.25,438.0996742248535,27.25C456.3411458333333,27.25,467.5846354166667,27.25,473.2063802083333,27.25L478.828125,27.25" marker-end="url(#arrowhead7055)" style="fill:none"></path><defs><marker id="arrowhead7055" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M245.56139147209063,131.8495475537882L253.5779819681159,136.54128962815682C261.59457246414115,141.23303170252547,277.62775345619167,150.61651585126273,291.2276775398634,155.3915912589647C304.82760162353514,160.16666666666666,315.99426879882816,160.33333333333334,321.5776023864746,160.41666666666666L327.1609359741211,160.5" marker-end="url(#arrowhead7056)" style="fill:none"></path><defs><marker id="arrowhead7056" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M396.8411053460267,144.74345062190562L404.88386383002234,139.91120885158801C412.92662231401783,135.0789670812704,429.01213928200895,125.4144835406352,442.55489776600444,120.5822417703176C456.09765625,115.75,467.09765625,115.75,472.59765625,115.75L478.09765625,115.75" marker-end="url(#arrowhead7057)" style="fill:none"></path><defs><marker id="arrowhead7057" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-F" style="opacity: 1;" id="L-C-F"><path class="path" d="M578.328125,27.25L582.6165364583334,27.25C586.9049479166666,27.25,595.4817708333334,27.25,606.832329184322,31.416666666666668C618.1828875353108,35.583333333333336,632.3071813206215,43.916666666666664,639.3693282132768,48.083333333333336L646.4314751059322,52.25" marker-end="url(#arrowhead7058)" style="fill:none"></path><defs><marker id="arrowhead7058" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M579.05859375,115.75L583.2252604166666,115.75C587.3919270833334,115.75,595.7252604166666,115.75,606.9540739759888,111.58333333333333C618.1828875353108,107.41666666666667,632.3071813206215,99.08333333333333,639.3693282132768,94.91666666666667L646.4314751059322,90.75" marker-end="url(#arrowhead7059)" style="fill:none"></path><defs><marker id="arrowhead7059" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-G" style="opacity: 1;" id="L-D-G"><path class="path" d="M396.84110341990134,176.25654825246195L404.8838622249178,180.92212354371827C412.92662102993427,185.58769883497462,429.01213863996713,194.9188494174873,446.6349755699835,199.58442470874365C464.2578125,204.25,483.41796875,204.25,492.998046875,204.25L502.578125,204.25" marker-end="url(#arrowhead7060)" style="fill:none"></path><defs><marker id="arrowhead7060" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(369.3792953491211,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000085830688477" height="18.500099182128906"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(293.6609344482422,160)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000085830688477" height="18.500099182128906"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(445.09765625,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000085830688477" height="18.500099182128906"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7698" transform="translate(67.26953125,115.75)"><rect rx="0" ry="0" x="-59.26953125" y="-19.25" width="118.5390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-49.26953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sb,open,close</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7699" transform="translate(218.5999984741211,115.75)"><polygon points="42.0609375,0 84.121875,-42.0609375 42.0609375,-84.121875 0,-42.0609375" transform="translate(-42.0609375,42.0609375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-17.484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">open</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7701" transform="translate(528.578125,27.25)"><rect rx="0" ry="0" x="-49.75" y="-19.25" width="99.5" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.75,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">放(,open++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7703" transform="translate(369.3792953491211,160)"><polygon points="42.718359375,0 85.43671875,-42.718359375 42.718359375,-85.43671875 0,-42.718359375" transform="translate(-42.718359375,42.718359375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.21484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">close</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7705" transform="translate(528.578125,115.75)"><rect rx="0" ry="0" x="-50.48046875" y="-19.25" width="100.9609375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.48046875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">放),close++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7707" transform="translate(679.05859375,71.5)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归后撤销</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-G-7711" transform="translate(528.578125,204.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">收集</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    List&lt;String&gt; res = new ArrayList&lt;&gt;();    public List&lt;String&gt; generateParenthesis(int n) {        backtrack(new StringBuilder(), 0, 0, n); return res;    }    void backtrack(StringBuilder sb, int open, int close, int n) {        if (sb.length() == 2 * n) { res.add(sb.toString()); return; }        if (open &lt; n) { sb.append(&#39;(&#39;); backtrack(sb, open + 1, close, n); sb.deleteCharAt(sb.length() - 1); }        if (close &lt; open) { sb.append(&#39;)&#39;); backtrack(sb, open, close + 1, n); sb.deleteCharAt(sb.length() - 1); }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(4^n/√n)（卡特兰数），空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>close &lt; open</code> 是合法性核心：右括号必须匹配一个已放的左括号。</li></ul><h2 id="79.-%E5%8D%95%E8%AF%8D%E6%90%9C%E7%B4%A2" tabindex="-1"><a href="https://leetcode.cn/problems/word-search/" target="_blank">79. 单词搜索</a></h2><blockquote><p>字符网格搜索单词，字母不可重复用。</p></blockquote><p><strong>思路脉络</strong>：每个格子作起点 DFS 四向扩展，进入先标记 <code>#</code> 防重复、退出恢复。回溯的&quot;撤销&quot;在网格题的体现。和热题 100 相同。</p><div class="mermaid"><svg id="render3087527010" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 941.1953125px;" viewBox="0 0 941.1953125 54.5"><style>#render3087527010 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3087527010 .error-icon{fill:#552222;}#render3087527010 .error-text{fill:#552222;stroke:#552222;}#render3087527010 .edge-thickness-normal{stroke-width:2px;}#render3087527010 .edge-thickness-thick{stroke-width:3.5px;}#render3087527010 .edge-pattern-solid{stroke-dasharray:0;}#render3087527010 .edge-pattern-dashed{stroke-dasharray:3;}#render3087527010 .edge-pattern-dotted{stroke-dasharray:2;}#render3087527010 .marker{fill:#333333;stroke:#333333;}#render3087527010 .marker.cross{stroke:#333333;}#render3087527010 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3087527010 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3087527010 .cluster-label text{fill:#333;}#render3087527010 .cluster-label span{color:#333;}#render3087527010 .label text,#render3087527010 span{fill:#333;color:#333;}#render3087527010 .node rect,#render3087527010 .node circle,#render3087527010 .node ellipse,#render3087527010 .node polygon,#render3087527010 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3087527010 .node .label{text-align:center;}#render3087527010 .node.clickable{cursor:pointer;}#render3087527010 .arrowheadPath{fill:#333333;}#render3087527010 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3087527010 .flowchart-link{stroke:#333333;fill:none;}#render3087527010 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3087527010 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3087527010 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3087527010 .cluster text{fill:#333;}#render3087527010 .cluster span{color:#333;}#render3087527010 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3087527010 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M108,27.25L112.16666666666667,27.25C116.33333333333333,27.25,124.66666666666667,27.25,133,27.25C141.33333333333334,27.25,149.66666666666666,27.25,153.83333333333334,27.25L158,27.25" marker-end="url(#arrowhead7087)" style="fill:none"></path><defs><marker id="arrowhead7087" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M291.3515625,27.25L295.5182291666667,27.25C299.6848958333333,27.25,308.0182291666667,27.25,316.3515625,27.25C324.6848958333333,27.25,333.0182291666667,27.25,337.1848958333333,27.25L341.3515625,27.25" marker-end="url(#arrowhead7088)" style="fill:none"></path><defs><marker id="arrowhead7088" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M401.8359375,27.25L406.0026041666667,27.25C410.1692708333333,27.25,418.5026041666667,27.25,426.8359375,27.25C435.1692708333333,27.25,443.5026041666667,27.25,447.6692708333333,27.25L451.8359375,27.25" marker-end="url(#arrowhead7089)" style="fill:none"></path><defs><marker id="arrowhead7089" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M559.4140625,27.25L563.5807291666666,27.25C567.7473958333334,27.25,576.0807291666666,27.25,584.4140625,27.25C592.7473958333334,27.25,601.0807291666666,27.25,605.2473958333334,27.25L609.4140625,27.25" marker-end="url(#arrowhead7090)" style="fill:none"></path><defs><marker id="arrowhead7090" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M753.1640625,27.25L757.3307291666666,27.25C761.4973958333334,27.25,769.8307291666666,27.25,778.1640625,27.25C786.4973958333334,27.25,794.8307291666666,27.25,798.9973958333334,27.25L803.1640625,27.25" marker-end="url(#arrowhead7091)" style="fill:none"></path><defs><marker id="arrowhead7091" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7722" transform="translate(58,27.25)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">每格作起点</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7723" transform="translate(224.67578125,27.25)"><rect rx="0" ry="0" x="-66.67578125" y="-19.25" width="133.3515625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.67578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">DFS匹配word[k]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7725" transform="translate(371.59375,27.25)"><rect rx="0" ry="0" x="-30.2421875" y="-19.25" width="60.484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20.2421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">标记#</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7727" transform="translate(505.625,27.25)"><rect rx="0" ry="0" x="-53.7890625" y="-19.25" width="107.578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.7890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">四向DFS k+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7729" transform="translate(681.2890625,27.25)"><rect rx="0" ry="0" x="-71.875" y="-19.25" width="143.75" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-61.875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">恢复原字符(撤销)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7731" transform="translate(868.1796875,27.25)"><rect rx="0" ry="0" x="-65.015625" y="-19.25" width="130.03125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">任一成功→true</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean exist(char[][] board, String word) {        int m = board.length, n = board[0].length;        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++)                if (dfs(board, word, i, j, 0)) return true;        return false;    }    boolean dfs(char[][] b, String w, int i, int j, int k) {        if (k == w.length()) return true;        if (i &lt; 0 || i &gt;= b.length || j &lt; 0 || j &gt;= b[0].length || b[i][j] != w.charAt(k)) return false;        char t = b[i][j]; b[i][j] = &#39;#&#39;;        boolean f = dfs(b, w, i+1, j, k+1) || dfs(b, w, i-1, j, k+1) || dfs(b, w, i, j+1, k+1) || dfs(b, w, i, j-1, k+1);        b[i][j] = t;        return f;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn·3^L)，空间 O(L)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>标记和恢复必须配对：<code>char t</code> 存原值，DFS 后还原。漏恢复会让其他起点的搜索踩到 <code>#</code> 误判。</li></ul><hr /><h1 id="14.-%E5%88%86%E6%B2%BB" tabindex="-1">14. 分治</h1><p>分治：找中点 → 递归处理两半 → 合并。难点在&quot;如何合并&quot;和递归终止。</p><h2 id="108.-%E5%B0%86%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E8%BD%AC%E6%8D%A2%E4%B8%BA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91" tabindex="-1"><a href="https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/" target="_blank">108. 将有序数组转换为二叉搜索树</a></h2><blockquote><p>升序数组构造高度平衡 BST。</p></blockquote><p><strong>思路脉络</strong>：BST 中序升序，平衡则每次取中点作根，左半递归造左子、右半造右子。</p><div class="mermaid"><svg id="render4202085615" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 658.9296875px;" viewBox="0 0 658.9296875 54.5"><style>#render4202085615 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render4202085615 .error-icon{fill:#552222;}#render4202085615 .error-text{fill:#552222;stroke:#552222;}#render4202085615 .edge-thickness-normal{stroke-width:2px;}#render4202085615 .edge-thickness-thick{stroke-width:3.5px;}#render4202085615 .edge-pattern-solid{stroke-dasharray:0;}#render4202085615 .edge-pattern-dashed{stroke-dasharray:3;}#render4202085615 .edge-pattern-dotted{stroke-dasharray:2;}#render4202085615 .marker{fill:#333333;stroke:#333333;}#render4202085615 .marker.cross{stroke:#333333;}#render4202085615 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render4202085615 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render4202085615 .cluster-label text{fill:#333;}#render4202085615 .cluster-label span{color:#333;}#render4202085615 .label text,#render4202085615 span{fill:#333;color:#333;}#render4202085615 .node rect,#render4202085615 .node circle,#render4202085615 .node ellipse,#render4202085615 .node polygon,#render4202085615 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render4202085615 .node .label{text-align:center;}#render4202085615 .node.clickable{cursor:pointer;}#render4202085615 .arrowheadPath{fill:#333333;}#render4202085615 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render4202085615 .flowchart-link{stroke:#333333;fill:none;}#render4202085615 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render4202085615 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render4202085615 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render4202085615 .cluster text{fill:#333;}#render4202085615 .cluster span{color:#333;}#render4202085615 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render4202085615 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M89.28125,27.25L93.44791666666667,27.25C97.61458333333333,27.25,105.94791666666667,27.25,114.28125,27.25C122.61458333333333,27.25,130.94791666666666,27.25,135.11458333333334,27.25L139.28125,27.25" marker-end="url(#arrowhead7108)" style="fill:none"></path><defs><marker id="arrowhead7108" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M259.6796875,27.25L263.8463541666667,27.25C268.0130208333333,27.25,276.3463541666667,27.25,284.6796875,27.25C293.0130208333333,27.25,301.3463541666667,27.25,305.5130208333333,27.25L309.6796875,27.25" marker-end="url(#arrowhead7109)" style="fill:none"></path><defs><marker id="arrowhead7109" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M453.296875,27.25L457.4635416666667,27.25C461.6302083333333,27.25,469.9635416666667,27.25,478.296875,27.25C486.6302083333333,27.25,494.9635416666667,27.25,499.1302083333333,27.25L503.296875,27.25" marker-end="url(#arrowhead7110)" style="fill:none"></path><defs><marker id="arrowhead7110" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7738" transform="translate(48.640625,27.25)"><rect rx="0" ry="0" x="-40.640625" y="-19.25" width="81.28125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">取中点m</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7739" transform="translate(199.48046875,27.25)"><rect rx="0" ry="0" x="-60.19921875" y="-19.25" width="120.3984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.19921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">root=nums[m]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7741" transform="translate(381.48828125,27.25)"><rect rx="0" ry="0" x="-71.80859375" y="-19.25" width="143.6171875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-61.80859375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左子:build(l,m-1)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7743" transform="translate(577.11328125,27.25)"><rect rx="0" ry="0" x="-73.81640625" y="-19.25" width="147.6328125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-63.81640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">右子:build(m+1,r)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public TreeNode sortedArrayToBST(int[] nums) {        return build(nums, 0, nums.length - 1);    }    TreeNode build(int[] nums, int l, int r) {        if (l &gt; r) return null;        int m = (l + r) / 2;        TreeNode root = new TreeNode(nums[m]);        root.left = build(nums, l, m - 1); root.right = build(nums, m + 1, r);        return root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(log n)。</p></blockquote><h2 id="148.-%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/sort-list/" target="_blank">148. 排序链表</a></h2><blockquote><p>链表排序，O(n log n)。</p></blockquote><p><strong>思路脉络</strong>：归并排序适配链表。快慢找中点 → 断开 → 递归排两半 → 合并。</p><div class="mermaid"><svg id="render4279116531" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 514.984375px;" viewBox="0 0 514.984375 143"><style>#render4279116531 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render4279116531 .error-icon{fill:#552222;}#render4279116531 .error-text{fill:#552222;stroke:#552222;}#render4279116531 .edge-thickness-normal{stroke-width:2px;}#render4279116531 .edge-thickness-thick{stroke-width:3.5px;}#render4279116531 .edge-pattern-solid{stroke-dasharray:0;}#render4279116531 .edge-pattern-dashed{stroke-dasharray:3;}#render4279116531 .edge-pattern-dotted{stroke-dasharray:2;}#render4279116531 .marker{fill:#333333;stroke:#333333;}#render4279116531 .marker.cross{stroke:#333333;}#render4279116531 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render4279116531 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render4279116531 .cluster-label text{fill:#333;}#render4279116531 .cluster-label span{color:#333;}#render4279116531 .label text,#render4279116531 span{fill:#333;color:#333;}#render4279116531 .node rect,#render4279116531 .node circle,#render4279116531 .node ellipse,#render4279116531 .node polygon,#render4279116531 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render4279116531 .node .label{text-align:center;}#render4279116531 .node.clickable{cursor:pointer;}#render4279116531 .arrowheadPath{fill:#333333;}#render4279116531 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render4279116531 .flowchart-link{stroke:#333333;fill:none;}#render4279116531 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render4279116531 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render4279116531 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render4279116531 .cluster text{fill:#333;}#render4279116531 .cluster span{color:#333;}#render4279116531 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render4279116531 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M108,71.5L112.16666666666667,71.5C116.33333333333333,71.5,124.66666666666667,71.5,133,71.5C141.33333333333334,71.5,149.66666666666666,71.5,153.83333333333334,71.5L158,71.5" marker-end="url(#arrowhead7129)" style="fill:none"></path><defs><marker id="arrowhead7129" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M206.1864406779661,52.25L210.98870056497174,48.083333333333336C215.79096045197738,43.916666666666664,225.39548022598868,35.583333333333336,234.36440677966104,31.416666666666668C243.33333333333334,27.25,251.66666666666666,27.25,255.83333333333334,27.25L260,27.25" marker-end="url(#arrowhead7130)" style="fill:none"></path><defs><marker id="arrowhead7130" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M206.1864406779661,90.75L210.98870056497174,94.91666666666667C215.79096045197738,99.08333333333333,225.39548022598868,107.41666666666667,234.36440677966104,111.58333333333333C243.33333333333334,115.75,251.66666666666666,115.75,255.83333333333334,115.75L260,115.75" marker-end="url(#arrowhead7131)" style="fill:none"></path><defs><marker id="arrowhead7131" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M360,27.25L364.1666666666667,27.25C368.3333333333333,27.25,376.6666666666667,27.25,387.7535016478343,31.416666666666668C398.8403366290019,35.583333333333336,412.68067325800376,43.916666666666664,419.60084157250475,48.083333333333336L426.5210098870057,52.25" marker-end="url(#arrowhead7132)" style="fill:none"></path><defs><marker id="arrowhead7132" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M360,115.75L364.1666666666667,115.75C368.3333333333333,115.75,376.6666666666667,115.75,387.7535016478343,111.58333333333333C398.8403366290019,107.41666666666667,412.68067325800376,99.08333333333333,419.60084157250475,94.91666666666667L426.5210098870057,90.75" marker-end="url(#arrowhead7133)" style="fill:none"></path><defs><marker id="arrowhead7133" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7754" transform="translate(58,71.5)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">快慢找中点</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7755" transform="translate(184,71.5)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">断开</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7757" transform="translate(310,27.25)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归排左半</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7759" transform="translate(310,115.75)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">递归排右半</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7761" transform="translate(458.4921875,71.5)"><rect rx="0" ry="0" x="-48.4921875" y="-19.25" width="96.984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.4921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">merge两半</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode sortList(ListNode head) {        if (head == null || head.next == null) return head;        ListNode mid = mid(head), right = mid.next; mid.next = null;        return merge(sortList(head), sortList(right));    }    ListNode mid(ListNode head) {        ListNode slow = head, fast = head.next;        while (fast != null &amp;&amp; fast.next != null) { slow = slow.next; fast = fast.next.next; }        return slow;    }    ListNode merge(ListNode a, ListNode b) {        ListNode dummy = new ListNode(0), cur = dummy;        while (a != null &amp;&amp; b != null) {            if (a.val &lt;= b.val) { cur.next = a; a = a.next; } else { cur.next = b; b = b.next; }            cur = cur.next;        }        cur.next = a != null ? a : b;        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n)，空间 O(log n)。</p></blockquote><h2 id="427.-%E5%BB%BA%E7%AB%8B%E5%9B%9B%E5%8F%89%E6%A0%91" tabindex="-1"><a href="https://leetcode.cn/problems/construct-quad-tree/" target="_blank">427. 建立四叉树</a></h2><blockquote><p><code>n×n</code> 0/1 矩阵（n 是 2 的幂），构造四叉树。</p></blockquote><p><strong>思路脉络</strong>：递归。当前块全 0 或全 1 建叶节点；否则分四象限各递归。判断全同值直接遍历。</p><div class="mermaid"><svg id="render1347814790" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 603.875px;" viewBox="0 0 603.875 143"><style>#render1347814790 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1347814790 .error-icon{fill:#552222;}#render1347814790 .error-text{fill:#552222;stroke:#552222;}#render1347814790 .edge-thickness-normal{stroke-width:2px;}#render1347814790 .edge-thickness-thick{stroke-width:3.5px;}#render1347814790 .edge-pattern-solid{stroke-dasharray:0;}#render1347814790 .edge-pattern-dashed{stroke-dasharray:3;}#render1347814790 .edge-pattern-dotted{stroke-dasharray:2;}#render1347814790 .marker{fill:#333333;stroke:#333333;}#render1347814790 .marker.cross{stroke:#333333;}#render1347814790 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1347814790 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1347814790 .cluster-label text{fill:#333;}#render1347814790 .cluster-label span{color:#333;}#render1347814790 .label text,#render1347814790 span{fill:#333;color:#333;}#render1347814790 .node rect,#render1347814790 .node circle,#render1347814790 .node ellipse,#render1347814790 .node polygon,#render1347814790 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1347814790 .node .label{text-align:center;}#render1347814790 .node.clickable{cursor:pointer;}#render1347814790 .arrowheadPath{fill:#333333;}#render1347814790 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1347814790 .flowchart-link{stroke:#333333;fill:none;}#render1347814790 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1347814790 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1347814790 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1347814790 .cluster text{fill:#333;}#render1347814790 .cluster span{color:#333;}#render1347814790 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1347814790 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M106.32274011299435,52.25L114.4147834274953,48.083333333333336C122.50682674199624,43.916666666666664,138.69091337099812,35.583333333333336,150.94962335216573,31.416666666666668C163.20833333333334,27.25,171.54166666666666,27.25,175.70833333333334,27.25L179.875,27.25" marker-end="url(#arrowhead7151)" style="fill:none"></path><defs><marker id="arrowhead7151" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-A LE-C" style="opacity: 1;" id="L-A-C"><path class="path" d="M106.32274011299435,90.75L114.4147834274953,94.91666666666667C122.50682674199624,99.08333333333333,138.69091337099812,107.41666666666667,150.94962335216573,111.58333333333333C163.20833333333334,115.75,171.54166666666666,115.75,175.70833333333334,115.75L179.875,115.75" marker-end="url(#arrowhead7152)" style="fill:none"></path><defs><marker id="arrowhead7152" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M263.875,115.75L268.0416666666667,115.75C272.2083333333333,115.75,280.5416666666667,115.75,288.875,115.75C297.2083333333333,115.75,305.5416666666667,115.75,309.7083333333333,115.75L313.875,115.75" marker-end="url(#arrowhead7153)" style="fill:none"></path><defs><marker id="arrowhead7153" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M429.875,115.75L434.0416666666667,115.75C438.2083333333333,115.75,446.5416666666667,115.75,454.875,115.75C463.2083333333333,115.75,471.5416666666667,115.75,475.7083333333333,115.75L479.875,115.75" marker-end="url(#arrowhead7154)" style="fill:none"></path><defs><marker id="arrowhead7154" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7772" transform="translate(68.9375,71.5)"><rect rx="0" ry="0" x="-60.9375" y="-19.25" width="121.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">当前块全相同?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7773" transform="translate(221.875,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">建叶节点</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7775" transform="translate(221.875,115.75)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">分四象限</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7777" transform="translate(371.875,115.75)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">各递归建子树</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7779" transform="translate(537.875,115.75)"><rect rx="0" ry="0" x="-58" y="-19.25" width="116" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回内部节点</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public Node construct(int[][] grid) {        return build(grid, 0, 0, grid.length);    }    Node build(int[][] g, int r, int c, int len) {        if (same(g, r, c, len)) return new Node(g[r][c] == 1, true);        Node node = new Node(true, false);        int half = len / 2;        node.topLeft = build(g, r, c, half);        node.topRight = build(g, r, c + half, half);        node.bottomLeft = build(g, r + half, c, half);        node.bottomRight = build(g, r + half, c + half, half);        return node;    }    boolean same(int[][] g, int r, int c, int len) {        int v = g[r][c];        for (int i = r; i &lt; r + len; i++)            for (int j = c; j &lt; c + len; j++)                if (g[i][j] != v) return false;        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(log n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>象限顺序：左上、右上、左下、右下，坐标偏移别搞反。叶节点 <code>val = g[r][c]==1</code>、<code>isLeaf=true</code>。</li></ul><h2 id="23.-%E5%90%88%E5%B9%B6-k-%E4%B8%AA%E5%8D%87%E5%BA%8F%E9%93%BE%E8%A1%A8" tabindex="-1"><a href="https://leetcode.cn/problems/merge-k-sorted-lists/" target="_blank">23. 合并 K 个升序链表</a></h2><blockquote><p>合并 <code>k</code> 个升序链表。</p></blockquote><p><strong>思路脉络</strong>：小顶堆，<code>k</code> 个头入堆，每次弹最小接上、其 next 入堆。O(n log k)。</p><div class="mermaid"><svg id="render1495537011" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="110.3687515258789" style="max-width: 662.6359252929688px;" viewBox="0 0 662.6359252929688 110.3687515258789"><style>#render1495537011 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1495537011 .error-icon{fill:#552222;}#render1495537011 .error-text{fill:#552222;stroke:#552222;}#render1495537011 .edge-thickness-normal{stroke-width:2px;}#render1495537011 .edge-thickness-thick{stroke-width:3.5px;}#render1495537011 .edge-pattern-solid{stroke-dasharray:0;}#render1495537011 .edge-pattern-dashed{stroke-dasharray:3;}#render1495537011 .edge-pattern-dotted{stroke-dasharray:2;}#render1495537011 .marker{fill:#333333;stroke:#333333;}#render1495537011 .marker.cross{stroke:#333333;}#render1495537011 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1495537011 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1495537011 .cluster-label text{fill:#333;}#render1495537011 .cluster-label span{color:#333;}#render1495537011 .label text,#render1495537011 span{fill:#333;color:#333;}#render1495537011 .node rect,#render1495537011 .node circle,#render1495537011 .node ellipse,#render1495537011 .node polygon,#render1495537011 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1495537011 .node .label{text-align:center;}#render1495537011 .node.clickable{cursor:pointer;}#render1495537011 .arrowheadPath{fill:#333333;}#render1495537011 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1495537011 .flowchart-link{stroke:#333333;fill:none;}#render1495537011 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1495537011 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1495537011 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1495537011 .cluster text{fill:#333;}#render1495537011 .cluster span{color:#333;}#render1495537011 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1495537011 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M100.0703125,59L104.23697916666667,59C108.40364583333333,59,116.73697916666667,59,125.0703125,59C133.40364583333334,59,141.73697916666666,59,145.90364583333334,59L150.0703125,59" marker-end="url(#arrowhead7182)" style="fill:none"></path><defs><marker id="arrowhead7182" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M245.5427534448819,39.75L250.46401328740157,37.666666666666664C255.38527312992127,35.583333333333336,265.22779281496065,31.416666666666668,274.315719324147,29.333333333333332C283.4036458333333,27.25,291.7369791666667,27.25,295.9036458333333,27.25L300.0703125,27.25" marker-end="url(#arrowhead7183)" style="fill:none"></path><defs><marker id="arrowhead7183" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M399.8984375,27.25L404.0651041666667,27.25C408.2317708333333,27.25,416.5651041666667,27.25,427.27397839624973,30.332792182770422C437.9828526258329,33.415584365540845,451.0672677516657,39.581168731081696,457.60947531458214,42.663960913852115L464.15168287749856,45.74675309662254" marker-end="url(#arrowhead7184)" style="fill:none"></path><defs><marker id="arrowhead7184" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M537.135935974121,59.5L542.5526034037272,59.416666666666664C547.9692708333333,59.333333333333336,558.8026056925455,59.166666666666664,569.7192731221517,59.083333333333336C580.6359405517578,59,591.6359405517578,59,597.1359405517578,59L602.6359405517578,59" marker-end="url(#arrowhead7185)" style="fill:none"></path><defs><marker id="arrowhead7185" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M464.15168496147254,73.25324593559364L457.6094770512271,76.16937161299471C451.06726914098164,79.08549729039576,437.98285332049085,84.91774864519788,418.95496832691214,87.83387432259894C399.9270833333333,90.75,374.9557291666667,90.75,349.984375,90.75C325.0130208333333,90.75,300.0416666666667,90.75,282.6347297408136,88.66666666666667C265.22779281496065,86.58333333333333,255.38527312992127,82.41666666666667,250.46401328740157,80.33333333333333L245.5427534448819,78.25" marker-end="url(#arrowhead7186)" style="fill:none"></path><defs><marker id="arrowhead7186" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(569.6359405517578,59)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000226974487305" height="18.500263214111328"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(349.984375,90.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000226974487305" height="18.500263214111328"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7790" transform="translate(54.03515625,59)"><rect rx="0" ry="0" x="-46.03515625" y="-19.25" width="92.0703125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.03515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">k个头入堆</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7791" transform="translate(200.0703125,59)"><rect rx="0" ry="0" x="-50" y="-19.25" width="100" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">弹最小接上</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7793" transform="translate(349.984375,27.25)"><rect rx="0" ry="0" x="-49.9140625" y="-19.25" width="99.828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.9140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">其next入堆</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7795" transform="translate(493.2671890258789,59)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">堆空?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7797" transform="translate(628.6359405517578,59)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">返回</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public ListNode mergeKLists(ListNode[] lists) {        if (lists == null || lists.length == 0) return null;        PriorityQueue&lt;ListNode&gt; pq = new PriorityQueue&lt;&gt;((a, b) -&gt; a.val - b.val);        for (ListNode h : lists) if (h != null) pq.offer(h);        ListNode dummy = new ListNode(0), cur = dummy;        while (!pq.isEmpty()) { ListNode n = pq.poll(); cur.next = n; cur = cur.next; if (n.next != null) pq.offer(n.next); }        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log k)，空间 O(k)。</p></blockquote><blockquote><p>[!tip] 另解</p></blockquote><ul><li>分治两两合并也是 O(n log k)，不依赖堆。把合并两个有序链表当子程序，两两配对合并到剩一个。</li></ul><hr /><h1 id="15.-kadane-%E7%AE%97%E6%B3%95" tabindex="-1">15. Kadane 算法</h1><p>Kadane 求最大子数组和：<code>dp[i] = max(nums[i], dp[i-1]+nums[i])</code>，压缩成 <code>pre</code> 变量。环形版用&quot;总和 − 最小子数组和&quot;求跨边界的最大。</p><h2 id="53.-%E6%9C%80%E5%A4%A7%E5%AD%90%E6%95%B0%E7%BB%84%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/maximum-subarray/" target="_blank">53. 最大子数组和</a></h2><blockquote><p>找和最大的连续子数组。</p></blockquote><p><strong>思路脉络</strong>：DP 压缩。<code>pre</code> = 以当前结尾的最大和，<code>pre = max(num, pre+num)</code>，取 max。<code>ans</code> 初值用 <code>MIN_VALUE</code>（全负）。</p><div class="mermaid"><svg id="render3651777144" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 538.953125px;" viewBox="0 0 538.953125 89.5"><style>#render3651777144 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3651777144 .error-icon{fill:#552222;}#render3651777144 .error-text{fill:#552222;stroke:#552222;}#render3651777144 .edge-thickness-normal{stroke-width:2px;}#render3651777144 .edge-thickness-thick{stroke-width:3.5px;}#render3651777144 .edge-pattern-solid{stroke-dasharray:0;}#render3651777144 .edge-pattern-dashed{stroke-dasharray:3;}#render3651777144 .edge-pattern-dotted{stroke-dasharray:2;}#render3651777144 .marker{fill:#333333;stroke:#333333;}#render3651777144 .marker.cross{stroke:#333333;}#render3651777144 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3651777144 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3651777144 .cluster-label text{fill:#333;}#render3651777144 .cluster-label span{color:#333;}#render3651777144 .label text,#render3651777144 span{fill:#333;color:#333;}#render3651777144 .node rect,#render3651777144 .node circle,#render3651777144 .node ellipse,#render3651777144 .node polygon,#render3651777144 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3651777144 .node .label{text-align:center;}#render3651777144 .node.clickable{cursor:pointer;}#render3651777144 .arrowheadPath{fill:#333333;}#render3651777144 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3651777144 .flowchart-link{stroke:#333333;fill:none;}#render3651777144 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3651777144 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3651777144 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3651777144 .cluster text{fill:#333;}#render3651777144 .cluster span{color:#333;}#render3651777144 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3651777144 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M90.765625,37.46537013063434L94.93229166666667,35.762808442195286C99.09895833333333,34.060246753756225,107.43229166666667,30.655123376878112,115.765625,28.952561688439058C124.09895833333333,27.25,132.43229166666666,27.25,136.59895833333334,27.25L140.765625,27.25" marker-end="url(#arrowhead7202)" style="fill:none"></path><defs><marker id="arrowhead7202" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M334.125,27.25L338.2916666666667,27.25C342.4583333333333,27.25,350.7916666666667,27.25,359.72030409946234,28.5625C368.64894153225805,29.875,378.1728830645161,32.5,382.9348538306452,33.8125L387.6968245967742,35.125" marker-end="url(#arrowhead7203)" style="fill:none"></path><defs><marker id="arrowhead7203" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M387.6968245967742,73.625L382.9348538306452,74.9375C378.1728830645161,76.25,368.64894153225805,78.875,343.60702284946234,80.1875C318.5651041666667,81.5,278.0052083333333,81.5,237.4453125,81.5C196.88541666666666,81.5,156.32552083333334,81.5,131.87890625,79.79743831156094C107.43229166666667,78.09487662312189,99.09895833333333,74.68975324624377,94.93229166666667,72.9871915578047L90.765625,71.28462986936566" marker-end="url(#arrowhead7204)" style="fill:none"></path><defs><marker id="arrowhead7204" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7806" transform="translate(49.3828125,54.375)"><rect rx="0" ry="0" x="-41.3828125" y="-19.25" width="82.765625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.3828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历num</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7807" transform="translate(237.4453125,27.25)"><rect rx="0" ry="0" x="-96.6796875" y="-19.25" width="193.359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-86.6796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">pre=max(num,pre+num)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7809" transform="translate(457.5390625,54.375)"><rect rx="0" ry="0" x="-73.4140625" y="-19.25" width="146.828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-63.4140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans=max(ans,pre)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxSubArray(int[] nums) {        int pre = 0, ans = Integer.MIN_VALUE;        for (int n : nums) { pre = Math.max(n, pre + n); ans = Math.max(ans, pre); }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="918.-%E7%8E%AF%E5%BD%A2%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%A4%A7%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/maximum-sum-circular-subarray/" target="_blank">918. 环形子数组的最大和</a></h2><blockquote><p>子数组可跨首尾相连（环形），求最大和。</p></blockquote><p><strong>思路脉络</strong>：两种情况——最大子数组不跨边界（普通 Kadane）、跨边界（= 总和 − 最小子数组）。注意全负特例：若总和 − 最小子数组 = 0（最小子数组就是整个数组），这种情况不合法（子数组不能空），退化成普通最大。</p><div class="mermaid"><svg id="render3929255987" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 1167.9296875px;" viewBox="0 0 1167.9296875 54.5"><style>#render3929255987 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3929255987 .error-icon{fill:#552222;}#render3929255987 .error-text{fill:#552222;stroke:#552222;}#render3929255987 .edge-thickness-normal{stroke-width:2px;}#render3929255987 .edge-thickness-thick{stroke-width:3.5px;}#render3929255987 .edge-pattern-solid{stroke-dasharray:0;}#render3929255987 .edge-pattern-dashed{stroke-dasharray:3;}#render3929255987 .edge-pattern-dotted{stroke-dasharray:2;}#render3929255987 .marker{fill:#333333;stroke:#333333;}#render3929255987 .marker.cross{stroke:#333333;}#render3929255987 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3929255987 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3929255987 .cluster-label text{fill:#333;}#render3929255987 .cluster-label span{color:#333;}#render3929255987 .label text,#render3929255987 span{fill:#333;color:#333;}#render3929255987 .node rect,#render3929255987 .node circle,#render3929255987 .node ellipse,#render3929255987 .node polygon,#render3929255987 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3929255987 .node .label{text-align:center;}#render3929255987 .node.clickable{cursor:pointer;}#render3929255987 .arrowheadPath{fill:#333333;}#render3929255987 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3929255987 .flowchart-link{stroke:#333333;fill:none;}#render3929255987 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3929255987 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3929255987 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3929255987 .cluster text{fill:#333;}#render3929255987 .cluster span{color:#333;}#render3929255987 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3929255987 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M68.7421875,27.25L72.90885416666667,27.25C77.07552083333333,27.25,85.40885416666667,27.25,93.7421875,27.25C102.07552083333333,27.25,110.40885416666667,27.25,114.57552083333333,27.25L118.7421875,27.25" marker-end="url(#arrowhead7226)" style="fill:none"></path><defs><marker id="arrowhead7226" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M418.9453125,27.25L423.1119791666667,27.25C427.2786458333333,27.25,435.6119791666667,27.25,443.9453125,27.25C452.2786458333333,27.25,460.6119791666667,27.25,464.7786458333333,27.25L468.9453125,27.25" marker-end="url(#arrowhead7227)" style="fill:none"></path><defs><marker id="arrowhead7227" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M756.6796875,27.25L760.8463541666666,27.25C765.0130208333334,27.25,773.3463541666666,27.25,781.6796875,27.25C790.0130208333334,27.25,798.3463541666666,27.25,802.5130208333334,27.25L806.6796875,27.25" marker-end="url(#arrowhead7228)" style="fill:none"></path><defs><marker id="arrowhead7228" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M886.6015625,27.25L890.7682291666666,27.25C894.9348958333334,27.25,903.2682291666666,27.25,911.6015625,27.25C919.9348958333334,27.25,928.2682291666666,27.25,932.4348958333334,27.25L936.6015625,27.25" marker-end="url(#arrowhead7229)" style="fill:none"></path><defs><marker id="arrowhead7229" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7820" transform="translate(38.37109375,27.25)"><rect rx="0" ry="0" x="-30.37109375" y="-19.25" width="60.7421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20.37109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7821" transform="translate(268.84375,27.25)"><rect rx="0" ry="0" x="-150.1015625" y="-19.25" width="300.203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-140.1015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">curMax=max(n,curMax+n),maxSum更新</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7823" transform="translate(612.8125,27.25)"><rect rx="0" ry="0" x="-143.8671875" y="-19.25" width="287.734375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-133.8671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">curMin=min(n,curMin+n),minSum更新</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7825" transform="translate(846.640625,27.25)"><rect rx="0" ry="0" x="-39.9609375" y="-19.25" width="79.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.9609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">total+=n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7827" transform="translate(1048.265625,27.25)"><rect rx="0" ry="0" x="-111.6640625" y="-19.25" width="223.328125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-101.6640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">max(maxSum,total-minSum)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxSubarraySumCircular(int[] nums) {        int total = 0, curMax = 0, maxSum = nums[0], curMin = 0, minSum = nums[0];        for (int n : nums) {            total += n;            curMax = Math.max(n, curMax + n); maxSum = Math.max(maxSum, curMax);            curMin = Math.min(n, curMin + n); minSum = Math.min(minSum, curMin);        }        return maxSum &gt; 0 ? Math.max(maxSum, total - minSum) : maxSum;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>maxSum &gt; 0</code> 判断关键：若 <code>maxSum &lt;= 0</code> 说明全负，<code>total - minSum = 0</code>（空子数组）不合法，只能返回普通最大 <code>maxSum</code>。否则取 <code>max(普通最大, total - 最小子数组)</code>。</li></ul><hr /><h1 id="16.-%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE" tabindex="-1">16. 二分查找</h1><p>二分的本质是在单调性上收缩边界。难点全在边界处理：精确值、左界、右界三种目标的循环条件和收缩方式不同。</p><blockquote><p>[!tip] 左闭右闭三态<br />统一 <code>[l,r]</code> + <code>while(l&lt;=r)</code>：精确值命中直接返回；左界命中也 <code>r=m-1</code> 往左压、记候选；右界命中 <code>l=m+1</code> 往右压。</p></blockquote><h2 id="35.-%E6%90%9C%E7%B4%A2%E6%8F%92%E5%85%A5%E4%BD%8D%E7%BD%AE" tabindex="-1"><a href="https://leetcode.cn/problems/search-insert-position/" target="_blank">35. 搜索插入位置</a></h2><blockquote><p>有序数组找 <code>target</code>，找到返回下标，没有返回应插入位置。</p></blockquote><p><strong>思路脉络</strong>：标准精确二分。找不到时 <code>l</code> 正好停在应插入位置（循环结束 <code>l=r+1</code>，<code>l</code> 指向第一个大于 target 的位置）。</p><div class="mermaid"><svg id="render2881073712" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="326.6875" style="max-width: 534.125px;" viewBox="0 0 534.125 326.6875"><style>#render2881073712 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2881073712 .error-icon{fill:#552222;}#render2881073712 .error-text{fill:#552222;stroke:#552222;}#render2881073712 .edge-thickness-normal{stroke-width:2px;}#render2881073712 .edge-thickness-thick{stroke-width:3.5px;}#render2881073712 .edge-pattern-solid{stroke-dasharray:0;}#render2881073712 .edge-pattern-dashed{stroke-dasharray:3;}#render2881073712 .edge-pattern-dotted{stroke-dasharray:2;}#render2881073712 .marker{fill:#333333;stroke:#333333;}#render2881073712 .marker.cross{stroke:#333333;}#render2881073712 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2881073712 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2881073712 .cluster-label text{fill:#333;}#render2881073712 .cluster-label span{color:#333;}#render2881073712 .label text,#render2881073712 span{fill:#333;color:#333;}#render2881073712 .node rect,#render2881073712 .node circle,#render2881073712 .node ellipse,#render2881073712 .node polygon,#render2881073712 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2881073712 .node .label{text-align:center;}#render2881073712 .node.clickable{cursor:pointer;}#render2881073712 .arrowheadPath{fill:#333333;}#render2881073712 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2881073712 .flowchart-link{stroke:#333333;fill:none;}#render2881073712 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2881073712 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2881073712 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2881073712 .cluster text{fill:#333;}#render2881073712 .cluster span{color:#333;}#render2881073712 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2881073712 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M99.39961946494465,191.6875L113.3798912207872,175.13541666666666C127.36016297662975,158.58333333333334,155.32070648831487,125.47916666666667,173.55097824415745,109.01041666666667C191.78125,92.54166666666667,200.28125,92.70833333333333,204.53125,92.79166666666667L208.78125,92.875" marker-end="url(#arrowhead7253)" style="fill:none"></path><defs><marker id="arrowhead7253" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M368.18288352272725,83.52663352272727L375.1576112689393,82.56386126893939C382.1323390151515,81.60108901515152,396.08179450757575,79.67554450757575,408.5565222537878,78.71277225378788C421.03125,77.75,432.03125,77.75,437.53125,77.75L443.03125,77.75" marker-end="url(#arrowhead7254)" style="fill:none"></path><defs><marker id="arrowhead7254" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M354.43090197215776,115.97534802784223L363.69762664346473,119.41695668986853C372.9643513147718,122.85856535189482,391.4978006573859,129.7417826759474,407.92468157869297,137.51160148783745C424.3515625,145.28142029972753,438.671875,153.93784059945503,445.83203125,158.2660507493188L452.9921875,162.59426089918256" marker-end="url(#arrowhead7255)" style="fill:none"></path><defs><marker id="arrowhead7255" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-E" style="opacity: 1;" id="L-B-E"><path class="path" d="M328.5022794445684,141.90397055543164L342.09044120380696,160.78455879619304C355.67860296304553,179.66514703695444,382.8549264815228,217.42632351847723,403.6878799074281,239.18599895269912C424.5208333333333,260.945674386921,439.0104166666667,266.70384877384197,446.2552083333333,269.5829359673025L453.5,272.46202316076295" marker-end="url(#arrowhead7256)" style="fill:none"></path><defs><marker id="arrowhead7256" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M452.9921875,200.43647820163488L445.83203125,204.68664850136238C438.671875,208.9368188010899,424.3515625,217.43715940054494,397.62890625,221.68732970027247C370.90625,225.9375,331.78125,225.9375,293.9895833333333,225.9375C256.1979166666667,225.9375,219.73958333333334,225.9375,191.90299479166666,224.49841043844594C164.06640625,223.0593208768919,144.8515625,220.18114175378375,135.244140625,218.7420521922297L125.63671875,217.3029626306756" marker-end="url(#arrowhead7257)" style="fill:none"></path><defs><marker id="arrowhead7257" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M453.5,290.90957084468664L446.2552083333333,292.3308923705722C439.0104166666667,293.75221389645776,424.5208333333333,296.5948569482289,397.7135416666667,298.01617847411444C370.90625,299.4375,331.78125,299.4375,293.9895833333333,299.4375C256.1979166666667,299.4375,219.73958333333334,299.4375,188.45064589218455,287.8958333333333C157.1617084510358,276.3541666666667,131.04216690207156,253.27083333333334,117.98239612758947,241.72916666666666L104.92262535310735,230.1875" marker-end="url(#arrowhead7258)" style="fill:none"></path><defs><marker id="arrowhead7258" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(410.03125,77.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(410.03125,255.1875)"><g transform="translate(-7.3671875,-9.25)" class="label"><rect rx="0" ry="0" width="14.734375" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">&gt;t</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7841" transform="translate(83.140625,210.9375)"><rect rx="0" ry="0" x="-42.49609375" y="-19.25" width="84.9921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.49609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7842" transform="translate(292.65625,92.375)"><polygon points="84.375,0 168.75,-84.375 84.375,-168.75 0,-84.375" transform="translate(-84.375,84.375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-64.5,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[m]==target?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7844" transform="translate(484.578125,77.75)"><rect rx="0" ry="0" x="-41.546875" y="-19.25" width="83.09375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return m</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7846" transform="translate(484.578125,181.6875)"><rect rx="0" ry="0" x="-31.5859375" y="-19.25" width="63.171875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.5859375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=m+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7848" transform="translate(484.578125,284.8125)"><rect rx="0" ry="0" x="-31.078125" y="-19.25" width="62.15625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r=m-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7853" transform="translate(83.140625,299.4375)"><rect rx="0" ry="0" x="-75.140625" y="-19.25" width="150.28125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-65.140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return l(插入位置)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int searchInsert(int[] nums, int target) {        int l = 0, r = nums.length - 1;        while (l &lt;= r) {            int m = (l + r) / 2;            if (nums[m] == target) return m;            else if (nums[m] &lt; target) l = m + 1; else r = m - 1;        }        return l;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><h2 id="74.-%E6%90%9C%E7%B4%A2%E4%BA%8C%E7%BB%B4%E7%9F%A9%E9%98%B5" tabindex="-1"><a href="https://leetcode.cn/problems/search-a-2d-matrix/" target="_blank">74. 搜索二维矩阵</a></h2><blockquote><p>每行升序，行间首大于上行尾，找 <code>target</code>。</p></blockquote><p><strong>思路脉络</strong>：行间严格衔接等价一维升序——<code>mid</code> 映射到 <code>matrix[mid/n][mid%n]</code>，一次二分。</p><div class="mermaid"><svg id="render505041591" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="268.2269592285156" style="max-width: 725.8132934570312px;" viewBox="0 0 725.8132934570312 268.2269592285156"><style>#render505041591 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render505041591 .error-icon{fill:#552222;}#render505041591 .error-text{fill:#552222;stroke:#552222;}#render505041591 .edge-thickness-normal{stroke-width:2px;}#render505041591 .edge-thickness-thick{stroke-width:3.5px;}#render505041591 .edge-pattern-solid{stroke-dasharray:0;}#render505041591 .edge-pattern-dashed{stroke-dasharray:3;}#render505041591 .edge-pattern-dotted{stroke-dasharray:2;}#render505041591 .marker{fill:#333333;stroke:#333333;}#render505041591 .marker.cross{stroke:#333333;}#render505041591 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render505041591 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render505041591 .cluster-label text{fill:#333;}#render505041591 .cluster-label span{color:#333;}#render505041591 .label text,#render505041591 span{fill:#333;color:#333;}#render505041591 .node rect,#render505041591 .node circle,#render505041591 .node ellipse,#render505041591 .node polygon,#render505041591 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render505041591 .node .label{text-align:center;}#render505041591 .node.clickable{cursor:pointer;}#render505041591 .arrowheadPath{fill:#333333;}#render505041591 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render505041591 .flowchart-link{stroke:#333333;fill:none;}#render505041591 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render505041591 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render505041591 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render505041591 .cluster text{fill:#333;}#render505041591 .cluster span{color:#333;}#render505041591 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render505041591 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M72.8258631405416,162.8519515991211L83.54629220045133,146.66829299926758C94.26672126036107,130.48463439941406,115.70757938018055,98.11731719970703,130.59467510675694,81.93365859985352C145.48177083333334,65.75,153.81510416666666,65.75,157.98177083333334,65.75L162.1484375,65.75" marker-end="url(#arrowhead7292)" style="fill:none"></path><defs><marker id="arrowhead7292" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M400.2265625,65.75L404.3932291666667,65.75C408.5598958333333,65.75,416.8932291666667,65.75,425.30989608764645,65.83333333333333C433.7265630086263,65.91666666666667,442.22656351725254,66.08333333333333,446.4765637715657,66.16666666666667L450.7265640258789,66.25" marker-end="url(#arrowhead7293)" style="fill:none"></path><defs><marker id="arrowhead7293" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M547.2360644691158,50.05559724499463L556.3713292573035,46.254664370828856C565.5065940454913,42.45373149666309,583.7771236218667,34.85186574833154,601.6500186183878,31.05093287416577C619.5229136149088,27.25,636.9981740315756,27.25,645.7358042399088,27.25L654.4734344482422,27.25" marker-end="url(#arrowhead7294)" style="fill:none"></path><defs><marker id="arrowhead7294" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M551.7361980750975,77.94427220078165L560.1214405956216,80.07856016731805C568.5066831161457,82.21284813385444,585.277168157194,86.48142406692722,601.1297378361164,93.02004133339035C616.982307515039,99.55865859985352,631.9169618318357,108.36731719970703,639.384288990234,112.77164649963379L646.8516161486325,117.17597579956055" marker-end="url(#arrowhead7295)" style="fill:none"></path><defs><marker id="arrowhead7295" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-F" style="opacity: 1;" id="L-C-F"><path class="path" d="M530.9869222215186,98.6935480543604L542.8303773843058,114.79478601189378C554.6738325470932,130.89602396942712,578.3607428726676,163.09849988449383,597.6715251938532,183.60406714195395C616.982307515039,204.10963439941406,631.9169618318357,212.91829299926758,639.384288990234,217.32262229919434L646.8516161486325,221.7269515991211" marker-end="url(#arrowhead7296)" style="fill:none"></path><defs><marker id="arrowhead7296" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M641.1648406982422,151.08680177686193L634.6453094482422,153.58083078064502C628.1257781982422,156.07485978442813,615.0867156982422,161.06291779199435,592.6556612650553,163.55694679577743C570.2246068318685,166.05097579956055,538.4015604654948,166.05097579956055,508.9313786824544,166.05097579956055C479.46119689941406,166.05097579956055,452.34387969970703,166.05097579956055,414.77871068318683,166.05097579956055C377.2135416666667,166.05097579956055,329.2005208333333,166.05097579956055,281.1875,166.05097579956055C233.17447916666666,166.05097579956055,185.16145833333334,166.05097579956055,156.98828125,166.91869871542212C128.81510416666666,167.78642163128367,120.48177083333333,169.52186746300683,116.31510416666667,170.3895903788684L112.1484375,171.25731329472995" marker-end="url(#arrowhead7297)" style="fill:none"></path><defs><marker id="arrowhead7297" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-A" style="opacity: 1;" id="L-F-A"><path class="path" d="M641.6726531982422,243.23544466343384L635.0684865315756,243.62986248604838C628.4643198649088,244.02428030866292,615.2559865315756,244.81311595389198,592.740296681722,245.20753377650655C570.2246068318685,245.6019515991211,538.4015604654948,245.6019515991211,508.9313786824544,245.6019515991211C479.46119689941406,245.6019515991211,452.34387969970703,245.6019515991211,414.77871068318683,245.6019515991211C377.2135416666667,245.6019515991211,329.2005208333333,245.6019515991211,281.1875,245.6019515991211C233.17447916666666,245.6019515991211,185.16145833333334,245.6019515991211,152.20341463664698,238.2269515991211C119.24537093996064,230.8519515991211,101.34230437992126,216.1019515991211,92.39077109990158,208.7269515991211L83.4392378198819,201.3519515991211" marker-end="url(#arrowhead7298)" style="fill:none"></path><defs><marker id="arrowhead7298" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(602.0476531982422,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000024795532227" height="18.500028610229492"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(602.0476531982422,90.75)"><g transform="translate(-14.1171875,-9.25)" class="label"><rect rx="0" ry="0" width="28.226606369018555" height="18.500028610229492"></rect><text><tspan xml:space="preserve" dy="1em" x="1">&amp;lt;</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(602.0476531982422,195.30097579956055)"><g transform="translate(-4.1953125,-9.25)" class="label"><rect rx="0" ry="0" width="8.39063835144043" height="18.500028610229492"></rect><text><tspan xml:space="preserve" dy="1em" x="1">&gt;</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7868" transform="translate(60.07421875,182.1019515991211)"><rect rx="0" ry="0" x="-52.07421875" y="-19.25" width="104.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-42.07421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=m*n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7869" transform="translate(281.1875,65.75)"><rect rx="0" ry="0" x="-119.0390625" y="-19.25" width="238.078125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-109.0390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">mid映射matrix[mid/n][mid%n]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7871" transform="translate(506.5785140991211,65.75)"><polygon points="56.351953125,0 112.70390625,-56.351953125 56.351953125,-112.70390625 0,-56.351953125" transform="translate(-56.351953125,56.351953125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-33.36328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">==target?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7873" transform="translate(679.4890594482422,27.25)"><rect rx="0" ry="0" x="-25.015625" y="-19.25" width="50.03125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-15.015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">true</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7875" transform="translate(679.4890594482422,136.42597579956055)"><rect rx="0" ry="0" x="-38.32421875" y="-19.25" width="76.6484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-28.32421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=mid+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7877" transform="translate(679.4890594482422,240.9769515991211)"><rect rx="0" ry="0" x="-37.81640625" y="-19.25" width="75.6328125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27.81640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r=mid-1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean searchMatrix(int[][] matrix, int target) {        int m = matrix.length, n = matrix[0].length, l = 0, r = m * n - 1;        while (l &lt;= r) {            int mid = (l + r) / 2, v = matrix[mid / n][mid % n];            if (v == target) return true;            else if (v &lt; target) l = mid + 1; else r = mid - 1;        }        return false;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log mn)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 辨析</p></blockquote><ul><li>注意和 [[#240. 搜索二维矩阵 II]] 区别：那题每行升、每列升但行间未必衔接，不能压成一维，得用右上角 O(m+n)。本题行间严格衔接，才能一维二分。</li></ul><h2 id="162.-%E5%AF%BB%E6%89%BE%E5%B3%B0%E5%80%BC" tabindex="-1"><a href="https://leetcode.cn/problems/find-peak-element/" target="_blank">162. 寻找峰值</a></h2><blockquote><p>数组找任一峰值（大于相邻），<code>nums[-1]=nums[n]=-∞</code>，要求 O(log n)。</p></blockquote><p><strong>思路脉络</strong>：二分。<code>nums[mid] &lt; nums[mid+1]</code> 说明右侧必有峰值（上坡必到峰），去右；否则左侧（含 mid）有峰值。相邻不等必有峰。</p><div class="mermaid"><svg id="render2615960549" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 722.0257568359375px;" viewBox="0 0 722.0257568359375 143"><style>#render2615960549 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2615960549 .error-icon{fill:#552222;}#render2615960549 .error-text{fill:#552222;stroke:#552222;}#render2615960549 .edge-thickness-normal{stroke-width:2px;}#render2615960549 .edge-thickness-thick{stroke-width:3.5px;}#render2615960549 .edge-pattern-solid{stroke-dasharray:0;}#render2615960549 .edge-pattern-dashed{stroke-dasharray:3;}#render2615960549 .edge-pattern-dotted{stroke-dasharray:2;}#render2615960549 .marker{fill:#333333;stroke:#333333;}#render2615960549 .marker.cross{stroke:#333333;}#render2615960549 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2615960549 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2615960549 .cluster-label text{fill:#333;}#render2615960549 .cluster-label span{color:#333;}#render2615960549 .label text,#render2615960549 span{fill:#333;color:#333;}#render2615960549 .node rect,#render2615960549 .node circle,#render2615960549 .node ellipse,#render2615960549 .node polygon,#render2615960549 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2615960549 .node .label{text-align:center;}#render2615960549 .node.clickable{cursor:pointer;}#render2615960549 .arrowheadPath{fill:#333333;}#render2615960549 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2615960549 .flowchart-link{stroke:#333333;fill:none;}#render2615960549 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2615960549 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2615960549 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2615960549 .cluster text{fill:#333;}#render2615960549 .cluster span{color:#333;}#render2615960549 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2615960549 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92.9921875,71.5L97.15885416666667,71.5C101.32552083333333,71.5,109.65885416666667,71.5,118.0755210876465,71.58333333333333C126.49218800862631,71.66666666666667,134.9921885172526,71.83333333333333,139.24218877156576,71.91666666666667L143.4921890258789,72" marker-end="url(#arrowhead7325)" style="fill:none"></path><defs><marker id="arrowhead7325" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M233.91368401991664,53.72540429579556L242.37611638297088,49.312836913162975C250.83854874602517,44.90026953053037,267.76341347213366,36.07513476526518,281.72584583518795,31.66256738263259C295.6882781982422,27.25,306.6882781982422,27.25,312.1882781982422,27.25L317.6882781982422,27.25" marker-end="url(#arrowhead7326)" style="fill:none"></path><defs><marker id="arrowhead7326" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M233.91368604551724,90.27459673036164L242.37611807097142,94.52049727530137C250.83855009642556,98.76639782024108,267.7634141473339,107.25819891012054,281.8925128394547,111.50409945506027C296.0216115315755,115.75,307.3549448649089,115.75,313.0216115315755,115.75L318.6882781982422,115.75" marker-end="url(#arrowhead7327)" style="fill:none"></path><defs><marker id="arrowhead7327" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M440.6101531982422,27.25L444.7768198649089,27.25C448.9434865315755,27.25,457.2768198649089,27.25,468.45462202620143,31.94719771155084C479.63242418749405,36.64439542310168,493.6546951767459,46.03879084620335,500.66583067137185,50.73598855775419L507.6769661659978,55.43318626930503" marker-end="url(#arrowhead7328)" style="fill:none"></path><defs><marker id="arrowhead7328" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M439.6101531982422,115.75L443.9434865315755,115.75C448.2768198649089,115.75,456.9434865315755,115.75,468.28795551218445,111.21946885345233C479.6324244927933,106.68893770690464,493.65469578734445,97.62787541380926,500.66583143462003,93.09734426726159L507.6769670818956,88.56681312071392" marker-end="url(#arrowhead7329)" style="fill:none"></path><defs><marker id="arrowhead7329" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M573.9945274353026,72L579.4111944834391,71.91666666666667C584.8278615315754,71.83333333333333,595.6611956278483,71.66666666666667,606.5778626759846,71.58333333333333C617.4945297241211,71.5,628.4945297241211,71.5,633.9945297241211,71.5L639.4945297241211,71.5" marker-end="url(#arrowhead7330)" style="fill:none"></path><defs><marker id="arrowhead7330" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(284.6882781982422,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000049591064453" height="18.500059127807617"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(284.6882781982422,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000049591064453" height="18.500059127807617"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(606.4945297241211,71.5)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000049591064453" height="18.500059127807617"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7894" transform="translate(50.49609375,71.5)"><rect rx="0" ry="0" x="-42.49609375" y="-19.25" width="84.9921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.49609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7895" transform="translate(197.3402328491211,71.5)"><polygon points="54.348046875,0 108.69609375,-54.348046875 54.348046875,-108.69609375 0,-54.348046875" transform="translate(-54.348046875,54.348046875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-31.13671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[m]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7897" transform="translate(379.1492156982422,27.25)"><rect rx="0" ry="0" x="-61.4609375" y="-19.25" width="122.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-51.4609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=m+1(右有峰)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7899" transform="translate(379.1492156982422,115.75)"><rect rx="0" ry="0" x="-60.4609375" y="-19.25" width="120.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.4609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r=m(m可能峰)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7901" transform="translate(532.0523414611816,71.5)"><polygon points="41.4421875,0 82.884375,-41.4421875 41.4421875,-82.884375 0,-41.4421875" transform="translate(-41.4421875,41.4421875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-16.796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l==r?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7905" transform="translate(676.7601547241211,71.5)"><rect rx="0" ry="0" x="-37.265625" y="-19.25" width="74.53125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-27.265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return l</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int findPeakElement(int[] nums) {        int l = 0, r = nums.length - 1;        while (l &lt; r) {            int m = (l + r) / 2;            if (nums[m] &lt; nums[m + 1]) l = m + 1; else r = m;        }        return l;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>nums[m] &lt; nums[m+1]</code> 去 <code>l=m+1</code>（右半必有峰），否则 <code>r=m</code>（m 自己可能是峰）。<code>l&lt;r</code> 不是 <code>l&lt;=r</code>，因为比较 <code>m+1</code> 要防越界。</li></ul><h2 id="33.-%E6%90%9C%E7%B4%A2%E6%97%8B%E8%BD%AC%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84" tabindex="-1"><a href="https://leetcode.cn/problems/search-in-rotated-sorted-array/" target="_blank">33. 搜索旋转排序数组</a></h2><blockquote><p>升序数组旋转过，找 <code>target</code>，O(log n)。</p></blockquote><p><strong>思路脉络</strong>：对 mid 切一刀必有一半有序。<code>nums[l]&lt;=nums[m]</code> 左半有序，看 target 在不在左半范围决定搜哪边；否则右半有序。</p><div class="mermaid"><svg id="render752793044" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="347.6015625" style="max-width: 704.48046875px;" viewBox="0 0 704.48046875 347.6015625"><style>#render752793044 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render752793044 .error-icon{fill:#552222;}#render752793044 .error-text{fill:#552222;stroke:#552222;}#render752793044 .edge-thickness-normal{stroke-width:2px;}#render752793044 .edge-thickness-thick{stroke-width:3.5px;}#render752793044 .edge-pattern-solid{stroke-dasharray:0;}#render752793044 .edge-pattern-dashed{stroke-dasharray:3;}#render752793044 .edge-pattern-dotted{stroke-dasharray:2;}#render752793044 .marker{fill:#333333;stroke:#333333;}#render752793044 .marker.cross{stroke:#333333;}#render752793044 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render752793044 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render752793044 .cluster-label text{fill:#333;}#render752793044 .cluster-label span{color:#333;}#render752793044 .label text,#render752793044 span{fill:#333;color:#333;}#render752793044 .node rect,#render752793044 .node circle,#render752793044 .node ellipse,#render752793044 .node polygon,#render752793044 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render752793044 .node .label{text-align:center;}#render752793044 .node.clickable{cursor:pointer;}#render752793044 .arrowheadPath{fill:#333333;}#render752793044 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render752793044 .flowchart-link{stroke:#333333;fill:none;}#render752793044 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render752793044 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render752793044 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render752793044 .cluster text{fill:#333;}#render752793044 .cluster span{color:#333;}#render752793044 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render752793044 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M70.7578125,173.80078125L74.92447916666667,173.80078125C79.09114583333333,173.80078125,87.42447916666667,173.80078125,95.84114583333333,173.88411458333334C104.2578125,173.96744791666666,112.7578125,174.13411458333334,117.0078125,174.21744791666666L121.2578125,174.30078125" marker-end="url(#arrowhead7357)" style="fill:none"></path><defs><marker id="arrowhead7357" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M258.4953825142388,141.66335126423886L275.1745375118657,131.11952449103237C291.85369250949253,120.5756977178259,325.2120025047463,99.48804417141294,353.2974075023731,89.0275507315398C381.3828125,78.56705729166667,404.1953125,78.73372395833333,415.6015625,78.81705729166667L427.0078125,78.900390625" marker-end="url(#arrowhead7358)" style="fill:none"></path><defs><marker id="arrowhead7358" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M548.7077651537961,59.799562028796124L557.3079032531634,56.483033461496774C565.9080413525307,53.16650489419741,583.1083175512654,46.53344775959871,597.2930910672993,44.881433728003564C611.4778645833334,43.22941969640841,622.6471354166666,46.55844876781682,628.2317708333334,48.22296330352103L633.81640625,49.887477839225234" marker-end="url(#arrowhead7359)" style="fill:none"></path><defs><marker id="arrowhead7359" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M530.7922976421004,115.91668673289958L542.378346993417,128.7723691524163C553.9643963447336,141.62805157193304,577.1364950473668,167.33941641096652,597.3148299181412,192.88683060131657C617.4931647889156,218.43424479166666,634.6777358278313,243.81770833333334,643.270021347289,256.5094401041667L651.8623068667468,269.201171875" marker-end="url(#arrowhead7360)" style="fill:none"></path><defs><marker id="arrowhead7360" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-F" style="opacity: 1;" id="L-B-F"><path class="path" d="M258.4953825142388,206.93821123576114L275.1745375118657,217.31537134230095C291.85369250949253,227.69253144884078,325.2120025047463,248.4468516619204,353.2974075023731,258.9073451017935C381.3828125,269.3678385416667,404.1953125,269.5345052083333,415.6015625,269.6178385416667L427.0078125,269.701171875" marker-end="url(#arrowhead7361)" style="fill:none"></path><defs><marker id="arrowhead7361" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-E" style="opacity: 1;" id="L-F-E"><path class="path" d="M548.7077651537961,288.8020004712039L557.3079032531634,291.95186237183657C565.9080413525307,295.10172427246926,583.1083175512654,301.4014480737346,597.2084556506327,302.91202123601C611.30859375,304.42259439828536,622.30859375,301.1440169215707,627.80859375,299.5047281832134L633.30859375,297.86543944485607" marker-end="url(#arrowhead7362)" style="fill:none"></path><defs><marker id="arrowhead7362" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-D" style="opacity: 1;" id="L-F-D"><path class="path" d="M530.7922976421004,232.6848757671004L542.378346993417,219.662526680917C553.9643963447336,206.6401775947336,577.1364950473668,180.5954794223668,597.3148299181412,154.88139856535005C617.4931647889156,129.16731770833334,634.6777358278313,103.78385416666667,643.270021347289,91.09212239583333L651.8623068667468,78.400390625" marker-end="url(#arrowhead7363)" style="fill:none"></path><defs><marker id="arrowhead7363" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(358.5703125,78.400390625)"><g transform="translate(-42.9375,-9.25)" class="label"><rect rx="0" ry="0" width="85.85204315185547" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是,左半有序</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(600.30859375,39.900390625)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000089645385742" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(600.30859375,193.05078125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000089645385742" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(358.5703125,269.201171875)"><g transform="translate(-42.9375,-9.25)" class="label"><rect rx="0" ry="0" width="85.85204315185547" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否,右半有序</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(600.30859375,307.701171875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000089645385742" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(600.30859375,154.55078125)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000089645385742" height="18.500102996826172"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7920" transform="translate(39.37890625,173.80078125)"><rect rx="0" ry="0" x="-31.37890625" y="-19.25" width="62.7578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.37890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">取mid</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7921" transform="translate(205.6953125,173.80078125)"><polygon points="84.9375,0 169.875,-84.9375 84.9375,-169.875 0,-84.9375" transform="translate(-84.9375,84.9375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-65.125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[l]≤nums[m]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7923" transform="translate(496.908203125,78.400390625)"><polygon points="70.400390625,0 140.80078125,-70.400390625 70.400390625,-140.80078125 0,-70.400390625" transform="translate(-70.400390625,70.400390625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-48.97265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">target在左半?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7925" transform="translate(664.89453125,59.150390625)"><rect rx="0" ry="0" x="-31.078125" y="-19.25" width="62.15625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r=m-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7927" transform="translate(664.89453125,288.451171875)"><rect rx="0" ry="0" x="-31.5859375" y="-19.25" width="63.171875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.5859375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=m+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7929" transform="translate(496.908203125,269.201171875)"><polygon points="70.400390625,0 140.80078125,-70.400390625 70.400390625,-140.80078125 0,-70.400390625" transform="translate(-70.400390625,70.400390625)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-48.97265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">target在右半?</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int search(int[] nums, int target) {        int l = 0, r = nums.length - 1;        while (l &lt;= r) {            int m = (l + r) / 2;            if (nums[m] == target) return m;            if (nums[l] &lt;= nums[m]) {                if (nums[l] &lt;= target &amp;&amp; target &lt; nums[m]) r = m - 1; else l = m + 1;            } else {                if (nums[m] &lt; target &amp;&amp; target &lt;= nums[r]) l = m + 1; else r = m - 1;            }        }        return -1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>判&quot;左半有序&quot;用 <code>nums[l] &lt;= nums[m]</code>（含等号，处理 <code>l==m</code>）。范围判断用闭开区间 <code>[nums[l], nums[m])</code>。</li></ul><h2 id="34.-%E5%9C%A8%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%92%8C%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E4%BD%8D%E7%BD%AE" tabindex="-1"><a href="https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/" target="_blank">34. 在排序数组中查找元素的第一个和最后一个位置</a></h2><blockquote><p>找 <code>target</code> 起止下标，没有返回 <code>[-1,-1]</code>。</p></blockquote><p><strong>思路脉络</strong>：两次二分，一次找左界、一次找右界。命中不停，继续往对应方向压。</p><div class="mermaid"><svg id="render228988708" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="231.5" style="max-width: 583.21875px;" viewBox="0 0 583.21875 231.5"><style>#render228988708 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render228988708 .error-icon{fill:#552222;}#render228988708 .error-text{fill:#552222;stroke:#552222;}#render228988708 .edge-thickness-normal{stroke-width:2px;}#render228988708 .edge-thickness-thick{stroke-width:3.5px;}#render228988708 .edge-pattern-solid{stroke-dasharray:0;}#render228988708 .edge-pattern-dashed{stroke-dasharray:3;}#render228988708 .edge-pattern-dotted{stroke-dasharray:2;}#render228988708 .marker{fill:#333333;stroke:#333333;}#render228988708 .marker.cross{stroke:#333333;}#render228988708 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render228988708 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render228988708 .cluster-label text{fill:#333;}#render228988708 .cluster-label span{color:#333;}#render228988708 .label text,#render228988708 span{fill:#333;color:#333;}#render228988708 .node rect,#render228988708 .node circle,#render228988708 .node ellipse,#render228988708 .node polygon,#render228988708 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render228988708 .node .label{text-align:center;}#render228988708 .node.clickable{cursor:pointer;}#render228988708 .arrowheadPath{fill:#333333;}#render228988708 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render228988708 .flowchart-link{stroke:#333333;fill:none;}#render228988708 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render228988708 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render228988708 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render228988708 .cluster text{fill:#333;}#render228988708 .cluster span{color:#333;}#render228988708 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render228988708 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M140.5234375,115.75L152.77734375,115.75C165.03125,115.75,189.5390625,115.75,206.04296875,115.83333333333333C222.546875,115.91666666666667,231.046875,116.08333333333333,235.296875,116.16666666666667L239.546875,116.25" marker-end="url(#arrowhead7379)" style="fill:none"></path><defs><marker id="arrowhead7379" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M321.0129271582734,88.87230215827338L330.9925434652278,78.60191846522783C340.97215977218224,68.33153477218225,360.93139238609115,47.790767386091126,376.41100869304563,37.52038369304557C391.890625,27.25,402.890625,27.25,408.390625,27.25L413.890625,27.25" marker-end="url(#arrowhead7380)" style="fill:none"></path><defs><marker id="arrowhead7380" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M348.390625,116.25L353.8072916666667,116.16666666666667C359.2239583333333,116.08333333333333,370.0572916666667,115.91666666666667,389.1536458333333,115.83333333333333C408.25,115.75,435.609375,115.75,449.2890625,115.75L462.96875,115.75" marker-end="url(#arrowhead7381)" style="fill:none"></path><defs><marker id="arrowhead7381" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-E" style="opacity: 1;" id="L-B-E"><path class="path" d="M321.0129271582734,143.62769784172662L330.9925434652278,153.7314148681055C340.97215977218224,163.83513189448442,360.93139238609115,184.0425659472422,384.67533160971226,194.14628297362108C408.4192708333333,204.25,435.9479166666667,204.25,449.7122395833333,204.25L463.4765625,204.25" marker-end="url(#arrowhead7382)" style="fill:none"></path><defs><marker id="arrowhead7382" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(380.890625,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(380.890625,204.25)"><g transform="translate(-7.3671875,-9.25)" class="label"><rect rx="0" ry="0" width="14.734375" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">&gt;t</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7943" transform="translate(98.5234375,115.75)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">左界二分</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7944" transform="translate(293.46875,115.75)"><polygon points="54.421875,0 108.84375,-54.421875 54.421875,-108.84375 0,-54.421875" transform="translate(-54.421875,54.421875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-31.21875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">a[m]==t?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7946" transform="translate(494.5546875,27.25)"><rect rx="0" ry="0" x="-80.6640625" y="-19.25" width="161.328125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-70.6640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans=m,r=m-1往左压</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7948" transform="translate(494.5546875,115.75)"><rect rx="0" ry="0" x="-31.5859375" y="-19.25" width="63.171875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.5859375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=m+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7950" transform="translate(494.5546875,204.25)"><rect rx="0" ry="0" x="-31.078125" y="-19.25" width="62.15625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r=m-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7951" transform="translate(98.5234375,204.25)"><rect rx="0" ry="0" x="-90.5234375" y="-19.25" width="181.046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-80.5234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">右界:命中l=m+1往右压</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int[] searchRange(int[] nums, int target) {        return new int[]{findLeft(nums, target), findRight(nums, target)};    }    int findLeft(int[] a, int t) {        int l = 0, r = a.length - 1, ans = -1;        while (l &lt;= r) { int m = (l+r)/2; if (a[m]==t) { ans=m; r=m-1; } else if (a[m]&lt;t) l=m+1; else r=m-1; }        return ans;    }    int findRight(int[] a, int t) {        int l = 0, r = a.length - 1, ans = -1;        while (l &lt;= r) { int m = (l+r)/2; if (a[m]==t) { ans=m; l=m+1; } else if (a[m]&lt;t) l=m+1; else r=m-1; }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><h2 id="153.-%E5%AF%BB%E6%89%BE%E6%97%8B%E8%BD%AC%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E6%9C%80%E5%B0%8F%E5%80%BC" tabindex="-1"><a href="https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/" target="_blank">153. 寻找旋转排序数组中的最小值</a></h2><blockquote><p>旋转过的升序数组（无重复）找最小值，O(log n)。</p></blockquote><p><strong>思路脉络</strong>：和 <code>right</code> 比。<code>nums[m] &gt; nums[r]</code> 最小在右半；否则最小在 m 或左半。<code>r = m</code>（不减一，m 可能是最小）。</p><div class="mermaid"><svg id="render2805356343" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="187.22500610351562" style="max-width: 849.546875px;" viewBox="0 0 849.546875 187.22500610351562"><style>#render2805356343 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2805356343 .error-icon{fill:#552222;}#render2805356343 .error-text{fill:#552222;stroke:#552222;}#render2805356343 .edge-thickness-normal{stroke-width:2px;}#render2805356343 .edge-thickness-thick{stroke-width:3.5px;}#render2805356343 .edge-pattern-solid{stroke-dasharray:0;}#render2805356343 .edge-pattern-dashed{stroke-dasharray:3;}#render2805356343 .edge-pattern-dotted{stroke-dasharray:2;}#render2805356343 .marker{fill:#333333;stroke:#333333;}#render2805356343 .marker.cross{stroke:#333333;}#render2805356343 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2805356343 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2805356343 .cluster-label text{fill:#333;}#render2805356343 .cluster-label span{color:#333;}#render2805356343 .label text,#render2805356343 span{fill:#333;color:#333;}#render2805356343 .node rect,#render2805356343 .node circle,#render2805356343 .node ellipse,#render2805356343 .node polygon,#render2805356343 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2805356343 .node .label{text-align:center;}#render2805356343 .node.clickable{cursor:pointer;}#render2805356343 .arrowheadPath{fill:#333333;}#render2805356343 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2805356343 .flowchart-link{stroke:#333333;fill:none;}#render2805356343 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2805356343 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2805356343 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2805356343 .cluster text{fill:#333;}#render2805356343 .cluster span{color:#333;}#render2805356343 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2805356343 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92.9921875,93.61250305175781L97.15885416666667,93.61250305175781C101.32552083333333,93.61250305175781,109.65885416666667,93.61250305175781,118.07552134195964,93.69583638509114C126.4921885172526,93.77916971842448,134.9921895345052,93.94583638509114,139.2421900431315,94.0291697184245L143.4921905517578,94.11250305175783" marker-end="url(#arrowhead7409)" style="fill:none"></path><defs><marker id="arrowhead7409" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M291.4561387196117,70.85145121961166L300.749647866929,67.26995985830268C310.04315701424633,63.688468496993714,328.630175308881,56.52548577437576,343.4236844561983,52.94399441306678C358.2171936035156,49.36250305175781,369.2171936035156,49.36250305175781,374.7171936035156,49.36250305175781L380.2171936035156,49.36250305175781" marker-end="url(#arrowhead7410)" style="fill:none"></path><defs><marker id="arrowhead7410" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M291.4561342744309,117.37355322556913L300.7496441626117,120.78837819660056C310.04315405079245,124.20320316763203,328.63017382715407,131.0328531096949,343.59035038200153,134.44767808072638C358.55052693684894,137.8625030517578,369.8838602701823,137.8625030517578,375.55052693684894,137.8625030517578L381.2171936035156,137.8625030517578" marker-end="url(#arrowhead7411)" style="fill:none"></path><defs><marker id="arrowhead7411" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M519.1390686035156,49.36250305175781L523.3057352701823,49.36250305175781C527.472401936849,49.36250305175781,535.8057352701823,49.36250305175781,546.9835374314749,54.05970076330865C558.1613395927675,58.75689847485947,572.1836105820192,68.15129389796112,579.1947460766452,72.84849160951195L586.2058815712711,77.54568932106278" marker-end="url(#arrowhead7412)" style="fill:none"></path><defs><marker id="arrowhead7412" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M518.1390686035156,137.8625030517578L522.472401936849,137.8625030517578C526.8057352701823,137.8625030517578,535.472401936849,137.8625030517578,546.8168709174579,133.33197190521014C558.1613398980668,128.80144075866247,572.1836111926178,119.74037846556712,579.1947468398935,115.20984731901945L586.205882487169,110.67931617247177" marker-end="url(#arrowhead7413)" style="fill:none"></path><defs><marker id="arrowhead7413" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-F" style="opacity: 1;" id="L-E-F"><path class="path" d="M652.5234428405762,94.11250305175783L657.9401098887125,94.02916971842448C663.356776936849,93.94583638509114,674.1901110331218,93.77916971842448,685.1067780812582,93.69583638509114C696.0234451293945,93.61250305175781,707.0234451293945,93.61250305175781,712.5234451293945,93.61250305175781L718.0234451293945,93.61250305175781" marker-end="url(#arrowhead7414)" style="fill:none"></path><defs><marker id="arrowhead7414" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(347.2171936035156,49.36250305175781)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000534057617188" height="18.500619888305664"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(347.2171936035156,137.8625030517578)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000534057617188" height="18.500619888305664"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(685.0234451293945,93.61250305175781)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000534057617188" height="18.500619888305664"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7964" transform="translate(50.49609375,93.61250305175781)"><rect rx="0" ry="0" x="-42.49609375" y="-19.25" width="84.9921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.49609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=0,r=n-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7965" transform="translate(228.6046905517578,93.61250305175781)"><polygon points="85.6125,0 171.225,-85.6125 85.6125,-171.225 0,-85.6125" transform="translate(-85.6125,85.6125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-65.875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">nums[m]&gt;nums[r]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7967" transform="translate(449.6781311035156,49.36250305175781)"><rect rx="0" ry="0" x="-69.4609375" y="-19.25" width="138.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-59.4609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=m+1(最小在右)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7969" transform="translate(449.6781311035156,137.8625030517578)"><rect rx="0" ry="0" x="-68.4609375" y="-19.25" width="136.921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.4609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r=m(m可能最小)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7971" transform="translate(610.5812568664551,93.61250305175781)"><polygon points="41.4421875,0 82.884375,-41.4421875 41.4421875,-82.884375 0,-41.4421875" transform="translate(-41.4421875,41.4421875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-16.796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l==r?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-7975" transform="translate(779.7851638793945,93.61250305175781)"><rect rx="0" ry="0" x="-61.76171875" y="-19.25" width="123.5234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-51.76171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return nums[l]</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int findMin(int[] nums) {        int l = 0, r = nums.length - 1;        while (l &lt; r) {            int m = (l + r) / 2;            if (nums[m] &gt; nums[r]) l = m + 1; else r = m;        }        return nums[l];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><h2 id="4.-%E5%AF%BB%E6%89%BE%E4%B8%A4%E4%B8%AA%E6%AD%A3%E5%BA%8F%E6%95%B0%E7%BB%84%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/median-of-two-sorted-arrays/" target="_blank">4. 寻找两个正序数组的中位数</a></h2><blockquote><p>两个升序数组找中位数，O(log(m+n))。</p></blockquote><p><strong>思路脉络</strong>：中位数是&quot;把合并数组平分的分界点&quot;。二分较短数组的划分数 <code>i</code>，<code>j = half - i</code>，合法划分要 <code>aL&lt;=bR</code> 且 <code>bL&lt;=aR</code>。边界用 <code>±∞</code>。</p><div class="mermaid"><svg id="render228351033" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="294.0874938964844" style="max-width: 809.2203369140625px;" viewBox="0 0 809.2203369140625 294.0874938964844"><style>#render228351033 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render228351033 .error-icon{fill:#552222;}#render228351033 .error-text{fill:#552222;stroke:#552222;}#render228351033 .edge-thickness-normal{stroke-width:2px;}#render228351033 .edge-thickness-thick{stroke-width:3.5px;}#render228351033 .edge-pattern-solid{stroke-dasharray:0;}#render228351033 .edge-pattern-dashed{stroke-dasharray:3;}#render228351033 .edge-pattern-dotted{stroke-dasharray:2;}#render228351033 .marker{fill:#333333;stroke:#333333;}#render228351033 .marker.cross{stroke:#333333;}#render228351033 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render228351033 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render228351033 .cluster-label text{fill:#333;}#render228351033 .cluster-label span{color:#333;}#render228351033 .label text,#render228351033 span{fill:#333;color:#333;}#render228351033 .node rect,#render228351033 .node circle,#render228351033 .node ellipse,#render228351033 .node polygon,#render228351033 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render228351033 .node .label{text-align:center;}#render228351033 .node.clickable{cursor:pointer;}#render228351033 .arrowheadPath{fill:#333333;}#render228351033 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render228351033 .flowchart-link{stroke:#333333;fill:none;}#render228351033 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render228351033 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render228351033 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render228351033 .cluster text{fill:#333;}#render228351033 .cluster span{color:#333;}#render228351033 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render228351033 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M90.6487179709639,188.71249389648438L103.80101497580324,171.0906194051107C116.9533119806426,153.46874491373697,143.2579059903213,118.22499593098958,160.57686966182732,100.60312143961589C177.89583333333334,82.98124694824219,186.22916666666666,82.98124694824219,190.39583333333334,82.98124694824219L194.5625,82.98124694824219" marker-end="url(#arrowhead7457)" style="fill:none"></path><defs><marker id="arrowhead7457" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M267.0390625,82.98124694824219L271.2057291666667,82.98124694824219C275.3723958333333,82.98124694824219,283.7057291666667,82.98124694824219,292.0390625,82.98124694824219C300.3723958333333,82.98124694824219,308.7057291666667,82.98124694824219,312.8723958333333,82.98124694824219L317.0390625,82.98124694824219" marker-end="url(#arrowhead7458)" style="fill:none"></path><defs><marker id="arrowhead7458" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M424.1328125,82.98124694824219L428.2994791666667,82.98124694824219C432.4661458333333,82.98124694824219,440.7994791666667,82.98124694824219,449.2161463419596,83.06458028157552C457.63281351725254,83.14791361490886,466.13281453450514,83.31458028157552,470.3828150431314,83.39791361490886L474.63281555175774,83.48124694824219" marker-end="url(#arrowhead7459)" style="fill:none"></path><defs><marker id="arrowhead7459" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M603.2091033085544,62.095040808554494L614.4505538232094,57.49274183183578C625.6920043378644,52.890442855117065,648.1749053671743,43.68584490167962,667.176772548496,39.08354592496091C686.1786397298177,34.48124694824219,701.699473063151,34.48124694824219,709.4598897298177,34.48124694824219L717.2203063964844,34.48124694824219" marker-end="url(#arrowhead7460)" style="fill:none"></path><defs><marker id="arrowhead7460" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M616.3582569775627,91.71830552243722L625.4081818807164,92.76212909340472C634.45810678387,93.80595266437221,652.5579565901771,95.8935998063072,671.3246783683308,102.97082033792555C690.0914001464844,110.04804086954391,709.5249938964844,122.11483479084562,719.2417907714844,128.14823175149647L728.9585876464844,134.18162871214733" marker-end="url(#arrowhead7461)" style="fill:none"></path><defs><marker id="arrowhead7461" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-G" style="opacity: 1;" id="L-D-G"><path class="path" d="M584.5618229647268,123.51473953527328L598.9111535366865,139.9643652621418C613.260484108646,156.41399098901033,641.9591452525652,189.31324244274734,665.8156372828581,210.55335037233286C689.672129313151,231.79345830191838,708.6864522298177,241.3744227073524,718.193613688151,246.1649049100694L727.7007751464844,250.9553871127864" marker-end="url(#arrowhead7462)" style="fill:none"></path><defs><marker id="arrowhead7462" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-A" style="opacity: 1;" id="L-F-A"><path class="path" d="M728.9585876464844,166.63662695262158L719.2417907714844,171.02427144326535C709.5249938964844,175.41191593390917,690.0914001464844,184.18720491519676,660.117312113444,188.57484940584058C630.1432240804037,192.96249389648438,589.6286417643229,192.96249389648438,552.7078094482422,192.96249389648438C515.7869771321615,192.96249389648438,482.45989481608075,192.96249389648438,452.70520782470703,192.96249389648438C422.9505208333333,192.96249389648438,396.7682291666667,192.96249389648438,370.5859375,192.96249389648438C344.4036458333333,192.96249389648438,318.2213541666667,192.96249389648438,294.923828125,192.96249389648438C271.6263020833333,192.96249389648438,251.21354166666666,192.96249389648438,230.80078125,192.96249389648438C210.38802083333334,192.96249389648438,189.97526041666666,192.96249389648438,175.60221354166666,193.63251064690314C161.22916666666666,194.3025273973219,152.89583333333334,195.64256089815942,148.72916666666666,196.31257764857818L144.5625,196.98259439899692" marker-end="url(#arrowhead7463)" style="fill:none"></path><defs><marker id="arrowhead7463" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-G LE-A" style="opacity: 1;" id="L-G-A"><path class="path" d="M727.7007751464844,272.04255410290637L718.193613688151,273.6125440685027C708.6864522298177,275.18253403409904,689.672129313151,278.3225139652917,659.9076766967773,279.89250393088804C630.1432240804037,281.4624938964844,589.6286417643229,281.4624938964844,552.7078094482422,281.4624938964844C515.7869771321615,281.4624938964844,482.45989481608075,281.4624938964844,452.70520782470703,281.4624938964844C422.9505208333333,281.4624938964844,396.7682291666667,281.4624938964844,370.5859375,281.4624938964844C344.4036458333333,281.4624938964844,318.2213541666667,281.4624938964844,294.923828125,281.4624938964844C271.6263020833333,281.4624938964844,251.21354166666666,281.4624938964844,230.80078125,281.4624938964844C210.38802083333334,281.4624938964844,189.97526041666666,281.4624938964844,168.29380580357142,272.4208272298177C146.61235119047618,263.37916056315106,123.66220238095237,245.29582722981772,112.18712797619047,236.25416056315103L100.71205357142857,227.21249389648438" marker-end="url(#arrowhead7464)" style="fill:none"></path><defs><marker id="arrowhead7464" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(670.6578063964844,34.48124694824219)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000085830688477" height="18.500099182128906"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(670.6578063964844,97.98124694824219)"><g transform="translate(-21.5625,-9.25)" class="label"><rect rx="0" ry="0" width="43.1174201965332" height="18.500099182128906"></rect><text><tspan xml:space="preserve" dy="1em" x="1">aL&gt;bR</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(670.6578063964844,222.21249389648438)"><g transform="translate(-21.5625,-9.25)" class="label"><rect rx="0" ry="0" width="43.1174201965332" height="18.500099182128906"></rect><text><tspan xml:space="preserve" dy="1em" x="1">bL&gt;aR</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-7992" transform="translate(76.28125,207.96249389648438)"><rect rx="0" ry="0" x="-68.28125" y="-19.25" width="136.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">二分短数组切点i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-7993" transform="translate(230.80078125,82.98124694824219)"><rect rx="0" ry="0" x="-36.23828125" y="-19.25" width="72.4765625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-26.23828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">j=half-i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-7995" transform="translate(370.5859375,82.98124694824219)"><rect rx="0" ry="0" x="-53.546875" y="-19.25" width="107.09375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">aL,aR,bL,bR</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-7997" transform="translate(549.1140594482422,82.98124694824219)"><polygon points="74.98125,0 149.9625,-74.98125 74.98125,-149.9625 0,-74.98125" transform="translate(-74.98125,74.98125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-54.0625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">aL≤bR且bL≤aR?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-7999" transform="translate(759.2203063964844,34.48124694824219)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">算中位数</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-8001" transform="translate(759.2203063964844,152.97187042236328)"><rect rx="0" ry="0" x="-30.26171875" y="-19.25" width="60.5234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20.26171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">hi=i-1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-G-8003" transform="translate(759.2203063964844,266.8374938964844)"><rect rx="0" ry="0" x="-31.51953125" y="-19.25" width="63.0390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.51953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">lo=i+1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public double findMedianSortedArrays(int[] a, int[] b) {        if (a.length &gt; b.length) return findMedianSortedArrays(b, a);        int m = a.length, n = b.length, half = (m + n + 1) / 2, lo = 0, hi = m;        while (lo &lt;= hi) {            int i = (lo + hi) / 2, j = half - i;            int aL = i == 0 ? Integer.MIN_VALUE : a[i - 1];            int aR = i == m ? Integer.MAX_VALUE : a[i];            int bL = j == 0 ? Integer.MIN_VALUE : b[j - 1];            int bR = j == n ? Integer.MAX_VALUE : b[j];            if (aL &lt;= bR &amp;&amp; bL &lt;= aR) {                if ((m + n) % 2 == 1) return Math.max(aL, bL);                return (Math.max(aL, bL) + Math.min(aR, bR)) / 2.0;            } else if (aL &gt; bR) hi = i - 1; else lo = i + 1;        }        return 0;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log(min(m,n)))，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须在<strong>短数组</strong>上二分：否则 <code>j = half - i</code> 可能越界。开头 <code>swap</code> 保证 <code>a</code> 短。边界用 <code>±∞</code>：<code>i==0</code> 时 <code>aL</code> 取 <code>MIN_VALUE</code>。</li></ul><hr /><h1 id="17.-%E5%A0%86" tabindex="-1">17. 堆</h1><p>堆维护&quot;半个有序集&quot;，快速取最大/最小。两类：Top-K（大小 K 的堆，比堆顶差就丢）、动态中位数（大顶堆装小半 + 小顶堆装大半，平衡规模）。</p><h2 id="215.-%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E7%AC%AC-k-%E4%B8%AA%E6%9C%80%E5%A4%A7%E5%85%83%E7%B4%A0" tabindex="-1"><a href="https://leetcode.cn/problems/kth-largest-element-in-an-array/" target="_blank">215. 数组中的第 K 个最大元素</a></h2><blockquote><p>找第 K 大，要求 O(n) 期望。</p></blockquote><p><strong>思路脉络</strong>：快速选择 O(n) 期望。第 K 大 = 升序第 (n-K) 个，快排 partition 每次只递归一侧。</p><div class="mermaid"><svg id="render1097138387" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="256.0382843017578" style="max-width: 549.2484130859375px;" viewBox="0 0 549.2484130859375 256.0382843017578"><style>#render1097138387 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1097138387 .error-icon{fill:#552222;}#render1097138387 .error-text{fill:#552222;stroke:#552222;}#render1097138387 .edge-thickness-normal{stroke-width:2px;}#render1097138387 .edge-thickness-thick{stroke-width:3.5px;}#render1097138387 .edge-pattern-solid{stroke-dasharray:0;}#render1097138387 .edge-pattern-dashed{stroke-dasharray:3;}#render1097138387 .edge-pattern-dotted{stroke-dasharray:2;}#render1097138387 .marker{fill:#333333;stroke:#333333;}#render1097138387 .marker.cross{stroke:#333333;}#render1097138387 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1097138387 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1097138387 .cluster-label text{fill:#333;}#render1097138387 .cluster-label span{color:#333;}#render1097138387 .label text,#render1097138387 span{fill:#333;color:#333;}#render1097138387 .node rect,#render1097138387 .node circle,#render1097138387 .node ellipse,#render1097138387 .node polygon,#render1097138387 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1097138387 .node .label{text-align:center;}#render1097138387 .node.clickable{cursor:pointer;}#render1097138387 .arrowheadPath{fill:#333333;}#render1097138387 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1097138387 .flowchart-link{stroke:#333333;fill:none;}#render1097138387 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1097138387 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1097138387 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1097138387 .cluster text{fill:#333;}#render1097138387 .cluster span{color:#333;}#render1097138387 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1097138387 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M67.1484375,169.91328048706055L71.31510416666667,169.91328048706055C75.48177083333333,169.91328048706055,83.81510416666667,169.91328048706055,92.1484375,169.91328048706055C100.48177083333333,169.91328048706055,108.81510416666667,169.91328048706055,112.98177083333333,169.91328048706055L117.1484375,169.91328048706055" marker-end="url(#arrowhead7494)" style="fill:none"></path><defs><marker id="arrowhead7494" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M170.74828291484903,150.66328048706055L179.76158992904084,136.51106707255045C188.77489694323268,122.35885365804036,206.80151097161635,94.05442682902019,220.06481811296473,79.98554674784343C233.32812525431314,65.91666666666667,241.8281255086263,66.08333333333333,246.07812563578287,66.16666666666667L250.32812576293946,66.25" marker-end="url(#arrowhead7495)" style="fill:none"></path><defs><marker id="arrowhead7495" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M324.5269318422113,52.12224510515074L333.0794741975296,47.97687092095895C341.6320165528479,43.83149673676716,358.7371012634845,35.54074836838358,373.5708936188028,31.39537418419179C388.4046859741211,27.25,400.9671859741211,27.25,407.2484359741211,27.25L413.5296859741211,27.25" marker-end="url(#arrowhead7496)" style="fill:none"></path><defs><marker id="arrowhead7496" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M328.3217541995269,76.58293406341262L336.24182616195924,78.94411171951052C344.1618981243916,81.30528937560841,360.0020420492563,86.0276446878042,379.94620255335536,93.07565437997022C399.8903630574544,100.12366407213624,423.9385401407878,109.49732814427246,435.9626286824544,114.18416018034058L447.9867172241211,118.8709922164087" marker-end="url(#arrowhead7497)" style="fill:none"></path><defs><marker id="arrowhead7497" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-F" style="opacity: 1;" id="L-C-F"><path class="path" d="M312.0981688043002,92.8065194586393L322.722171665937,108.87320625612112C333.34617452757385,124.93989305360294,354.5941802508475,157.0732666485666,377.32690707081764,177.8597752576696C400.0596338907878,198.64628386677256,424.2770818074544,208.08592749001483,436.3858057657878,212.80574930163598L448.4945297241211,217.52557111325714" marker-end="url(#arrowhead7498)" style="fill:none"></path><defs><marker id="arrowhead7498" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-B" style="opacity: 1;" id="L-E-B"><path class="path" d="M447.9867172241211,138.90939739847718L435.9626286824544,142.41727120598603C423.9385401407878,145.92514501349487,399.8903630574544,152.94089262851256,374.22447776794434,156.44876643602143C348.55859247843426,159.95664024353027,321.2749989827474,159.95664024353027,296.10598882039386,159.95664024353027C270.93697865804035,159.95664024353027,247.88255182902017,159.95664024353027,232.18867174784342,160.5819959674757C216.49479166666666,161.20735169142117,208.16145833333334,162.4580631393121,203.99479166666666,163.08341886325752L199.828125,163.70877458720298" marker-end="url(#arrowhead7499)" style="fill:none"></path><defs><marker id="arrowhead7499" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-B" style="opacity: 1;" id="L-F-B"><path class="path" d="M448.4945297241211,230.11663319973198L436.3858057657878,230.67330104036503C424.2770818074544,231.22996888099806,400.0596338907878,232.34330456226417,374.309113184611,232.89997240289722C348.55859247843426,233.45664024353027,321.2749989827474,233.45664024353027,296.10598882039386,233.45664024353027C270.93697865804035,233.45664024353027,247.88255182902017,233.45664024353027,228.64822688030668,226.074413617452C209.41390193159324,218.6921869913737,193.99967886318646,203.92773373921713,186.29256732898307,196.54550711313883L178.58545579477968,189.16328048706055" marker-end="url(#arrowhead7500)" style="fill:none"></path><defs><marker id="arrowhead7500" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(375.8421859741211,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00044059753418" height="18.50050926208496"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(375.8421859741211,90.75)"><g transform="translate(-4.45703125,-9.25)" class="label"><rect rx="0" ry="0" width="8.914307594299316" height="18.50050926208496"></rect><text><tspan xml:space="preserve" dy="1em" x="1">p</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(375.8421859741211,189.20664024353027)"><g transform="translate(-12.6875,-9.25)" class="label"><rect rx="0" ry="0" width="25.36788558959961" height="18.50050926208496"></rect><text><tspan xml:space="preserve" dy="1em" x="1">p&gt;k</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8022" transform="translate(37.57421875,169.91328048706055)"><rect rx="0" ry="0" x="-29.57421875" y="-19.25" width="59.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-19.57421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">k=n-k</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8023" transform="translate(158.48828125,169.91328048706055)"><rect rx="0" ry="0" x="-41.33984375" y="-19.25" width="82.6796875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.33984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">partition</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8025" transform="translate(293.99140548706055,65.75)"><polygon points="44.163281250000004,0 88.32656250000001,-44.163281250000004 44.163281250000004,-88.32656250000001 0,-44.163281250000004" transform="translate(-44.163281250000004,44.163281250000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-19.8203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">p==k?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8027" transform="translate(477.3890609741211,27.25)"><rect rx="0" ry="0" x="-63.859375" y="-19.25" width="127.71875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.859375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return nums[p]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8029" transform="translate(477.3890609741211,130.33164024353027)"><rect rx="0" ry="0" x="-29.40234375" y="-19.25" width="58.8046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-19.40234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=p+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-8031" transform="translate(477.3890609741211,228.78828048706055)"><rect rx="0" ry="0" x="-28.89453125" y="-19.25" width="57.7890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.89453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r=p-1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int findKthLargest(int[] nums, int k) {        k = nums.length - k;        int l = 0, r = nums.length - 1;        while (l &lt; r) {            int p = partition(nums, l, r);            if (p == k) return nums[p]; else if (p &lt; k) l = p + 1; else r = p - 1;        }        return nums[l];    }    int partition(int[] a, int l, int r) {        int pivot = a[r], i = l;        for (int j = l; j &lt; r; j++) if (a[j] &lt;= pivot) swap(a, i++, j);        swap(a, i, r); return i;    }    void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n) 期望，空间 O(1)。</p></blockquote><h2 id="502.-ipo" tabindex="-1"><a href="https://leetcode.cn/problems/ipo/" target="_blank">502. IPO</a></h2><blockquote><p><code>k</code> 个项目，各有资本 <code>w[i]</code> 和利润 <code>p[i]</code>，从初始资本 <code>W</code> 开始，每次选能做的（<code>w[i] &lt;= W</code>）里利润最大的，求做完 k 个后的总资本。</p></blockquote><p><strong>思路脉络</strong>：贪心 + 两个堆。按资本升序排项目，用最大利润堆维护&quot;当前能做的项目&quot;。每次做利润最大的（堆顶），做完资本增加，把新解锁的项目入堆，做 k 次。</p><div class="mermaid"><svg id="render1736251696" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 869.671875px;" viewBox="0 0 869.671875 54.5"><style>#render1736251696 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1736251696 .error-icon{fill:#552222;}#render1736251696 .error-text{fill:#552222;stroke:#552222;}#render1736251696 .edge-thickness-normal{stroke-width:2px;}#render1736251696 .edge-thickness-thick{stroke-width:3.5px;}#render1736251696 .edge-pattern-solid{stroke-dasharray:0;}#render1736251696 .edge-pattern-dashed{stroke-dasharray:3;}#render1736251696 .edge-pattern-dotted{stroke-dasharray:2;}#render1736251696 .marker{fill:#333333;stroke:#333333;}#render1736251696 .marker.cross{stroke:#333333;}#render1736251696 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1736251696 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1736251696 .cluster-label text{fill:#333;}#render1736251696 .cluster-label span{color:#333;}#render1736251696 .label text,#render1736251696 span{fill:#333;color:#333;}#render1736251696 .node rect,#render1736251696 .node circle,#render1736251696 .node ellipse,#render1736251696 .node polygon,#render1736251696 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1736251696 .node .label{text-align:center;}#render1736251696 .node.clickable{cursor:pointer;}#render1736251696 .arrowheadPath{fill:#333333;}#render1736251696 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1736251696 .flowchart-link{stroke:#333333;fill:none;}#render1736251696 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1736251696 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1736251696 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1736251696 .cluster text{fill:#333;}#render1736251696 .cluster span{color:#333;}#render1736251696 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1736251696 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M140,27.25L144.16666666666666,27.25C148.33333333333334,27.25,156.66666666666666,27.25,165,27.25C173.33333333333334,27.25,181.66666666666666,27.25,185.83333333333334,27.25L190,27.25" marker-end="url(#arrowhead7522)" style="fill:none"></path><defs><marker id="arrowhead7522" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M349.4453125,27.25L353.6119791666667,27.25C357.7786458333333,27.25,366.1119791666667,27.25,374.4453125,27.25C382.7786458333333,27.25,391.1119791666667,27.25,395.2786458333333,27.25L399.4453125,27.25" marker-end="url(#arrowhead7523)" style="fill:none"></path><defs><marker id="arrowhead7523" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M559.1953125,27.25L563.3619791666666,27.25C567.5286458333334,27.25,575.8619791666666,27.25,584.1953125,27.25C592.5286458333334,27.25,600.8619791666666,27.25,605.0286458333334,27.25L609.1953125,27.25" marker-end="url(#arrowhead7524)" style="fill:none"></path><defs><marker id="arrowhead7524" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M689.8828125,27.25L694.0494791666666,27.25C698.2161458333334,27.25,706.5494791666666,27.25,714.8828125,27.25C723.2161458333334,27.25,731.5494791666666,27.25,735.7161458333334,27.25L739.8828125,27.25" marker-end="url(#arrowhead7525)" style="fill:none"></path><defs><marker id="arrowhead7525" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8044" transform="translate(74,27.25)"><rect rx="0" ry="0" x="-66" y="-19.25" width="132" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">按资本排序项目</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8045" transform="translate(269.72265625,27.25)"><rect rx="0" ry="0" x="-79.72265625" y="-19.25" width="159.4453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-69.72265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">解锁:capital≤w入堆</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8047" transform="translate(479.3203125,27.25)"><rect rx="0" ry="0" x="-79.875" y="-19.25" width="159.75" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-69.875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">做利润最大的(堆顶)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8049" transform="translate(649.5390625,27.25)"><rect rx="0" ry="0" x="-40.34375" y="-19.25" width="80.6875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.34375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">w+=利润</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8051" transform="translate(800.77734375,27.25)"><rect rx="0" ry="0" x="-60.89453125" y="-19.25" width="121.7890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.89453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">k次后return w</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {        int n = profits.length, i = 0;        int[][] projects = new int[n][2];        for (int j = 0; j &lt; n; j++) projects[j] = new int[]{capital[j], profits[j]};        Arrays.sort(projects, (a, b) -&gt; a[0] - b[0]);        PriorityQueue&lt;Integer&gt; maxHeap = new PriorityQueue&lt;&gt;(Collections.reverseOrder());        for (int cnt = 0; cnt &lt; k; cnt++) {            while (i &lt; n &amp;&amp; projects[i][0] &lt;= w) maxHeap.offer(projects[i++][1]);            if (maxHeap.isEmpty()) break;            w += maxHeap.poll();        }        return w;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n + k log n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须按资本升序排，才能用指针 <code>i</code> 一次性把&quot;新解锁&quot;的项目入堆（资本只增不减，入过堆的不用退）。堆里选利润最大——贪心选择。</li></ul><h2 id="373.-%E6%9F%A5%E6%89%BE%E5%92%8C%E6%9C%80%E5%B0%8F%E7%9A%84-k-%E5%AF%B9%E6%95%B0%E5%AD%97" tabindex="-1"><a href="https://leetcode.cn/problems/find-k-pairs-with-smallest-sums/" target="_blank">373. 查找和最小的 K 对数字</a></h2><blockquote><p>两个升序数组 <code>nums1</code>/<code>nums2</code>，找和最小的 <code>k</code> 个数对 <code>(u,v)</code>。</p></blockquote><p><strong>思路脉络</strong>：最小堆。<code>(0,0)</code> 入堆，每次弹最小对 <code>(i,j)</code>，把 <code>(i+1,j)</code> 和 <code>(i,j+1)</code> 入堆。用 Set 防重复入堆。</p><div class="mermaid"><svg id="render3846747527" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 639.7578125px;" viewBox="0 0 639.7578125 89.5"><style>#render3846747527 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3846747527 .error-icon{fill:#552222;}#render3846747527 .error-text{fill:#552222;stroke:#552222;}#render3846747527 .edge-thickness-normal{stroke-width:2px;}#render3846747527 .edge-thickness-thick{stroke-width:3.5px;}#render3846747527 .edge-pattern-solid{stroke-dasharray:0;}#render3846747527 .edge-pattern-dashed{stroke-dasharray:3;}#render3846747527 .edge-pattern-dotted{stroke-dasharray:2;}#render3846747527 .marker{fill:#333333;stroke:#333333;}#render3846747527 .marker.cross{stroke:#333333;}#render3846747527 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3846747527 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3846747527 .cluster-label text{fill:#333;}#render3846747527 .cluster-label span{color:#333;}#render3846747527 .label text,#render3846747527 span{fill:#333;color:#333;}#render3846747527 .node rect,#render3846747527 .node circle,#render3846747527 .node ellipse,#render3846747527 .node polygon,#render3846747527 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3846747527 .node .label{text-align:center;}#render3846747527 .node.clickable{cursor:pointer;}#render3846747527 .arrowheadPath{fill:#333333;}#render3846747527 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3846747527 .flowchart-link{stroke:#333333;fill:none;}#render3846747527 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3846747527 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3846747527 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3846747527 .cluster text{fill:#333;}#render3846747527 .cluster span{color:#333;}#render3846747527 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3846747527 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M94.40625,54.375L98.57291666666667,54.375C102.73958333333333,54.375,111.07291666666667,54.375,119.40625,54.375C127.73958333333333,54.375,136.07291666666666,54.375,140.23958333333334,54.375L144.40625,54.375" marker-end="url(#arrowhead7546)" style="fill:none"></path><defs><marker id="arrowhead7546" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M256.4609375,35.61908836716L260.6276041666667,34.22424030596667C264.7942708333333,32.82939224477334,273.1276041666667,30.03969612238667,281.4609375,28.644848061193333C289.7942708333333,27.25,298.1276041666667,27.25,302.2942708333333,27.25L306.4609375,27.25" marker-end="url(#arrowhead7547)" style="fill:none"></path><defs><marker id="arrowhead7547" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M390.4609375,27.25L394.6276041666667,27.25C398.7942708333333,27.25,407.1276041666667,27.25,417.13209845430106,28.5625C427.13659274193543,29.875,438.812247983871,32.5,444.6500756048387,33.8125L450.48790322580646,35.125" marker-end="url(#arrowhead7548)" style="fill:none"></path><defs><marker id="arrowhead7548" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M450.48790322580646,73.625L444.6500756048387,74.9375C438.812247983871,76.25,427.13659274193543,78.875,410.13209845430106,80.1875C393.1276041666667,81.5,370.7942708333333,81.5,348.4609375,81.5C326.1276041666667,81.5,303.7942708333333,81.5,288.4609375,80.10515193880667C273.1276041666667,78.71030387761333,264.7942708333333,75.92060775522667,260.6276041666667,74.52575969403334L256.4609375,73.13091163284" marker-end="url(#arrowhead7549)" style="fill:none"></path><defs><marker id="arrowhead7549" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8060" transform="translate(51.203125,54.375)"><rect rx="0" ry="0" x="-43.203125" y="-19.25" width="86.40625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-33.203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">(0,0)入堆</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8061" transform="translate(200.43359375,54.375)"><rect rx="0" ry="0" x="-56.02734375" y="-19.25" width="112.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-46.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">弹最小对(i,j)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8063" transform="translate(348.4609375,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">加入结果</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8065" transform="translate(536.109375,54.375)"><rect rx="0" ry="0" x="-95.6484375" y="-19.25" width="191.296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-85.6484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">(i+1,j),(i,j+1)入堆(去重)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; kSmallestPairs(int[] nums1, int[] nums2, int k) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        PriorityQueue&lt;int[]&gt; pq = new PriorityQueue&lt;&gt;((a, b) -&gt; nums1[a[0]] + nums2[a[1]] - nums1[b[0]] - nums2[b[1]]);        Set&lt;Long&gt; seen = new HashSet&lt;&gt;();        pq.offer(new int[]{0, 0}); seen.add(0L);        while (!pq.isEmpty() &amp;&amp; res.size() &lt; k) {            int[] p = pq.poll(); int i = p[0], j = p[1];            res.add(Arrays.asList(nums1[i], nums2[j]));            if (i + 1 &lt; nums1.length &amp;&amp; seen.add((long)(i+1) * 200 + j)) pq.offer(new int[]{i+1, j});            if (j + 1 &lt; nums2.length &amp;&amp; seen.add((long)i * 200 + j + 1)) pq.offer(new int[]{i, j+1});        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(k log k)，空间 O(k)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>防重复是关键：<code>seen</code> 用 <code>(i+1)*200+j</code> 编码（200 因长度 ≤ 100，不冲突）。<code>seen.add</code> 返回 true 才入堆。</li></ul><h2 id="295.-%E6%95%B0%E6%8D%AE%E6%B5%81%E7%9A%84%E4%B8%AD%E4%BD%8D%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/find-median-from-data-stream/" target="_blank">295. 数据流的中位数</a></h2><blockquote><p>数据流动态加数，随时取中位数。</p></blockquote><p><strong>思路脉络</strong>：两个堆——大顶堆 <code>lo</code> 装较小半、小顶堆 <code>hi</code> 装较大半，平衡规模差 ≤1。<code>addNum</code>：先入 <code>lo</code>，把 <code>lo</code> 最大推到 <code>hi</code>，若 <code>lo</code> 比 <code>hi</code> 少则回补。</p><div class="mermaid"><svg id="render68393029" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 621.578125px;" viewBox="0 0 621.578125 54.5"><style>#render68393029 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render68393029 .error-icon{fill:#552222;}#render68393029 .error-text{fill:#552222;stroke:#552222;}#render68393029 .edge-thickness-normal{stroke-width:2px;}#render68393029 .edge-thickness-thick{stroke-width:3.5px;}#render68393029 .edge-pattern-solid{stroke-dasharray:0;}#render68393029 .edge-pattern-dashed{stroke-dasharray:3;}#render68393029 .edge-pattern-dotted{stroke-dasharray:2;}#render68393029 .marker{fill:#333333;stroke:#333333;}#render68393029 .marker.cross{stroke:#333333;}#render68393029 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render68393029 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render68393029 .cluster-label text{fill:#333;}#render68393029 .cluster-label span{color:#333;}#render68393029 .label text,#render68393029 span{fill:#333;color:#333;}#render68393029 .node rect,#render68393029 .node circle,#render68393029 .node ellipse,#render68393029 .node polygon,#render68393029 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render68393029 .node .label{text-align:center;}#render68393029 .node.clickable{cursor:pointer;}#render68393029 .arrowheadPath{fill:#333333;}#render68393029 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render68393029 .flowchart-link{stroke:#333333;fill:none;}#render68393029 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render68393029 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render68393029 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render68393029 .cluster text{fill:#333;}#render68393029 .cluster span{color:#333;}#render68393029 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render68393029 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M121.6484375,27.25L125.81510416666667,27.25C129.98177083333334,27.25,138.31510416666666,27.25,146.6484375,27.25C154.98177083333334,27.25,163.31510416666666,27.25,167.48177083333334,27.25L171.6484375,27.25" marker-end="url(#arrowhead7566)" style="fill:none"></path><defs><marker id="arrowhead7566" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M266.2578125,27.25L270.4244791666667,27.25C274.5911458333333,27.25,282.9244791666667,27.25,291.2578125,27.25C299.5911458333333,27.25,307.9244791666667,27.25,312.0911458333333,27.25L316.2578125,27.25" marker-end="url(#arrowhead7567)" style="fill:none"></path><defs><marker id="arrowhead7567" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M349.5625,27.25L353.7291666666667,27.25C357.8958333333333,27.25,366.2291666666667,27.25,374.5625,27.25C382.8958333333333,27.25,391.2291666666667,27.25,395.3958333333333,27.25L399.5625,27.25" marker-end="url(#arrowhead7568)" style="fill:none"></path><defs><marker id="arrowhead7568" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8074" transform="translate(64.82421875,27.25)"><rect rx="0" ry="0" x="-56.82421875" y="-19.25" width="113.6484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-46.82421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">addNum:入lo</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8075" transform="translate(218.953125,27.25)"><rect rx="0" ry="0" x="-47.3046875" y="-19.25" width="94.609375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-37.3046875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">lo最大给hi</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8077" transform="translate(332.91015625,27.25)"><rect rx="0" ry="0" x="-16.65234375" y="-19.25" width="33.3046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-6.65234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">lo</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8079" transform="translate(506.5703125,27.25)"><rect rx="0" ry="0" x="-107.0078125" y="-19.25" width="214.015625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-97.0078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">findMedian:lo顶或两顶平均</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class MedianFinder {    PriorityQueue&lt;Integer&gt; lo = new PriorityQueue&lt;&gt;(Collections.reverseOrder());    PriorityQueue&lt;Integer&gt; hi = new PriorityQueue&lt;&gt;();    public void addNum(int num) {        lo.offer(num); hi.offer(lo.poll());        if (lo.size() &lt; hi.size()) lo.offer(hi.poll());    }    public double findMedian() {        return lo.size() &gt; hi.size() ? lo.peek() : (lo.peek() + hi.peek()) / 2.0;    }}</code></pre><blockquote><p>[!info] 复杂度<br /><code>addNum</code> O(log n)，<code>findMedian</code> O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>平衡方向：保证 <code>lo.size &gt;= hi.size</code>，奇数中位数取 <code>lo.peek</code>。<code>lo.poll()</code> 给 <code>hi</code> 保证&quot;小半最大 ≤ 大半最小&quot;不变量。</li></ul><hr /><h1 id="18.-%E4%BD%8D%E8%BF%90%E7%AE%97" tabindex="-1">18. 位运算</h1><p>位运算处理底层 bit 操作：异或抵消、位统计、移位取位。关键是熟悉 <code>&amp; | ^ ~ &lt;&lt; &gt;&gt;</code> 的语义和常见套路（<code>n &amp; (n-1)</code> 去最低 1、<code>n ^ n = 0</code>）。</p><h2 id="67.-%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%B1%82%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/add-binary/" target="_blank">67. 二进制求和</a></h2><blockquote><p>两个二进制字符串求和。</p></blockquote><p><strong>思路脉络</strong>：模拟加法，从低位（字符串末尾）向高位加，处理进位。比转 int 求和更稳（防大数溢出）。</p><div class="mermaid"><svg id="render2044354992" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="54.5" style="max-width: 699.2421875px;" viewBox="0 0 699.2421875 54.5"><style>#render2044354992 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2044354992 .error-icon{fill:#552222;}#render2044354992 .error-text{fill:#552222;stroke:#552222;}#render2044354992 .edge-thickness-normal{stroke-width:2px;}#render2044354992 .edge-thickness-thick{stroke-width:3.5px;}#render2044354992 .edge-pattern-solid{stroke-dasharray:0;}#render2044354992 .edge-pattern-dashed{stroke-dasharray:3;}#render2044354992 .edge-pattern-dotted{stroke-dasharray:2;}#render2044354992 .marker{fill:#333333;stroke:#333333;}#render2044354992 .marker.cross{stroke:#333333;}#render2044354992 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2044354992 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2044354992 .cluster-label text{fill:#333;}#render2044354992 .cluster-label span{color:#333;}#render2044354992 .label text,#render2044354992 span{fill:#333;color:#333;}#render2044354992 .node rect,#render2044354992 .node circle,#render2044354992 .node ellipse,#render2044354992 .node polygon,#render2044354992 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2044354992 .node .label{text-align:center;}#render2044354992 .node.clickable{cursor:pointer;}#render2044354992 .arrowheadPath{fill:#333333;}#render2044354992 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2044354992 .flowchart-link{stroke:#333333;fill:none;}#render2044354992 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2044354992 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2044354992 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2044354992 .cluster text{fill:#333;}#render2044354992 .cluster span{color:#333;}#render2044354992 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2044354992 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M134.53125,27.25L138.69791666666666,27.25C142.86458333333334,27.25,151.19791666666666,27.25,159.53125,27.25C167.86458333333334,27.25,176.19791666666666,27.25,180.36458333333334,27.25L184.53125,27.25" marker-end="url(#arrowhead7585)" style="fill:none"></path><defs><marker id="arrowhead7585" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M312.1796875,27.25L316.3463541666667,27.25C320.5130208333333,27.25,328.8463541666667,27.25,337.1796875,27.25C345.5130208333333,27.25,353.8463541666667,27.25,358.0130208333333,27.25L362.1796875,27.25" marker-end="url(#arrowhead7586)" style="fill:none"></path><defs><marker id="arrowhead7586" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M589.2421875,27.25L593.4088541666666,27.25C597.5755208333334,27.25,605.9088541666666,27.25,614.2421875,27.25C622.5755208333334,27.25,630.9088541666666,27.25,635.0755208333334,27.25L639.2421875,27.25" marker-end="url(#arrowhead7587)" style="fill:none"></path><defs><marker id="arrowhead7587" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8086" transform="translate(71.265625,27.25)"><rect rx="0" ry="0" x="-63.265625" y="-19.25" width="126.53125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">从末位加,carry</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8087" transform="translate(248.35546875,27.25)"><rect rx="0" ry="0" x="-63.82421875" y="-19.25" width="127.6484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-53.82421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sum=carry+a+b</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8089" transform="translate(475.7109375,27.25)"><rect rx="0" ry="0" x="-113.53125" y="-19.25" width="227.0625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-103.53125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">append(sum%2),carry=sum/2</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8091" transform="translate(665.2421875,27.25)"><rect rx="0" ry="0" x="-26" y="-19.25" width="52" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">反转</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public String addBinary(String a, String b) {        StringBuilder sb = new StringBuilder();        int i = a.length() - 1, j = b.length() - 1, carry = 0;        while (i &gt;= 0 || j &gt;= 0 || carry != 0) {            int sum = carry;            if (i &gt;= 0) sum += a.charAt(i--) - &#39;0&#39;;            if (j &gt;= 0) sum += b.charAt(j--) - &#39;0&#39;;            sb.append(sum % 2); carry = sum / 2;        }        return sb.reverse().toString();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(max(m,n))，空间 O(max(m,n))。</p></blockquote><h2 id="190.-%E9%A2%A0%E5%80%92%E4%BA%8C%E8%BF%9B%E5%88%B6%E4%BD%8D" tabindex="-1"><a href="https://leetcode.cn/problems/reverse-bits/" target="_blank">190. 颠倒二进制位</a></h2><blockquote><p>32 位无符号整数的二进制位颠倒。</p></blockquote><p><strong>思路脉络</strong>：逐位处理。每次取 <code>n</code> 最低位，左移到结果的高位（<code>res = (res&lt;&lt;1) | (n&amp;1)</code>），<code>n</code> 右移。32 次循环。</p><div class="mermaid"><svg id="render612152354" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 504.3671875px;" viewBox="0 0 504.3671875 89.5"><style>#render612152354 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render612152354 .error-icon{fill:#552222;}#render612152354 .error-text{fill:#552222;stroke:#552222;}#render612152354 .edge-thickness-normal{stroke-width:2px;}#render612152354 .edge-thickness-thick{stroke-width:3.5px;}#render612152354 .edge-pattern-solid{stroke-dasharray:0;}#render612152354 .edge-pattern-dashed{stroke-dasharray:3;}#render612152354 .edge-pattern-dotted{stroke-dasharray:2;}#render612152354 .marker{fill:#333333;stroke:#333333;}#render612152354 .marker.cross{stroke:#333333;}#render612152354 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render612152354 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render612152354 .cluster-label text{fill:#333;}#render612152354 .cluster-label span{color:#333;}#render612152354 .label text,#render612152354 span{fill:#333;color:#333;}#render612152354 .node rect,#render612152354 .node circle,#render612152354 .node ellipse,#render612152354 .node polygon,#render612152354 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render612152354 .node .label{text-align:center;}#render612152354 .node.clickable{cursor:pointer;}#render612152354 .arrowheadPath{fill:#333333;}#render612152354 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render612152354 .flowchart-link{stroke:#333333;fill:none;}#render612152354 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render612152354 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render612152354 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render612152354 .cluster text{fill:#333;}#render612152354 .cluster span{color:#333;}#render612152354 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render612152354 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92.78125,37.31260143751449L96.94791666666667,35.63550119792874C101.11458333333333,33.95840095834299,109.44791666666667,30.604200479171496,117.78125,28.92710023958575C126.11458333333333,27.25,134.44791666666666,27.25,138.61458333333334,27.25L142.78125,27.25" marker-end="url(#arrowhead7603)" style="fill:none"></path><defs><marker id="arrowhead7603" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M375.671875,27.25L379.8385416666667,27.25C384.0052083333333,27.25,392.3385416666667,27.25,400.671875,29.12282887781302C409.0052083333333,30.99565775562604,417.3385416666667,34.741315511252076,421.5052083333333,36.614144389065096L425.671875,38.48697326687812" marker-end="url(#arrowhead7604)" style="fill:none"></path><defs><marker id="arrowhead7604" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M425.671875,70.26302673312189L421.5052083333333,72.13585561093491C417.3385416666667,74.00868448874793,409.0052083333333,77.75434224437396,381.2643229166667,79.62717112218698C353.5234375,81.5,306.375,81.5,259.2265625,81.5C212.078125,81.5,164.9296875,81.5,137.18880208333334,79.82289976041424C109.44791666666667,78.1457995208285,101.11458333333333,74.79159904165701,96.94791666666667,73.11449880207125L92.78125,71.43739856248551" marker-end="url(#arrowhead7605)" style="fill:none"></path><defs><marker id="arrowhead7605" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8098" transform="translate(50.390625,54.375)"><rect rx="0" ry="0" x="-42.390625" y="-19.25" width="84.78125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">32次循环</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8099" transform="translate(259.2265625,27.25)"><rect rx="0" ry="0" x="-116.4453125" y="-19.25" width="232.890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-106.4453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">res=(res&amp;lt;&amp;lt;1)|(n&amp;amp;1)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8101" transform="translate(461.01953125,54.375)"><rect rx="0" ry="0" x="-35.34765625" y="-19.25" width="70.6953125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-25.34765625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">n&gt;&gt;&gt;=1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">public class Solution {    public int reverseBits(int n) {        int res = 0;        for (int i = 0; i &lt; 32; i++) {            res = (res &lt;&lt; 1) | (n &amp; 1);            n &gt;&gt;&gt;= 1;        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(1)（固定 32 位），空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>用 <code>&gt;&gt;&gt;</code> 无符号右移，不是 <code>&gt;&gt;</code>（算术右移会填符号位）。</li></ul><h2 id="191.-%E4%BD%8D-1-%E7%9A%84%E4%B8%AA%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/number-of-1-bits/" target="_blank">191. 位 1 的个数</a></h2><blockquote><p>32 位整数的 1 的个数。</p></blockquote><p><strong>思路脉络</strong>：<code>n &amp; (n-1)</code> 消去最低位的 1，直到 <code>n=0</code>，操作次数即 1 的个数。比逐位检查快（只数 1）。</p><div class="mermaid"><svg id="render2959461056" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 366.765625px;" viewBox="0 0 366.765625 89.5"><style>#render2959461056 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2959461056 .error-icon{fill:#552222;}#render2959461056 .error-text{fill:#552222;stroke:#552222;}#render2959461056 .edge-thickness-normal{stroke-width:2px;}#render2959461056 .edge-thickness-thick{stroke-width:3.5px;}#render2959461056 .edge-pattern-solid{stroke-dasharray:0;}#render2959461056 .edge-pattern-dashed{stroke-dasharray:3;}#render2959461056 .edge-pattern-dotted{stroke-dasharray:2;}#render2959461056 .marker{fill:#333333;stroke:#333333;}#render2959461056 .marker.cross{stroke:#333333;}#render2959461056 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2959461056 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2959461056 .cluster-label text{fill:#333;}#render2959461056 .cluster-label span{color:#333;}#render2959461056 .label text,#render2959461056 span{fill:#333;color:#333;}#render2959461056 .node rect,#render2959461056 .node circle,#render2959461056 .node ellipse,#render2959461056 .node polygon,#render2959461056 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2959461056 .node .label{text-align:center;}#render2959461056 .node.clickable{cursor:pointer;}#render2959461056 .arrowheadPath{fill:#333333;}#render2959461056 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2959461056 .flowchart-link{stroke:#333333;fill:none;}#render2959461056 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2959461056 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2959461056 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2959461056 .cluster text{fill:#333;}#render2959461056 .cluster span{color:#333;}#render2959461056 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2959461056 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M59.3984375,40.6254526542877L63.565104166666664,38.39621054523975C67.73177083333333,36.1669684361918,76.06510416666667,31.7084842180959,84.3984375,29.47924210904795C92.73177083333333,27.25,101.06510416666667,27.25,105.23177083333333,27.25L109.3984375,27.25" marker-end="url(#arrowhead7621)" style="fill:none"></path><defs><marker id="arrowhead7621" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M248.9765625,27.25L253.14322916666666,27.25C257.3098958333333,27.25,265.6432291666667,27.25,273.9765625,29.30887236414526C282.3098958333333,31.367744728290518,290.6432291666667,35.48548945658104,294.8098958333333,37.5443618207263L298.9765625,39.60323418487156" marker-end="url(#arrowhead7622)" style="fill:none"></path><defs><marker id="arrowhead7622" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M298.9765625,69.14676581512845L294.8098958333333,71.2056381792737C290.6432291666667,73.26451054341896,282.3098958333333,77.38225527170948,262.3450520833333,79.44112763585474C242.38020833333334,81.5,210.78385416666666,81.5,179.1875,81.5C147.59114583333334,81.5,115.99479166666667,81.5,96.02994791666667,79.27075789095205C76.06510416666667,77.0415157819041,67.73177083333333,72.5830315638082,63.565104166666664,70.35378945476025L59.3984375,68.1245473457123" marker-end="url(#arrowhead7623)" style="fill:none"></path><defs><marker id="arrowhead7623" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8110" transform="translate(33.69921875,54.375)"><rect rx="0" ry="0" x="-25.69921875" y="-19.25" width="51.3984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-15.69921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">n≠0?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8111" transform="translate(179.1875,27.25)"><rect rx="0" ry="0" x="-69.7890625" y="-19.25" width="139.578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-59.7890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">n&amp;=n-1(去最低1)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8113" transform="translate(328.87109375,54.375)"><rect rx="0" ry="0" x="-29.89453125" y="-19.25" width="59.7890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-19.89453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cnt++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">public class Solution {    public int hammingWeight(int n) {        int cnt = 0;        while (n != 0) { n &amp;= n - 1; cnt++; }        return cnt;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(1)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li><code>n &amp; (n-1)</code> 去最低 1：数 1、判 2 的幂（2 的幂只有一个 1，操作一次变 0）。</li></ul><h2 id="136.-%E5%8F%AA%E5%87%BA%E7%8E%B0%E4%B8%80%E6%AC%A1%E7%9A%84%E6%95%B0%E5%AD%97" tabindex="-1"><a href="https://leetcode.cn/problems/single-number/" target="_blank">136. 只出现一次的数字</a></h2><blockquote><p>除一个数外每个出现两次，找那个。</p></blockquote><p><strong>思路脉络</strong>：异或——<code>a^a=0</code>、<code>a^0=a</code>、交换结合。全部异或，成对的抵消，剩单个的。</p><div class="mermaid"><svg id="render3655617817" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 273.1171875px;" viewBox="0 0 273.1171875 143"><style>#render3655617817 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3655617817 .error-icon{fill:#552222;}#render3655617817 .error-text{fill:#552222;stroke:#552222;}#render3655617817 .edge-thickness-normal{stroke-width:2px;}#render3655617817 .edge-thickness-thick{stroke-width:3.5px;}#render3655617817 .edge-pattern-solid{stroke-dasharray:0;}#render3655617817 .edge-pattern-dashed{stroke-dasharray:3;}#render3655617817 .edge-pattern-dotted{stroke-dasharray:2;}#render3655617817 .marker{fill:#333333;stroke:#333333;}#render3655617817 .marker.cross{stroke:#333333;}#render3655617817 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3655617817 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3655617817 .cluster-label text{fill:#333;}#render3655617817 .cluster-label span{color:#333;}#render3655617817 .label text,#render3655617817 span{fill:#333;color:#333;}#render3655617817 .node rect,#render3655617817 .node circle,#render3655617817 .node ellipse,#render3655617817 .node polygon,#render3655617817 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3655617817 .node .label{text-align:center;}#render3655617817 .node.clickable{cursor:pointer;}#render3655617817 .arrowheadPath{fill:#333333;}#render3655617817 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3655617817 .flowchart-link{stroke:#333333;fill:none;}#render3655617817 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3655617817 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3655617817 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3655617817 .cluster text{fill:#333;}#render3655617817 .cluster span{color:#333;}#render3655617817 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3655617817 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M129.41015625,22.86631744569806L138.93815104166666,21.930264538081715C148.46614583333334,20.994211630465372,167.52213541666666,19.122105815232686,181.216796875,18.990902124145933C194.91145833333334,18.859698433059183,203.24479166666666,20.469396866118363,207.41145833333334,21.274246082647952L211.578125,22.079095299177546" marker-end="url(#arrowhead7632)" style="fill:none"></path><defs><marker id="arrowhead7632" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-A" style="opacity: 1;" id="L-B-A"><path class="path" d="M211.578125,32.420904700822454L207.41145833333334,33.225753917352044C203.24479166666666,34.030603133881634,194.91145833333334,35.64030156694082,181.216796875,35.50909787585407C167.52213541666666,35.377894184767314,148.46614583333334,33.50578836953463,138.93815104166666,32.569735461918285L129.41015625,31.63368255430194" marker-end="url(#arrowhead7633)" style="fill:none"></path><defs><marker id="arrowhead7633" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8121" transform="translate(84.7890625,27.25)"><rect rx="0" ry="0" x="-44.62109375" y="-19.25" width="89.2421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.62109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历nums</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8122" transform="translate(238.34765625,27.25)"><rect rx="0" ry="0" x="-26.76953125" y="-19.25" width="53.5390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16.76953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">x^=n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8125" transform="translate(84.7890625,115.75)"><rect rx="0" ry="0" x="-76.7890625" y="-19.25" width="153.578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-66.7890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return x(成对抵消)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int singleNumber(int[] nums) {        int x = 0; for (int n : nums) x ^= n; return x;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="137.-%E5%8F%AA%E5%87%BA%E7%8E%B0%E4%B8%80%E6%AC%A1%E7%9A%84%E6%95%B0%E5%AD%97-ii" tabindex="-1"><a href="https://leetcode.cn/problems/single-number-ii/" target="_blank">137. 只出现一次的数字 II</a></h2><blockquote><p>除一个数外每个出现三次，找那个。</p></blockquote><p><strong>思路脉络</strong>：位统计。对每一位，统计所有数该位的 1 个数，<code>% 3</code> 余数就是答案该位。32 位各算一次。</p><div class="mermaid"><svg id="render1817574382" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 482.5625px;" viewBox="0 0 482.5625 89.5"><style>#render1817574382 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1817574382 .error-icon{fill:#552222;}#render1817574382 .error-text{fill:#552222;stroke:#552222;}#render1817574382 .edge-thickness-normal{stroke-width:2px;}#render1817574382 .edge-thickness-thick{stroke-width:3.5px;}#render1817574382 .edge-pattern-solid{stroke-dasharray:0;}#render1817574382 .edge-pattern-dashed{stroke-dasharray:3;}#render1817574382 .edge-pattern-dotted{stroke-dasharray:2;}#render1817574382 .marker{fill:#333333;stroke:#333333;}#render1817574382 .marker.cross{stroke:#333333;}#render1817574382 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1817574382 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1817574382 .cluster-label text{fill:#333;}#render1817574382 .cluster-label span{color:#333;}#render1817574382 .label text,#render1817574382 span{fill:#333;color:#333;}#render1817574382 .node rect,#render1817574382 .node circle,#render1817574382 .node ellipse,#render1817574382 .node polygon,#render1817574382 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1817574382 .node .label{text-align:center;}#render1817574382 .node.clickable{cursor:pointer;}#render1817574382 .arrowheadPath{fill:#333333;}#render1817574382 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1817574382 .flowchart-link{stroke:#333333;fill:none;}#render1817574382 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1817574382 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1817574382 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1817574382 .cluster text{fill:#333;}#render1817574382 .cluster span{color:#333;}#render1817574382 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1817574382 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M92.78125,37.31260143751449L96.94791666666667,35.63550119792874C101.11458333333333,33.95840095834299,109.44791666666667,30.604200479171496,117.78125,28.92710023958575C126.11458333333333,27.25,134.44791666666666,27.25,138.61458333333334,27.25L142.78125,27.25" marker-end="url(#arrowhead7649)" style="fill:none"></path><defs><marker id="arrowhead7649" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M283.171875,27.25L287.3385416666667,27.25C291.5052083333333,27.25,299.8385416666667,27.25,308.6356266801075,28.5625C317.43271169354836,29.875,326.69354838709677,32.5,331.323966733871,33.8125L335.9543850806452,35.125" marker-end="url(#arrowhead7650)" style="fill:none"></path><defs><marker id="arrowhead7650" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M335.9543850806452,73.625L331.323966733871,74.9375C326.69354838709677,76.25,317.43271169354836,78.875,296.9364079301075,80.1875C276.4401041666667,81.5,244.70833333333334,81.5,212.9765625,81.5C181.24479166666666,81.5,149.51302083333334,81.5,129.48046875,79.82289976041424C109.44791666666667,78.1457995208285,101.11458333333333,74.79159904165701,96.94791666666667,73.11449880207125L92.78125,71.43739856248551" marker-end="url(#arrowhead7651)" style="fill:none"></path><defs><marker id="arrowhead7651" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8132" transform="translate(50.390625,54.375)"><rect rx="0" ry="0" x="-42.390625" y="-19.25" width="84.78125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">32位循环</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8133" transform="translate(212.9765625,27.25)"><rect rx="0" ry="0" x="-70.1953125" y="-19.25" width="140.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-60.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">统计该位1的个数</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8135" transform="translate(403.8671875,54.375)"><rect rx="0" ry="0" x="-70.6953125" y="-19.25" width="141.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-60.6953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans|=(cnt%3)&amp;lt;</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int singleNumber(int[] nums) {        int ans = 0;        for (int i = 0; i &lt; 32; i++) {            int cnt = 0;            for (int n : nums) cnt += (n &gt;&gt; i) &amp; 1;            ans |= (cnt % 3) &lt;&lt; i;        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(32n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>32 位逐位统计，<code>|=</code> 累加各位。<code>% 3</code> 因为其他数出现 3 次该位贡献 3 的倍数，被模掉。</li></ul><h2 id="201.-%E6%95%B0%E5%AD%97%E8%8C%83%E5%9B%B4%E6%8C%89%E4%BD%8D%E4%B8%8E" tabindex="-1"><a href="https://leetcode.cn/problems/bitwise-and-of-numbers-range/" target="_blank">201. 数字范围按位与</a></h2><blockquote><p><code>[m, n]</code> 范围内所有整数按位与的结果。</p></blockquote><p><strong>思路脉络</strong>：按位与中只要某位在范围内出现过 0，该位必 0。只保留 m 和 n 的公共前缀，低位全 0。右移到相等再左移回来。</p><div class="mermaid"><svg id="render408610249" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 347.046875px;" viewBox="0 0 347.046875 143"><style>#render408610249 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render408610249 .error-icon{fill:#552222;}#render408610249 .error-text{fill:#552222;stroke:#552222;}#render408610249 .edge-thickness-normal{stroke-width:2px;}#render408610249 .edge-thickness-thick{stroke-width:3.5px;}#render408610249 .edge-pattern-solid{stroke-dasharray:0;}#render408610249 .edge-pattern-dashed{stroke-dasharray:3;}#render408610249 .edge-pattern-dotted{stroke-dasharray:2;}#render408610249 .marker{fill:#333333;stroke:#333333;}#render408610249 .marker.cross{stroke:#333333;}#render408610249 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render408610249 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render408610249 .cluster-label text{fill:#333;}#render408610249 .cluster-label span{color:#333;}#render408610249 .label text,#render408610249 span{fill:#333;color:#333;}#render408610249 .node rect,#render408610249 .node circle,#render408610249 .node ellipse,#render408610249 .node polygon,#render408610249 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render408610249 .node .label{text-align:center;}#render408610249 .node.clickable{cursor:pointer;}#render408610249 .arrowheadPath{fill:#333333;}#render408610249 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render408610249 .flowchart-link{stroke:#333333;fill:none;}#render408610249 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render408610249 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render408610249 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render408610249 .cluster text{fill:#333;}#render408610249 .cluster span{color:#333;}#render408610249 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render408610249 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M91.80859375,23.760895883777238L100.56184895833333,22.675746569814365C109.31510416666667,21.590597255851492,126.82161458333333,19.420298627925746,139.74153645833334,18.714422058475268C152.66145833333334,18.008545489024794,160.99479166666666,18.76709097804959,165.16145833333334,19.146363722561986L169.328125,19.525636467074385" marker-end="url(#arrowhead7660)" style="fill:none"></path><defs><marker id="arrowhead7660" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-A" style="opacity: 1;" id="L-B-A"><path class="path" d="M169.328125,34.974363532925615L165.16145833333334,35.353636277438014C160.99479166666666,35.73290902195041,152.66145833333334,36.49145451097521,139.74153645833334,35.78557794152473C126.82161458333333,35.079701372074254,109.31510416666667,32.90940274414851,100.56184895833333,31.824253430185635L91.80859375,30.739104116222762" marker-end="url(#arrowhead7661)" style="fill:none"></path><defs><marker id="arrowhead7661" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8143" transform="translate(63.6640625,27.25)"><rect rx="0" ry="0" x="-28.14453125" y="-19.25" width="56.2890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.14453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">m≠n?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8144" transform="translate(254.1875,27.25)"><rect rx="0" ry="0" x="-84.859375" y="-19.25" width="169.71875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-74.859375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">m&gt;&gt;=1,n&gt;&gt;=1,shift++</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8147" transform="translate(63.6640625,115.75)"><rect rx="0" ry="0" x="-55.6640625" y="-19.25" width="111.328125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-45.6640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return m&amp;lt;</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int rangeBitwiseAnd(int m, int n) {        int shift = 0;        while (m != n) { m &gt;&gt;= 1; n &gt;&gt;= 1; shift++; }        return m &lt;&lt; shift;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>公共前缀才保留——范围内 m 到 n 经历了后缀从全 0 到全 1，按位与后缀必 0。<code>shift</code> 记右移次数，最后左移还原。</li></ul><hr /><h1 id="19.-%E6%95%B0%E5%AD%A6" tabindex="-1">19. 数学</h1><p>数学题靠数论、模拟、二分逼近。注意溢出（用 long）和边界（0、负数）。</p><h2 id="9.-%E5%9B%9E%E6%96%87%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/palindrome-number/" target="_blank">9. 回文数</a></h2><blockquote><p>整数是否回文，不转字符串。</p></blockquote><p><strong>思路脉络</strong>：反转后半数字，和前半比较。<code>x % 10</code> 取低位、<code>x / 10</code> 去低位，反转数 <code>rev = rev*10 + 低位</code>。当 <code>rev &gt;= x</code> 时反转到一半。</p><div class="mermaid"><svg id="render3948078409" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="166.90469360351562" style="max-width: 471.62030029296875px;" viewBox="0 0 471.62030029296875 166.90469360351562"><style>#render3948078409 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3948078409 .error-icon{fill:#552222;}#render3948078409 .error-text{fill:#552222;stroke:#552222;}#render3948078409 .edge-thickness-normal{stroke-width:2px;}#render3948078409 .edge-thickness-thick{stroke-width:3.5px;}#render3948078409 .edge-pattern-solid{stroke-dasharray:0;}#render3948078409 .edge-pattern-dashed{stroke-dasharray:3;}#render3948078409 .edge-pattern-dotted{stroke-dasharray:2;}#render3948078409 .marker{fill:#333333;stroke:#333333;}#render3948078409 .marker.cross{stroke:#333333;}#render3948078409 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3948078409 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3948078409 .cluster-label text{fill:#333;}#render3948078409 .cluster-label span{color:#333;}#render3948078409 .label text,#render3948078409 span{fill:#333;color:#333;}#render3948078409 .node rect,#render3948078409 .node circle,#render3948078409 .node ellipse,#render3948078409 .node polygon,#render3948078409 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3948078409 .node .label{text-align:center;}#render3948078409 .node.clickable{cursor:pointer;}#render3948078409 .arrowheadPath{fill:#333333;}#render3948078409 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3948078409 .flowchart-link{stroke:#333333;fill:none;}#render3948078409 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3948078409 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3948078409 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3948078409 .cluster text{fill:#333;}#render3948078409 .cluster span{color:#333;}#render3948078409 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3948078409 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M63.62680537012599,90.75L68.44942114177167,94.34192721048991C73.27203691341732,97.93385442097981,82.91726845670867,105.11770884195964,91.98988448266749,108.79296938578288C101.06250050862631,112.46822992960612,109.56250101725261,112.63489659627278,113.81250127156578,112.71822992960612L118.06250152587893,112.80156326293945" marker-end="url(#arrowhead7680)" style="fill:none"></path><defs><marker id="arrowhead7680" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M194.61847923082212,96.15129096788272L202.81019153431143,91.46800301705883C211.00190383780068,86.78471506623497,227.38532844477925,77.4181391645872,246.70681538099188,69.14292400327342C266.02830231720446,60.86770884195963,288.2878515826512,53.68385442097982,299.41762621537447,50.09192721048991L310.5474008480978,46.5" marker-end="url(#arrowhead7681)" style="fill:none"></path><defs><marker id="arrowhead7681" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M276.7687530517578,16.442457129615327L271.2687530517578,15.806214274679439C265.7687530517578,15.169971419743552,254.7687530517578,13.897485709871775,236.00156529744467,13.261242854935887C217.2343775431315,12.625,190.70000203450522,12.625,165.49895985921225,12.625C140.29791768391928,12.625,116.43020884195964,12.625,98.35139467221832,19.229166666666668C80.272580502477,25.833333333333332,67.982661004954,39.041666666666664,61.837701256192496,45.645833333333336L55.692741507430995,52.25" marker-end="url(#arrowhead7682)" style="fill:none"></path><defs><marker id="arrowhead7682" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M202.19356664596916,121.87674509109142L209.12276438026728,123.48921478639943C216.05196211456538,125.10168448170744,229.9103575831616,128.32662387232344,244.39359177579306,129.93909356763143C258.8768259684245,131.55156326293945,273.9848988850911,131.55156326293945,281.5389353434245,131.55156326293945L289.0929718017578,131.55156326293945" marker-end="url(#arrowhead7683)" style="fill:none"></path><defs><marker id="arrowhead7683" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(243.7687530517578,68.05156326293945)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000598907470703" height="18.500694274902344"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(243.7687530517578,131.55156326293945)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000598907470703" height="18.500694274902344"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8156" transform="translate(37.78125,71.5)"><rect rx="0" ry="0" x="-29.78125" y="-19.25" width="59.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-19.78125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">rev=0</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8157" transform="translate(164.1656265258789,112.30156326293945)"><polygon points="46.603125,0 93.20625,-46.603125 46.603125,-93.20625 0,-46.603125" transform="translate(-46.603125,46.603125)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-22.53125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">x&gt;rev?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8159" transform="translate(370.1945343017578,27.25)"><rect rx="0" ry="0" x="-93.42578125" y="-19.25" width="186.8515625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-83.42578125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">rev=rev*10+x%10,x/=10</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8163" transform="translate(370.1945343017578,131.55156326293945)"><rect rx="0" ry="0" x="-81.1015625" y="-19.25" width="162.203125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-71.1015625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">x==rev或x==rev/10?</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isPalindrome(int x) {        if (x &lt; 0 || (x % 10 == 0 &amp;&amp; x != 0)) return false;        int rev = 0;        while (x &gt; rev) { rev = rev * 10 + x % 10; x /= 10; }        return x == rev || x == rev / 10;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>排除末尾 0 的非零数（10 不是回文）。奇数位反转多一位，<code>x == rev/10</code> 处理中间位。</li></ul><h2 id="66.-%E5%8A%A0%E4%B8%80" tabindex="-1"><a href="https://leetcode.cn/problems/plus-one/" target="_blank">66. 加一</a></h2><blockquote><p>数组表示的大整数加一。</p></blockquote><p><strong>思路脉络</strong>：从低位加，处理进位。全进位则首位变 1、长度+1。</p><div class="mermaid"><svg id="render1225179519" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="143" style="max-width: 582.9312744140625px;" viewBox="0 0 582.9312744140625 143"><style>#render1225179519 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1225179519 .error-icon{fill:#552222;}#render1225179519 .error-text{fill:#552222;stroke:#552222;}#render1225179519 .edge-thickness-normal{stroke-width:2px;}#render1225179519 .edge-thickness-thick{stroke-width:3.5px;}#render1225179519 .edge-pattern-solid{stroke-dasharray:0;}#render1225179519 .edge-pattern-dashed{stroke-dasharray:3;}#render1225179519 .edge-pattern-dotted{stroke-dasharray:2;}#render1225179519 .marker{fill:#333333;stroke:#333333;}#render1225179519 .marker.cross{stroke:#333333;}#render1225179519 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1225179519 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1225179519 .cluster-label text{fill:#333;}#render1225179519 .cluster-label span{color:#333;}#render1225179519 .label text,#render1225179519 span{fill:#333;color:#333;}#render1225179519 .node rect,#render1225179519 .node circle,#render1225179519 .node ellipse,#render1225179519 .node polygon,#render1225179519 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1225179519 .node .label{text-align:center;}#render1225179519 .node.clickable{cursor:pointer;}#render1225179519 .arrowheadPath{fill:#333333;}#render1225179519 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1225179519 .flowchart-link{stroke:#333333;fill:none;}#render1225179519 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1225179519 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1225179519 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1225179519 .cluster text{fill:#333;}#render1225179519 .cluster span{color:#333;}#render1225179519 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1225179519 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M100.390625,71.5L104.55729166666667,71.5C108.72395833333333,71.5,117.05729166666667,71.5,125.4739584604899,71.58333333333333C133.89062525431314,71.66666666666667,142.39062550862627,71.83333333333333,146.64062563578284,71.91666666666667L150.8906257629394,72" marker-end="url(#arrowhead7703)" style="fill:none"></path><defs><marker id="arrowhead7703" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M225.39973344431894,55.60910768137944L233.54821562457892,50.88258973448287C241.69669780483892,46.15607178758629,257.9936621653589,36.70303589379315,271.6421443456189,31.976517946896575C285.2906265258789,27.25,296.2906265258789,27.25,301.7906265258789,27.25L307.2906265258789,27.25" marker-end="url(#arrowhead7704)" style="fill:none"></path><defs><marker id="arrowhead7704" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M225.39973246872643,88.39089176833416L233.54821481158515,92.95074314027846C241.6966971544439,97.51059451222277,257.9936618401614,106.63029725611138,272.00282126635346,111.1901486280557C286.0119806925456,115.75,297.7333348592122,115.75,303.5940119425456,115.75L309.4546890258789,115.75" marker-end="url(#arrowhead7705)" style="fill:none"></path><defs><marker id="arrowhead7705" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M391.7203140258789,115.75L396.2476577758789,115.75C400.7750015258789,115.75,409.8296890258789,115.75,418.5236994425456,115.75C427.2177098592122,115.75,435.5510431925456,115.75,439.7177098592122,115.75L443.8843765258789,115.75" marker-end="url(#arrowhead7706)" style="fill:none"></path><defs><marker id="arrowhead7706" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(274.2906265258789,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000043869018555" height="18.500051498413086"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(274.2906265258789,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000043869018555" height="18.500051498413086"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8172" transform="translate(54.1953125,71.5)"><rect rx="0" ry="0" x="-46.1953125" y="-19.25" width="92.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">从末位加1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8173" transform="translate(195.84062576293945,71.5)"><polygon points="45.45,0 90.9,-45.45 45.45,-90.9 0,-45.45" transform="translate(-45.45,45.45)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-21.25,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">&amp;lt;9?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8175" transform="translate(350.5875015258789,27.25)"><rect rx="0" ry="0" x="-43.296875" y="-19.25" width="86.59375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-33.296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">+1 return</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8177" transform="translate(350.5875015258789,115.75)"><rect rx="0" ry="0" x="-41.1328125" y="-19.25" width="82.265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.1328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">置0,进位</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8179" transform="translate(509.4078140258789,115.75)"><rect rx="0" ry="0" x="-65.5234375" y="-19.25" width="131.046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-55.5234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">首位变1,长度+1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int[] plusOne(int[] digits) {        for (int i = digits.length - 1; i &gt;= 0; i--) {            if (digits[i] &lt; 9) { digits[i]++; return digits; }            digits[i] = 0;        }        int[] res = new int[digits.length + 1]; res[0] = 1;        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)（除全进位新建数组）。</p></blockquote><h2 id="172.-%E9%98%B6%E4%B9%98%E5%90%8E%E7%9A%84%E9%9B%B6" tabindex="-1"><a href="https://leetcode.cn/problems/factorial-trailing-zeroes/" target="_blank">172. 阶乘后的零</a></h2><blockquote><p><code>n!</code> 末尾 0 的个数。</p></blockquote><p><strong>思路脉络</strong>：0 由因子 2×5 产生，2 充足，数 5 的个数。25/125 等含多个 5，累加 <code>n/5 + n/25 + n/125 + ...</code>。</p><div class="mermaid"><svg id="render526556381" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="109" style="max-width: 479.4085998535156px;" viewBox="0 0 479.4085998535156 109"><style>#render526556381 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render526556381 .error-icon{fill:#552222;}#render526556381 .error-text{fill:#552222;stroke:#552222;}#render526556381 .edge-thickness-normal{stroke-width:2px;}#render526556381 .edge-thickness-thick{stroke-width:3.5px;}#render526556381 .edge-pattern-solid{stroke-dasharray:0;}#render526556381 .edge-pattern-dashed{stroke-dasharray:3;}#render526556381 .edge-pattern-dotted{stroke-dasharray:2;}#render526556381 .marker{fill:#333333;stroke:#333333;}#render526556381 .marker.cross{stroke:#333333;}#render526556381 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render526556381 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render526556381 .cluster-label text{fill:#333;}#render526556381 .cluster-label span{color:#333;}#render526556381 .label text,#render526556381 span{fill:#333;color:#333;}#render526556381 .node rect,#render526556381 .node circle,#render526556381 .node ellipse,#render526556381 .node polygon,#render526556381 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render526556381 .node .label{text-align:center;}#render526556381 .node.clickable{cursor:pointer;}#render526556381 .arrowheadPath{fill:#333333;}#render526556381 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render526556381 .flowchart-link{stroke:#333333;fill:none;}#render526556381 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render526556381 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render526556381 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render526556381 .cluster text{fill:#333;}#render526556381 .cluster span{color:#333;}#render526556381 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render526556381 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M61.9140625,42.527046838583566L66.08072916666667,39.980872365486306C70.24739583333333,37.434697892389046,78.58072916666667,32.34234894619453,86.9140625,29.796174473097263C95.24739583333333,27.25,103.58072916666667,27.25,107.74739583333333,27.25L111.9140625,27.25" marker-end="url(#arrowhead7729)" style="fill:none"></path><defs><marker id="arrowhead7729" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M181.0625,27.25L185.22916666666666,27.25C189.39583333333334,27.25,197.72916666666666,27.25,208.34810902633163,30.4227244341583C218.96705138599657,33.5954488683166,231.87160277199314,39.9408977366332,238.32387846499145,43.1136221707915L244.7761541579897,46.2863466049498" marker-end="url(#arrowhead7730)" style="fill:none"></path><defs><marker id="arrowhead7730" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M244.77615313051112,72.71365389345057L238.32387760875926,75.71971157787547C231.87160208700743,78.72576926230037,218.96705104350372,84.73788463115018,202.58573906341852,87.7439423155751C186.20442708333334,90.75,166.34635416666666,90.75,146.48828125,90.75C126.63020833333333,90.75,106.77213541666667,90.75,92.67643229166667,88.20382552690273C78.58072916666667,85.65765105380547,70.24739583333333,80.56530210761095,66.08072916666667,78.01912763451368L61.9140625,75.47295316141643" marker-end="url(#arrowhead7731)" style="fill:none"></path><defs><marker id="arrowhead7731" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M312.47109298706056,59.5L317.8877595265706,59.416666666666664C323.30442606608074,59.333333333333336,334.1377591451009,59.166666666666664,345.05442568461103,59.083333333333336C355.9710922241211,59,366.9710922241211,59,372.4710922241211,59L377.9710922241211,59" marker-end="url(#arrowhead7732)" style="fill:none"></path><defs><marker id="arrowhead7732" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(146.48828125,90.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000076293945312" height="18.500089645385742"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(344.9710922241211,59)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000076293945312" height="18.500089645385742"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8188" transform="translate(34.95703125,59)"><rect rx="0" ry="0" x="-26.95703125" y="-19.25" width="53.9140625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-16.95703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">n/=5</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8189" transform="translate(146.48828125,27.25)"><rect rx="0" ry="0" x="-34.57421875" y="-19.25" width="69.1484375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24.57421875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans+=n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8191" transform="translate(271.51679611206055,59)"><polygon points="40.454296875000004,0 80.90859375000001,-40.454296875000004 40.454296875000004,-80.90859375000001 0,-40.454296875000004" transform="translate(-40.454296875000004,40.454296875000004)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-15.69921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">n&gt;0?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8195" transform="translate(424.6898422241211,59)"><rect rx="0" ry="0" x="-46.71875" y="-19.25" width="93.4375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-36.71875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">return ans</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int trailingZeroes(int n) {        int ans = 0;        while (n &gt; 0) { n /= 5; ans += n; }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>25 贡献两个 5——<code>n/5</code> 算第一个、<code>n/25</code> 算第二个，循环累加全计入。不是直接 <code>n/5</code>。</li></ul><h2 id="69.-x-%E7%9A%84%E5%B9%B3%E6%96%B9%E6%A0%B9" tabindex="-1"><a href="https://leetcode.cn/problems/sqrtx/" target="_blank">69. x 的平方根</a></h2><blockquote><p>求 <code>x</code> 的整数平方根（向下取整）。</p></blockquote><p><strong>思路脉络</strong>：二分。<code>mid*mid &lt;= x</code> 则候选并往右，否则往左。用 <code>mid &lt;= x/mid</code> 代替 <code>mid*mid</code> 防溢出。</p><div class="mermaid"><svg id="render3293073244" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="233.2859344482422" style="max-width: 420.50469970703125px;" viewBox="0 0 420.50469970703125 233.2859344482422"><style>#render3293073244 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3293073244 .error-icon{fill:#552222;}#render3293073244 .error-text{fill:#552222;stroke:#552222;}#render3293073244 .edge-thickness-normal{stroke-width:2px;}#render3293073244 .edge-thickness-thick{stroke-width:3.5px;}#render3293073244 .edge-pattern-solid{stroke-dasharray:0;}#render3293073244 .edge-pattern-dashed{stroke-dasharray:3;}#render3293073244 .edge-pattern-dotted{stroke-dasharray:2;}#render3293073244 .marker{fill:#333333;stroke:#333333;}#render3293073244 .marker.cross{stroke:#333333;}#render3293073244 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3293073244 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3293073244 .cluster-label text{fill:#333;}#render3293073244 .cluster-label span{color:#333;}#render3293073244 .label text,#render3293073244 span{fill:#333;color:#333;}#render3293073244 .node rect,#render3293073244 .node circle,#render3293073244 .node ellipse,#render3293073244 .node polygon,#render3293073244 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3293073244 .node .label{text-align:center;}#render3293073244 .node.clickable{cursor:pointer;}#render3293073244 .arrowheadPath{fill:#333333;}#render3293073244 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3293073244 .flowchart-link{stroke:#333333;fill:none;}#render3293073244 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3293073244 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3293073244 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3293073244 .cluster text{fill:#333;}#render3293073244 .cluster span{color:#333;}#render3293073244 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3293073244 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M56.26359442959061,127.91093444824219L64.05299535799217,116.60585657755534C71.84239628639374,105.30077870686848,87.42119814319688,82.6906229654948,99.46059932591159,71.46887842814128C111.50000050862631,60.24713389078776,120.00000101725261,60.41380055745443,124.25000127156578,60.49713389078776L128.50000152587893,60.580467224121094" marker-end="url(#arrowhead7755)" style="fill:none"></path><defs><marker id="arrowhead7755" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M232.6609359741211,60.58046722412109L238.0776023864746,60.49713389078775C243.49426879882813,60.41380055745443,254.32760162353517,60.24713389078776,268.1284517639123,64.21217282613118C281.9293019042895,68.17721176147461,298.6976693603369,76.27395629882812,307.0818530883605,80.32232856750488L315.4660368163842,84.37070083618164" marker-end="url(#arrowhead7756)" style="fill:none"></path><defs><marker id="arrowhead7756" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M202.58041914472986,90.6609871052701L213.0105050286486,104.95264499576545C223.44059091256727,119.2443028862608,244.30076268040474,147.8276186672515,264.5798068976568,165.3550459218085C284.8588511149089,182.88247317636555,304.5567677815755,189.35401190448894,314.4057261149089,192.5897812685506L324.2546844482422,195.8255506326123" marker-end="url(#arrowhead7757)" style="fill:none"></path><defs><marker id="arrowhead7757" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M315.4660368163842,122.87070083618164L307.0818530883605,126.9190731048584C298.6976693603369,130.96744537353516,281.9293019042895,139.06418991088867,259.365040305579,143.11256217956543C236.8007787068685,147.1609344482422,208.44062296549478,147.1609344482422,181.41380055745444,147.1609344482422C154.38697814941406,147.1609344482422,128.69348907470703,147.1609344482422,111.68007787068684,147.1609344482422C94.66666666666667,147.1609344482422,86.33333333333333,147.1609344482422,82.16666666666667,147.1609344482422L78,147.1609344482422" marker-end="url(#arrowhead7758)" style="fill:none"></path><defs><marker id="arrowhead7758" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M324.2546844482422,211.07650367367972L314.4057261149089,212.67390880277347C304.5567677815755,214.2713139318672,284.8588511149089,217.46612419005473,260.8298149108887,219.06352931914844C236.8007787068685,220.6609344482422,208.44062296549478,220.6609344482422,181.41380055745444,220.6609344482422C154.38697814941406,220.6609344482422,128.69348907470703,220.6609344482422,108.46579215640112,211.61926778157553C88.23809523809524,202.57760111490884,73.47619047619048,184.49426778157553,66.0952380952381,175.45260111490884L58.714285714285715,166.4109344482422" marker-end="url(#arrowhead7759)" style="fill:none"></path><defs><marker id="arrowhead7759" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(265.1609344482422,60.080467224121094)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000320434570312" height="18.5003719329834"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(265.1609344482422,176.4109344482422)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000320434570312" height="18.5003719329834"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8206" transform="translate(43,147.1609344482422)"><rect rx="0" ry="0" x="-35" y="-19.25" width="70" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-25,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">l=1,r=x</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8207" transform="translate(180.0804672241211,60.080467224121094)"><polygon points="52.08046875,0 104.1609375,-52.08046875 52.08046875,-104.1609375 0,-52.08046875" transform="translate(-52.08046875,52.08046875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-28.6171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">m≤x/m?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8209" transform="translate(355.3328094482422,103.62070083618164)"><rect rx="0" ry="0" x="-57.171875" y="-19.25" width="114.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-47.171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans=m,l=m+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8211" transform="translate(355.3328094482422,206.0359344482422)"><rect rx="0" ry="0" x="-31.078125" y="-19.25" width="62.15625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-21.078125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">r=m-1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int mySqrt(int x) {        int l = 1, r = x, ans = 0;        while (l &lt;= r) {            int m = (l + r) / 2;            if (m &lt;= x / m) { ans = m; l = m + 1; } else r = m - 1;        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log x)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>m * m</code> 会溢出，用 <code>m &lt;= x / m</code> 等价判断（整数除法向下取整，安全）。</li></ul><h2 id="50.-pow(x%2C-n)" tabindex="-1"><a href="https://leetcode.cn/problems/powx-n/" target="_blank">50. Pow(x, n)</a></h2><blockquote><p>求 <code>x</code> 的 <code>n</code> 次幂，O(log n)。</p></blockquote><p><strong>思路脉络</strong>：快速幂（二分）。偶数 <code>x^n = (x^2)^(n/2)</code>，奇数多乘 <code>x</code>。<code>n</code> 负取倒数。迭代。</p><div class="mermaid"><svg id="render4144829492" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="142" style="max-width: 546.015625px;" viewBox="0 1 546.015625 142"><style>#render4144829492 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render4144829492 .error-icon{fill:#552222;}#render4144829492 .error-text{fill:#552222;stroke:#552222;}#render4144829492 .edge-thickness-normal{stroke-width:2px;}#render4144829492 .edge-thickness-thick{stroke-width:3.5px;}#render4144829492 .edge-pattern-solid{stroke-dasharray:0;}#render4144829492 .edge-pattern-dashed{stroke-dasharray:3;}#render4144829492 .edge-pattern-dotted{stroke-dasharray:2;}#render4144829492 .marker{fill:#333333;stroke:#333333;}#render4144829492 .marker.cross{stroke:#333333;}#render4144829492 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render4144829492 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render4144829492 .cluster-label text{fill:#333;}#render4144829492 .cluster-label span{color:#333;}#render4144829492 .label text,#render4144829492 span{fill:#333;color:#333;}#render4144829492 .node rect,#render4144829492 .node circle,#render4144829492 .node ellipse,#render4144829492 .node polygon,#render4144829492 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render4144829492 .node .label{text-align:center;}#render4144829492 .node.clickable{cursor:pointer;}#render4144829492 .arrowheadPath{fill:#333333;}#render4144829492 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render4144829492 .flowchart-link{stroke:#333333;fill:none;}#render4144829492 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render4144829492 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render4144829492 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render4144829492 .cluster text{fill:#333;}#render4144829492 .cluster span{color:#333;}#render4144829492 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render4144829492 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M125.21875,80.75L129.38541666666666,80.75C133.55208333333334,80.75,141.88541666666666,80.75,150.21875,80.75C158.55208333333334,80.75,166.88541666666666,80.75,171.05208333333334,80.75L175.21875,80.75" marker-end="url(#arrowhead7785)" style="fill:none"></path><defs><marker id="arrowhead7785" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M256.1640625,80.75L261.6640625,80.75C267.1640625,80.75,278.1640625,80.75,289.1640625,80.75C300.1640625,80.75,311.1640625,80.75,316.6640625,80.75L322.1640625,80.75" marker-end="url(#arrowhead7786)" style="fill:none"></path><defs><marker id="arrowhead7786" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M237.9646130659449,61.5L246.4978546382874,54.125C255.03109621062993,46.75,272.09757935531496,32,291.62300842765745,24.625C311.1484375,17.25,333.1328125,17.25,353.7838541666667,17.25C374.4348958333333,17.25,393.7526041666667,17.25,412.11891250410105,24.625C430.48522084153547,32,447.9001291830709,46.75,456.6075833538386,54.125L465.3150375246063,61.5" marker-end="url(#arrowhead7787)" style="fill:none"></path><defs><marker id="arrowhead7787" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M388.0703125,80.75L392.2369791666667,80.75C396.4036458333333,80.75,404.7369791666667,80.75,413.0703125,80.75C421.4036458333333,80.75,429.7369791666667,80.75,433.9036458333333,80.75L438.0703125,80.75" marker-end="url(#arrowhead7788)" style="fill:none"></path><defs><marker id="arrowhead7788" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-B" style="opacity: 1;" id="L-D-B"><path class="path" d="M461.4397681451613,100L453.37819220430106,105.83333333333333C445.31661626344084,111.66666666666667,429.1934643817204,123.33333333333333,411.4730342741936,129.16666666666666C393.7526041666667,135,374.4348958333333,135,353.7838541666667,135C333.1328125,135,311.1484375,135,292.25596438172045,129.16666666666666C273.36349126344084,123.33333333333333,257.5629200268817,111.66666666666667,249.66263440860214,105.83333333333333L241.7623487903226,100" marker-end="url(#arrowhead7789)" style="fill:none"></path><defs><marker id="arrowhead7789" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(289.1640625,80.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(355.1171875,17.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16" height="18.5"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8226" transform="translate(66.609375,80.75)"><rect rx="0" ry="0" x="-58.609375" y="-19.25" width="117.21875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-48.609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">N=n,负则取倒</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8227" transform="translate(215.69140625,80.75)"><rect rx="0" ry="0" x="-40.47265625" y="-19.25" width="80.9453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.47265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">N&amp;1==1?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8229" transform="translate(355.1171875,80.75)"><rect rx="0" ry="0" x="-32.953125" y="-19.25" width="65.90625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-22.953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">ans*=x</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8231" transform="translate(488.04296875,80.75)"><rect rx="0" ry="0" x="-49.97265625" y="-19.25" width="99.9453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-39.97265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">x*=x,N&gt;&gt;=1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public double myPow(double x, int n) {        long N = n;        if (N &lt; 0) { x = 1 / x; N = -N; }        double ans = 1;        while (N &gt; 0) {            if ((N &amp; 1) == 1) ans *= x;            x *= x; N &gt;&gt;= 1;        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>n</code> 用 <code>long</code> 接收：<code>Integer.MIN_VALUE</code> 取反溢出。<code>N &amp; 1</code> 判奇偶，<code>N &gt;&gt;= 1</code> 折半。</li></ul><h2 id="149.-%E7%9B%B4%E7%BA%BF%E4%B8%8A%E6%9C%80%E5%A4%9A%E7%9A%84%E7%82%B9%E6%95%B0" tabindex="-1"><a href="https://leetcode.cn/problems/max-points-on-a-line/" target="_blank">149. 直线上最多的点数</a></h2><blockquote><p>平面上一组点，找一条直线经过的点最多。</p></blockquote><p><strong>思路脉络</strong>：枚举每个点作基点，对其他点算斜率，哈希统计相同斜率个数。同斜率即在同一条过基点的线上。重复点单独计数。</p><div class="mermaid"><svg id="render1294793448" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 663.7578125px;" viewBox="0 0 663.7578125 89.5"><style>#render1294793448 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1294793448 .error-icon{fill:#552222;}#render1294793448 .error-text{fill:#552222;stroke:#552222;}#render1294793448 .edge-thickness-normal{stroke-width:2px;}#render1294793448 .edge-thickness-thick{stroke-width:3.5px;}#render1294793448 .edge-pattern-solid{stroke-dasharray:0;}#render1294793448 .edge-pattern-dashed{stroke-dasharray:3;}#render1294793448 .edge-pattern-dotted{stroke-dasharray:2;}#render1294793448 .marker{fill:#333333;stroke:#333333;}#render1294793448 .marker.cross{stroke:#333333;}#render1294793448 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1294793448 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1294793448 .cluster-label text{fill:#333;}#render1294793448 .cluster-label span{color:#333;}#render1294793448 .label text,#render1294793448 span{fill:#333;color:#333;}#render1294793448 .node rect,#render1294793448 .node circle,#render1294793448 .node ellipse,#render1294793448 .node polygon,#render1294793448 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1294793448 .node .label{text-align:center;}#render1294793448 .node.clickable{cursor:pointer;}#render1294793448 .arrowheadPath{fill:#333333;}#render1294793448 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1294793448 .flowchart-link{stroke:#333333;fill:none;}#render1294793448 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1294793448 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1294793448 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1294793448 .cluster text{fill:#333;}#render1294793448 .cluster span{color:#333;}#render1294793448 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1294793448 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M96.5625,37.03800180423997L100.72916666666667,35.40666817019997C104.89583333333333,33.77533453615998,113.22916666666667,30.51266726807999,121.5625,28.881333634039994C129.89583333333334,27.25,138.22916666666666,27.25,142.39583333333334,27.25L146.5625,27.25" marker-end="url(#arrowhead7812)" style="fill:none"></path><defs><marker id="arrowhead7812" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M306.3125,27.25L310.4791666666667,27.25C314.6458333333333,27.25,322.9791666666667,27.25,331.3125,27.25C339.6458333333333,27.25,347.9791666666667,27.25,352.1458333333333,27.25L356.3125,27.25" marker-end="url(#arrowhead7813)" style="fill:none"></path><defs><marker id="arrowhead7813" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M504.3125,27.25L508.4791666666667,27.25C512.6458333333334,27.25,520.9791666666666,27.25,529.3125,28.742562978247786C537.6458333333334,30.23512595649557,545.9791666666666,33.220251912991145,550.1458333333334,34.71281489123893L554.3125,36.20537786948672" marker-end="url(#arrowhead7814)" style="fill:none"></path><defs><marker id="arrowhead7814" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M554.3125,72.54462213051329L550.1458333333334,74.03718510876108C545.9791666666666,75.52974808700885,537.6458333333334,78.51487404350443,516.9791666666666,80.00743702175221C496.3125,81.5,463.3125,81.5,430.3125,81.5C397.3125,81.5,364.3125,81.5,330.3333333333333,81.5C296.3541666666667,81.5,261.3958333333333,81.5,226.4375,81.5C191.47916666666666,81.5,156.52083333333334,81.5,134.875,79.86866636596001C113.22916666666667,78.23733273192,104.89583333333333,74.97466546384003,100.72916666666667,73.34333182980002L96.5625,71.71199819576003" marker-end="url(#arrowhead7815)" style="fill:none"></path><defs><marker id="arrowhead7815" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8244" transform="translate(52.28125,54.375)"><rect rx="0" ry="0" x="-44.28125" y="-19.25" width="88.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">枚举基点i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8245" transform="translate(226.4375,27.25)"><rect rx="0" ry="0" x="-79.875" y="-19.25" width="159.75" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-69.875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">其他点算斜率(约分)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8247" transform="translate(430.3125,27.25)"><rect rx="0" ry="0" x="-74" y="-19.25" width="148" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-64,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">哈希统计相同斜率</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8249" transform="translate(605.03515625,54.375)"><rect rx="0" ry="0" x="-50.72265625" y="-19.25" width="101.4453125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.72265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">max+dup+1</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxPoints(int[][] points) {        int ans = 1, n = points.length;        for (int i = 0; i &lt; n; i++) {            Map&lt;String, Integer&gt; slopes = new HashMap&lt;&gt;();            int dup = 0, max = 0;            for (int j = i + 1; j &lt; n; j++) {                int dx = points[j][0] - points[i][0], dy = points[j][1] - points[i][1];                if (dx == 0 &amp;&amp; dy == 0) { dup++; continue; }                int g = gcd(dx, dy);                String slope = (dx / g) + &quot;/&quot; + (dy / g);                max = Math.max(max, slopes.merge(slope, 1, Integer::sum));            }            ans = Math.max(ans, max + dup + 1);        }        return ans;    }    int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>斜率用约分后的字符串作 key（<code>dx/g + &quot;/&quot; + dy/g</code>），不能用浮点（精度问题）。重复点单独计 <code>dup</code>，加基点 <code>+1</code>。</li></ul><hr /><h1 id="20.-%E4%B8%80%E7%BB%B4%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92" tabindex="-1">20. 一维动态规划</h1><p>DP 三问：状态定义、转移方程、遍历顺序。一维 DP 压缩成两三个变量。关键是找&quot;子问题如何推出当前&quot;。</p><h2 id="70.-%E7%88%AC%E6%A5%BC%E6%A2%AF" tabindex="-1"><a href="https://leetcode.cn/problems/climbing-stairs/" target="_blank">70. 爬楼梯</a></h2><blockquote><p>每次 1 或 2 阶，爬到 n 阶几种方法。</p></blockquote><p><strong>思路脉络</strong>：斐波那契。<code>dp[n] = dp[n-1] + dp[n-2]</code>——到 n 只能从 n-1 爬一步或 n-2 爬两步。压缩成两个变量。</p><div class="mermaid"><svg id="render2309421136" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 358.8125px;" viewBox="0 0 358.8125 89.5"><style>#render2309421136 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2309421136 .error-icon{fill:#552222;}#render2309421136 .error-text{fill:#552222;stroke:#552222;}#render2309421136 .edge-thickness-normal{stroke-width:2px;}#render2309421136 .edge-thickness-thick{stroke-width:3.5px;}#render2309421136 .edge-pattern-solid{stroke-dasharray:0;}#render2309421136 .edge-pattern-dashed{stroke-dasharray:3;}#render2309421136 .edge-pattern-dotted{stroke-dasharray:2;}#render2309421136 .marker{fill:#333333;stroke:#333333;}#render2309421136 .marker.cross{stroke:#333333;}#render2309421136 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2309421136 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2309421136 .cluster-label text{fill:#333;}#render2309421136 .cluster-label span{color:#333;}#render2309421136 .label text,#render2309421136 span{fill:#333;color:#333;}#render2309421136 .node rect,#render2309421136 .node circle,#render2309421136 .node ellipse,#render2309421136 .node polygon,#render2309421136 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2309421136 .node .label{text-align:center;}#render2309421136 .node.clickable{cursor:pointer;}#render2309421136 .arrowheadPath{fill:#333333;}#render2309421136 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2309421136 .flowchart-link{stroke:#333333;fill:none;}#render2309421136 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2309421136 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2309421136 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2309421136 .cluster text{fill:#333;}#render2309421136 .cluster span{color:#333;}#render2309421136 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2309421136 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M84.7578125,37.949537750385204L88.92447916666667,36.16628145865434C93.09114583333333,34.38302516692347,101.42447916666667,30.816512583461733,109.7578125,29.03325629173087C118.09114583333333,27.25,126.42447916666667,27.25,130.59114583333334,27.25L134.7578125,27.25" marker-end="url(#arrowhead7831)" style="fill:none"></path><defs><marker id="arrowhead7831" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M259.46875,27.25L263.6354166666667,27.25C267.8020833333333,27.25,276.1354166666667,27.25,284.46875,29.724626525259435C292.8020833333333,32.19925305051887,301.1354166666667,37.148506101037746,305.3020833333333,39.62313262629718L309.46875,42.097759151556616" marker-end="url(#arrowhead7832)" style="fill:none"></path><defs><marker id="arrowhead7832" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M309.46875,66.65224084844338L305.3020833333333,69.12686737370282C301.1354166666667,71.60149389896226,292.8020833333333,76.55074694948114,274.076171875,79.02537347474056C255.35026041666666,81.5,226.23177083333334,81.5,197.11328125,81.5C167.99479166666666,81.5,138.87630208333334,81.5,120.150390625,79.71674370826914C101.42447916666667,77.93348741653826,93.09114583333333,74.36697483307653,88.92447916666667,72.58371854134566L84.7578125,70.8004622496148" marker-end="url(#arrowhead7833)" style="fill:none"></path><defs><marker id="arrowhead7833" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8258" transform="translate(46.37890625,54.375)"><rect rx="0" ry="0" x="-38.37890625" y="-19.25" width="76.7578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-28.37890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">a=1,b=2</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8259" transform="translate(197.11328125,27.25)"><rect rx="0" ry="0" x="-62.35546875" y="-19.25" width="124.7109375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-52.35546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">c=a+b,a=b,b=c</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8261" transform="translate(330.140625,54.375)"><rect rx="0" ry="0" x="-20.671875" y="-19.25" width="41.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-10.671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">i++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int climbStairs(int n) {        if (n &lt;= 2) return n;        int a = 1, b = 2;        for (int i = 3; i &lt;= n; i++) { int c = a + b; a = b; b = c; }        return b;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="198.-%E6%89%93%E5%AE%B6%E5%8A%AB%E8%88%8D" tabindex="-1"><a href="https://leetcode.cn/problems/house-robber/" target="_blank">198. 打家劫舍</a></h2><blockquote><p>偷一排房子，相邻不能同偷，求最大金额。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i]</code> = 前 i 间最大金额。偷第 i 间则 <code>dp[i-2]+nums[i]</code>，不偷则 <code>dp[i-1]</code>，取大。压缩成 <code>prev2/prev1</code>。</p><div class="mermaid"><svg id="render3652966719" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 558.4453125px;" viewBox="0 0 558.4453125 89.5"><style>#render3652966719 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3652966719 .error-icon{fill:#552222;}#render3652966719 .error-text{fill:#552222;stroke:#552222;}#render3652966719 .edge-thickness-normal{stroke-width:2px;}#render3652966719 .edge-thickness-thick{stroke-width:3.5px;}#render3652966719 .edge-pattern-solid{stroke-dasharray:0;}#render3652966719 .edge-pattern-dashed{stroke-dasharray:3;}#render3652966719 .edge-pattern-dotted{stroke-dasharray:2;}#render3652966719 .marker{fill:#333333;stroke:#333333;}#render3652966719 .marker.cross{stroke:#333333;}#render3652966719 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3652966719 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3652966719 .cluster-label text{fill:#333;}#render3652966719 .cluster-label span{color:#333;}#render3652966719 .label text,#render3652966719 span{fill:#333;color:#333;}#render3652966719 .node rect,#render3652966719 .node circle,#render3652966719 .node ellipse,#render3652966719 .node polygon,#render3652966719 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3652966719 .node .label{text-align:center;}#render3652966719 .node.clickable{cursor:pointer;}#render3652966719 .arrowheadPath{fill:#333333;}#render3652966719 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3652966719 .flowchart-link{stroke:#333333;fill:none;}#render3652966719 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3652966719 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3652966719 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3652966719 .cluster text{fill:#333;}#render3652966719 .cluster span{color:#333;}#render3652966719 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3652966719 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M68.7421875,39.49691358024691L72.90885416666667,37.455761316872426C77.07552083333333,35.41460905349794,85.40885416666667,31.33230452674897,93.7421875,29.291152263374485C102.07552083333333,27.25,110.40885416666667,27.25,114.57552083333333,27.25L118.7421875,27.25" marker-end="url(#arrowhead7849)" style="fill:none"></path><defs><marker id="arrowhead7849" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M314.6484375,27.25L318.8151041666667,27.25C322.9817708333333,27.25,331.3151041666667,27.25,341.18653393817203,28.5625C351.0579637096774,29.875,362.4674899193548,32.5,368.1722530241936,33.8125L373.87701612903226,35.125" marker-end="url(#arrowhead7850)" style="fill:none"></path><defs><marker id="arrowhead7850" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M373.87701612903226,73.625L368.1722530241935,74.9375C362.4674899193548,76.25,351.0579637096774,78.875,324.8610131048387,80.1875C298.6640625,81.5,257.6796875,81.5,216.6953125,81.5C175.7109375,81.5,134.7265625,81.5,110.06770833333333,79.45884773662551C85.40885416666667,77.41769547325103,77.07552083333333,73.33539094650206,72.90885416666667,71.29423868312757L68.7421875,69.25308641975309" marker-end="url(#arrowhead7851)" style="fill:none"></path><defs><marker id="arrowhead7851" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8270" transform="translate(38.37109375,54.375)"><rect rx="0" ry="0" x="-30.37109375" y="-19.25" width="60.7421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-20.37109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历n</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8271" transform="translate(216.6953125,27.25)"><rect rx="0" ry="0" x="-97.953125" y="-19.25" width="195.90625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-87.953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">cur=max(prev1,prev2+n)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8273" transform="translate(457.546875,54.375)"><rect rx="0" ry="0" x="-92.8984375" y="-19.25" width="185.796875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-82.8984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">prev2=prev1,prev1=cur</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int rob(int[] nums) {        int prev2 = 0, prev1 = 0;        for (int n : nums) { int cur = Math.max(prev1, prev2 + n); prev2 = prev1; prev1 = cur; }        return prev1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><h2 id="139.-%E5%8D%95%E8%AF%8D%E6%8B%86%E5%88%86" tabindex="-1"><a href="https://leetcode.cn/problems/word-break/" target="_blank">139. 单词拆分</a></h2><blockquote><p>字符串能否拆成字典里的若干词。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i]</code> = 前 i 个字符能否拆分。枚举 j，<code>dp[j]</code> 为真且 <code>s[j..i)</code> 在字典则 <code>dp[i] = true</code>。</p><div class="mermaid"><svg id="render3252748174" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="230.97189331054688" style="max-width: 793.5656127929688px;" viewBox="0 0 793.5656127929688 230.97189331054688"><style>#render3252748174 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3252748174 .error-icon{fill:#552222;}#render3252748174 .error-text{fill:#552222;stroke:#552222;}#render3252748174 .edge-thickness-normal{stroke-width:2px;}#render3252748174 .edge-thickness-thick{stroke-width:3.5px;}#render3252748174 .edge-pattern-solid{stroke-dasharray:0;}#render3252748174 .edge-pattern-dashed{stroke-dasharray:3;}#render3252748174 .edge-pattern-dotted{stroke-dasharray:2;}#render3252748174 .marker{fill:#333333;stroke:#333333;}#render3252748174 .marker.cross{stroke:#333333;}#render3252748174 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3252748174 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3252748174 .cluster-label text{fill:#333;}#render3252748174 .cluster-label span{color:#333;}#render3252748174 .label text,#render3252748174 span{fill:#333;color:#333;}#render3252748174 .node rect,#render3252748174 .node circle,#render3252748174 .node ellipse,#render3252748174 .node polygon,#render3252748174 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3252748174 .node .label{text-align:center;}#render3252748174 .node.clickable{cursor:pointer;}#render3252748174 .arrowheadPath{fill:#333333;}#render3252748174 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3252748174 .flowchart-link{stroke:#333333;fill:none;}#render3252748174 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3252748174 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3252748174 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3252748174 .cluster text{fill:#333;}#render3252748174 .cluster span{color:#333;}#render3252748174 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3252748174 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M78.74200497767556,141.22890853881836L87.18344164806297,134.02174695332846C95.62487831845037,126.81458536783855,112.50775165922518,112.40026219685872,125.11585499627927,105.19310061136882C137.72395833333334,97.9859390258789,146.05729166666666,97.9859390258789,150.22395833333334,97.9859390258789L154.390625,97.9859390258789" marker-end="url(#arrowhead7885)" style="fill:none"></path><defs><marker id="arrowhead7885" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M212.2578125,91.1535596502876L217.7578125,89.8547895462195C223.2578125,88.55601944215137,234.2578125,85.95847923401514,246.9350648680504,85.58662317425014C259.6123172361008,85.21476711448514,273.96682197220167,87.06859520309138,281.1440743402521,87.99550924739448L288.3213267083025,88.9224232916976" marker-end="url(#arrowhead7886)" style="fill:none"></path><defs><marker id="arrowhead7886" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M458.729685974121,98.4859390258789L464.1463534037271,98.40260569254558C469.56302083333327,98.31927235921223,480.3963556925455,98.15260569254558,491.31302312215166,98.06927235921223C502.2296905517578,97.9859390258789,513.2296905517578,97.9859390258789,518.7296905517578,97.9859390258789L524.2296905517578,97.9859390258789" marker-end="url(#arrowhead7887)" style="fill:none"></path><defs><marker id="arrowhead7887" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-B" style="opacity: 1;" id="L-C-B"><path class="path" d="M303.96908457673436,123.69720957673445L294.1838725639453,127.38999273708176C284.39866055115624,131.0827758974291,264.8282365255781,138.46834221812372,248.87183559530504,137.3914637929811C232.91543466503205,136.31458536783853,220.57305683006408,126.77526219685872,214.40186791258012,122.00560061136882L208.23067899509613,117.2359390258789" marker-end="url(#arrowhead7888)" style="fill:none"></path><defs><marker id="arrowhead7888" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M663.0031280517578,97.9859390258789L667.1697947184244,97.9859390258789C671.3364613850912,97.9859390258789,679.6697947184244,97.9859390258789,690.9038783177967,105.19310061136882C702.137961917169,112.40026219685872,716.2727957825804,126.81458536783855,723.3402127152858,134.02174695332846L730.4076296479915,141.22890853881836" marker-end="url(#arrowhead7889)" style="fill:none"></path><defs><marker id="arrowhead7889" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M730.4076296479915,179.72890853881836L723.3402127152858,186.93607012430826C716.2727957825804,194.1432317097982,702.137961917169,208.55755488077799,679.3394251927967,215.7647164662679C656.5408884684244,222.9718780517578,625.0786488850912,222.9718780517578,592.2830759684244,222.9718780517578C559.4875030517578,222.9718780517578,525.3585968017578,222.9718780517578,487.79648717244464,222.9718780517578C450.23437754313153,222.9718780517578,409.2390645345052,222.9718780517578,368.2437515258789,222.9718780517578C327.2484385172526,222.9718780517578,286.2531255086263,222.9718780517578,255.43320337931314,222.9718780517578C224.61328125,222.9718780517578,203.96875,222.9718780517578,184.65755208333334,222.9718780517578C165.34635416666666,222.9718780517578,147.36848958333334,222.9718780517578,129.93812062127927,215.7647164662679C112.50775165922518,208.55755488077799,95.62487831845037,194.1432317097982,87.18344164806297,186.93607012430826L78.74200497767556,179.72890853881836" marker-end="url(#arrowhead7890)" style="fill:none"></path><defs><marker id="arrowhead7890" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(491.2296905517578,97.9859390258789)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00021743774414" height="18.50025177001953"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(245.2578125,145.85390853881836)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00021743774414" height="18.50025177001953"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8288" transform="translate(56.1953125,160.47890853881836)"><rect rx="0" ry="0" x="-48.1953125" y="-19.25" width="96.390625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-38.1953125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[0]=true</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8289" transform="translate(183.32421875,97.9859390258789)"><rect rx="0" ry="0" x="-28.93359375" y="-19.25" width="57.8671875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-18.93359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">枚举j</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8291" transform="translate(368.2437515258789,97.9859390258789)"><polygon points="89.9859375,0 179.971875,-89.9859375 89.9859375,-179.971875 0,-89.9859375" transform="translate(-89.9859375,89.9859375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-70.734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[j]且s[j..i)在dict?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8293" transform="translate(593.6164093017578,97.9859390258789)"><rect rx="0" ry="0" x="-69.38671875" y="-19.25" width="138.7734375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-59.38671875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[i]=true,break</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8297" transform="translate(749.2843780517578,160.47890853881836)"><rect rx="0" ry="0" x="-36.28125" y="-19.25" width="72.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-26.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一个i</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean wordBreak(String s, List&lt;String&gt; wordDict) {        Set&lt;String&gt; dict = new HashSet&lt;&gt;(wordDict);        boolean[] dp = new boolean[s.length() + 1];        dp[0] = true;        for (int i = 1; i &lt;= s.length(); i++)            for (int j = 0; j &lt; i; j++)                if (dp[j] &amp;&amp; dict.contains(s.substring(j, i))) { dp[i] = true; break; }        return dp[s.length()];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n² × 子串哈希)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>内层 <code>break</code> 找到一个就停。子串 <code>substring(j, i)</code> 是 <code>[j, i)</code> 左闭右开。</li></ul><h2 id="322.-%E9%9B%B6%E9%92%B1%E5%85%91%E6%8D%A2" tabindex="-1"><a href="https://leetcode.cn/problems/coin-change/" target="_blank">322. 零钱兑换</a></h2><blockquote><p>用若干面额硬币凑成 amount 的最少枚数。</p></blockquote><p><strong>思路脉络</strong>：完全背包。<code>dp[i]</code> = 凑成金额 i 的最少硬币数。<code>dp[i] = min(dp[i-coin] + 1)</code>。初始化 <code>amount+1</code> 代替 <code>MAX_VALUE</code> 防 <code>+1</code> 溢出。</p><div class="mermaid"><svg id="render1176045693" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 739.765625px;" viewBox="0 0 739.765625 89.5"><style>#render1176045693 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1176045693 .error-icon{fill:#552222;}#render1176045693 .error-text{fill:#552222;stroke:#552222;}#render1176045693 .edge-thickness-normal{stroke-width:2px;}#render1176045693 .edge-thickness-thick{stroke-width:3.5px;}#render1176045693 .edge-pattern-solid{stroke-dasharray:0;}#render1176045693 .edge-pattern-dashed{stroke-dasharray:3;}#render1176045693 .edge-pattern-dotted{stroke-dasharray:2;}#render1176045693 .marker{fill:#333333;stroke:#333333;}#render1176045693 .marker.cross{stroke:#333333;}#render1176045693 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1176045693 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1176045693 .cluster-label text{fill:#333;}#render1176045693 .cluster-label span{color:#333;}#render1176045693 .label text,#render1176045693 span{fill:#333;color:#333;}#render1176045693 .node rect,#render1176045693 .node circle,#render1176045693 .node ellipse,#render1176045693 .node polygon,#render1176045693 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1176045693 .node .label{text-align:center;}#render1176045693 .node.clickable{cursor:pointer;}#render1176045693 .arrowheadPath{fill:#333333;}#render1176045693 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1176045693 .flowchart-link{stroke:#333333;fill:none;}#render1176045693 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1176045693 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1176045693 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1176045693 .cluster text{fill:#333;}#render1176045693 .cluster span{color:#333;}#render1176045693 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1176045693 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M189.78414818548387,35.125L195.63652973790326,33.8125C201.4889112903226,32.5,213.1936743951613,29.875,223.21272261424733,28.5625C233.23177083333334,27.25,241.56510416666666,27.25,245.73177083333334,27.25L249.8984375,27.25" marker-end="url(#arrowhead7913)" style="fill:none"></path><defs><marker id="arrowhead7913" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M331.7109375,27.25L335.8776041666667,27.25C340.0442708333333,27.25,348.3776041666667,27.25,356.7109375,27.25C365.0442708333333,27.25,373.3776041666667,27.25,377.5442708333333,27.25L381.7109375,27.25" marker-end="url(#arrowhead7914)" style="fill:none"></path><defs><marker id="arrowhead7914" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M609.203125,27.25L613.3697916666666,27.25C617.5364583333334,27.25,625.8697916666666,27.25,634.203125,29.094297127315997C642.5364583333334,30.93859425463199,650.8697916666666,34.62718850926398,655.0364583333334,36.47148563657998L659.203125,38.31578276389597" marker-end="url(#arrowhead7915)" style="fill:none"></path><defs><marker id="arrowhead7915" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M659.203125,70.43421723610403L655.0364583333334,72.27851436342003C650.8697916666666,74.12281149073603,642.5364583333334,77.811405745368,615.2454427083334,79.655702872684C587.9544270833334,81.5,541.7057291666666,81.5,495.45703125,81.5C449.2083333333333,81.5,402.9596354166667,81.5,368.8509114583333,81.5C334.7421875,81.5,312.7734375,81.5,290.8046875,81.5C268.8359375,81.5,246.8671875,81.5,230.03043094758064,80.1875C213.1936743951613,78.875,201.4889112903226,76.25,195.6365297379032,74.9375L189.78414818548387,73.625" marker-end="url(#arrowhead7916)" style="fill:none"></path><defs><marker id="arrowhead7916" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8308" transform="translate(103.94921875,54.375)"><rect rx="0" ry="0" x="-95.94921875" y="-19.25" width="191.8984375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-85.94921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[0]=0,其余=amount+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8309" transform="translate(290.8046875,27.25)"><rect rx="0" ry="0" x="-40.90625" y="-19.25" width="81.8125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.90625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">枚举coin</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8311" transform="translate(495.45703125,27.25)"><rect rx="0" ry="0" x="-113.74609375" y="-19.25" width="227.4921875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-103.74609375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[i]=min(dp[i],dp[i-coin]+1)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8313" transform="translate(695.484375,54.375)"><rect rx="0" ry="0" x="-36.28125" y="-19.25" width="72.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-26.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一个i</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int coinChange(int[] coins, int amount) {        int[] dp = new int[amount + 1];        Arrays.fill(dp, amount + 1); dp[0] = 0;        for (int i = 1; i &lt;= amount; i++)            for (int c : coins)                if (c &lt;= i) dp[i] = Math.min(dp[i], dp[i - c] + 1);        return dp[amount] &gt; amount ? -1 : dp[amount];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(amount × 种类数)，空间 O(amount)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>初始化用 <code>amount+1</code> 而非 <code>MAX_VALUE</code>：<code>MAX+1</code> 会溢出成负数。<code>amount+1</code> 是&quot;全用 1 元最多 amount 枚&quot;的上界，超过即不可达。</li></ul><h2 id="300.-%E6%9C%80%E9%95%BF%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97" tabindex="-1"><a href="https://leetcode.cn/problems/longest-increasing-subsequence/" target="_blank">300. 最长递增子序列</a></h2><blockquote><p>找最长严格递增子序列长度。</p></blockquote><p><strong>思路脉络</strong>：二分 O(n log n)。维护 <code>tails</code> 数组，<code>tails[k]</code> = 长度为 k+1 的递增子序列的最小末尾。每来一个数二分找第一个 ≥ 它的位置替换。<code>tails</code> 长度即 LIS。</p><div class="mermaid"><svg id="render2556809241" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 556.9609375px;" viewBox="0 0 556.9609375 89.5"><style>#render2556809241 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2556809241 .error-icon{fill:#552222;}#render2556809241 .error-text{fill:#552222;stroke:#552222;}#render2556809241 .edge-thickness-normal{stroke-width:2px;}#render2556809241 .edge-thickness-thick{stroke-width:3.5px;}#render2556809241 .edge-pattern-solid{stroke-dasharray:0;}#render2556809241 .edge-pattern-dashed{stroke-dasharray:3;}#render2556809241 .edge-pattern-dotted{stroke-dasharray:2;}#render2556809241 .marker{fill:#333333;stroke:#333333;}#render2556809241 .marker.cross{stroke:#333333;}#render2556809241 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2556809241 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2556809241 .cluster-label text{fill:#333;}#render2556809241 .cluster-label span{color:#333;}#render2556809241 .label text,#render2556809241 span{fill:#333;color:#333;}#render2556809241 .node rect,#render2556809241 .node circle,#render2556809241 .node ellipse,#render2556809241 .node polygon,#render2556809241 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2556809241 .node .label{text-align:center;}#render2556809241 .node.clickable{cursor:pointer;}#render2556809241 .arrowheadPath{fill:#333333;}#render2556809241 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2556809241 .flowchart-link{stroke:#333333;fill:none;}#render2556809241 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2556809241 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2556809241 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2556809241 .cluster text{fill:#333;}#render2556809241 .cluster span{color:#333;}#render2556809241 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2556809241 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M90.765625,37.46537013063434L94.93229166666667,35.762808442195286C99.09895833333333,34.060246753756225,107.43229166666667,30.655123376878112,115.765625,28.952561688439058C124.09895833333333,27.25,132.43229166666666,27.25,136.59895833333334,27.25L140.765625,27.25" marker-end="url(#arrowhead7932)" style="fill:none"></path><defs><marker id="arrowhead7932" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M342.4296875,27.25L346.5963541666667,27.25C350.7630208333333,27.25,359.0963541666667,27.25,368.25974462365593,28.5625C377.4231350806452,29.875,387.4165826612903,32.5,392.4133064516129,33.8125L397.4100302419355,35.125" marker-end="url(#arrowhead7933)" style="fill:none"></path><defs><marker id="arrowhead7933" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-A" style="opacity: 1;" id="L-C-A"><path class="path" d="M397.4100302419355,73.625L392.4133064516129,74.9375C387.4165826612903,76.25,377.4231350806452,78.875,351.45440608198925,80.1875C325.4856770833333,81.5,283.5416666666667,81.5,241.59765625,81.5C199.65364583333334,81.5,157.70963541666666,81.5,132.57096354166666,79.79743831156094C107.43229166666667,78.09487662312189,99.09895833333333,74.68975324624377,94.93229166666667,72.9871915578047L90.765625,71.28462986936566" marker-end="url(#arrowhead7934)" style="fill:none"></path><defs><marker id="arrowhead7934" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8322" transform="translate(49.3828125,54.375)"><rect rx="0" ry="0" x="-41.3828125" y="-19.25" width="82.765625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-31.3828125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历num</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8323" transform="translate(241.59765625,27.25)"><rect rx="0" ry="0" x="-100.83203125" y="-19.25" width="201.6640625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-90.83203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">二分找tails中第一个≥num</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8325" transform="translate(470.6953125,54.375)"><rect rx="0" ry="0" x="-78.265625" y="-19.25" width="156.53125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-68.265625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">替换,l==len则len++</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int lengthOfLIS(int[] nums) {        int[] tails = new int[nums.length], len = 0;        for (int num : nums) {            int l = 0, r = len;            while (l &lt; r) { int m = (l + r) / 2; if (tails[m] &lt; num) l = m + 1; else r = m; }            tails[l] = num; if (l == len) len++;        }        return len;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>tails</code> 存的不是真实子序列，而是&quot;各长度最小末尾&quot;，替换保证后续更易接上更长。求具体序列要回溯，不能直接读 <code>tails</code>。</li></ul><hr /><h1 id="21.-%E5%A4%9A%E7%BB%B4%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92" tabindex="-1">21. 多维动态规划</h1><p>状态升到二维（行列/区间维度）。难点在状态含义和遍历顺序——依赖跨行跨列，要确保填表时依赖项已就绪。压缩成一维时注意左上角的覆盖问题。</p><h2 id="120.-%E4%B8%89%E8%A7%92%E5%BD%A2%E6%9C%80%E5%B0%8F%E8%B7%AF%E5%BE%84%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/triangle/" target="_blank">120. 三角形最小路径和</a></h2><blockquote><p>三角形从顶到底相邻下走，最小路径和。</p></blockquote><p><strong>思路脉络</strong>：自底向上 DP。<code>dp[i][j]</code> = 从 <code>(i,j)</code> 到底的最小路径和。<code>dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]</code>。底部等于自身。可原地修改。</p><div class="mermaid"><svg id="render3957119687" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 754.0078125px;" viewBox="0 0 754.0078125 89.5"><style>#render3957119687 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3957119687 .error-icon{fill:#552222;}#render3957119687 .error-text{fill:#552222;stroke:#552222;}#render3957119687 .edge-thickness-normal{stroke-width:2px;}#render3957119687 .edge-thickness-thick{stroke-width:3.5px;}#render3957119687 .edge-pattern-solid{stroke-dasharray:0;}#render3957119687 .edge-pattern-dashed{stroke-dasharray:3;}#render3957119687 .edge-pattern-dotted{stroke-dasharray:2;}#render3957119687 .marker{fill:#333333;stroke:#333333;}#render3957119687 .marker.cross{stroke:#333333;}#render3957119687 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3957119687 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3957119687 .cluster-label text{fill:#333;}#render3957119687 .cluster-label span{color:#333;}#render3957119687 .label text,#render3957119687 span{fill:#333;color:#333;}#render3957119687 .node rect,#render3957119687 .node circle,#render3957119687 .node ellipse,#render3957119687 .node polygon,#render3957119687 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3957119687 .node .label{text-align:center;}#render3957119687 .node.clickable{cursor:pointer;}#render3957119687 .arrowheadPath{fill:#333333;}#render3957119687 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3957119687 .flowchart-link{stroke:#333333;fill:none;}#render3957119687 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3957119687 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3957119687 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3957119687 .cluster text{fill:#333;}#render3957119687 .cluster span{color:#333;}#render3957119687 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3957119687 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M147.31602822580646,35.125L151.96648185483872,33.8125C156.61693548387098,32.5,165.9178427419355,29.875,174.7349630376344,28.5625C183.55208333333334,27.25,191.88541666666666,27.25,196.05208333333334,27.25L200.21875,27.25" marker-end="url(#arrowhead7957)" style="fill:none"></path><defs><marker id="arrowhead7957" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M284.21875,27.25L288.3854166666667,27.25C292.5520833333333,27.25,300.8854166666667,27.25,309.21875,27.25C317.5520833333333,27.25,325.8854166666667,27.25,330.0520833333333,27.25L334.21875,27.25" marker-end="url(#arrowhead7958)" style="fill:none"></path><defs><marker id="arrowhead7958" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M628.0078125,27.25L632.1744791666666,27.25C636.3411458333334,27.25,644.6744791666666,27.25,653.0078125,29.165607344632765C661.3411458333334,31.081214689265536,669.6744791666666,34.91242937853107,673.8411458333334,36.82803672316384L678.0078125,38.74364406779661" marker-end="url(#arrowhead7959)" style="fill:none"></path><defs><marker id="arrowhead7959" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M678.0078125,70.00635593220339L673.8411458333334,71.92196327683617C669.6744791666666,73.83757062146893,661.3411458333334,77.66878531073446,628.525390625,79.58439265536724C595.7096354166666,81.5,538.4114583333334,81.5,481.11328125,81.5C423.8151041666667,81.5,366.5169270833333,81.5,326.701171875,81.5C286.8854166666667,81.5,264.5520833333333,81.5,242.21875,81.5C219.88541666666666,81.5,197.55208333333334,81.5,181.7349630376344,80.1875C165.9178427419355,78.875,156.61693548387098,76.25,151.96648185483872,74.9375L147.31602822580646,73.625" marker-end="url(#arrowhead7960)" style="fill:none"></path><defs><marker id="arrowhead7960" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8336" transform="translate(79.109375,54.375)"><rect rx="0" ry="0" x="-71.109375" y="-19.25" width="142.21875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-61.109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">底部dp=最后一行</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8337" transform="translate(242.21875,27.25)"><rect rx="0" ry="0" x="-42" y="-19.25" width="84" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">自底向上</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8339" transform="translate(481.11328125,27.25)"><rect rx="0" ry="0" x="-146.89453125" y="-19.25" width="293.7890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-136.89453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[j]=min(dp[j],dp[j+1])+triangle[i][j]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8341" transform="translate(712.0078125,54.375)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">上一行</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int minimumTotal(List&lt;List&lt;Integer&gt;&gt; triangle) {        int n = triangle.size();        int[] dp = triangle.get(n - 1).stream().mapToInt(Integer::intValue).toArray();  // 底部        for (int i = n - 2; i &gt;= 0; i--)            for (int j = 0; j &lt;= i; j++)                dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j);        return dp[0];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>自底向上比自顶向下简单（不用处理边界，底部直接是自身）。一维 <code>dp</code> 从底向上更新，<code>dp[j]</code> 和 <code>dp[j+1]</code> 是下一层的值。</li></ul><h2 id="64.-%E6%9C%80%E5%B0%8F%E8%B7%AF%E5%BE%84%E5%92%8C" tabindex="-1"><a href="https://leetcode.cn/problems/minimum-path-sum/" target="_blank">64. 最小路径和</a></h2><blockquote><p>网格每格有代价，左上到右下只能右或下，最小和。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]</code>。首行首列只能从左/上来。原地修改 grid 省空间。和热题 100 相同。</p><div class="mermaid"><svg id="render2947700488" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 737.75px;" viewBox="0 0 737.75 89.5"><style>#render2947700488 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2947700488 .error-icon{fill:#552222;}#render2947700488 .error-text{fill:#552222;stroke:#552222;}#render2947700488 .edge-thickness-normal{stroke-width:2px;}#render2947700488 .edge-thickness-thick{stroke-width:3.5px;}#render2947700488 .edge-pattern-solid{stroke-dasharray:0;}#render2947700488 .edge-pattern-dashed{stroke-dasharray:3;}#render2947700488 .edge-pattern-dotted{stroke-dasharray:2;}#render2947700488 .marker{fill:#333333;stroke:#333333;}#render2947700488 .marker.cross{stroke:#333333;}#render2947700488 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2947700488 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2947700488 .cluster-label text{fill:#333;}#render2947700488 .cluster-label span{color:#333;}#render2947700488 .label text,#render2947700488 span{fill:#333;color:#333;}#render2947700488 .node rect,#render2947700488 .node circle,#render2947700488 .node ellipse,#render2947700488 .node polygon,#render2947700488 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2947700488 .node .label{text-align:center;}#render2947700488 .node.clickable{cursor:pointer;}#render2947700488 .arrowheadPath{fill:#333333;}#render2947700488 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2947700488 .flowchart-link{stroke:#333333;fill:none;}#render2947700488 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2947700488 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2947700488 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2947700488 .cluster text{fill:#333;}#render2947700488 .cluster span{color:#333;}#render2947700488 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2947700488 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M88.0546875,37.67830540037244L92.22135416666667,35.94025450031037C96.38802083333333,34.20220360024829,104.72135416666667,30.726101800124145,113.0546875,28.988050900062074C121.38802083333333,27.25,129.72135416666666,27.25,133.88802083333334,27.25L138.0546875,27.25" marker-end="url(#arrowhead7990)" style="fill:none"></path><defs><marker id="arrowhead7990" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M222.8359375,27.25L227.00260416666666,27.25C231.16927083333334,27.25,239.50260416666666,27.25,247.8359375,27.25C256.1692708333333,27.25,264.5026041666667,27.25,268.6692708333333,27.25L272.8359375,27.25" marker-end="url(#arrowhead7991)" style="fill:none"></path><defs><marker id="arrowhead7991" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M357.6171875,27.25L361.7838541666667,27.25C365.9505208333333,27.25,374.2838541666667,27.25,382.6171875,27.25C390.9505208333333,27.25,399.2838541666667,27.25,403.4505208333333,27.25L407.6171875,27.25" marker-end="url(#arrowhead7992)" style="fill:none"></path><defs><marker id="arrowhead7992" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M611.75,27.25L615.9166666666666,27.25C620.0833333333334,27.25,628.4166666666666,27.25,636.75,29.165607344632765C645.0833333333334,31.081214689265536,653.4166666666666,34.91242937853107,657.5833333333334,36.82803672316384L661.75,38.74364406779661" marker-end="url(#arrowhead7993)" style="fill:none"></path><defs><marker id="arrowhead7993" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M661.75,70.00635593220339L657.5833333333334,71.92196327683617C653.4166666666666,73.83757062146893,645.0833333333334,77.66878531073446,619.7389322916666,79.58439265536724C594.39453125,81.5,552.0390625,81.5,509.68359375,81.5C467.328125,81.5,424.97265625,81.5,392.5631510416667,81.5C360.1536458333333,81.5,337.6901041666667,81.5,315.2265625,81.5C292.7630208333333,81.5,270.2994791666667,81.5,247.8359375,81.5C225.37239583333334,81.5,202.90885416666666,81.5,180.4453125,81.5C157.98177083333334,81.5,135.51822916666666,81.5,120.11979166666667,79.76194909993792C104.72135416666667,78.02389819987586,96.38802083333333,74.5477963997517,92.22135416666667,72.80974549968964L88.0546875,71.07169459962756" marker-end="url(#arrowhead7994)" style="fill:none"></path><defs><marker id="arrowhead7994" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8354" transform="translate(48.02734375,54.375)"><rect rx="0" ry="0" x="-40.02734375" y="-19.25" width="80.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历(i,j)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8355" transform="translate(180.4453125,27.25)"><rect rx="0" ry="0" x="-42.390625" y="-19.25" width="84.78125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">首行+=左</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8357" transform="translate(315.2265625,27.25)"><rect rx="0" ry="0" x="-42.390625" y="-19.25" width="84.78125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.390625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">首列+=上</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8359" transform="translate(509.68359375,27.25)"><rect rx="0" ry="0" x="-102.06640625" y="-19.25" width="204.1328125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-92.06640625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">内部:min(上,左)+grid[i][j]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8361" transform="translate(695.75,54.375)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一格</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int minPathSum(int[][] grid) {        int m = grid.length, n = grid[0].length;        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++) {                if (i == 0 &amp;&amp; j == 0) continue;                else if (i == 0) grid[i][j] += grid[i][j - 1];                else if (j == 0) grid[i][j] += grid[i - 1][j];                else grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);            }        return grid[m - 1][n - 1];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(1)（原地）。</p></blockquote><h2 id="63.-%E4%B8%8D%E5%90%8C%E8%B7%AF%E5%BE%84-ii" tabindex="-1"><a href="https://leetcode.cn/problems/unique-paths-ii/" target="_blank">63. 不同路径 II</a></h2><blockquote><p>网格有障碍，左上到右下只能右或下，几种走法。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i][j]</code> = 到 <code>(i,j)</code> 的走法。障碍处为 0，否则 <code>dp[i][j] = dp[i-1][j] + dp[i][j-1]</code>。首行首列特殊处理（有障碍则后续全 0）。</p><div class="mermaid"><svg id="render3328135952" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="190.0593719482422" style="max-width: 538.7296752929688px;" viewBox="0 0 538.7296752929688 190.0593719482422"><style>#render3328135952 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render3328135952 .error-icon{fill:#552222;}#render3328135952 .error-text{fill:#552222;stroke:#552222;}#render3328135952 .edge-thickness-normal{stroke-width:2px;}#render3328135952 .edge-thickness-thick{stroke-width:3.5px;}#render3328135952 .edge-pattern-solid{stroke-dasharray:0;}#render3328135952 .edge-pattern-dashed{stroke-dasharray:3;}#render3328135952 .edge-pattern-dotted{stroke-dasharray:2;}#render3328135952 .marker{fill:#333333;stroke:#333333;}#render3328135952 .marker.cross{stroke:#333333;}#render3328135952 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render3328135952 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render3328135952 .cluster-label text{fill:#333;}#render3328135952 .cluster-label span{color:#333;}#render3328135952 .label text,#render3328135952 span{fill:#333;color:#333;}#render3328135952 .node rect,#render3328135952 .node circle,#render3328135952 .node ellipse,#render3328135952 .node polygon,#render3328135952 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render3328135952 .node .label{text-align:center;}#render3328135952 .node.clickable{cursor:pointer;}#render3328135952 .arrowheadPath{fill:#333333;}#render3328135952 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render3328135952 .flowchart-link{stroke:#333333;fill:none;}#render3328135952 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render3328135952 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render3328135952 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render3328135952 .cluster text{fill:#333;}#render3328135952 .cluster span{color:#333;}#render3328135952 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render3328135952 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M79.97314721923452,91.43437576293945L85.48673726602878,88.11197980244954C91.00032731282302,84.78958384195964,102.02750740641152,78.14479192097981,111.79109770751892,74.90572929382324C121.55468800862631,71.66666666666667,130.05468851725263,71.83333333333333,134.30468877156576,71.91666666666667L138.55468902587893,72" marker-end="url(#arrowhead8021)" style="fill:none"></path><defs><marker id="arrowhead8021" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M209.38199958001766,56.08981055413872L217.45036474197434,51.283175461782264C225.51872990393107,46.47654036942581,241.65546022784443,36.86327018471291,259.3664035148011,32.05663509235645C277.0773468017578,27.25,296.3625030517578,27.25,306.0050811767578,27.25L315.6476593017578,27.25" marker-end="url(#arrowhead8022)" style="fill:none"></path><defs><marker id="arrowhead8022" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M209.38199764782274,87.91018832629831L217.45036313181194,92.55015693858194C225.5187286158011,97.19012555086555,241.65545958377947,106.47006277543278,255.22382506776864,111.11003138771639C268.7921905517578,115.75,279.7921905517578,115.75,285.2921905517578,115.75L290.7921905517578,115.75" marker-end="url(#arrowhead8023)" style="fill:none"></path><defs><marker id="arrowhead8023" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M387.8742218017578,27.25L396.1834665934245,27.25C404.4927113850911,27.25,421.1112009684245,27.25,437.1148902045356,38.791666666666664C453.1185794406467,50.333333333333336,468.50746832953564,73.41666666666667,476.20191277398004,84.95833333333333L483.8963572184245,96.5" marker-end="url(#arrowhead8024)" style="fill:none"></path><defs><marker id="arrowhead8024" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M412.7296905517578,115.75L416.8963572184245,115.75C421.0630238850911,115.75,429.3963572184245,115.75,437.7296905517578,115.75C446.0630238850911,115.75,454.3963572184245,115.75,458.5630238850911,115.75L462.7296905517578,115.75" marker-end="url(#arrowhead8025)" style="fill:none"></path><defs><marker id="arrowhead8025" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M479.60164512933693,135L472.6229860330738,142.84322929382324C465.64432693681056,150.68645858764648,451.6870087442842,166.37291717529297,430.38022464802106,174.2161464691162C409.0734405517578,182.05937576293945,380.4171905517578,182.05937576293945,350.4276072184245,182.05937576293945C320.4380238850911,182.05937576293945,289.1151072184245,182.05937576293945,260.725523630778,182.05937576293945C232.3359400431315,182.05937576293945,206.87968953450522,182.05937576293945,182.75677235921225,182.05937576293945C158.63385518391928,182.05937576293945,135.84427134195963,182.05937576293945,116.53459257224951,173.37187576293945C97.2249138025394,164.68437576293945,81.39514010507881,147.30937576293945,73.48025325634852,138.62187576293945L65.56536640761821,129.93437576293945" marker-end="url(#arrowhead8026)" style="fill:none"></path><defs><marker id="arrowhead8026" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(257.7921905517578,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000394821166992" height="18.500455856323242"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(257.7921905517578,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000394821166992" height="18.500455856323242"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8376" transform="translate(48.02734375,110.68437576293945)"><rect rx="0" ry="0" x="-40.02734375" y="-19.25" width="80.0546875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.02734375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历(i,j)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8377" transform="translate(181.4234390258789,71.5)"><polygon points="43.36875,0 86.7375,-43.36875 43.36875,-86.7375 0,-43.36875" transform="translate(-43.36875,43.36875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-18.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">障碍?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8379" transform="translate(351.7609405517578,27.25)"><rect rx="0" ry="0" x="-36.11328125" y="-19.25" width="72.2265625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-26.11328125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[j]=0</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8381" transform="translate(351.7609405517578,115.75)"><rect rx="0" ry="0" x="-60.96875" y="-19.25" width="121.9375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.96875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[j]+=dp[j-1]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8383" transform="translate(496.7296905517578,115.75)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">下一格</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int uniquePathsWithObstacles(int[][] g) {        int m = g.length, n = g[0].length;        int[] dp = new int[n]; dp[0] = g[0][0] == 0 ? 1 : 0;        for (int i = 0; i &lt; m; i++) {            for (int j = 0; j &lt; n; j++) {                if (g[i][j] == 1) { dp[j] = 0; continue; }                if (j &gt; 0) dp[j] += dp[j - 1];            }        }        return dp[n - 1];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>一维压缩：<code>dp[j] += dp[j-1]</code>，<code>dp[j]</code> 是上一行（上）、<code>dp[j-1]</code> 是本行已更新（左）。障碍处 <code>dp[j]=0</code> 且 <code>continue</code>（不累加左）。</li></ul><h2 id="5.-%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2" tabindex="-1"><a href="https://leetcode.cn/problems/longest-palindromic-substring/" target="_blank">5. 最长回文子串</a></h2><blockquote><p>找最长回文子串。</p></blockquote><p><strong>思路脉络</strong>：中心扩展法。回文以中心对称，中心可单字符（奇）或间隙（偶）。每个中心向两侧扩展，记录最长。和热题 100 相同。</p><div class="mermaid"><svg id="render2131171439" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="178" style="max-width: 409.6875px;" viewBox="0 0 409.6875 178"><style>#render2131171439 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render2131171439 .error-icon{fill:#552222;}#render2131171439 .error-text{fill:#552222;stroke:#552222;}#render2131171439 .edge-thickness-normal{stroke-width:2px;}#render2131171439 .edge-thickness-thick{stroke-width:3.5px;}#render2131171439 .edge-pattern-solid{stroke-dasharray:0;}#render2131171439 .edge-pattern-dashed{stroke-dasharray:3;}#render2131171439 .edge-pattern-dotted{stroke-dasharray:2;}#render2131171439 .marker{fill:#333333;stroke:#333333;}#render2131171439 .marker.cross{stroke:#333333;}#render2131171439 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render2131171439 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render2131171439 .cluster-label text{fill:#333;}#render2131171439 .cluster-label span{color:#333;}#render2131171439 .label text,#render2131171439 span{fill:#333;color:#333;}#render2131171439 .node rect,#render2131171439 .node circle,#render2131171439 .node ellipse,#render2131171439 .node polygon,#render2131171439 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render2131171439 .node .label{text-align:center;}#render2131171439 .node.clickable{cursor:pointer;}#render2131171439 .arrowheadPath{fill:#333333;}#render2131171439 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render2131171439 .flowchart-link{stroke:#333333;fill:none;}#render2131171439 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render2131171439 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render2131171439 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render2131171439 .cluster text{fill:#333;}#render2131171439 .cluster span{color:#333;}#render2131171439 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render2131171439 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M67.35090042372882,96.5L76.38616701977402,84.95833333333333C85.42143361581923,73.41666666666667,103.4919668079096,50.333333333333336,118.09233757062147,38.791666666666664C132.69270833333334,27.25,143.82291666666666,27.25,149.38802083333334,27.25L154.953125,27.25" marker-end="url(#arrowhead8044)" style="fill:none"></path><defs><marker id="arrowhead8044" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-A LE-C" style="opacity: 1;" id="L-A-C"><path class="path" d="M96.5625,115.75L100.72916666666667,115.75C104.89583333333333,115.75,113.22916666666667,115.75,121.5625,115.75C129.89583333333334,115.75,138.22916666666666,115.75,142.39583333333334,115.75L146.5625,115.75" marker-end="url(#arrowhead8045)" style="fill:none"></path><defs><marker id="arrowhead8045" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M275.296875,27.25L280.8619791666667,27.25C286.4270833333333,27.25,297.5572916666667,27.25,310.81684027777777,38.791666666666664C324.0763888888889,50.333333333333336,339.46527777777777,73.41666666666667,347.15972222222223,84.95833333333333L354.8541666666667,96.5" marker-end="url(#arrowhead8046)" style="fill:none"></path><defs><marker id="arrowhead8046" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M283.6875,115.75L287.8541666666667,115.75C292.0208333333333,115.75,300.3541666666667,115.75,308.6875,115.75C317.0208333333333,115.75,325.3541666666667,115.75,329.5208333333333,115.75L333.6875,115.75" marker-end="url(#arrowhead8047)" style="fill:none"></path><defs><marker id="arrowhead8047" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-A" style="opacity: 1;" id="L-D-A"><path class="path" d="M346.75201612903226,135L340.40793010752685,140.83333333333334C334.0638440860215,146.66666666666666,321.37567204301075,158.33333333333334,299.43783602150535,164.16666666666666C277.5,170,246.3125,170,215.125,170C183.9375,170,152.75,170,129.70665322580643,164.16666666666666C106.66330645161291,158.33333333333334,91.76411290322581,146.66666666666666,84.31451612903226,140.83333333333334L76.8649193548387,135" marker-end="url(#arrowhead8048)" style="fill:none"></path><defs><marker id="arrowhead8048" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8398" transform="translate(52.28125,115.75)"><rect rx="0" ry="0" x="-44.28125" y="-19.25" width="88.5625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.28125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">每个中心i</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8399" transform="translate(215.125,27.25)"><rect rx="0" ry="0" x="-60.171875" y="-19.25" width="120.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-50.171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">奇:expand(i,i)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8401" transform="translate(215.125,115.75)"><rect rx="0" ry="0" x="-68.5625" y="-19.25" width="137.125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.5625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">偶:expand(i,i+1)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8403" transform="translate(367.6875,115.75)"><rect rx="0" ry="0" x="-34" y="-19.25" width="68" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-24,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">取最长</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public String longestPalindrome(String s) {        int st = 0, max = 1;        for (int i = 0; i &lt; s.length(); i++) {            int l1 = expand(s, i, i), l2 = expand(s, i, i + 1);            int len = Math.max(l1, l2);            if (len &gt; max) { max = len; st = i - (len - 1) / 2; }        }        return s.substring(st, st + max);    }    int expand(String s, int l, int r) {        while (l &gt;= 0 &amp;&amp; r &lt; s.length() &amp;&amp; s.charAt(l) == s.charAt(r)) { l--; r++; }        return r - l - 1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>expand</code> 返回 <code>r - l - 1</code>：退出时 <code>l/r</code> 已是不匹配位置，真实回文 <code>(l+1, r-1)</code>，长 <code>r-1-(l+1)+1 = r-l-1</code>。起点 <code>i - (len-1)/2</code> 对奇偶都成立。</li></ul><h2 id="97.-%E4%BA%A4%E9%94%99%E5%AD%97%E7%AC%A6%E4%B8%B2" tabindex="-1"><a href="https://leetcode.cn/problems/interleaving-string/" target="_blank">97. 交错字符串</a></h2><blockquote><p><code>s3</code> 是否由 <code>s1</code> 和 <code>s2</code> 交错组成（保持各自字符顺序）。</p></blockquote><p><strong>思路脉络</strong>：二维 DP。<code>dp[i][j]</code> = <code>s1</code> 前 i 个和 <code>s2</code> 前 j 个能否交错成 <code>s3</code> 前 <code>i+j</code> 个。<code>dp[i][j] = (dp[i-1][j] &amp;&amp; s1[i-1]==s3[i+j-1]) || (dp[i][j-1] &amp;&amp; s2[j-1]==s3[i+j-1])</code>。</p><div class="mermaid"><svg id="render170043871" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="273.3484191894531" style="max-width: 833.2101440429688px;" viewBox="0 0 833.2101440429688 273.3484191894531"><style>#render170043871 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render170043871 .error-icon{fill:#552222;}#render170043871 .error-text{fill:#552222;stroke:#552222;}#render170043871 .edge-thickness-normal{stroke-width:2px;}#render170043871 .edge-thickness-thick{stroke-width:3.5px;}#render170043871 .edge-pattern-solid{stroke-dasharray:0;}#render170043871 .edge-pattern-dashed{stroke-dasharray:3;}#render170043871 .edge-pattern-dotted{stroke-dasharray:2;}#render170043871 .marker{fill:#333333;stroke:#333333;}#render170043871 .marker.cross{stroke:#333333;}#render170043871 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render170043871 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render170043871 .cluster-label text{fill:#333;}#render170043871 .cluster-label span{color:#333;}#render170043871 .label text,#render170043871 span{fill:#333;color:#333;}#render170043871 .node rect,#render170043871 .node circle,#render170043871 .node ellipse,#render170043871 .node polygon,#render170043871 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render170043871 .node .label{text-align:center;}#render170043871 .node.clickable{cursor:pointer;}#render170043871 .arrowheadPath{fill:#333333;}#render170043871 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render170043871 .flowchart-link{stroke:#333333;fill:none;}#render170043871 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render170043871 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render170043871 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render170043871 .cluster text{fill:#333;}#render170043871 .cluster span{color:#333;}#render170043871 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render170043871 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M220.6640625,104.08710861206055L224.83072916666666,104.08710861206055C228.99739583333334,104.08710861206055,237.33072916666666,104.08710861206055,245.74739634195964,104.17044194539388C254.16406351725263,104.25377527872722,262.66406453450526,104.42044194539388,266.9140650431316,104.50377527872722L271.1640655517579,104.58710861206056" marker-end="url(#arrowhead8071)" style="fill:none"></path><defs><marker id="arrowhead8071" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M405.57726977842105,71.32609408872378L416.53743937427015,63.980078407269815C427.49760897011924,56.63406272581585,449.4179481618174,41.942031362907926,472.7802008366867,34.59601568145396C496.14245351155597,27.25,520.9466196695963,27.25,533.3487027486166,27.25L545.7507858276367,27.25" marker-end="url(#arrowhead8072)" style="fill:none"></path><defs><marker id="arrowhead8072" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M405.5772660963763,137.8481207139264L416.5374363058995,145.02747013229217C427.4976065154228,152.20681955065797,449.41794693446917,166.56551838738952,465.9614507316389,173.82820113908863C482.50495452880864,181.09088389078775,493.67162170410165,181.25755055745444,499.2549552917481,181.3408838907878L504.8382888793946,181.42421722412112" marker-end="url(#arrowhead8073)" style="fill:none"></path><defs><marker id="arrowhead8073" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M650.5799364581584,158.31743035464282L659.847734015425,154.71022816622255C669.1155315726916,151.10302597780225,687.6511266872247,143.88862160096167,702.4189242444912,140.2814194125414C717.1867218017578,136.6742172241211,728.1867218017578,136.6742172241211,733.6867218017578,136.6742172241211L739.1867218017578,136.6742172241211" marker-end="url(#arrowhead8074)" style="fill:none"></path><defs><marker id="arrowhead8074" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-F" style="opacity: 1;" id="L-D-F"><path class="path" d="M650.5799386746545,204.5310049288611L659.847735862505,207.97154031140442C669.1155330503556,211.41207569394774,687.6511274260566,218.29314645903443,705.0673621139073,221.73368184157778C722.4835968017578,225.1742172241211,738.7804718017578,225.1742172241211,746.9289093017578,225.1742172241211L755.0773468017578,225.1742172241211" marker-end="url(#arrowhead8075)" style="fill:none"></path><defs><marker id="arrowhead8075" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(471.3382873535156,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000274658203125" height="18.500316619873047"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(471.3382873535156,180.9242172241211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000274658203125" height="18.500316619873047"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(706.1867218017578,136.6742172241211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000274658203125" height="18.500316619873047"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(706.1867218017578,225.1742172241211)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.000274658203125" height="18.500316619873047"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8418" transform="translate(114.33203125,104.08710861206055)"><rect rx="0" ry="0" x="-106.33203125" y="-19.25" width="212.6640625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-96.33203125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[i][j]:s1前i与s2前j交错s3</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8419" transform="translate(354.5011749267578,104.08710861206055)"><polygon points="83.837109375,0 167.67421875,-83.837109375 83.837109375,-167.67421875 0,-83.837109375" transform="translate(-83.837109375,83.837109375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-63.90234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">s1[i-1]==s3[i+j-1]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8421" transform="translate(588.7625045776367,27.25)"><rect rx="0" ry="0" x="-43.01171875" y="-19.25" width="86.0234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-33.01171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[i-1][j]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8423" transform="translate(588.7625045776367,180.9242172241211)"><polygon points="84.42421875000001,0 168.84843750000002,-84.42421875000001 84.42421875000001,-168.84843750000002 0,-84.42421875000001" transform="translate(-84.42421875000001,84.42421875000001)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-64.5546875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">s2[j-1]==s3[i+j-1]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8425" transform="translate(782.1984405517578,136.6742172241211)"><rect rx="0" ry="0" x="-43.01171875" y="-19.25" width="86.0234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-33.01171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[i][j-1]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-8427" transform="translate(782.1984405517578,225.1742172241211)"><rect rx="0" ry="0" x="-27.12109375" y="-19.25" width="54.2421875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-17.12109375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">false</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public boolean isInterleave(String s1, String s2, String s3) {        int m = s1.length(), n = s2.length();        if (m + n != s3.length()) return false;        boolean[] dp = new boolean[n + 1];        dp[0] = true;        for (int j = 1; j &lt;= n; j++) dp[j] = dp[j - 1] &amp;&amp; s2.charAt(j - 1) == s3.charAt(j - 1);        for (int i = 1; i &lt;= m; i++) {            dp[0] = dp[0] &amp;&amp; s1.charAt(i - 1) == s3.charAt(i - 1);            for (int j = 1; j &lt;= n; j++)                dp[j] = (dp[j] &amp;&amp; s1.charAt(i - 1) == s3.charAt(i + j - 1))                     || (dp[j - 1] &amp;&amp; s2.charAt(j - 1) == s3.charAt(i + j - 1));        }        return dp[n];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>长度不等先 return。一维压缩时 <code>dp[0]</code> 每行开头要单独更新（只依赖 s1）。<code>dp[j]</code>（上）和 <code>dp[j-1]</code>（左）分别对应 s1/s2 匹配。</li></ul><h2 id="72.-%E7%BC%96%E8%BE%91%E8%B7%9D%E7%A6%BB" tabindex="-1"><a href="https://leetcode.cn/problems/edit-distance/" target="_blank">72. 编辑距离</a></h2><blockquote><p><code>word1</code> 变成 <code>word2</code> 的最少操作（增/删/改）次数。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i][j]</code> = <code>w1</code> 前 i 变 <code>w2</code> 前 j 的最少操作。字符相同 <code>dp[i-1][j-1]</code>；不同取删/增/改三方向 min+1。边界 <code>dp[i][0]=i</code>、<code>dp[0][j]=j</code>。和热题 100 相同。</p><div class="mermaid"><svg id="render4169802938" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="320" style="max-width: 605.5257568359375px;" viewBox="0 0 605.5257568359375 320"><style>#render4169802938 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render4169802938 .error-icon{fill:#552222;}#render4169802938 .error-text{fill:#552222;stroke:#552222;}#render4169802938 .edge-thickness-normal{stroke-width:2px;}#render4169802938 .edge-thickness-thick{stroke-width:3.5px;}#render4169802938 .edge-pattern-solid{stroke-dasharray:0;}#render4169802938 .edge-pattern-dashed{stroke-dasharray:3;}#render4169802938 .edge-pattern-dotted{stroke-dasharray:2;}#render4169802938 .marker{fill:#333333;stroke:#333333;}#render4169802938 .marker.cross{stroke:#333333;}#render4169802938 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render4169802938 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render4169802938 .cluster-label text{fill:#333;}#render4169802938 .cluster-label span{color:#333;}#render4169802938 .label text,#render4169802938 span{fill:#333;color:#333;}#render4169802938 .node rect,#render4169802938 .node circle,#render4169802938 .node ellipse,#render4169802938 .node polygon,#render4169802938 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render4169802938 .node .label{text-align:center;}#render4169802938 .node.clickable{cursor:pointer;}#render4169802938 .arrowheadPath{fill:#333333;}#render4169802938 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render4169802938 .flowchart-link{stroke:#333333;fill:none;}#render4169802938 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render4169802938 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render4169802938 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render4169802938 .cluster text{fill:#333;}#render4169802938 .cluster span{color:#333;}#render4169802938 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render4169802938 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M79.7578125,160L83.92447916666667,160C88.09114583333333,160,96.42447916666667,160,104.84114634195964,160.08333333333334C113.25781351725261,160.16666666666666,121.75781453450521,160.33333333333334,126.00781504313153,160.41666666666666L130.25781555175783,160.5" marker-end="url(#arrowhead8101)" style="fill:none"></path><defs><marker id="arrowhead8101" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M230.9306703298333,121.06113602807548L242.92048150044704,105.4259466900629C254.91029267106077,89.79075735205032,278.8899150122882,58.52037867602516,299.1114970162352,42.88518933801257C319.3330790201823,27.25,335.79662068684894,27.25,344.0283915201823,27.25L352.2601623535156,27.25" marker-end="url(#arrowhead8102)" style="fill:none"></path><defs><marker id="arrowhead8102" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M249.32507805083912,139.45554374908133L258.2491546012852,135.5046197909011C267.1732311517313,131.55369583272088,285.0213842526235,123.65184791636045,300.6342628864029,119.70092395818023C316.2471415201823,115.75,329.62474568684894,115.75,336.3135477701823,115.75L343.0023498535156,115.75" marker-end="url(#arrowhead8103)" style="fill:none"></path><defs><marker id="arrowhead8103" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-E" style="opacity: 1;" id="L-B-E"><path class="path" d="M249.32507378079143,181.54445441745082L258.24915104291216,185.32871201454236C267.1732283050328,189.11296961163387,285.0213828292742,196.68148480581695,300.63426217472824,200.4657424029085C316.2471415201823,204.25,329.62474568684894,204.25,336.3135477701823,204.25L343.0023498535156,204.25" marker-end="url(#arrowhead8104)" style="fill:none"></path><defs><marker id="arrowhead8104" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-F" style="opacity: 1;" id="L-B-F"><path class="path" d="M230.93066766237183,199.9388605358705L242.92047927756246,215.40738377989206C254.9102908927531,230.87590702391367,278.88991412313436,261.81295351195683,296.379725738325,277.2814767559784C313.8695373535156,292.75,324.8695373535156,292.75,330.3695373535156,292.75L335.8695373535156,292.75" marker-end="url(#arrowhead8105)" style="fill:none"></path><defs><marker id="arrowhead8105" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-G" style="opacity: 1;" id="L-D-G"><path class="path" d="M461.8070373535156,115.75L467.1625061035156,115.75C472.5179748535156,115.75,483.2289123535156,115.75,496.9691044291983,127.29166666666667C510.70929650488097,138.83333333333334,527.4787431562463,161.91666666666666,535.863466481929,173.45833333333334L544.2481898076117,185" marker-end="url(#arrowhead8106)" style="fill:none"></path><defs><marker id="arrowhead8106" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-G" style="opacity: 1;" id="L-E-G"><path class="path" d="M461.8070373535156,204.25L467.1625061035156,204.25C472.5179748535156,204.25,483.2289123535156,204.25,492.7510477701823,204.25C502.27318318684894,204.25,510.6065165201823,204.25,514.773183186849,204.25L518.9398498535156,204.25" marker-end="url(#arrowhead8107)" style="fill:none"></path><defs><marker id="arrowhead8107" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-F LE-G" style="opacity: 1;" id="L-F-G"><path class="path" d="M468.9398498535156,292.75L473.1065165201823,292.75C477.27318318684894,292.75,485.6065165201823,292.75,498.1579065125316,281.2083333333333C510.70929650488097,269.6666666666667,527.4787431562463,246.58333333333334,535.863466481929,235.04166666666666L544.2481898076117,223.5" marker-end="url(#arrowhead8108)" style="fill:none"></path><defs><marker id="arrowhead8108" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(302.8695373535156,27.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00006103515625" height="18.500070571899414"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(302.8695373535156,115.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00006103515625" height="18.500070571899414"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(302.8695373535156,204.25)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00006103515625" height="18.500070571899414"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(302.8695373535156,292.75)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00006103515625" height="18.500070571899414"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8444" transform="translate(43.87890625,160)"><rect rx="0" ry="0" x="-35.87890625" y="-19.25" width="71.7578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-25.87890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[i][j]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8445" transform="translate(199.8136749267578,160)"><polygon points="70.055859375,0 140.11171875,-70.055859375 70.055859375,-140.11171875 0,-70.055859375" transform="translate(-70.055859375,70.055859375)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-48.58984375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">w1[i]==w2[j]?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8447" transform="translate(402.4046936035156,27.25)"><rect rx="0" ry="0" x="-50.14453125" y="-19.25" width="100.2890625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-40.14453125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[i-1][j-1]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8449" transform="translate(402.4046936035156,115.75)"><rect rx="0" ry="0" x="-59.40234375" y="-19.25" width="118.8046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-49.40234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">删dp[i-1][j]+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8451" transform="translate(402.4046936035156,204.25)"><rect rx="0" ry="0" x="-59.40234375" y="-19.25" width="118.8046875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-49.40234375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">增dp[i][j-1]+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-F-8453" transform="translate(402.4046936035156,292.75)"><rect rx="0" ry="0" x="-66.53515625" y="-19.25" width="133.0703125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-56.53515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">改dp[i-1][j-1]+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-G-8455" transform="translate(558.2328186035156,204.25)"><rect rx="0" ry="0" x="-39.29296875" y="-19.25" width="78.5859375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-29.29296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">三选min</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int minDistance(String w1, String w2) {        int m = w1.length(), n = w2.length();        int[] dp = new int[n + 1];        for (int j = 0; j &lt;= n; j++) dp[j] = j;        for (int i = 1; i &lt;= m; i++) {            int prev = dp[0]; dp[0] = i;                       // dp[i][0] = i            for (int j = 1; j &lt;= n; j++) {                int tmp = dp[j];                if (w1.charAt(i - 1) == w2.charAt(j - 1)) dp[j] = prev;                else dp[j] = Math.min(Math.min(dp[j], dp[j - 1]), prev) + 1;                prev = tmp;            }        }        return dp[n];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>一维压缩时 <code>dp[i-1][j-1]</code>（左上角）会被 <code>dp[j-1]</code> 覆盖，用 <code>prev</code> 暂存上一轮的 <code>dp[j-1]</code>（即对角线值）。删/增/改分别对应 <code>dp[j]</code>/<code>dp[j-1]</code>/<code>prev</code>。</li></ul><h2 id="123.-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-iii" tabindex="-1"><a href="https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/" target="_blank">123. 买卖股票的最佳时机 III</a></h2><blockquote><p>最多 2 次买卖，求最大利润。</p></blockquote><p><strong>思路脉络</strong>：4 状态 DP。<code>buy1</code>/<code>sell1</code> = 第 1 次买/卖后的最大收益，<code>buy2</code>/<code>sell2</code> = 第 2 次。状态转移：<code>buy1 = max(buy1, -price)</code>、<code>sell1 = max(sell1, buy1+price)</code>、<code>buy2 = max(buy2, sell1-price)</code>、<code>sell2 = max(sell2, buy2+price)</code>。</p><div class="mermaid"><svg id="render306090822" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="89.5" style="max-width: 1040.1875px;" viewBox="0 0 1040.1875 89.5"><style>#render306090822 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render306090822 .error-icon{fill:#552222;}#render306090822 .error-text{fill:#552222;stroke:#552222;}#render306090822 .edge-thickness-normal{stroke-width:2px;}#render306090822 .edge-thickness-thick{stroke-width:3.5px;}#render306090822 .edge-pattern-solid{stroke-dasharray:0;}#render306090822 .edge-pattern-dashed{stroke-dasharray:3;}#render306090822 .edge-pattern-dotted{stroke-dasharray:2;}#render306090822 .marker{fill:#333333;stroke:#333333;}#render306090822 .marker.cross{stroke:#333333;}#render306090822 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render306090822 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render306090822 .cluster-label text{fill:#333;}#render306090822 .cluster-label span{color:#333;}#render306090822 .label text,#render306090822 span{fill:#333;color:#333;}#render306090822 .node rect,#render306090822 .node circle,#render306090822 .node ellipse,#render306090822 .node polygon,#render306090822 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render306090822 .node .label{text-align:center;}#render306090822 .node.clickable{cursor:pointer;}#render306090822 .arrowheadPath{fill:#333333;}#render306090822 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render306090822 .flowchart-link{stroke:#333333;fill:none;}#render306090822 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render306090822 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render306090822 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render306090822 .cluster text{fill:#333;}#render306090822 .cluster span{color:#333;}#render306090822 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render306090822 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M96.34375,37.053478653715835L100.51041666666667,35.419565544763195C104.67708333333333,33.785652435810555,113.01041666666667,30.51782621790528,121.34375,28.88391310895264C129.67708333333334,27.25,138.01041666666666,27.25,142.17708333333334,27.25L146.34375,27.25" marker-end="url(#arrowhead8138)" style="fill:none"></path><defs><marker id="arrowhead8138" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M304.7265625,27.25L308.8932291666667,27.25C313.0598958333333,27.25,321.3932291666667,27.25,329.7265625,27.25C338.0598958333333,27.25,346.3932291666667,27.25,350.5598958333333,27.25L354.7265625,27.25" marker-end="url(#arrowhead8139)" style="fill:none"></path><defs><marker id="arrowhead8139" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M547.75,27.25L551.9166666666666,27.25C556.0833333333334,27.25,564.4166666666666,27.25,572.75,27.25C581.0833333333334,27.25,589.4166666666666,27.25,593.5833333333334,27.25L597.75,27.25" marker-end="url(#arrowhead8140)" style="fill:none"></path><defs><marker id="arrowhead8140" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M789.1640625,27.25L793.3307291666666,27.25C797.4973958333334,27.25,805.8307291666666,27.25,815.8769951276881,28.5625C825.9232610887096,29.875,837.6824596774194,32.5,843.5620589717741,33.8125L849.441658266129,35.125" marker-end="url(#arrowhead8141)" style="fill:none"></path><defs><marker id="arrowhead8141" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M849.441658266129,73.625L843.5620589717741,74.9375C837.6824596774194,76.25,825.9232610887096,78.875,799.9258232526881,80.1875C773.9283854166666,81.5,733.6927083333334,81.5,693.45703125,81.5C653.2213541666666,81.5,612.9856770833334,81.5,572.6158854166666,81.5C532.24609375,81.5,491.7421875,81.5,451.23828125,81.5C410.734375,81.5,370.23046875,81.5,332.61328125,81.5C294.99609375,81.5,260.265625,81.5,225.53515625,81.5C190.8046875,81.5,156.07421875,81.5,134.54231770833334,79.86608689104736C113.01041666666667,78.23217378209472,104.67708333333333,74.96434756418944,100.51041666666667,73.3304344552368L96.34375,71.69652134628416" marker-end="url(#arrowhead8142)" style="fill:none"></path><defs><marker id="arrowhead8142" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8470" transform="translate(52.171875,54.375)"><rect rx="0" ry="0" x="-44.171875" y="-19.25" width="88.34375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-34.171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历price</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8471" transform="translate(225.53515625,27.25)"><rect rx="0" ry="0" x="-79.19140625" y="-19.25" width="158.3828125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-69.19140625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">buy1=max(buy1,-p)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8473" transform="translate(451.23828125,27.25)"><rect rx="0" ry="0" x="-96.51171875" y="-19.25" width="193.0234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-86.51171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sell1=max(sell1,buy1+p)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8475" transform="translate(693.45703125,27.25)"><rect rx="0" ry="0" x="-95.70703125" y="-19.25" width="191.4140625" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-85.70703125,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">buy2=max(buy2,sell1-p)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8477" transform="translate(935.67578125,54.375)"><rect rx="0" ry="0" x="-96.51171875" y="-19.25" width="193.0234375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-86.51171875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sell2=max(sell2,buy2+p)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxProfit(int[] prices) {        int buy1 = Integer.MIN_VALUE, sell1 = 0, buy2 = Integer.MIN_VALUE, sell2 = 0;        for (int p : prices) {            buy1 = Math.max(buy1, -p);            sell1 = Math.max(sell1, buy1 + p);            buy2 = Math.max(buy2, sell1 - p);            sell2 = Math.max(sell2, buy2 + p);        }        return sell2;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>buy</code> 初值 <code>MIN_VALUE</code>（表示还没买，取 max 后第一次会变成 <code>-p</code>）。<code>buy2</code> 依赖 <code>sell1</code>（第二次买要在第一次卖后），状态顺序不能乱。</li></ul><h2 id="188.-%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA-iv" tabindex="-1"><a href="https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/" target="_blank">188. 买卖股票的最佳时机 IV</a></h2><blockquote><p>最多 <code>k</code> 次买卖，求最大利润。</p></blockquote><p><strong>思路脉络</strong>：<code>buy[j]</code>/<code>sell[j]</code> = 第 j 次买/卖后最大收益。遍历每天，更新 1…k 次。<code>buy[j] = max(buy[j], sell[j-1]-price)</code>、<code>sell[j] = max(sell[j], buy[j]+price)</code>。k ≥ n/2 时退化成无限次（贪心）。</p><div class="mermaid"><svg id="render400725536" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="178" style="max-width: 826.3515625px;" viewBox="0 0 826.3515625 178"><style>#render400725536 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render400725536 .error-icon{fill:#552222;}#render400725536 .error-text{fill:#552222;stroke:#552222;}#render400725536 .edge-thickness-normal{stroke-width:2px;}#render400725536 .edge-thickness-thick{stroke-width:3.5px;}#render400725536 .edge-pattern-solid{stroke-dasharray:0;}#render400725536 .edge-pattern-dashed{stroke-dasharray:3;}#render400725536 .edge-pattern-dotted{stroke-dasharray:2;}#render400725536 .marker{fill:#333333;stroke:#333333;}#render400725536 .marker.cross{stroke:#333333;}#render400725536 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render400725536 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render400725536 .cluster-label text{fill:#333;}#render400725536 .cluster-label span{color:#333;}#render400725536 .label text,#render400725536 span{fill:#333;color:#333;}#render400725536 .node rect,#render400725536 .node circle,#render400725536 .node ellipse,#render400725536 .node polygon,#render400725536 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render400725536 .node .label{text-align:center;}#render400725536 .node.clickable{cursor:pointer;}#render400725536 .arrowheadPath{fill:#333333;}#render400725536 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render400725536 .flowchart-link{stroke:#333333;fill:none;}#render400725536 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render400725536 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render400725536 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render400725536 .cluster text{fill:#333;}#render400725536 .cluster span{color:#333;}#render400725536 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render400725536 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M54.74772687146893,96.5L62.43300155955743,84.95833333333333C70.11827624764595,73.41666666666667,85.48882562382296,50.333333333333336,97.38699093691149,38.791666666666664C109.28515625,27.25,117.7109375,27.25,121.923828125,27.25L126.13671875,27.25" marker-end="url(#arrowhead8166)" style="fill:none"></path><defs><marker id="arrowhead8166" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-A LE-C" style="opacity: 1;" id="L-A-C"><path class="path" d="M75.859375,115.75L80.02604166666667,115.75C84.19270833333333,115.75,92.52604166666667,115.75,100.859375,115.75C109.19270833333333,115.75,117.52604166666667,115.75,121.69270833333333,115.75L125.859375,115.75" marker-end="url(#arrowhead8167)" style="fill:none"></path><defs><marker id="arrowhead8167" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-D" style="opacity: 1;" id="L-C-D"><path class="path" d="M264.2890625,115.75L268.4557291666667,115.75C272.6223958333333,115.75,280.9557291666667,115.75,289.2890625,115.75C297.6223958333333,115.75,305.9557291666667,115.75,310.1223958333333,115.75L314.2890625,115.75" marker-end="url(#arrowhead8168)" style="fill:none"></path><defs><marker id="arrowhead8168" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-D LE-E" style="opacity: 1;" id="L-D-E"><path class="path" d="M547.6484375,115.75L551.8151041666666,115.75C555.9817708333334,115.75,564.3151041666666,115.75,575.0310399865592,117.0625C585.7469758064516,118.375,598.8455141129033,121,605.3947832661291,122.3125L611.9440524193549,123.625" marker-end="url(#arrowhead8169)" style="fill:none"></path><defs><marker id="arrowhead8169" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M611.9440524193549,162.125L605.3947832661291,163.4375C598.8455141129033,164.75,585.7469758064516,167.375,555.5844254032259,168.6875C525.421875,170,478.1953125,170,430.96875,170C383.7421875,170,336.515625,170,297.1998697916667,170C257.8841145833333,170,226.47916666666666,170,195.07421875,170C163.66927083333334,170,132.26432291666666,170,110.22532342069893,164.16666666666666C88.18632392473118,158.33333333333334,75.51327284946237,146.66666666666666,69.17674731182795,140.83333333333334L62.84022177419355,135" marker-end="url(#arrowhead8170)" style="fill:none"></path><defs><marker id="arrowhead8170" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8490" transform="translate(41.9296875,115.75)"><rect rx="0" ry="0" x="-33.9296875" y="-19.25" width="67.859375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-23.9296875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">k≥n/2?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8491" transform="translate(195.07421875,27.25)"><rect rx="0" ry="0" x="-68.9375" y="-19.25" width="137.875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-58.9375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">贪心:累加上升段</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8493" transform="translate(195.07421875,115.75)"><rect rx="0" ry="0" x="-69.21484375" y="-19.25" width="138.4296875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-59.21484375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">遍历price,1..k次</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8495" transform="translate(430.96875,115.75)"><rect rx="0" ry="0" x="-116.6796875" y="-19.25" width="233.359375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-106.6796875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">buy[j]=max(buy[j],sell[j-1]-p)</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8497" transform="translate(708,142.875)"><rect rx="0" ry="0" x="-110.3515625" y="-19.25" width="220.703125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-100.3515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">sell[j]=max(sell[j],buy[j]+p)</tspan></text></g></g></g></g></g></g></svg></div><pre><code class="language-java">class Solution {    public int maxProfit(int k, int[] prices) {        int n = prices.length;        if (k &gt;= n / 2) return infinite(prices);        int[] buy = new int[k + 1], sell = new int[k + 1];        Arrays.fill(buy, Integer.MIN_VALUE);        for (int p : prices)            for (int j = 1; j &lt;= k; j++) {                buy[j] = Math.max(buy[j], sell[j - 1] - p);                sell[j] = Math.max(sell[j], buy[j] + p);            }        return sell[k];    }    int infinite(int[] prices) {        int ans = 0;        for (int i = 1; i &lt; prices.length; i++) if (prices[i] &gt; prices[i - 1]) ans += prices[i] - prices[i - 1];        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(nk)，空间 O(k)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>k &gt;= n/2</code> 时退化为无限次买卖（每天最多一次买卖，k 超过这个上限没意义），用贪心累加所有上升段。<code>buy[j]</code> 依赖 <code>sell[j-1]</code>（第 j 次买要在第 j-1 次卖后）。</li></ul><h2 id="221.-%E6%9C%80%E5%A4%A7%E6%AD%A3%E6%96%B9%E5%BD%A2" tabindex="-1"><a href="https://leetcode.cn/problems/maximal-square/" target="_blank">221. 最大正方形</a></h2><blockquote><p>0/1 矩阵里只含 1 的最大正方形面积。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i][j]</code> = 以 <code>(i,j)</code> 为右下角的最大全 1 正方形边长。<code>dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1</code>（当 <code>matrix[i][j]=='1'</code>）。三者 min + 1 保证能扩成正方形。</p><div class="mermaid"><svg id="render1019754748" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="200.376953125" style="max-width: 613.6640625px;" viewBox="0 0 613.6640625 200.376953125"><style>#render1019754748 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#render1019754748 .error-icon{fill:#552222;}#render1019754748 .error-text{fill:#552222;stroke:#552222;}#render1019754748 .edge-thickness-normal{stroke-width:2px;}#render1019754748 .edge-thickness-thick{stroke-width:3.5px;}#render1019754748 .edge-pattern-solid{stroke-dasharray:0;}#render1019754748 .edge-pattern-dashed{stroke-dasharray:3;}#render1019754748 .edge-pattern-dotted{stroke-dasharray:2;}#render1019754748 .marker{fill:#333333;stroke:#333333;}#render1019754748 .marker.cross{stroke:#333333;}#render1019754748 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#render1019754748 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#render1019754748 .cluster-label text{fill:#333;}#render1019754748 .cluster-label span{color:#333;}#render1019754748 .label text,#render1019754748 span{fill:#333;color:#333;}#render1019754748 .node rect,#render1019754748 .node circle,#render1019754748 .node ellipse,#render1019754748 .node polygon,#render1019754748 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#render1019754748 .node .label{text-align:center;}#render1019754748 .node.clickable{cursor:pointer;}#render1019754748 .arrowheadPath{fill:#333333;}#render1019754748 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#render1019754748 .flowchart-link{stroke:#333333;fill:none;}#render1019754748 .edgeLabel{background-color:#e8e8e8;text-align:center;}#render1019754748 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#render1019754748 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#render1019754748 .cluster text{fill:#333;}#render1019754748 .cluster span{color:#333;}#render1019754748 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#render1019754748 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath LS-A LE-B" style="opacity: 1;" id="L-A-B"><path class="path" d="M67.63370651921097,101.751953125L73.82105751600915,96.73795572916667C80.00840851280732,91.72395833333333,92.38311050640367,81.69596354166667,102.82046150320184,76.76529947916667C113.2578125,71.83463541666667,121.7578125,72.00130208333333,126.0078125,72.08463541666667L130.2578125,72.16796875" marker-end="url(#arrowhead8196)" style="fill:none"></path><defs><marker id="arrowhead8196" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-C" style="opacity: 1;" id="L-B-C"><path class="path" d="M237.60121361746363,52.175432367463614L246.3499696812197,48.049188431219676C255.09872574497572,43.922944494975745,272.59623787248785,35.67045662248787,286.84499393624395,31.544212686243934C301.09375,27.41796875,312.09375,27.41796875,317.59375,27.41796875L323.09375,27.41796875" marker-end="url(#arrowhead8197)" style="fill:none"></path><defs><marker id="arrowhead8197" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-B LE-D" style="opacity: 1;" id="L-B-D"><path class="path" d="M237.60121361746363,92.16050513253639L246.3499696812197,96.12008240211367C255.09872574497572,100.07965967169093,272.59623787248785,107.99881421084547,294.86647831124395,111.95839148042273C317.13671875,115.91796875,344.1796875,115.91796875,357.701171875,115.91796875L371.22265625,115.91796875" marker-end="url(#arrowhead8198)" style="fill:none"></path><defs><marker id="arrowhead8198" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-C LE-E" style="opacity: 1;" id="L-C-E"><path class="path" d="M473.9609375,27.41796875L478.1276041666667,27.41796875C482.2942708333333,27.41796875,490.6276041666667,27.41796875,503.5119466383614,39.806966145833336C516.396289110056,52.195963541666664,533.831640720112,76.97395833333333,542.5493165251401,89.36295572916667L551.266992330168,101.751953125" marker-end="url(#arrowhead8199)" style="fill:none"></path><defs><marker id="arrowhead8199" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath LS-E LE-A" style="opacity: 1;" id="L-E-A"><path class="path" d="M547.0521836690018,140.251953125L539.0369759741682,148.939453125C531.0217682793345,157.626953125,514.9913528896673,175.001953125,490.23721290316695,183.689453125C465.4830729166667,192.376953125,432.0052083333333,192.376953125,397.1940104166667,192.376953125C362.3828125,192.376953125,326.23828125,192.376953125,292.0546875,192.376953125C257.87109375,192.376953125,225.6484375,192.376953125,194.75911458333334,192.376953125C163.86979166666666,192.376953125,134.31380208333334,192.376953125,112.12585285318154,183.689453125C89.93790362302978,175.001953125,75.11799474605955,157.626953125,67.70804030757444,148.939453125L60.29808586908932,140.251953125" marker-end="url(#arrowhead8200)" style="fill:none"></path><defs><marker id="arrowhead8200" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(290.09375,27.41796875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00015640258789" height="18.500181198120117"></rect><text><tspan xml:space="preserve" dy="1em" x="1">是</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform="translate(290.09375,115.91796875)"><g transform="translate(-8,-9.25)" class="label"><rect rx="0" ry="0" width="16.00015640258789" height="18.500181198120117"></rect><text><tspan xml:space="preserve" dy="1em" x="1">否</tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g><g class="edgeLabel" style="opacity: 1;" transform=""><g transform="translate(0,0)" class="label"><rect rx="0" ry="0" width="0" height="0"></rect><text><tspan xml:space="preserve" dy="1em" x="1"></tspan></text></g></g></g><g class="nodes"><g class="node default" style="opacity: 1;" id="flowchart-A-8510" transform="translate(43.87890625,121.001953125)"><rect rx="0" ry="0" x="-35.87890625" y="-19.25" width="71.7578125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-25.87890625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp[i][j]</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-B-8511" transform="translate(193.42578125,71.66796875)"><polygon points="63.66796875,0 127.3359375,-63.66796875 63.66796875,-127.3359375 0,-63.66796875" transform="translate(-63.66796875,63.66796875)" class="label-container"></polygon><g class="label" transform="translate(0,0)"><g transform="translate(-41.4921875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">matrix=='1'?</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-C-8513" transform="translate(398.52734375,27.41796875)"><rect rx="0" ry="0" x="-75.43359375" y="-19.25" width="150.8671875" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-65.43359375,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">min(上,左,左上)+1</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-D-8515" transform="translate(398.52734375,115.91796875)"><rect rx="0" ry="0" x="-27.3046875" y="-19.25" width="54.609375" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-17.3046875,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">dp=0</tspan></text></g></g></g><g class="node default" style="opacity: 1;" id="flowchart-E-8517" transform="translate(564.8125,121.001953125)"><rect rx="0" ry="0" x="-40.8515625" y="-19.25" width="81.703125" height="38.5" class="label-container"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-30.8515625,-9.25)"><text style=""><tspan xml:space="preserve" dy="1em" x="1">max更新</tspan></text></g></g></g></g></g></g></svg></div><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0221-max-square.gif" alt="" /></p><pre><code class="language-java">class Solution {    public int maximalSquare(char[][] matrix) {        int m = matrix.length, n = matrix[0].length, max = 0, prev = 0;        int[] dp = new int[n + 1];        for (int i = 1; i &lt;= m; i++)            for (int j = 1; j &lt;= n; j++) {                int tmp = dp[j];                if (matrix[i - 1][j - 1] == &#39;1&#39;) {                    dp[j] = Math.min(Math.min(dp[j], dp[j - 1]), prev) + 1;                    max = Math.max(max, dp[j]);                } else dp[j] = 0;                prev = tmp;            }        return max * max;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>一维压缩时 <code>dp[i-1][j-1]</code>（左上角）会被覆盖，用 <code>prev</code> 暂存。<code>dp[j]</code>（上）、<code>dp[j-1]</code>（左）、<code>prev</code>（左上）三者 min + 1。返回面积 <code>max*max</code>。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[LeetCode 热题 100]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/leetcode热题100" />
                <id>tag:https://lymboy.com,2025-08-07:leetcode热题100</id>
                <published>2025-08-07T20:32:58+08:00</published>
                <updated>2025-08-07T20:32:58+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="leetcode-热题-100--java-题解">LeetCode 热题 100 · Java 题解</h1><blockquote><p>[!abstract] 这份笔记是什么<br />力扣站内刷题发烧友投票最多的 100 道题，按 17 个专题整理。每题给：<strong>思路脉络（为什么这么想）+ Mermaid 图 + 可跑的 Java 代码 + 复杂度 + 易错点</strong>。目标是建立&quot;代码思维&quot;——遇到新题能快速归类、套模板、处理边界，而不是背答案。</p></blockquote><h2 id="怎么用这份笔记">怎么用这份笔记</h2><ul><li>先看每题的<strong>思路脉络</strong>，理解从暴力到最优的跃迁动因，这比代码本身重要。</li><li><strong>Mermaid 图</strong>画的是数据结构怎么动，配合代码看。</li><li>代码是 LeetCode 原题方法签名，复制即可提交；关键行有注释。</li><li><code>[!warning]</code> 是真实踩坑，<code>[!tip]</code> 是通用套路，<code>[!info]</code> 是复杂度。</li></ul><h2 id="配图说明">配图说明</h2><p>复杂题配可视化分镜，放在 <code>assets/</code> 目录：</p><ul><li><strong>动图 <code>.gif</code></strong>：算法关键状态逐帧切换，看&quot;过程怎么动&quot;。已全部上传阿里云 OSS，文档里用 <code>![](https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/xxx.gif)</code> 外链嵌入，全域分发无需带 assets 目录。</li></ul><blockquote><p>[!info] 制图流程<br />真实浏览器渲染 HTML → <code>emulate</code> 把 viewport 精确锁到内容尺寸（杜绝黑边）→ 逐帧截图 → ffmpeg 合成高分辨率 GIF。这套流程对过程感强的题（柱状图、链表翻转、DP 表填充、单调栈等）效果最好；简单题不配，免得冗余。GIF 通过 PicGo（HTTP server <code>:36677</code>）传到阿里云 OSS，原文件名保留。</p></blockquote><h2 id="题型分布">题型分布</h2><table><thead><tr><th>专题</th><th>题数</th><th>核心思维</th></tr></thead><tbody><tr><td>哈希</td><td>3</td><td>用空间换时间，O(1) 查询建立&quot;逆向映射&quot;</td></tr><tr><td>双指针</td><td>4</td><td>有序/对撞/快慢，把两层循环压成一层</td></tr><tr><td>滑动窗口</td><td>2</td><td>左右指针维护一个满足约束的动态区间</td></tr><tr><td>子串</td><td>3</td><td>前缀和 + 哈希 / 单调队列优化窗口最值</td></tr><tr><td>普通数组</td><td>5</td><td>原地操作、区间合并、下标映射</td></tr><tr><td>矩阵</td><td>4</td><td>边界模拟、原地翻转、Z 字查找</td></tr><tr><td>链表</td><td>14</td><td>指针操作、哨兵节点、快慢指针</td></tr><tr><td>二叉树</td><td>15</td><td>递归三部曲：边界、单层逻辑、返回值传递</td></tr><tr><td>图论</td><td>4</td><td>DFS/BFS 染色、拓扑排序、Trie</td></tr><tr><td>回溯</td><td>8</td><td>选→递→撤销，用&quot;做选择&quot;的视角统一所有搜索题</td></tr><tr><td>二分查找</td><td>6</td><td>单调性 + 收缩边界，难点在边界处理</td></tr><tr><td>栈</td><td>5</td><td>后进先出匹配、单调栈求&quot;下一个更大&quot;</td></tr><tr><td>堆</td><td>3</td><td>Top-K、动态中位数，用堆维护&quot;半个有序集&quot;</td></tr><tr><td>贪心算法</td><td>4</td><td>局部最优推全局最优，需证明交换不会更差</td></tr><tr><td>动态规划</td><td>10</td><td>状态定义 + 转移方程 + 遍历顺序</td></tr><tr><td>多维动态规划</td><td>5</td><td>二维状态，行列/区间维度的扩展</td></tr><tr><td>技巧</td><td>5</td><td>位运算、摩尔投票、下标原地哈希</td></tr></tbody></table><h2 id="三条贯穿全程的代码思维">三条贯穿全程的代码思维</h2><blockquote><p>[!tip] 1. 暴力法是起点，不是终点<br />每道题先想清楚&quot;暴力怎么做&quot;，再问&quot;哪一步在重复计算/可以跳过&quot;。哈希、双指针、DP 的优化本质都是<strong>消除重复</strong>。</p></blockquote><blockquote><p>[!tip] 2. 数据结构是工具，先想&quot;我需要什么操作&quot;<br />需要&quot;查询是否存在&quot;→哈希；需要&quot;两端伸缩&quot;→双指针；需要&quot;最近相关性&quot;→栈；需要&quot;动态最值&quot;→堆/单调队列。先定操作需求，再选结构。</p></blockquote><blockquote><p>[!tip] 3. 边界是 Bug 的老家<br />空输入、单元素、全相同、最大最小值溢出、负数……每题写完先在脑子里跑这五个 case。</p></blockquote><h2 id="目录">目录</h2><ul><li>[[#1. 哈希]] · 3 题</li><li>[[#2. 双指针]] · 4 题</li><li>[[#3. 滑动窗口]] · 2 题</li><li>[[#4. 子串]] · 3 题</li><li>[[#5. 普通数组]] · 5 题</li><li>[[#6. 矩阵]] · 4 题</li><li>[[#7. 链表]] · 14 题</li><li>[[#8. 二叉树]] · 15 题</li><li>[[#9. 图论]] · 4 题</li><li>[[#10. 回溯]] · 8 题</li><li>[[#11. 二分查找]] · 6 题</li><li>[[#12. 栈]] · 5 题</li><li>[[#13. 堆]] · 3 题</li><li>[[#14. 贪心算法]] · 4 题</li><li>[[#15. 动态规划]] · 10 题</li><li>[[#16. 多维动态规划]] · 5 题</li><li>[[#17. 技巧]] · 5 题</li></ul><hr /><h1 id="1-哈希">1. 哈希</h1><p>哈希的本质是<strong>建立逆向映射</strong>：数组的值→下标、字符→出现次数、元素→是否存在。代价是 O(n) 空间，收益是把&quot;查找&quot;从 O(n) 降到 O(1)。</p><h2 id="1-两数之和">1. 两数之和</h2><blockquote><p>给数组 <code>nums</code> 和目标 <code>target</code>，返回和为 <code>target</code> 的两个元素的下标。恰好有一个解，同一元素不能重复用。</p></blockquote><p><strong>思路脉络</strong>：暴力是两层循环枚举所有数对，O(n²)。优化点在于——当我固定了 <code>num</code>，要找的就是 <code>target - num</code> 这个确定的值。确定值 + 要 O(1) 查询 → 哈希表。</p><p>关键细节：<strong>边查边存</strong>。如果先把所有元素存进 map，遇到 <code>[3,3], target=6</code> 这种重复值会覆盖下标，且没法保证不用同一元素。边查边存能保证查到的一定是当前元素之前出现过的，天然避免重复使用。</p><div class="mermaid">graph LR    A[&quot;遍历 i, num=nums[i]&quot;] --&gt; B{&quot;map 有 target-num?&quot;}    B -- 有 --&gt; C[&quot;返回 [map.get(target-num), i]&quot;]    B -- 没有 --&gt; D[&quot;map.put(num, i)&quot;]    D --&gt; A</div><blockquote><p>[!info] 流程图（图片版，用于不支持 Mermaid 的平台）<br />![[assets/mermaid/0001-two-sum.svg]]<br />PNG 版：<code>assets/mermaid/0001-two-sum.png</code></p></blockquote><blockquote><p>[!example] 边查边存（动图）<br /><code>[2,7,11,15]</code> target=9：i=0 值2，need=7 不在 map，存 {2:0}；i=1 值7，need=2 在 map（下标0），命中返回 [0,1]。关键是先查再存——保证查到的是之前出现过的，天然避免用到自己（[3,3] target=6 也不会误匹配）。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0001-two-sum.gif" alt="0001-two-sum" /></p></blockquote><pre><code class="language-java">class Solution {    public int[] twoSum(int[] nums, int target) {        Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;(); // 值 -&gt; 下标        for (int i = 0; i &lt; nums.length; i++) {            int need = target - nums[i];            if (map.containsKey(need)) {                return new int[]{map.get(need), i}; // 先查再存，避免用到自己            }            map.put(nums[i], i);        }        return new int[0]; // 题目保证有解，走不到    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点<br />先存后查会让 <code>[3,3] target=6</code> 返回错误（自己匹配自己）。必须<strong>先查再存</strong>。</p></blockquote><h2 id="49-字母异位词分组">49. 字母异位词分组</h2><blockquote><p>把互为字母异位词的字符串归到一组。字母异位词：字母相同、排列不同。</p></blockquote><p><strong>思路脉络</strong>：异位词的本质是&quot;字母构成相同&quot;。要分组，就得给同组一个<strong>相同的 key</strong>。问题变成：怎么把一个字符串规范化成 key？</p><p>两种规范化方式：</p><ul><li><strong>排序</strong>：把字符串字符排序，<code>&quot;eat&quot;→&quot;aet&quot;</code>。O(k log k)。</li><li><strong>计数</strong>：26 个字母的出现次数，拼成字符串当 key。O(k)。</li></ul><p>排序写起来短，工程上 k 不大时排序往往更快（常数小）。计数在大 k 下渐进更优。</p><div class="mermaid">graph LR    A[&quot;字符串 str&quot;] --&gt; B[&quot;规范化为 key&quot;]    B --&gt; C{&quot;map 含 key?&quot;}    C -- 有 --&gt; D[&quot;加入该组列表&quot;]    C -- 没有 --&gt; E[&quot;新建列表并加入&quot;]    D --&gt; F[&quot;map.values() 即结果&quot;]    E --&gt; F</div><p>排序法代码最直观：</p><pre><code class="language-java">class Solution {    public List&lt;List&lt;String&gt;&gt; groupAnagrams(String[] strs) {        Map&lt;String, List&lt;String&gt;&gt; map = new HashMap&lt;&gt;();        for (String s : strs) {            char[] cs = s.toCharArray();            Arrays.sort(cs);                  // 排序后作为 key            String key = new String(cs);            map.computeIfAbsent(key, k -&gt; new ArrayList&lt;&gt;()).add(s);        }        return new ArrayList&lt;&gt;(map.values());    }}</code></pre><p><code>computeIfAbsent</code> 是这里的关键 API：key 不存在就创建列表，存在就返回，省去手动 <code>containsKey</code> 判断。</p><blockquote><p>[!info] 复杂度<br />时间 O(n·k log k)（n 是字符串数，k 是最长串长度），空间 O(n·k)。</p></blockquote><blockquote><p>[!tip] 套路<br />&quot;按某种等价关系分组&quot;通用解法：<strong>找规范化的 key + HashMap 分组</strong>。后续回文串、同构字符串都吃这一套。</p></blockquote><h2 id="128-最长连续序列">128. 最长连续序列</h2><blockquote><p>给未排序数组，找最长连续整数序列的长度（如 <code>[100,4,1,101,3,2]</code> → <code>4</code>，因为 <code>1,2,3,4</code>）。要求 O(n)。</p></blockquote><p><strong>思路脉络</strong>：排序是 O(n log n)，不达标。要 O(n) 必须用哈希。朴素哈希做法：把所有数放 set，对每个数 <code>x</code> 往上数 <code>x+1, x+2...</code> 看能走多远。但这会 O(n²)——比如全是连续数时，每个数都从头数一遍。</p><p>优化点：<strong>只有序列起点才值得扩展</strong>。如果 <code>x-1</code> 也在 set 里，那 <code>x</code> 不是起点，从 <code>x</code> 开始数一定会被从更小起点数过的结果覆盖。所以加一个判断 <code>!set.contains(x-1)</code>，只从起点往上数。</p><p>为什么是 O(n)？每个元素只在&quot;作为起点向上扩展&quot;时被访问，且扩展过程中访问的每个数都不会再作为起点（因为它们的前驱存在）。整体每个数最多被访问常数次。</p><div class="mermaid">graph LR    A[&quot;全部数放入 Set&quot;] --&gt; B[&quot;遍历每个数 x&quot;]    B --&gt; C{&quot;x-1 在 Set 中?&quot;}    C -- 是 → 非起点，跳过 --&gt; B    C -- 否 → x 是起点 --&gt; D[&quot;从 x 向上数 x+1, x+2...&quot;]    D --&gt; E[&quot;更新最长长度&quot;]    E --&gt; B</div><pre><code class="language-java">class Solution {    public int longestConsecutive(int[] nums) {        Set&lt;Integer&gt; set = new HashSet&lt;&gt;();        for (int n : nums) set.add(n);        int best = 0;        for (int x : set) {            if (set.contains(x - 1)) continue; // 不是起点，跳过            int cur = x, len = 1;            while (set.contains(cur + 1)) { cur++; len++; }            best = Math.max(best, len);        }        return best;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点<br />漏掉 <code>x-1</code> 判断会退化到 O(n²)。这个&quot;只从起点扩展&quot;的剪枝是本题 O(n) 的灵魂。</p></blockquote><hr /><h1 id="2-双指针">2. 双指针</h1><p>双指针的核心是<strong>用两个指针的协同移动，把两层嵌套循环压成一层线性扫描</strong>。常见的三种形态：</p><ul><li><strong>快慢指针</strong>：同向移动，一个探路一个收尾（移动零、删除重复项）。</li><li><strong>对撞指针</strong>：从两端向中间，配合单调性（盛水容器、三数之和）。</li><li><strong>归并指针</strong>：两个序列各一个指针，按序推进（合并有序链表/数组）。</li></ul><p>判定用哪种的依据是：<strong>指针移动后，被跳过的解是否能安全排除</strong>。这是双指针正确性的根基。</p><h2 id="283-移动零">283. 移动零</h2><blockquote><p>把数组中所有 <code>0</code> 移到末尾，保持非零元素相对顺序，原地操作。</p></blockquote><p><strong>思路脉络</strong>：本质是&quot;把非零元素挑出来排前面，剩下的位置填 0&quot;。用快慢指针：<code>slow</code> 标记下一个非零元素该放的位置，<code>fast</code> 扫描全数组。遇到非零就和 <code>slow</code> 交换，再 <code>slow++</code>。</p><p>为什么用交换而不是&quot;覆盖后补 0&quot;？交换一步到位，不用二次填充，且天然保持顺序。</p><div class="mermaid">graph LR    A[&quot;slow=0 非零落脚点&quot;] --&gt; B[&quot;fast 遍历数组&quot;]    B --&gt; C{&quot;nums[fast] != 0?&quot;}    C -- 是 --&gt; D[&quot;交换 slow 与 fast&quot;]    D --&gt; E[&quot;slow++&quot;]    C -- 否 --&gt; F[&quot;跳过&quot;]    E --&gt; B    F --&gt; B</div><pre><code class="language-java">class Solution {    public void moveZeroes(int[] nums) {        int slow = 0;        for (int fast = 0; fast &lt; nums.length; fast++) {            if (nums[fast] != 0) {                int t = nums[slow]; nums[slow] = nums[fast]; nums[fast] = t;                slow++;            }        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路<br />快慢指针处理&quot;原地分区/筛选&quot;问题：<code>slow</code> 永远指向下一个符合条件元素的落脚点。同款的还有 [[#26. 删除有序数组中的重复项]] 思路、[[#75. 颜色分类]]。</p></blockquote><h2 id="11-盛最多水的容器">11. 盛最多水的容器</h2><blockquote><p>数组 <code>height</code> 表示竖线高度，两根线与 x 轴围成容器，求最大盛水量。面积 = <code>min(h[l], h[r]) × (r - l)</code>。</p></blockquote><p><strong>思路脉络</strong>：暴力枚举所有数对 O(n²)。优化靠一个<strong>贪心剪枝</strong>：双指针从两端出发，宽 <code>(r-l)</code> 在缩小，要让面积变大只能指望高 <code>min(h[l],h[r])</code> 变大。此时该移动哪边？</p><p>移动高的一边：高被 <code>min</code> 限制，移动后宽更小，且新边若更高也不会提升 <code>min</code>（被原矮边卡住），若更矮则 <code>min</code> 更小——面积必减。<br />移动矮的一边：矮边是瓶颈，换掉它才有可能让 <code>min</code> 变大。虽然不一定变好，但<strong>只有这条路有可能变好</strong>。</p><p>所以策略：哪边矮移哪边。</p><div class="mermaid">graph LR    A[&quot;l=0, r=n-1&quot;] --&gt; B[&quot;算面积 min*宽&quot;]    B --&gt; C[&quot;更新答案&quot;]    C --&gt; D{&quot;h[l] &lt; h[r]?&quot;}    D -- 是 --&gt; E[&quot;l++ 移矮边&quot;]    D -- 否 --&gt; F[&quot;r-- 移矮边&quot;]    E --&gt; B    F --&gt; B</div><pre><code class="language-java">class Solution {    public int maxArea(int[] height) {        int l = 0, r = height.length - 1, ans = 0;        while (l &lt; r) {            int area = Math.min(height[l], height[r]) * (r - l);            ans = Math.max(ans, area);            if (height[l] &lt; height[r]) l++; else r--;        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />别写成&quot;移动高的&quot;。正确性证明要抓住&quot;宽必减、高受 min 限制&quot;两点，移动矮边是唯一可能变好的方向。</p></blockquote><h2 id="15-三数之和">15. 三数之和</h2><blockquote><p>找所有和为 0 的不重复三元组。</p></blockquote><p><strong>思路脉络</strong>：三数和 = 固定一个 + 两数和。两数和在有序数组上可以用对撞双指针。所以先<strong>排序</strong>。固定 <code>i</code>，然后在 <code>i+1..n-1</code> 上用双指针找 <code>nums[l]+nums[r] = -nums[i]</code>。</p><p>难点不在算法，在<strong>去重</strong>。三个位置都可能重复：</p><ul><li><code>i</code> 去重：<code>i &gt; 0 &amp;&amp; nums[i] == nums[i-1]</code> 时跳过。</li><li>找到一组解后，<code>l</code> 和 <code>r</code> 都要跳过相邻重复值。</li></ul><blockquote><p>[!tip] 剪枝<br />排序后若 <code>nums[i] &gt; 0</code>，三个正数加起来不可能为 0，直接 <code>break</code>。</p></blockquote><div class="mermaid">graph LR    A[&quot;排序&quot;] --&gt; B[&quot;固定 i&quot;]    B --&gt; C[&quot;l=i+1, r=n-1&quot;]    C --&gt; D[&quot;sum = nums[i]+nums[l]+nums[r]&quot;]    D --&gt; E{&quot;sum ? 0&quot;}    E -- &quot;&lt;0&quot; --&gt; F[&quot;l++&quot;]    E -- &quot;&gt;0&quot; --&gt; G[&quot;r--&quot;]    E -- &quot;=0&quot; --&gt; H[&quot;记录答案&quot;]    H --&gt; I[&quot;l,r 各自跳过重复&quot;]    I --&gt; J[&quot;l++, r--&quot;]    F --&gt; D    G --&gt; D    J --&gt; D</div><pre><code class="language-java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; threeSum(int[] nums) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        Arrays.sort(nums);        for (int i = 0; i &lt; nums.length - 2; i++) {            if (nums[i] &gt; 0) break;                       // 最小值已&gt;0            if (i &gt; 0 &amp;&amp; nums[i] == nums[i - 1]) continue; // i 去重            int l = i + 1, r = nums.length - 1;            while (l &lt; r) {                int sum = nums[i] + nums[l] + nums[r];                if (sum &lt; 0) l++;                else if (sum &gt; 0) r--;                else {                    res.add(Arrays.asList(nums[i], nums[l], nums[r]));                    while (l &lt; r &amp;&amp; nums[l] == nums[l + 1]) l++;  // l 去重                    while (l &lt; r &amp;&amp; nums[r] == nums[r - 1]) r--;  // r 去重                    l++; r--;                }            }        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)（排序 O(n log n) + 双指针扫描），空间 O(log n) 排序栈。</p></blockquote><blockquote><p>[!warning] 易错点<br />去重判断用 <code>nums[i] == nums[i-1]</code>（与前一个比）而不是 <code>nums[i] == nums[i+1]</code>。后者会漏掉 <code>-1,-1,2</code> 这种合法解中第一个 <code>-1</code>。核心原则：<strong>跳过的是&quot;后出现的重复&quot;，保留&quot;第一个出现的&quot;</strong>。</p></blockquote><h2 id="42-接雨水">42. 接雨水</h2><blockquote><p>柱状图接雨水，求能接住的水量。</p></blockquote><p><strong>思路脉络</strong>：每个位置 <code>i</code> 能接的水 = <code>min(左边最高, 右边最高) - height[i]</code>（若为正）。这是&quot;按列计算&quot;的本质。</p><blockquote><p>[!example] 按列计算分镜（动图）<br />拆解四步：原始柱状 → 左侧最高 <code>leftMax</code> → 右侧最高 <code>rightMax</code> → 每列水量 <code>min(左,右) − height</code>，合计 6。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0042-trap-water.gif" alt="0042-trap-water" /></p></blockquote><p>三种实现，一个比一个省空间：</p><p><strong>方法一：DP 预处理两个数组</strong>。<code>leftMax[i]</code>、<code>rightMax[i]</code> 各扫一遍得到，再遍历求和。O(n) 时间 O(n) 空间，最好理解。</p><p><strong>方法二：单调栈</strong>。按行算，遇到比栈顶矮的入栈，遇到更高的就弹出栈顶算一层。O(n) 时间 O(n) 空间，思路不同。</p><p><strong>方法三：双指针 O(1) 空间</strong>。本题重点。<code>leftMax</code> 和 <code>rightMax</code> 不预先算，而是双指针移动时实时维护。关键洞察：</p><blockquote><p>哪边矮就处理哪边。若 <code>height[l] &lt; height[r]</code>，则 <code>l</code> 处的水量由 <code>leftMax</code> 决定——因为 <code>rightMax</code> 至少是 <code>height[r]</code>，比 <code>height[l]</code> 大，所以 <code>min(leftMax, rightMax)</code> 的瓶颈在 <code>leftMax</code> 一侧。</p></blockquote><div class="mermaid">graph LR    A[&quot;l=0,r=n-1&lt;br/&gt;leftMax=rightMax=0&quot;] --&gt; B{&quot;h[l] &lt; h[r]?&quot;}    B -- 是 --&gt; C{&quot;h[l] &gt;= leftMax?&quot;}    C -- 是 --&gt; D[&quot;更新 leftMax&quot;]    C -- 否 --&gt; E[&quot;累加 leftMax-h[l]&quot;]    D --&gt; F[&quot;l++&quot;]    E --&gt; F    B -- 否 --&gt; G{&quot;h[r] &gt;= rightMax?&quot;}    G -- 是 --&gt; H[&quot;更新 rightMax&quot;]    G -- 否 --&gt; I[&quot;累加 rightMax-h[r]&quot;]    H --&gt; J[&quot;r--&quot;]    I --&gt; J    F --&gt; B    J --&gt; B</div><pre><code class="language-java">class Solution {    public int trap(int[] height) {        int l = 0, r = height.length - 1;        int leftMax = 0, rightMax = 0, ans = 0;        while (l &lt; r) {            if (height[l] &lt; height[r]) {          // 左边矮，瓶颈在左                if (height[l] &gt;= leftMax) leftMax = height[l];                else ans += leftMax - height[l];                l++;            } else {                              // 右边矮，瓶颈在右                if (height[r] &gt;= rightMax) rightMax = height[r];                else ans += rightMax - height[r];                r--;            }        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />双指针正确性的关键：处理某一边时，另一侧一定有一个 &gt;= 当前侧 max 的柱子挡着。这正是 <code>height[l] &lt; height[r]</code> 判断保证的——右边那个 <code>height[r]</code> 就充当了右侧最高的一部分保证。</p></blockquote><blockquote><p>[!tip] 关联<br />这题是&quot;单调性 + 双指针&quot;的巅峰，和 [[#11. 盛最多水的容器]] 一脉相承：都是&quot;移动矮边&quot;。但接雨水算的是&quot;夹缝里能存多少&quot;，盛水容器算的是&quot;两根线能围多少&quot;，方向相反。[[#84. 柱状图中最大的矩形]] 则用单调栈，思路又不同。</p></blockquote><hr /><h1 id="3-滑动窗口">3. 滑动窗口</h1><p>滑动窗口维护一个<strong>满足某约束的动态区间</strong>，靠左右指针交替前进避免重复计算。两类典型：</p><ul><li><strong>可变窗口</strong>：<code>r</code> 扩张到违反约束，<code>l</code> 收缩到重新合法，过程中记录答案（最长无重复子串）。</li><li><strong>固定窗口</strong>：窗口大小恒定，整体平移，维护窗口内状态（异位词、窗口最大值）。</li></ul><p>核心技巧是<strong>用一个&quot;状态量&quot;替代每次重算</strong>：维护 <code>valid</code>/<code>match</code> 计数，让&quot;窗口是否合法&quot;的判断变成 O(1)。</p><h2 id="3-无重复字符的最长子串">3. 无重复字符的最长子串</h2><blockquote><p>找不含重复字符的最长子串长度。</p></blockquote><p><strong>思路脉络</strong>：暴力是枚举所有子串 O(n³) 或固定起点向右延伸 O(n²)。观察：若子串 <code>[l,r]</code> 在 <code>r</code> 处出现重复字符 <code>c</code>，那么任何 <code>l</code> 不动的更长子串都含重复——<code>l</code> 必须跳过 <code>c</code> 上次出现的位置。</p><p>所以用哈希记下每个字符最近一次出现的下标，<code>r</code> 遇到重复时，<code>l</code> 直接跳到 <code>map.get(c)+1</code>。注意要用 <code>max(l, ...)</code>：因为重复字符可能已经在 <code>l</code> 左侧（窗口外），不能往回缩。</p><div class="mermaid">graph LR    A[&quot;l=0, r 遍历&quot;] --&gt; B{&quot;c 上次出现在窗口内?&quot;}    B -- 是 --&gt; C[&quot;l 跳到 上次下标+1&quot;]    B -- 否 --&gt; D[&quot;窗口合法&quot;]    C --&gt; E[&quot;更新 c 位置, 更新答案&quot;]    D --&gt; E    E --&gt; A</div><pre><code class="language-java">class Solution {    public int lengthOfLongestSubstring(String s) {        Map&lt;Character, Integer&gt; map = new HashMap&lt;&gt;(); // 字符 -&gt; 最近下标        int l = 0, ans = 0;        for (int r = 0; r &lt; s.length(); r++) {            char c = s.charAt(r);            if (map.containsKey(c)) {                l = Math.max(l, map.get(c) + 1); // 只能前进，不能后退            }            map.put(c, r);            ans = Math.max(ans, r - l + 1);        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(字符集大小)。</p></blockquote><blockquote><p>[!warning] 易错点<br />漏掉 <code>Math.max</code> 会导致 <code>l</code> 回退。例如 <code>abba</code>，处理第二个 <code>b</code> 时 <code>l</code> 跳到 2，处理第二个 <code>a</code> 时若直接用 <code>map.get('a')+1 = 1</code>，<code>l</code> 会从 2 退回 1，窗口反而变大且含重复。</p></blockquote><h2 id="438-找到字符串中所有字母异位词">438. 找到字符串中所有字母异位词</h2><blockquote><p>找 <code>p</code> 的所有异位词在 <code>s</code> 中的起始下标。</p></blockquote><p><strong>思路脉络</strong>：异位词 = 长度相同且字母计数相同。固定大小为 <code>m = p.length()</code> 的窗口在 <code>s</code> 上滑动，问哪些位置的窗口计数等于 <code>p</code> 的计数。</p><p>朴素做法每次滑完比较 26 个字母计数，O(26n)。优化：维护 <code>valid</code>——窗口与 <code>need</code> 计数相等的字母种类数。每次加入/移除一个字符，只影响该字符一处的相等关系，O(1) 更新 <code>valid</code>。当 <code>valid == 26</code> 时窗口与 <code>p</code> 完全匹配。</p><div class="mermaid">graph LR    A[&quot;need = p 的计数&quot;] --&gt; B[&quot;窗口先填前 m 个字符&quot;]    B --&gt; C[&quot;算初始 valid&quot;]    C --&gt; D[&quot;r 从 m 滑到 n-1&quot;]    D --&gt; E[&quot;加入 s[r], 移除 s[i-m]&quot;]    E --&gt; F[&quot;O(1) 更新 valid&quot;]    F --&gt; G{&quot;valid == 26?&quot;}    G -- 是 --&gt; H[&quot;记录起点&quot;]    G -- 否 --&gt; D    H --&gt; D</div><pre><code class="language-java">class Solution {    public List&lt;Integer&gt; findAnagrams(String s, String p) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        int n = s.length(), m = p.length();        if (n &lt; m) return res;        int[] need = new int[26], win = new int[26];        for (int i = 0; i &lt; m; i++) {            need[p.charAt(i) - 'a']++;            win[s.charAt(i) - 'a']++;        }        int valid = 0;        for (int i = 0; i &lt; 26; i++) if (need[i] == win[i]) valid++; // 含 need=0&amp;win=0 的种        if (valid == 26) res.add(0);        for (int i = m; i &lt; n; i++) {            char in = s.charAt(i), out = s.charAt(i - m);            // 加入 in            if (need[in - 'a'] == win[in - 'a']) valid--; // 加入前相等, 加入后将不等            win[in - 'a']++;            if (need[in - 'a'] == win[in - 'a']) valid++;            // 移除 out            if (need[out - 'a'] == win[out - 'a']) valid--;            win[out - 'a']--;            if (need[out - 'a'] == win[out - 'a']) valid++;            if (valid == 26) res.add(i - m + 1);        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)（26 个字母固定）。</p></blockquote><blockquote><p>[!tip] 套路<br />&quot;定长滑动窗口 + 状态计数&quot;是字符串匹配的万能模板。<code>valid</code> 计数法把&quot;两个数组是否相等&quot;压成单变量维护，是滑动窗口题提速的关键。后续 [[#76. 最小覆盖子串]] 是变长版本，思路同源。</p></blockquote><hr /><h1 id="4-子串">4. 子串</h1><p>这一类是「前缀和 / 滑动窗口」的进阶：要么用前缀和把区间问题转成两点关系，要么用单调队列给滑动窗口配一个能 O(1) 取最值的数据结构。</p><h2 id="560-和为-k-的子数组">560. 和为 K 的子数组</h2><blockquote><p>统计数组中和为 <code>k</code> 的连续子数组个数。数组含负数。</p></blockquote><p><strong>思路脉络</strong>：第一反应可能是&quot;排序 + 双指针&quot;，但<strong>子数组必须连续、且含负数</strong>，排序破坏连续性、双指针依赖单调性（负数让和不再单调）——这条路堵死。</p><p>正确切入点是<strong>前缀和</strong>。设 <code>pre[i]</code> 是前 <code>i</code> 项和，则子数组 <code>[j+1, i]</code> 的和 = <code>pre[i] - pre[j]</code>。要让它等于 <code>k</code>，就是找 <code>pre[j] = pre[i] - k</code>。</p><p>边算前缀和边用哈希记录&quot;之前出现过哪些前缀和、各几次&quot;，当前 <code>pre</code> 减 <code>k</code> 能在哈希里命中多少个，就有多少个以 <code>i</code> 结尾的合法子数组。一次遍历搞定。</p><div class="mermaid">graph LR    A[&quot;维护前缀和 pre&quot;] --&gt; B{&quot;map 含 pre-k?&quot;}    B -- 是 --&gt; C[&quot;ans += 出现次数&quot;]    B -- 否 --&gt; D[&quot;map[pre]++&quot;]    C --&gt; D    D --&gt; A</div><pre><code class="language-java">class Solution {    public int subarraySum(int[] nums, int k) {        Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();        map.put(0, 1);                  // 前缀和 0 出现 1 次, 处理从下标 0 起的子数组        int pre = 0, ans = 0;        for (int n : nums) {            pre += n;            if (map.containsKey(pre - k)) ans += map.get(pre - k);            map.merge(pre, 1, Integer::sum);        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点<br />必须初始化 <code>map.put(0, 1)</code>。否则形如 <code>nums=[k]</code>、整个数组本身就是答案时，<code>pre-k=0</code> 命中不到。另外，<strong>不能先存再查</strong>：要先查当前 <code>pre</code>，再把 <code>pre</code> 入 map，否则 <code>k=0</code> 时会把自己算进去。</p></blockquote><blockquote><p>[!tip] 辨析<br />「和为 K 的子数组」有三种变形，解法完全不同：</p><ul><li>数组含负数（本题）→ 前缀和 + 哈希，O(n)。</li><li>数组全正数 → 滑动窗口，O(n)。</li><li>求最大长度 → 同样前缀和+哈希，存最早出现位置。</li></ul></blockquote><h2 id="239-滑动窗口最大值">239. 滑动窗口最大值</h2><blockquote><p>大小为 <code>k</code> 的窗口从左滑到右，返回每个位置窗口内的最大值。</p></blockquote><p><strong>思路脉络</strong>：朴素是每个窗口扫一遍找最大，O(nk)。优化点：窗口每次只进出各一个元素，最大值的变化是&quot;局部&quot;的——一个元素一旦被右侧更大的元素超过，它在剩余窗口里就永远当不了最大，可以丢弃。</p><p>这就是<strong>单调递减队列</strong>：队列存下标，从队尾入队前，把所有比当前元素小的队尾弹出（它们没用了）。队首永远是当前窗口最大值。再处理一个过期：队首下标若已滑出窗口就弹出。</p><div class="mermaid">graph LR    A[&quot;遍历 i&quot;] --&gt; B[&quot;弹出队尾所有 ≤ nums[i] 的&quot;]    B --&gt; C[&quot;i 入队尾&quot;]    C --&gt; D{&quot;队首下标 &lt; i-k+1?&quot;}    D -- 是 --&gt; E[&quot;队首过期, 弹出&quot;]    D -- 否 --&gt; F{&quot;i ≥ k-1?&quot;}    E --&gt; F    F -- 是 --&gt; G[&quot;记录 nums[队首]&quot;]    F -- 否 --&gt; A    G --&gt; A</div><pre><code class="language-java">class Solution {    public int[] maxSlidingWindow(int[] nums, int k) {        int n = nums.length;        int[] res = new int[n - k + 1];        Deque&lt;Integer&gt; dq = new ArrayDeque&lt;&gt;(); // 存下标, 单调递减        for (int i = 0; i &lt; n; i++) {            while (!dq.isEmpty() &amp;&amp; nums[dq.peekLast()] &lt;= nums[i]) dq.pollLast();            dq.offerLast(i);            if (dq.peekFirst() &lt;= i - k) dq.pollFirst(); // 队首过期            if (i &gt;= k - 1) res[i - k + 1] = nums[dq.peekFirst()];        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)（每个元素入队出队各一次），空间 O(k)。</p></blockquote><blockquote><p>[!tip] 套路<br />单调队列处理&quot;滑动窗口最值&quot;。存下标而非值有两个好处：一能判断过期，二能通过下标取到原值。这是 [[#84. 柱状图中最大的矩形]] 单调栈的同族技巧，区别只在一端进出一端进出。</p></blockquote><h2 id="76-最小覆盖子串">76. 最小覆盖子串</h2><blockquote><p>在 <code>s</code> 中找覆盖 <code>t</code> 所有字符的最短子串。</p></blockquote><p><strong>思路脉络</strong>：这是 [[#438. 找到字符串中所有字母异位词]] 的变长升级版。窗口不再固定大小，而是：<code>r</code> 扩张到&quot;刚好覆盖 <code>t</code>&quot;，<code>l</code> 收缩到&quot;刚好不覆盖&quot;，这个临界位置就是一个候选最短窗口。</p><p>用 <code>valid</code> 记录&quot;窗口里满足需求计数的字符种类数&quot;，<code>valid == need.size()</code> 表示已覆盖。覆盖时不断缩 <code>l</code> 并更新最小答案；一旦缩到不覆盖，就退出去继续扩 <code>r</code>。</p><div class="mermaid">graph LR    A[&quot;r 扩张, 加入字符&quot;] --&gt; B{&quot;valid == 需求种类数?&quot;}    B -- 否 --&gt; A    B -- 是 --&gt; C[&quot;缩 l&quot;]    C --&gt; D[&quot;更新最小窗口&quot;]    D --&gt; E[&quot;移除 s[l], l++&quot;]    E --&gt; F{&quot;仍覆盖?&quot;}    F -- 是 --&gt; C    F -- 否 --&gt; A</div><pre><code class="language-java">class Solution {    public String minWindow(String s, String t) {        Map&lt;Character, Integer&gt; need = new HashMap&lt;&gt;();        for (char c : t.toCharArray()) need.merge(c, 1, Integer::sum);        Map&lt;Character, Integer&gt; win = new HashMap&lt;&gt;();        int l = 0, valid = 0, needCnt = need.size();        int start = 0, minLen = Integer.MAX_VALUE;        for (int r = 0; r &lt; s.length(); r++) {            char c = s.charAt(r);            if (need.containsKey(c)) {                win.merge(c, 1, Integer::sum);                if (win.get(c).equals(need.get(c))) valid++;            }            while (valid == needCnt) {          // 覆盖了, 尝试缩                if (r - l + 1 &lt; minLen) { minLen = r - l + 1; start = l; }                char d = s.charAt(l++);                if (need.containsKey(d)) {                    if (win.get(d).equals(need.get(d))) valid--; // 移除后该种将不满足                    win.merge(d, -1, Integer::sum);                }            }        }        return minLen == Integer.MAX_VALUE ? &quot;&quot; : s.substring(start, start + minLen);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(字符集)。</p></blockquote><blockquote><p>[!warning] 易错点<br /><code>valid</code> 只在&quot;刚好达到需求&quot;时 <code>++</code>，在&quot;刚好跌破需求&quot;时 <code>--</code>，避免多算。判断&quot;跌破&quot;必须在减少计数<strong>之前</strong>比较，顺序反了会漏更新。<code>needCnt</code> 用 <code>need.size()</code>（去重后的种类数），不是 <code>t.length()</code>，因为同字符重复只算一种。</p></blockquote><hr /><h1 id="5-普通数组">5. 普通数组</h1><p>数组的题多靠<strong>原地操作</strong>和<strong>下标映射</strong>。核心是利用&quot;下标本身也是一种信息&quot;——把元素归位、把下标当哈希。</p><h2 id="53-最大子数组和">53. 最大子数组和</h2><blockquote><p>找和最大的连续子数组，返回和。</p></blockquote><p><strong>思路脉络</strong>：两种等价视角。</p><p><strong>DP 视角</strong>：<code>dp[i]</code> 表示以 <code>i</code> 结尾的最大子数组和。要么把 <code>nums[i]</code> 续在前一段后面（<code>dp[i-1]+nums[i]</code>），要么从 <code>nums[i]</code> 重新开始（<code>nums[i]</code>），取大者。状态只依赖上一个，压缩成一个变量 <code>pre</code>。</p><p><strong>前缀和视角</strong>：子数组和 = 当前前缀和 − 之前最小前缀和。维护一个最小前缀和即可。</p><p>两种都 O(n)。DP 写法更常用。</p><div class="mermaid">graph LR    A[&quot;遍历 num&quot;] --&gt; B[&quot;pre = max(num, pre+num)&quot;]    B --&gt; C[&quot;ans = max(ans, pre)&quot;]    C --&gt; A</div><pre><code class="language-java">class Solution {    public int maxSubArray(int[] nums) {        int pre = 0, ans = Integer.MIN_VALUE;        for (int n : nums) {            pre = Math.max(n, pre + n); // 续上 或 从头            ans = Math.max(ans, pre);        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br /><code>ans</code> 初值用 <code>Integer.MIN_VALUE</code> 而非 <code>0</code>，因为全负数组答案也是负数。<code>pre</code> 初值 0 配合 <code>max(n, pre+n)</code> 自动处理首元素。</p></blockquote><h2 id="56-合并区间">56. 合并区间</h2><blockquote><p>合并所有重叠的区间。</p></blockquote><p><strong>思路脉络</strong>：区间若能合并，起点一定相邻。按<strong>起点排序</strong>后，遍历时只需关心&quot;当前区间起点是否落在上一个已合并区间的终点内&quot;——是则合并（终点取 max），否则新开一段。</p><div class="mermaid">graph LR    A[&quot;按起点排序&quot;] --&gt; B[&quot;遍历每个区间&quot;]    B --&gt; C{&quot;起点 ≤ 末区间终点?&quot;}    C -- 是 --&gt; D[&quot;合并: 终点取 max&quot;]    C -- 否 --&gt; E[&quot;新开一段&quot;]    D --&gt; B    E --&gt; B</div><pre><code class="language-java">class Solution {    public int[][] merge(int[][] intervals) {        Arrays.sort(intervals, (a, b) -&gt; a[0] - b[0]);        List&lt;int[]&gt; res = new ArrayList&lt;&gt;();        for (int[] iv : intervals) {            if (!res.isEmpty() &amp;&amp; iv[0] &lt;= res.get(res.size() - 1)[1]) {                res.get(res.size() - 1)[1] = Math.max(res.get(res.size() - 1)[1], iv[1]);            } else {                res.add(iv);            }        }        return res.toArray(new int[0][]);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n)（排序），空间 O(排序栈)。</p></blockquote><blockquote><p>[!warning] 易错点<br />合并时终点必须取 <code>max</code>，不能直接覆盖。反例：<code>[1,5]</code> 与 <code>[2,3]</code>，覆盖成 <code>[1,3]</code> 就错了。</p></blockquote><h2 id="189-轮转数组">189. 轮转数组</h2><blockquote><p>数组右轮转 <code>k</code> 步，原地。</p></blockquote><p><strong>思路脉络</strong>：三种做法。</p><p><strong>额外数组</strong>：<code>res[(i+k)%n] = nums[i]</code>，再拷回。O(n) 空间，最稳。<br /><strong>环状替换</strong>：从下标 0 出发，沿 <code>(i+k)%n</code> 链把元素逐个送到目标位置，回到起点换下一条链。需用 <code>gcd(n,k)</code> 算链数。O(1) 空间但容易写错。<br /><strong>三次反转（推荐）</strong>：整体反转 → 反转前 <code>k</code> 个 → 反转后 <code>n-k</code> 个。数学上恰好等价于右轮转 <code>k</code>，且代码极简。</p><blockquote><p>[!example] 三次反转推演（动图）<br /><code>[1,2,3,4,5,6,7]</code> k=3：整体反转让后 k 个挪到前面（倒序），再分别反转前后两段恢复内部顺序，得到 <code>[5,6,7,1,2,3,4]</code>。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0189-rotate.gif" alt="0189-rotate" /></p></blockquote><div class="mermaid">graph LR    A[&quot;k %= n&quot;] --&gt; B[&quot;反转 0..n-1&quot;]    B --&gt; C[&quot;反转 0..k-1&quot;]    C --&gt; D[&quot;反转 k..n-1&quot;]</div><pre><code class="language-java">class Solution {    public void rotate(int[] nums, int k) {        int n = nums.length;        k %= n;                          // k 可能大于 n        reverse(nums, 0, n - 1);        reverse(nums, 0, k - 1);        reverse(nums, k, n - 1);    }    void reverse(int[] a, int l, int r) {        while (l &lt; r) { int t = a[l]; a[l] = a[r]; a[r] = t; l++; r--; }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />忘记 <code>k %= n</code> 会导致越界或重复转。空数组、<code>k=0</code> 要先处理（上面的写法对 <code>k=0</code> 也成立，反转 0 个元素是空操作）。</p></blockquote><h2 id="238-除自身以外数组的乘积">238. 除自身以外数组的乘积</h2><blockquote><p>返回每个位置上&quot;除自身外其他元素乘积&quot;，不能用除法，O(n)。</p></blockquote><p><strong>思路脉络</strong>：不用除法就只能拆成左右两半乘积。<code>res[i] = 左侧所有元素乘积 × 右侧所有元素乘积</code>。两趟扫描：先从左累乘把左侧乘积填进 <code>res</code>，再从右用一个变量累乘右侧乘积乘进去。</p><div class="mermaid">graph LR    A[&quot;第一趟: 左到右&quot;] --&gt; B[&quot;res[i] = res[i-1]*nums[i-1]&quot;]    B --&gt; C[&quot;res 存好左侧乘积&quot;]    C --&gt; D[&quot;第二趟: 右到左&quot;]    D --&gt; E[&quot;res[i] *= right; right *= nums[i]&quot;]    E --&gt; F[&quot;res 存最终结果&quot;]</div><pre><code class="language-java">class Solution {    public int[] productExceptSelf(int[] nums) {        int n = nums.length;        int[] res = new int[n];        res[0] = 1;        for (int i = 1; i &lt; n; i++) res[i] = res[i - 1] * nums[i - 1]; // 左乘积        int right = 1;        for (int i = n - 1; i &gt;= 0; i--) {            res[i] *= right;     // 乘上右侧乘积            right *= nums[i];    // 更新右侧乘积        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)（结果数组不计额外空间）。</p></blockquote><blockquote><p>[!tip] 套路<br />&quot;不能用除法求两侧乘积&quot;标准两趟扫描。这个&quot;前缀积 + 后缀积&quot;的思想和 [[#除自身以外数组的乘积|前缀和]] 是一族，区别在一用积一用和。</p></blockquote><h2 id="41-缺失的第一个正数">41. 缺失的第一个正数</h2><blockquote><p>找数组中未出现的最小正整数。要求 O(n) 时间 O(1) 空间。</p></blockquote><p><strong>思路脉络</strong>：O(n) 时间暗示哈希，O(1) 空间又不能用额外哈希表——那就<strong>把数组本身当哈希表</strong>。</p><p>观察：答案一定在 <code>[1, n+1]</code>（<code>n</code> 是长度）。若数组含 <code>1..n</code> 全部，答案是 <code>n+1</code>；否则缺的那个在 <code>[1,n]</code> 内。所以只关心 <code>[1,n]</code> 范围内的数，把它们归位到&quot;下标 = 值-1&quot;的位置。归位后再扫一遍，第一个 <code>nums[i] != i+1</code> 的 <code>i+1</code> 即答案。</p><p>归位用 <code>while</code> 循环：交换后新来的元素可能也该归位，要持续交换直到当前位置的值不在范围或已在正确位置。</p><blockquote><p>[!example] 原地哈希归位（动图）<br /><code>[3,4,-1,1]</code>：把每个 [1,n] 内的值 v 交换到下标 v-1 处，[1,n] 外的（如 -1）忽略。归位后扫一遍，第一个 <code>nums[i]≠i+1</code> 的 <code>i+1</code> 即答案（本例 index1=-1≠2，答案 2）。<code>while</code> 持续交换是关键。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0041-first-missing.gif" alt="0041-first-missing" /></p></blockquote><div class="mermaid">graph LR    A[&quot;遍历每个位置&quot;] --&gt; B{&quot;nums[i] 在 [1,n] 且未归位?&quot;}    B -- 是 --&gt; C[&quot;交换到 nums[i]-1 位置&quot;]    C --&gt; B    B -- 否 --&gt; D[&quot;i++&quot;]    D --&gt; E[&quot;扫一遍找 nums[i]≠i+1&quot;]    E --&gt; F[&quot;返回 i+1, 否则 n+1&quot;]</div><pre><code class="language-java">class Solution {    public int firstMissingPositive(int[] nums) {        int n = nums.length;        for (int i = 0; i &lt; n; i++) {            while (1 &lt;= nums[i] &amp;&amp; nums[i] &lt;= n &amp;&amp; nums[i] != nums[nums[i] - 1]) {                int t = nums[nums[i] - 1];      // 先存目标位原值                nums[nums[i] - 1] = nums[i];    // 归位                nums[i] = t;                    // 换回来的继续判断            }        }        for (int i = 0; i &lt; n; i++) if (nums[i] != i + 1) return i + 1;        return n + 1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)（每个元素最多被交换到正确位置一次），空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />交换顺序坑死人不偿命：必须先存 <code>nums[nums[i]-1]</code> 到临时变量，再赋值 <code>nums[nums[i]-1] = nums[i]</code>，最后 <code>nums[i] = t</code>。若先写 <code>nums[i] = nums[nums[i]-1]</code>，则 <code>nums[i]</code> 已变，后续 <code>nums[i]-1</code> 这个索引就错了。<code>while</code> 条件里 <code>nums[i] != nums[nums[i]-1]</code> 防止重复值死循环。</p></blockquote><blockquote><p>[!tip] 关联<br />这是&quot;原地哈希&quot;的经典。同族的还有 [[#287. 寻找重复数]]（下标原地标记）、[[#442. 数组中重复的数据]]。套路都是：<strong>用下标承载信息，用值定位下标</strong>。</p></blockquote><hr /><h1 id="6-矩阵">6. 矩阵</h1><p>矩阵题集中在<strong>边界模拟</strong>和<strong>坐标变换</strong>。关键是要找到能简化判断的&quot;锚点&quot;——比如搜索二维矩阵的右上角、旋转图像的转置分解。</p><h2 id="73-矩阵置零">73. 矩阵置零</h2><blockquote><p>若 <code>matrix[i][j]==0</code>，把它所在的行和列全置零。原地。</p></blockquote><p><strong>思路脉络</strong>：朴素是开两个集合记录哪些行哪些列要置零，O(m+n) 空间。要 O(1) 空间，就把这个记录任务交给<strong>第一行和第一列</strong>本身：用 <code>matrix[i][0]</code> 标记第 <code>i</code> 行是否置零，<code>matrix[0][j]</code> 标记第 <code>j</code> 列是否置零。</p><p>但第一行/第一列自己也可能要置零，得用两个额外布尔变量单独记它们，否则会被自己的标记污染。处理顺序：先标记 → 再据标记清内部 → 最后清首行首列。</p><div class="mermaid">graph LR    A[&quot;两个布尔标记首行首列是否含0&quot;] --&gt; B[&quot;遍历内部, 用首行首列记0位&quot;]    B --&gt; C[&quot;据首行首列标记清内部&quot;]    C --&gt; D[&quot;最后据布尔标记清首行首列&quot;]</div><pre><code class="language-java">class Solution {    public void setZeroes(int[][] matrix) {        int m = matrix.length, n = matrix[0].length;        boolean firstRow = false, firstCol = false;        for (int j = 0; j &lt; n; j++) if (matrix[0][j] == 0) firstRow = true;        for (int i = 0; i &lt; m; i++) if (matrix[i][0] == 0) firstCol = true;        for (int i = 1; i &lt; m; i++)            for (int j = 1; j &lt; n; j++)                if (matrix[i][j] == 0) { matrix[i][0] = 0; matrix[0][j] = 0; }        for (int i = 1; i &lt; m; i++)            for (int j = 1; j &lt; n; j++)                if (matrix[i][0] == 0 || matrix[0][j] == 0) matrix[i][j] = 0;        if (firstRow) for (int j = 0; j &lt; n; j++) matrix[0][j] = 0;        if (firstCol) for (int i = 0; i &lt; m; i++) matrix[i][0] = 0;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />顺序不能乱：必须<strong>最后</strong>处理首行首列。若先清了首行，再用它当标记去清内部就全错了。</p></blockquote><h2 id="54-螺旋矩阵">54. 螺旋矩阵</h2><blockquote><p>按顺时针螺旋顺序返回矩阵所有元素。</p></blockquote><p><strong>思路脉络</strong>：维护 <code>top/bottom/left/right</code> 四个边界，按&quot;上→右→下→左&quot;四条边遍历，每遍历完一条就收缩对应边界。难点在最后一圈可能只剩一行或一列，要避免重复遍历——所以后两条边遍历前先判断边界是否还交叉。</p><blockquote><p>[!example] 四边界螺旋收缩（动图）<br />3×3 矩阵顺时针走边：→顶行后 top++、↓右列后 right--、←底行后 bottom--、↑左列后 left++，边界交叉即停。后两条边前判 <code>top≤bottom</code>/<code>left≤right</code> 防单行单列重复。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0054-spiral-matrix.gif" alt="0054-spiral-matrix" /></p></blockquote><div class="mermaid">graph LR    A[&quot;top/bottom/left/right 四边界&quot;] --&gt; B[&quot;→遍历顶行, top++&quot;]    B --&gt; C[&quot;↓遍历右列, right--&quot;]    C --&gt; D{&quot;top≤bottom? →遍历底行, bottom--&quot;}    D --&gt; E{&quot;left≤right? ↑遍历左列, left++&quot;}    E --&gt; F{&quot;边界未交叉?&quot;}    F -- 是 --&gt; B    F -- 否 --&gt; G[&quot;结束&quot;]</div><pre><code class="language-java">class Solution {    public List&lt;Integer&gt; spiralOrder(int[][] matrix) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        int top = 0, bottom = matrix.length - 1, left = 0, right = matrix[0].length - 1;        while (top &lt;= bottom &amp;&amp; left &lt;= right) {            for (int j = left; j &lt;= right; j++) res.add(matrix[top][j]); top++;            for (int i = top; i &lt;= bottom; i++) res.add(matrix[i][right]); right--;            if (top &lt;= bottom) { for (int j = right; j &gt;= left; j--) res.add(matrix[bottom][j]); bottom--; }            if (left &lt;= right) { for (int i = bottom; i &gt;= top; i--) res.add(matrix[i][left]); left++; }        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(1)（不计结果）。</p></blockquote><blockquote><p>[!warning] 易错点<br />后两条边（底行、左列）的遍历前必须检查 <code>top&lt;=bottom</code>、<code>left&lt;=right</code>，否则在只剩一行/一列时会重复添加。</p></blockquote><h2 id="48-旋转图像">48. 旋转图像</h2><blockquote><p><code>n×n</code> 矩阵顺时针旋转 90°，原地。</p></blockquote><p><strong>思路脉络</strong>：直接找坐标映射——顺时针 90° 把 <code>(i,j)</code> 移到 <code>(j, n-1-i)</code>。原地四点轮换能做，但要分组、易错。</p><p>更优雅的分解：<strong>顺时针 90° = 主对角线转置 + 左右翻转</strong>。</p><ul><li>转置：<code>(i,j)→(j,i)</code></li><li>左右翻转：<code>(j,i)→(j, n-1-i)</code></li><li>合起来 <code>(i,j)→(j, n-1-i)</code>，正是顺时针 90°。</li></ul><p>两步都用现成的原地操作，代码极简。</p><blockquote><p>[!example] 转置 + 行翻转（动图）<br />3×3 矩阵：先沿主对角线转置（行列互换），再每行左右翻转，合起来正是顺时针 90°。两步都是原地 O(n²) O(1)。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0048-rotate-image.gif" alt="0048-rotate-image" /></p></blockquote><div class="mermaid">graph LR    A[&quot;原矩阵&quot;] --&gt; B[&quot;沿主对角线转置&quot;]    B --&gt; C[&quot;每行左右翻转&quot;]    C --&gt; D[&quot;顺时针90°结果&quot;]</div><pre><code class="language-java">class Solution {    public void rotate(int[][] matrix) {        int n = matrix.length;        for (int i = 0; i &lt; n; i++)            // 转置            for (int j = i + 1; j &lt; n; j++) {                int t = matrix[i][j]; matrix[i][j] = matrix[j][i]; matrix[j][i] = t;            }        for (int[] row : matrix) {            // 每行翻转            int l = 0, r = n - 1;            while (l &lt; r) { int t = row[l]; row[l] = row[r]; row[r] = t; l++; r--; }        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 记忆<br />四种旋转的分解：</p><ul><li>顺时针 90°：转置 + 行翻转</li><li>逆时针 90°：转置 + 列翻转</li><li>180°：行翻转 + 列翻转<br />转置只遍历主对角线上方（<code>j&gt;i</code>），否则会交换两次回到原样。</li></ul></blockquote><h2 id="240-搜索二维矩阵-ii">240. 搜索二维矩阵 II</h2><blockquote><p><code>m×n</code> 矩阵，每行从左到右递增、每列从上到下递增。判断 <code>target</code> 是否存在。</p></blockquote><p><strong>思路脉络</strong>：暴力 O(mn)。逐行二分 O(m log n)。但有个更妙的 O(m+n)——从<strong>右上角</strong>出发。</p><p>右上角的妙处：往左是变小、往下是变大，两个方向<strong>单调性相反</strong>，每次比较能确定排除一行或一列。左上角不行（往右往下都变大，无法决策），右下角同理（都变小）。所以锚点必须选右上或左下。</p><blockquote><p>[!example] 右上角 Z 字查找（动图）<br />target=5：从右上角 15 出发，当前值大于 target 就左移（排除当前列下方），小于 target 就下移（排除当前行左侧），每步排除一行或一列，O(m+n) 命中。锚点选右上是因为两方向单调性相反、可决策。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0240-search-matrix2.gif" alt="0240-search-matrix2" /></p></blockquote><div class="mermaid">graph LR    A[&quot;从右上角出发&quot;] --&gt; B{&quot;当前值 ? target&quot;}    B -- &quot;相等&quot; --&gt; C[&quot;返回 true&quot;]    B -- &quot;更大&quot; --&gt; D[&quot;左移 j--&quot;]    B -- &quot;更小&quot; --&gt; E[&quot;下移 i++&quot;]    D --&gt; F{&quot;越界?&quot;}    E --&gt; F    F -- 否 --&gt; B    F -- 是 --&gt; G[&quot;返回 false&quot;]</div><pre><code class="language-java">class Solution {    public boolean searchMatrix(int[][] matrix, int target) {        int m = matrix.length, n = matrix[0].length;        int i = 0, j = n - 1;                       // 右上角        while (i &lt; m &amp;&amp; j &gt;= 0) {            if (matrix[i][j] == target) return true;            else if (matrix[i][j] &gt; target) j--;    // 太大, 这一列下面更大, 排除整列            else i++;                               // 太小, 这一行左边更小, 排除整行        }        return false;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(m+n)（每次排除一行或一列），空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点<br />起点选错就废。能作为决策锚点的前提是&quot;两个方向单调性相反&quot;，只有右上、左下两个角满足。选左上会陷入&quot;两边都可能&quot;的困境。</p></blockquote><hr /><h1 id="7-链表">7. 链表</h1><p>链表题的通用心法有三条：</p><ol><li><strong>哨兵节点（dummy）</strong>：在头前加一个假节点，统一处理&quot;头被改&quot;的情况，省掉 <code>if (head == ...)</code> 特判。链表题 80% 都该用它。</li><li><strong>快慢指针</strong>：找中点、找环、找倒数第 N 个，全靠两个指针步长差。</li><li><strong>画图</strong>：链表题最忌凭脑内想象改指针，一定画图看清 <code>next</code> 指向再动手。</li></ol><h2 id="160-相交链表">160. 相交链表</h2><blockquote><p>两个单链表在某节点开始共享后续节点，找这个交点。</p></blockquote><p><strong>思路脉络</strong>：两链表长度不同，对齐起点是关键。让两个指针走对方的路径：<code>pA</code> 走完 A 再走 B 的头，<code>pB</code> 走完 B 再走 A 的头。两者走过的总长度都是 <code>a+b</code>，长度差被抹平，若有交点必在某一刻相遇于交点；若无交点，两者同时走到 <code>null</code> 退出。</p><div class="mermaid">graph LR    A[&quot;pA 走 A, pB 走 B&quot;] --&gt; B[&quot;pA 到尾 → 转向 B 头&quot;]    B --&gt; C[&quot;pB 到尾 → 转向 A 头&quot;]    C --&gt; D[&quot;同速继续&quot;]    D --&gt; E{&quot;pA == pB?&quot;}    E -- 是 --&gt; F[&quot;返回交点&quot;]    E -- 否 --&gt; D</div><pre><code class="language-java">public class Solution {    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {        if (headA == null || headB == null) return null;        ListNode pA = headA, pB = headB;        while (pA != pB) {            pA = pA == null ? headB : pA.next; // 到尾就跳到另一条的头            pB = pB == null ? headA : pB.next;        }        return pA; // 相交则交点, 不相交则同时到 null    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(m+n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 关键<br />用 <code>p == null</code> 而非 <code>p.next == null</code> 判断&quot;到尾&quot;——这样不相交时两指针都会各自走完 a+b 到达 null 而退出，不会死循环。</p></blockquote><h2 id="206-反转链表">206. 反转链表</h2><blockquote><p>反转单链表。</p></blockquote><p><strong>思路脉络</strong>：迭代法用 <code>prev/cur</code> 双指针，逐个把 <code>cur.next</code> 指向 <code>prev</code>，再整体右移。注意先存 <code>next</code> 再改指针，否则丢链。</p><pre><code class="language-java">class Solution {    public ListNode reverseList(ListNode head) {        ListNode prev = null, cur = head;        while (cur != null) {            ListNode next = cur.next; // 先存            cur.next = prev;           // 反转指向            prev = cur;                // 右移            cur = next;        }        return prev; // 新头是原尾    }}</code></pre><p>递归版思路：<code>reverse(head)</code> 返回反转后的新头，并让 <code>head.next.next = head</code> 把自己接到尾部。面试常考递归写法，体会&quot;反过来的视角&quot;。</p><pre><code class="language-java">class Solution {    public ListNode reverseList(ListNode head) {        if (head == null || head.next == null) return head;        ListNode newHead = reverseList(head.next);        head.next.next = head; // 后继指回自己        head.next = null;      // 断开原方向        return newHead;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间迭代 O(1) / 递归 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点<br /><code>head.next.next = head</code> 这行是递归版的灵魂，理解为&quot;让我的后继指向我&quot;。漏了 <code>head.next = null</code> 会成环。</p></blockquote><h2 id="234-回文链表">234. 回文链表</h2><blockquote><p>判断链表是否回文。进阶 O(n) 时间 O(1) 空间。</p></blockquote><p><strong>思路脉络</strong>：最稳的 O(n) 空间做法是复制到数组双指针比对。要 O(1) 空间：用快慢指针找中点 → 反转后半段 → 前后两半逐个比较 → （可选）反转恢复。</p><p>快慢找中点：<code>fast</code> 走两步、<code>slow</code> 走一步，<code>fast</code> 到尾时 <code>slow</code> 在中点。奇数长度 <code>slow</code> 落在正中，后半段从 <code>slow.next</code> 开始反转更对称。</p><div class="mermaid">graph LR    A[&quot;快慢找中点 slow&quot;] --&gt; B[&quot;反转 slow 之后半段&quot;]    B --&gt; C[&quot;前半 vs 反转后半 逐个比&quot;]    C --&gt; D{&quot;全相等?&quot;}    D -- 是 --&gt; E[&quot;回文&quot;]    D -- 否 --&gt; F[&quot;非回文&quot;]</div><pre><code class="language-java">class Solution {    public boolean isPalindrome(ListNode head) {        ListNode slow = head, fast = head;        while (fast != null &amp;&amp; fast.next != null) {            slow = slow.next; fast = fast.next.next;        }        ListNode second = reverse(slow); // 反转后半        ListNode p1 = head, p2 = second;        while (p2 != null) {            if (p1.val != p2.val) return false;            p1 = p1.next; p2 = p2.next;        }        return true;    }    ListNode reverse(ListNode h) {        ListNode prev = null, cur = h;        while (cur != null) { ListNode nx = cur.next; cur.next = prev; prev = cur; cur = nx; }        return prev;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>比较循环用 <code>p2 != null</code> 而非 <code>p1 != null</code>：后半段反转后更短（奇数长度时中点不计），用短的作终止条件。否则 <code>p1</code> 会多走一个中点节点。</li></ul><h2 id="141-环形链表">141. 环形链表</h2><blockquote><p>判断链表是否有环。</p></blockquote><p><strong>思路脉络</strong>：快慢指针，<code>fast</code> 每次两步、<code>slow</code> 每次一步。若有环，<code>fast</code> 终会追上 <code>slow</code>（在环里套圈）；若无环，<code>fast</code> 先到 <code>null</code>。</p><pre><code class="language-java">public class Solution {    public boolean hasCycle(ListNode head) {        ListNode slow = head, fast = head;        while (fast != null &amp;&amp; fast.next != null) {            slow = slow.next;            fast = fast.next.next;            if (slow == fast) return true;        }        return false;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>循环条件 <code>fast != null &amp;&amp; fast.next != null</code>，缺 <code>fast.next</code> 会在 <code>fast</code> 是尾节点时 <code>fast.next.next</code> 空指针。<code>slow/fast</code> 同起点，先走再比，避免一开始就相等。</li></ul><h2 id="142-环形链表-ii">142. 环形链表 II</h2><blockquote><p>找到环的入口节点。无环返回 null。</p></blockquote><p><strong>思路脉络</strong>：[[#141. 环形链表]] 找&quot;有没有环&quot;，这题找&quot;入口在哪&quot;。复用快慢相遇，再加一段数学。</p><p>设：<code>a</code> = 头到入口距离，<code>b</code> = 入口到相遇点距离，<code>c</code> = 相遇点到入口距离（环长 = <code>b+c</code>）。</p><ul><li>慢指针走 <code>a+b</code>，快指针走 <code>a + b + n(b+c)</code>（多绕 n 圈，n≥1）。</li><li>快是慢两倍：<code>2(a+b) = a + b + n(b+c)</code> → <code>a = (n-1)(b+c) + c</code>。</li></ul><p>意思是：<strong>从头走 <code>a</code> 步 = 从相遇点在环里走 <code>c</code> 步</strong>，两者都恰好到入口。所以让一个指针回 <code>head</code>，两者同速前进，再次相遇即入口。</p><blockquote><p>[!example] 快慢相遇 → 回头同速找入口（动图）<br /><code>3→2→0→-4</code>（-4 指回 2）：快慢在下标2(值0)相遇 → 一指针回 head → 同速前进在下标1(值2)再次相遇，即环入口。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0142-cycle-list2.gif" alt="0142-cycle-list2" /></p></blockquote><div class="mermaid">graph LR    A[&quot;快慢相遇于 meet&quot;] --&gt; B[&quot;一个指针回 head&quot;]    B --&gt; C[&quot;两指针同速前进&quot;]    C --&gt; D[&quot;再次相遇 = 环入口&quot;]</div><pre><code class="language-java">public class Solution {    public ListNode detectCycle(ListNode head) {        ListNode slow = head, fast = head;        while (fast != null &amp;&amp; fast.next != null) {            slow = slow.next; fast = fast.next.next;            if (slow == fast) {                // 相遇                ListNode p = head;                while (p != slow) { p = p.next; slow = slow.next; }                return p;                      // 入口            }        }        return null;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 记忆</p></blockquote><ul><li>相遇后&quot;一个回起点同速再走&quot;是关键动作。背下 <code>a = (n-1)(b+c) + c</code> 这步推导，面试常被追问。</li></ul><h2 id="21-合并两个有序链表">21. 合并两个有序链表</h2><blockquote><p>合并两个升序链表。</p></blockquote><p><strong>思路脉络</strong>：经典归并的链表版。用 dummy 哨兵，逐个比较小的接上，剩余的直接挂上。</p><pre><code class="language-java">class Solution {    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {        ListNode dummy = new ListNode(0), cur = dummy;        while (l1 != null &amp;&amp; l2 != null) {            if (l1.val &lt;= l2.val) { cur.next = l1; l1 = l1.next; }            else { cur.next = l2; l2 = l2.next; }            cur = cur.next;        }        cur.next = l1 != null ? l1 : l2; // 剩余直接接上        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(m+n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>哨兵 <code>dummy</code> 是链表归并/构造类题的标配，省去头节点的空判。这题是 [[#23. 合并 K 个升序链表]] 的子问题。</li></ul><h2 id="2-两数相加">2. 两数相加</h2><blockquote><p>两个链表逆序存两个非负整数（头是低位），返回和的链表。</p></blockquote><p><strong>思路脉络</strong>：模拟手工加法，从低位（头）开始逐位相加，维护进位 <code>carry</code>。循环条件要把 <code>carry != 0</code> 也算进去，否则最高位的进位会漏。</p><pre><code class="language-java">class Solution {    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {        ListNode dummy = new ListNode(0), cur = dummy;        int carry = 0;        while (l1 != null || l2 != null || carry != 0) {            int sum = carry;            if (l1 != null) { sum += l1.val; l1 = l1.next; }            if (l2 != null) { sum += l2.val; l2 = l2.next; }            carry = sum / 10;            cur.next = new ListNode(sum % 10);            cur = cur.next;        }        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(max(m,n))，空间 O(max(m,n))。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>循环条件漏 <code>carry != 0</code>：形如 <code>5+5=10</code>，最后会多出一个进位节点 <code>0→1</code>，漏了就少一位。</li></ul><h2 id="19-删除链表的倒数第-n-个结点">19. 删除链表的倒数第 N 个结点</h2><blockquote><p>删除倒数第 N 个节点，要求一次遍历。</p></blockquote><p><strong>思路脉络</strong>：找倒数第 N 个，朴素是先数长度再走 <code>len-n</code>。一次遍历做法：快慢指针，<code>fast</code> 先走 <code>n</code> 步，再和 <code>slow</code> 同速，<code>fast</code> 到尾时 <code>slow</code> 恰在倒数第 N+1 个（待删的前驱）。用 dummy 是为了处理&quot;删头&quot;这种边界——否则删头时找不到前驱。</p><div class="mermaid">graph LR    A[&quot;fast 先走 n 步&quot;] --&gt; B[&quot;fast/slow 同速前进&quot;]    B --&gt; C{&quot;fast.next == null?&quot;}    C -- 是 --&gt; D[&quot;slow 在待删前驱&quot;]    D --&gt; E[&quot;slow.next = slow.next.next&quot;]    C -- 否 --&gt; B</div><pre><code class="language-java">class Solution {    public ListNode removeNthFromEnd(ListNode head, int n) {        ListNode dummy = new ListNode(0, head);        ListNode fast = dummy, slow = dummy;        for (int i = 0; i &lt; n; i++) fast = fast.next;     // fast 先走 n 步        while (fast.next != null) { fast = fast.next; slow = slow.next; }        slow.next = slow.next.next;                       // 删除        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>不加 <code>dummy</code>，删头（<code>n == 长度</code>）时 <code>slow</code> 是 null，无法 <code>slow.next</code>。<code>dummy</code> 把所有删除统一成&quot;删某个中间节点&quot;。</li></ul><h2 id="24-两两交换链表中的节点">24. 两两交换链表中的节点</h2><blockquote><p>两两交换相邻节点，如 <code>1→2→3→4</code> 变 <code>2→1→4→3</code>。</p></blockquote><p><strong>思路脉络</strong>：迭代法，用 <code>cur</code> 指向待交换对的前驱。每轮处理 <code>a=cur.next</code>、<code>b=a.next</code>，三步改指针：<code>a.next=b.next</code>、<code>b.next=a</code>、<code>cur.next=b</code>，然后 <code>cur=a</code> 推进。</p><pre><code class="language-java">class Solution {    public ListNode swapPairs(ListNode head) {        ListNode dummy = new ListNode(0, head), cur = dummy;        while (cur.next != null &amp;&amp; cur.next.next != null) {            ListNode a = cur.next, b = a.next;            a.next = b.next;            b.next = a;            cur.next = b;            cur = a; // a 现在是这对的尾, 作为下一对的前驱        }        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>链表&quot;改指针&quot;题的万能姿势：用 <code>cur</code> 作前驱，每轮只看 <code>cur.next</code> 和 <code>cur.next.next</code> 两个节点，改完 <code>cur</code> 推进。和 [[#25. K 个一组翻转链表]] 同构。</li></ul><h2 id="25-k-个一组翻转链表">25. K 个一组翻转链表</h2><blockquote><p>每 <code>k</code> 个节点一组翻转，不足 <code>k</code> 个保持原样。</p></blockquote><p><strong>思路脉络</strong>：[[#206. 反转链表]] + [[#24. 两两交换链表中的节点]] 的组合。外层按 <code>k</code> 分组，每组用区间反转，再把翻转后的头尾接回主链。</p><p>关键工具：一个 <code>reverse(head, tail)</code> 反转 <code>[head, tail]</code> 闭区间并返回 <code>[新头, 新尾]</code>。实现技巧是让 <code>prev</code> 从 <code>tail.next</code> 开始——这样反转后 <code>head</code>（变新尾）的 next 自动指向 <code>tail.next</code>，省去额外处理。</p><blockquote><p>[!example] 分组反转接回（动图）<br /><code>1→2→3→4→5</code> k=2：每组数 k 个定 tail，区间反转后头尾接回主链，不足 k 保持原样。最终 <code>2→1→4→3→5</code>。<code>reverse</code> 里 <code>prev</code> 从 <code>tail.next</code> 起是省处理的关键。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0025-reverse-kgroup.gif" alt="0025-reverse-kgroup" /></p></blockquote><div class="mermaid">graph LR    A[&quot;pre=dummy&quot;] --&gt; B[&quot;数 k 个找 tail&quot;]    B --&gt; C{&quot;不足 k?&quot;}    C -- 是 --&gt; D[&quot;返回 dummy.next&quot;]    C -- 否 --&gt; E[&quot;记 nxt=tail.next&quot;]    E --&gt; F[&quot;反转 head..tail&quot;]    F --&gt; G[&quot;pre.next=新头, 新尾.next=nxt&quot;]    G --&gt; H[&quot;pre=新尾, head=nxt&quot;]    H --&gt; B</div><pre><code class="language-java">class Solution {    public ListNode reverseKGroup(ListNode head, int k) {        ListNode dummy = new ListNode(0, head), pre = dummy;        while (head != null) {            ListNode tail = pre;            for (int i = 0; i &lt; k; i++) {                tail = tail.next;                if (tail == null) return dummy.next; // 不足 k, 不翻            }            ListNode nxt = tail.next;            ListNode[] rev = reverse(head, tail); // rev[0]=新头, rev[1]=新尾            head = rev[0]; tail = rev[1];            pre.next = head;   // 接回头            tail.next = nxt;   // 接回尾            pre = tail;            head = nxt;        }        return dummy.next;    }    ListNode[] reverse(ListNode head, ListNode tail) {        ListNode prev = tail.next, cur = head; // prev 从 tail.next 起        while (prev != tail) {            ListNode next = cur.next;            cur.next = prev;            prev = cur;            cur = next;        }        return new ListNode[]{tail, head}; // 翻转后 tail 是头, head 是尾    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>reverse</code> 里 <code>prev</code> 从 <code>tail.next</code> 开始而非 <code>null</code>，是为了让翻转后 <code>head.next</code> 直接指向原 <code>tail.next</code>，省一步。这是这题最妙的细节，想通了就不易错。</li><li><code>tail == null</code> 时直接返回，<strong>不能</strong>把剩余不足 k 的也翻转——题意要求保持原样。</li></ul><h2 id="138-随机链表的复制">138. 随机链表的复制</h2><blockquote><p>链表节点有 <code>next</code> 和 <code>random</code> 指针，深拷贝整个链表。</p></blockquote><p><strong>思路脉络</strong>：难点在 <code>random</code> 指向任意位置，复制时要建立&quot;旧节点→新节点&quot;的映射。哈希法两遍扫描：第一遍建所有新节点存 map，第二遍据旧节点的 <code>next</code>/<code>random</code> 查 map 连新节点。</p><pre><code class="language-java">class Solution {    public Node copyRandomList(Node head) {        if (head == null) return null;        Map&lt;Node, Node&gt; map = new HashMap&lt;&gt;();        for (Node p = head; p != null; p = p.next) map.put(p, new Node(p.val));        for (Node p = head; p != null; p = p.next) {            map.get(p).next = map.get(p.next);       // null 也安全: map.get(null)=null            map.get(p).random = map.get(p.random);        }        return map.get(head);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 进阶</p></blockquote><ul><li>O(1) 空间做法：原地把每个新节点插在旧节点后（<code>A→A'→B→B'</code>），再设 <code>random</code>，最后拆链。但哈希法更易写易读，面试首选。</li></ul><h2 id="148-排序链表">148. 排序链表</h2><blockquote><p>链表排序，O(n log n) 时间 O(1) 空间（递归栈不算）。</p></blockquote><p><strong>思路脉络</strong>：链表不支持随机访问，快排不友好。<strong>归并排序</strong>完美适配：找中点（快慢指针）→ 断开 → 递归排两半 → 合并（[[#21. 合并两个有序链表]]）。</p><p>找中点细节：<code>fast</code> 从 <code>head.next</code> 起步（而非 <code>head</code>），让 <code>slow</code> 落在前半末尾，方便断开。奇数长度时前半多一个，不影响归并。</p><div class="mermaid">graph LR    A[&quot;快慢找中点 mid&quot;] --&gt; B[&quot;断开: mid.next=null&quot;]    B --&gt; C[&quot;递归排前半&quot;]    B --&gt; D[&quot;递归排后半&quot;]    C --&gt; E[&quot;merge 两半&quot;]    D --&gt; E</div><pre><code class="language-java">class Solution {    public ListNode sortList(ListNode head) {        if (head == null || head.next == null) return head;        ListNode mid = mid(head);        ListNode right = mid.next;        mid.next = null;                          // 断开        return merge(sortList(head), sortList(right));    }    ListNode mid(ListNode head) {        ListNode slow = head, fast = head.next; // fast 先走一步        while (fast != null &amp;&amp; fast.next != null) { slow = slow.next; fast = fast.next.next; }        return slow;    }    ListNode merge(ListNode a, ListNode b) {        ListNode dummy = new ListNode(0), cur = dummy;        while (a != null &amp;&amp; b != null) {            if (a.val &lt;= b.val) { cur.next = a; a = a.next; }            else { cur.next = b; b = b.next; }            cur = cur.next;        }        cur.next = a != null ? a : b;        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n)，空间 O(log n)（递归栈，自底向上迭代可做到 O(1)）。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>不 <code>mid.next = null</code> 断开会死循环——递归时前半仍连着后半。</li><li><code>fast</code> 从 <code>head.next</code> 起：若从 <code>head</code> 起，两个节点时会停在第一个，前半空、死循环。</li></ul><h2 id="23-合并-k-个升序链表">23. 合并 K 个升序链表</h2><blockquote><p>合并 <code>k</code> 个升序链表。</p></blockquote><p><strong>思路脉络</strong>：朴素每次从 <code>k</code> 个头里找最小，O(kn·k)。优化用<strong>小顶堆</strong>：把 <code>k</code> 个头入堆，每次弹出最小接上，并把它的 <code>next</code> 入堆。O(n log k)。</p><pre><code class="language-java">class Solution {    public ListNode mergeKLists(ListNode[] lists) {        if (lists == null || lists.length == 0) return null;        PriorityQueue&lt;ListNode&gt; pq = new PriorityQueue&lt;&gt;((a, b) -&gt; a.val - b.val);        for (ListNode h : lists) if (h != null) pq.offer(h);        ListNode dummy = new ListNode(0), cur = dummy;        while (!pq.isEmpty()) {            ListNode node = pq.poll();            cur.next = node;            cur = cur.next;            if (node.next != null) pq.offer(node.next);        }        return dummy.next;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log k)（n 是总节点数，k 是链表数），空间 O(k)。</p></blockquote><blockquote><p>[!tip] 另解</p></blockquote><ul><li>分治两两合并，也是 O(n log k)，不依赖堆。把 [[#21. 合并两个有序链表]] 当子程序，两两配对合并 k/2 组，再 k/4…直到剩一个。</li></ul><h2 id="146-lru-缓存">146. LRU 缓存</h2><blockquote><p>实现 <code>get</code>/<code>put</code> 均 O(1) 的 LRU 缓存。容量满时淘汰最久未使用。</p></blockquote><p><strong>思路脉络</strong>：O(1) 查找 → 哈希；O(1) 调整顺序 → 双向链表。两者结合：哈希存 <code>key→节点</code>，双向链表维护访问顺序，<strong>最近访问的在头，最久未访问的在尾</strong>。</p><ul><li><code>get</code>：哈希查到节点 → 移到头部。</li><li><code>put</code>：存在则更新值并移头；不存在则新建加头，超容则删尾并从哈希删。</li></ul><p>双向链表用 <code>head</code>/<code>tail</code> 两个 dummy 节点包夹，省去空判。节点要存 <code>key</code>，因为淘汰尾节点时需要用 <code>key</code> 从哈希里删。</p><blockquote><p>[!example] 哈希+双向链表访问序（动图）<br />容量 2：<code>put(1,1)</code>→<code>put(2,2)</code>→<code>get(1)</code> 命中移头→<code>put(3,3)</code> 淘汰尾2→<code>put(4,4)</code> 淘汰尾1。头=最近(MRU)、尾=最久(LRU)，超容删尾。节点必须存 key 才能从哈希删。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0146-lru-cache.gif" alt="0146-lru-cache" /></p></blockquote><div class="mermaid">graph LR    A[&quot;dummy头 ↔ ... ↔ 尾前 ↔ dummy尾&quot;] --&gt; B[&quot;get: 查 map, 命中则 moveToHead&quot;]    B --&gt; C[&quot;put: 存在则改值 moveToHead&quot;]    C --&gt; D[&quot;put: 不存在则新建加头&quot;]    D --&gt; E{&quot;超容?&quot;}    E -- 是 --&gt; F[&quot;删尾, map.remove&quot;]    E -- 否 --&gt; G[&quot;完成&quot;]    F --&gt; G</div><pre><code class="language-java">class LRUCache {    class Node {        int key, val; Node prev, next;        Node(int k, int v) { key = k; val = v; }    }    private final Map&lt;Integer, Node&gt; map = new HashMap&lt;&gt;();    private final int cap;    private final Node head = new Node(0,0), tail = new Node(0,0); // dummy 头尾    public LRUCache(int capacity) {        cap = capacity;        head.next = tail; tail.prev = head;    }    public int get(int key) {        if (!map.containsKey(key)) return -1;        Node n = map.get(key);        moveToHead(n);        return n.val;    }    public void put(int key, int value) {        if (map.containsKey(key)) {            Node n = map.get(key);            n.val = value; moveToHead(n);        } else {            Node n = new Node(key, value);            map.put(key, n); addToHead(n);            if (map.size() &gt; cap) {                Node last = tail.prev;                removeNode(last);                map.remove(last.key); // 节点必须存 key 才能删 map            }        }    }    void addToHead(Node n) {        n.next = head.next; head.next.prev = n;        head.next = n; n.prev = head;    }    void removeNode(Node n) {        n.prev.next = n.next; n.next.prev = n.prev;    }    void moveToHead(Node n) { removeNode(n); addToHead(n); }}</code></pre><blockquote><p>[!info] 复杂度<br /><code>get</code>/<code>put</code> 均 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>节点必须存 <code>key</code>：淘汰尾节点时，需要知道它的 key 才能从哈希删。只存 <code>val</code> 会删不掉哈希项。</li><li>顺序：先 <code>removeNode</code> 再 <code>addToHead</code>，别直接改指针。dummy 头尾让&quot;头是最近、尾是最久&quot;的语义稳定。</li></ul><blockquote><p>[!tip] 关联</p></blockquote><ul><li>这是&quot;哈希 + 双向链表&quot;组合的经典，[[#460. LFU 缓存]] 是进阶版（加频次）。Java 自带的 <code>LinkedHashMap</code>（访问序）几行就能实现 LRU，但面试要求手写双向链表以考察原理。</li></ul><hr /><h1 id="8-二叉树">8. 二叉树</h1><p>树题的灵魂是<strong>递归</strong>。掌握一个通用框架，绝大多数题都能套：</p><blockquote><p>[!tip] 递归三部曲</p><ol><li><strong>边界条件</strong>：<code>root == null</code> 时返回什么。</li><li><strong>单层逻辑</strong>：假设左右子树已经递归出正确结果，当前层怎么用它们算出自己的结果。</li><li><strong>返回值</strong>：把什么向上传递给父节点。</li></ol></blockquote><p>第二步的&quot;假设子树已正确&quot;是关键——别试图展开整棵树，只关心一层。这是分治思想。</p><p>另外要分清两种遍历：</p><ul><li><strong>DFS（递归/栈）</strong>：求深度、路径、祖先类问题。</li><li><strong>BFS（队列层序）</strong>：按层处理、最短步数类问题。</li></ul><h2 id="94-二叉树的中序遍历">94. 二叉树的中序遍历</h2><blockquote><p>返回中序遍历结果。</p></blockquote><p><strong>思路脉络</strong>：递归三行最简。但要会<strong>迭代法</strong>——用显式栈模拟递归，面试常考。</p><p>迭代法精髓：一路向左压栈到底，弹出访问，再转向右子树重复。这模拟了&quot;左→根→右&quot;的访问顺序。</p><div class="mermaid">graph LR    A[&quot;一路向左压栈&quot;] --&gt; B[&quot;到底弹出访问&quot;]    B --&gt; C[&quot;转向右子树&quot;]    C --&gt; A</div><pre><code class="language-java">class Solution {    public List&lt;Integer&gt; inorderTraversal(TreeNode root) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        Deque&lt;TreeNode&gt; st = new ArrayDeque&lt;&gt;();        while (root != null || !st.isEmpty()) {            while (root != null) { st.push(root); root = root.left; } // 左到底            root = st.pop();   // 根            res.add(root.val);            root = root.right; // 右        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>前序迭代：访问放在&quot;压栈时&quot;而非&quot;弹出时&quot;。</li><li>后序迭代：前序的&quot;根左右&quot;改成&quot;根左右&quot;再反转，或用 <code>prev</code> 标记右子是否已访问。</li></ul><h2 id="104-二叉树的最大深度">104. 二叉树的最大深度</h2><blockquote><p>求最大深度。</p></blockquote><p><strong>思路脉络</strong>：递归三部曲最典型的应用。边界 null 返回 0；单层 = <code>max(左深度, 右深度) + 1</code>；返回该层深度。</p><pre><code class="language-java">class Solution {    public int maxDepth(TreeNode root) {        return root == null ? 0             : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!tip] 辨析</p></blockquote><ul><li>&quot;最大深度&quot;=&quot;高度&quot;，从根到最远叶子。这题递归是<strong>后序</strong>（先算左右再算自己）。也可 BFS 层数层数，但递归更简洁。</li></ul><h2 id="226-翻转二叉树">226. 翻转二叉树</h2><blockquote><p>镜像翻转整棵树。</p></blockquote><p><strong>思路脉络</strong>：每个节点交换左右子，递归处理。这题是递归思维的入门——不关心整体怎么翻，只管&quot;我这一层交换左右，剩下交给递归&quot;。</p><pre><code class="language-java">class Solution {    public TreeNode invertTree(TreeNode root) {        if (root == null) return null;        TreeNode t = root.left; root.left = root.right; root.right = t;        invertTree(root.left); invertTree(root.right);        return root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><h2 id="101-对称二叉树">101. 对称二叉树</h2><blockquote><p>判断树是否关于根轴对称。</p></blockquote><p><strong>思路脉络</strong>：不能单独看一棵子树，要<strong>两个指针比</strong>：左子树的左 vs 右子树的右，左子树的右 vs 右子树的左。这是&quot;双指针&quot;思想在树上的迁移。</p><pre><code class="language-java">class Solution {    public boolean isSymmetric(TreeNode root) {        return check(root.left, root.right);    }    boolean check(TreeNode a, TreeNode b) {        if (a == null &amp;&amp; b == null) return true;        if (a == null || b == null) return false; // 一空一非空        return a.val == b.val            &amp;&amp; check(a.left, b.right)    // 镜像侧            &amp;&amp; check(a.right, b.left);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>先判&quot;双 null&quot;再判&quot;单 null&quot;，顺序反了会漏判。这是所有&quot;两树对比&quot;题的标准开头。</li></ul><h2 id="543-二叉树的直径">543. 二叉树的直径</h2><blockquote><p>任意两节点间最长路径的长度（边数）。</p></blockquote><p><strong>思路脉络</strong>：最长路径一定经过某个&quot;最高点&quot;节点，长度 = 该节点左深度 + 右深度（两侧各往下延伸）。后序遍历每个节点，用&quot;左深度+右深度&quot;更新全局答案，返回单边最大深度给上层。</p><p>注意：直径是<strong>边数</strong>，<code>左深度 + 右深度</code> 正好是边数（深度即从该点到子叶的边数）。</p><pre><code class="language-java">class Solution {    int ans = 0;    public int diameterOfBinaryTree(TreeNode root) {        depth(root);        return ans;    }    int depth(TreeNode root) {        if (root == null) return 0;        int l = depth(root.left), r = depth(root.right);        ans = Math.max(ans, l + r);          // 经过当前节点的直径        return Math.max(l, r) + 1;            // 返回单边深度给上层    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>返回给上层的是 <strong>单边最大</strong>（<code>max(l,r)+1</code>），因为路径不能分叉往上走。但<strong>更新答案用两边和</strong>（<code>l+r</code>）。这两个用途要分清。</li></ul><h2 id="102-二叉树的层序遍历">102. 二叉树的层序遍历</h2><blockquote><p>自顶向下一层一层返回节点值。</p></blockquote><p><strong>思路脉络</strong>：BFS 标准模板。关键是<strong>每层开始时记录队列长度 <code>size</code></strong>，正好是这一层的节点数，按这个数循环出队入队，就把层切开了。</p><pre><code class="language-java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; levelOrder(TreeNode root) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Queue&lt;TreeNode&gt; q = new ArrayDeque&lt;&gt;();        q.offer(root);        while (!q.isEmpty()) {            int size = q.size();              // 本层节点数            List&lt;Integer&gt; level = new ArrayList&lt;&gt;();            for (int i = 0; i &lt; size; i++) {                TreeNode n = q.poll();                level.add(n.val);                if (n.left != null) q.offer(n.left);                if (n.right != null) q.offer(n.right);            }            res.add(level);        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>这套 BFS 模板是 [[#199. 二叉树的右视图]]、[[#103. 二叉树的锯齿形层序遍历]]、[[#107. 自底向上的层序遍历]] 的基底，改的只是&quot;每层怎么收集&quot;。</li></ul><h2 id="108-将有序数组转换为二叉搜索树">108. 将有序数组转换为二叉搜索树</h2><blockquote><p>升序数组构造高度平衡 BST。</p></blockquote><p><strong>思路脉络</strong>：BST 中序是升序。要平衡，每次取<strong>中间</strong>元素作根，左半递归造左子，右半造右子。分治。</p><pre><code class="language-java">class Solution {    public TreeNode sortedArrayToBST(int[] nums) {        return build(nums, 0, nums.length - 1);    }    TreeNode build(int[] nums, int l, int r) {        if (l &gt; r) return null;        int m = (l + r) / 2;        TreeNode root = new TreeNode(nums[m]);        root.left = build(nums, l, m - 1);        root.right = build(nums, m + 1, r);        return root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(log n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>左半是 <code>[l, m-1]</code>、右半是 <code>[m+1, r]</code>，<code>m</code> 本身已用作根，别带进去。<code>l &gt; r</code> 是终止（不是 <code>l &gt;= r</code>，否则单元素建不出节点）。</li></ul><h2 id="98-验证二叉搜索树">98. 验证二叉搜索树</h2><blockquote><p>判断是否合法 BST。</p></blockquote><p><strong>思路脉络</strong>：常见误区是只比&quot;左 &lt; 根 &lt; 右&quot;——这是<strong>局部</strong>正确，但 BST 要求<strong>整棵左子树都 &lt; 根</strong>，只比直接子节点不够（如 <code>5 / 4 6 / 3</code>，3 比 6 小但比 5 小也合法的位置错了）。</p><p>正确做法：递归传递<strong>上下界</strong> <code>(lo, hi)</code>，每个节点必须落在 <code>(lo, hi)</code> 内，进入左子收紧上界为当前值，进入右子收紧下界。用 <code>long</code> 避免整型极值越界。</p><div class="mermaid">graph LR    A[&quot;节点 root, 界 (lo,hi)&quot;] --&gt; B{&quot;lo &lt; val &lt; hi?&quot;}    B -- 否 --&gt; C[&quot;返回 false&quot;]    B -- 是 --&gt; D[&quot;左子 (lo, val)&quot;]    D --&gt; E[&quot;右子 (val, hi)&quot;]</div><pre><code class="language-java">class Solution {    public boolean isValidBST(TreeNode root) {        return check(root, Long.MIN_VALUE, Long.MAX_VALUE);    }    boolean check(TreeNode root, long lo, long hi) {        if (root == null) return true;        if (root.val &lt;= lo || root.val &gt;= hi) return false;        return check(root.left, lo, root.val) &amp;&amp; check(root.right, root.val, hi);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!tip] 另解</p></blockquote><ul><li>中序遍历应是严格递增，边遍历边比前驱。也是 O(n)，但上下界法更直观体现 BST 语义。</li></ul><h2 id="230-二叉搜索树中第-k-小的元素">230. 二叉搜索树中第 K 小的元素</h2><blockquote><p>BST 中找第 K 小。</p></blockquote><p><strong>思路脉络</strong>：BST 的中序遍历是升序，第 K 小就是中序第 K 个。递归中序遍历，用一个计数器数到第 K 个即停。比遍历完整棵再取第 K 个更高效——可提前终止。</p><pre><code class="language-java">class Solution {    int k, ans;    public int kthSmallest(TreeNode root, int k) {        this.k = k;        dfs(root);        return ans;    }    void dfs(TreeNode root) {        if (root == null || k == 0) return;  // k==0 表示已找到, 剪枝        dfs(root.left);        if (--k == 0) { ans = root.val; return; }        dfs(root.right);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(h+k)，空间 O(h)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;BST + 第 K 大/小&quot; 一律中序。第 K 大用逆中序（右→根→左）。</li></ul><h2 id="199-二叉树的右视图">199. 二叉树的右视图</h2><blockquote><p>从右侧看树，从上到下返回每层最右可见节点。</p></blockquote><p><strong>思路脉络</strong>：层序遍历，每层最后一个节点就是右视图看到的。复用 [[#102. 二叉树的层序遍历]] 模板，只把&quot;每层最后一个&quot;加入结果。</p><pre><code class="language-java">class Solution {    public List&lt;Integer&gt; rightSideView(TreeNode root) {        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        if (root == null) return res;        Queue&lt;TreeNode&gt; q = new ArrayDeque&lt;&gt;();        q.offer(root);        while (!q.isEmpty()) {            int size = q.size();            for (int i = 0; i &lt; size; i++) {                TreeNode n = q.poll();                if (i == size - 1) res.add(n.val); // 每层最后一个                if (n.left != null) q.offer(n.left);                if (n.right != null) q.offer(n.right);            }        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 另解</p></blockquote><ul><li>DFS 优先走右子，记录&quot;每层第一个被访问的节点&quot;——即该层最右。空间 O(h) 更省，但层序更直观。</li></ul><h2 id="114-二叉树展开为链表">114. 二叉树展开为链表</h2><blockquote><p>按前序遍历把树展开成右链（每个节点只有右孩子）。</p></blockquote><p><strong>思路脉络</strong>：两种经典法。</p><p><strong>递归后序法（右→左→中）</strong>：维护全局 <code>prev</code> 指向上一个处理的节点。后序保证处理当前节点时左右子已展平。把 <code>root.right = prev</code>、<code>root.left = null</code>，再 <code>prev = root</code>。逆前序的访问顺序恰好让链表从尾向头接起来。</p><p><strong>O(1) 空间法</strong>：对每个有左子的节点，把左子树的最右节点接到当前右子前，再把左子整体移到右边、左置空。</p><div class="mermaid">graph LR    A[&quot;后序: 右→左→中&quot;] --&gt; B[&quot;处理 root 时, prev 已是右链头&quot;]    B --&gt; C[&quot;root.right = prev&quot;]    C --&gt; D[&quot;root.left = null&quot;]    D --&gt; E[&quot;prev = root&quot;]</div><pre><code class="language-java">class Solution {    TreeNode prev = null;    public void flatten(TreeNode root) {        if (root == null) return;        flatten(root.right);  // 先右        flatten(root.left);   // 再左        root.right = prev;    // 接上已展平的链        root.left = null;        prev = root;          // 自己成为下一个的前驱    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)（递归栈）。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须先 <code>flatten(right)</code> 再 <code>flatten(left)</code>，否则 <code>prev</code> 顺序错乱。本质是模拟&quot;前序的逆序&quot;：前序是中左右，逆过来是右左中。</li></ul><h2 id="105-从前序与中序遍历序列构造二叉树">105. 从前序与中序遍历序列构造二叉树</h2><blockquote><p>给前序和中序，构造原二叉树。</p></blockquote><p><strong>思路脉络</strong>：前序第一个是根。在中序里找到根的位置，左边是左子树、右边是右子树。递归构造左右子树。</p><p>关键优化：中序里&quot;找根位置&quot;用哈希 O(1)，否则每次扫描 O(n)。前序用一个全局下标 <code>i</code> 顺序取根——因为前序天然是&quot;根→左子所有→右子所有&quot;，按顺序取并与中序的区间对应。</p><div class="mermaid">graph LR    A[&quot;前序取根 preorder[i++]&quot;] --&gt; B[&quot;中序找根位置 m&quot;]    B --&gt; C[&quot;左子: 中序 l..m-1&quot;]    C --&gt; D[&quot;右子: 中序 m+1..r&quot;]    D --&gt; E[&quot;返回 root&quot;]</div><pre><code class="language-java">class Solution {    int[] preorder; int i = 0;    Map&lt;Integer, Integer&gt; idx = new HashMap&lt;&gt;();    public TreeNode buildTree(int[] preorder, int[] inorder) {        this.preorder = preorder;        for (int j = 0; j &lt; inorder.length; j++) idx.put(inorder[j], j);        return build(0, inorder.length - 1);    }    TreeNode build(int l, int r) {        if (l &gt; r) return null;        int val = preorder[i++];            // 顺序取前序根        int m = idx.get(val);                // 中序定位        TreeNode root = new TreeNode(val);        root.left = build(l, m - 1);         // 左子对应中序左半        root.right = build(m + 1, r);        // 右子对应中序右半        return root;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>前序 <code>i</code> 是全局递增的，左子树构造完 <code>i</code> 会自动走到右子树对应的根——这依赖&quot;先建左子再建右子&quot;的顺序，顺序反了 <code>i</code> 就错位。</li></ul><h2 id="437-路径总和-iii">437. 路径总和 III</h2><blockquote><p>找树中所有&quot;向下路径&quot;和等于 targetSum 的条数。节点值可负。</p></blockquote><p><strong>思路脉络</strong>：这是 [[#560. 和为 K 的子数组]] 的树上版——<strong>前缀和 + 哈希</strong>。从根到当前节点累积一条前缀和 <code>sum</code>，问有多少个祖先的前缀和等于 <code>sum - target</code>，就有多少条以当前节点结尾的合法路径。</p><p>关键差异：树有多条分支，前缀和哈希要在<strong>回溯时撤销当前节点的贡献</strong>——因为退出当前分支后，它的前缀和不再属于其它分支的祖先链。这是和数组版最大的区别。</p><pre><code class="language-java">class Solution {    int ans = 0, target;    Map&lt;Long, Integer&gt; cnt = new HashMap&lt;&gt;();    public int pathSum(TreeNode root, int targetSum) {        target = targetSum;        cnt.put(0L, 1);                    // 前缀和 0 出现 1 次        dfs(root, 0L);        return ans;    }    void dfs(TreeNode root, long sum) {        if (root == null) return;        sum += root.val;        ans += cnt.getOrDefault(sum - target, 0);        cnt.merge(sum, 1, Integer::sum);    // 加入当前前缀和        dfs(root.left, sum);        dfs(root.right, sum);        cnt.merge(sum, -1, Integer::sum);  // 回溯撤销!    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>回溯撤销是这题灵魂。不撤销会把兄弟分支的前缀和也算进来，导致路径不连续。<code>targetSum</code> 用 <code>long</code> 防溢出。</li></ul><h2 id="236-二叉树的最近公共祖先">236. 二叉树的最近公共祖先</h2><blockquote><p>找两个节点 p、q 在树中的最近公共祖先。</p></blockquote><p><strong>思路脉络</strong>：递归的语义要先定义清楚——<code>lca(root)</code> 返回&quot;以 root 为根的子树里 p、q 的祖先情况&quot;：</p><ul><li>找到 p 或 q（含 root 自身）就返回该节点；</li><li>左右子树各返回非 null，说明 p、q 分居两侧，当前 root 就是 LCA；</li><li>只一侧非 null，说明 p、q 都在那侧（或那侧找到的本身就是另一个的祖先），返回那侧。</li></ul><blockquote><p>[!example] 后序递归找 LCA（动图）<br />找 p=5、q=4：后序深入，命中 p 直接返回自身；左右都非空→当前是 LCA；单边非空→透传那侧。本例 5 自身就是 p 且是 q 的祖先，命中即返回，5 就是 LCA。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0236-lca.gif" alt="0236-lca" /></p></blockquote><div class="mermaid">graph LR    A[&quot;root==null/p/q?&quot;] --&gt; B[&quot;返回 root&quot;]    B --&gt; C[&quot;递归左 l, 递归右 r&quot;]    C --&gt; D{&quot;l 和 r 都非空?&quot;}    D -- 是 --&gt; E[&quot;root 就是 LCA&quot;]    D -- &quot;只 l 非空&quot; --&gt; F[&quot;返回 l&quot;]    D -- &quot;只 r 非空&quot; --&gt; G[&quot;返回 r&quot;]</div><pre><code class="language-java">class Solution {    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {        if (root == null || root == p || root == q) return root;        TreeNode l = lowestCommonAncestor(root.left, p, q);        TreeNode r = lowestCommonAncestor(root.right, p, q);        if (l != null &amp;&amp; r != null) return root; // 分居两侧        return l != null ? l : r;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!tip] 妙处</p></blockquote><ul><li>这套递归巧妙处理了&quot;p 是 q 祖先&quot;的情况：找到 p 时直接返回 p，上层一侧非空一侧空，返回 p，p 自然就是 LCA。无需特判。</li></ul><h2 id="124-二叉树中的最大路径和">124. 二叉树中的最大路径和</h2><blockquote><p>找树中任意路径（节点序列，可不经过根）的最大和。</p></blockquote><p><strong>思路脉络</strong>：定义&quot;贡献&quot;——<code>gain(root)</code> 返回从 root <strong>向下一侧</strong>延伸能获得的最大和（负贡献截断为 0，因为不走比走负数更好）。</p><p>每经过一个节点，以它为&quot;最高点&quot;的路径和 = <code>左贡献 + 右贡献 + 自身</code>，用它更新答案。但<strong>返回给父节点的只能是单边</strong>（路径不能分叉），所以返回 <code>max(左贡献, 右贡献) + 自身</code>。</p><div class="mermaid">graph LR    A[&quot;gain(root)&quot;] --&gt; B[&quot;左贡献=max(0, gain(left))&quot;]    B --&gt; C[&quot;右贡献=max(0, gain(right))&quot;]    C --&gt; D[&quot;ans=max(ans, 左+右+root.val)&quot;]    D --&gt; E[&quot;返回 max(左,右)+root.val 给上层&quot;]</div><pre><code class="language-java">class Solution {    int ans = Integer.MIN_VALUE;    public int maxPathSum(TreeNode root) {        gain(root);        return ans;    }    int gain(TreeNode root) {        if (root == null) return 0;        int l = Math.max(0, gain(root.left));  // 负贡献截断        int r = Math.max(0, gain(root.right));        ans = Math.max(ans, l + r + root.val); // 经过当前的完整路径        return Math.max(l, r) + root.val;       // 单边向上贡献    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(h)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>ans</code> 用&quot;两边和&quot;（完整路径可分叉在当前节点），<code>return</code> 用&quot;单边&quot;（向上不能分叉）。这是和 [[#543. 二叉树的直径]] 完全同构的思路——一个记和、一个记边数，框架一致。</li></ul><blockquote><p>[!tip] 关联</p></blockquote><ul><li>[[#543. 二叉树的直径]]、[[#124. 二叉树中的最大路径和]]、[[#687. 最长同值路径]] 是一族&quot;后序 + 全局答案 + 单边返回&quot;题，套路完全一致，掌握一个其余即通。</li></ul><hr /><h1 id="9-图论">9. 图论</h1><p>图论的四把刀：</p><ol><li><strong>DFS 染色</strong>：连通分量、感染扩散，递归标记。</li><li><strong>BFS 层序</strong>：最短步数、多源同时扩散，队列按层处理。</li><li><strong>拓扑排序</strong>：依赖关系、有向无环判定，入度表 + BFS。</li><li><strong>Trie</strong>：字符串前缀检索，多叉树。</li></ol><h2 id="200-岛屿数量">200. 岛屿数量</h2><blockquote><p><code>1</code> 是陆地、<code>0</code> 是水，数岛屿数（四连通的 1 算一岛）。</p></blockquote><p><strong>思路脉络</strong>：遍历每个格子，遇到 <code>1</code> 就是一个新岛——DFS 把整片连通的 <code>1</code> 全染成 <code>0</code>（原地标记&quot;已访问&quot;），计数加一。原地修改省去 visited 数组。</p><div class="mermaid">graph LR    A[&quot;遍历 grid&quot;] --&gt; B{&quot;是 '1'?&quot;}    B -- 是 --&gt; C[&quot;DFS 感染整岛为 '0'&quot;]    C --&gt; D[&quot;ans++&quot;]    B -- 否 --&gt; A    D --&gt; A</div><pre><code class="language-java">class Solution {    public int numIslands(char[][] grid) {        int m = grid.length, n = grid[0].length, ans = 0;        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++)                if (grid[i][j] == '1') { dfs(grid, i, j); ans++; }        return ans;    }    void dfs(char[][] g, int i, int j) {        if (i &lt; 0 || i &gt;= g.length || j &lt; 0 || j &gt;= g[0].length || g[i][j] != '1') return;        g[i][j] = '0';                                    // 原地标记        dfs(g, i+1, j); dfs(g, i-1, j); dfs(g, i, j+1); dfs(g, i, j-1);    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(mn)（最坏递归栈）。</p></blockquote><blockquote><p>[!tip] 变形</p></blockquote><ul><li>八连通改 4 个方向为 8 个。求岛屿面积就 DFS 返回计数。求最大岛可先标记再统计。</li></ul><h2 id="994-腐烂的橘子">994. 腐烂的橘子</h2><blockquote><p>每分钟烂橘子感染四邻新鲜橘子，求全部腐烂的最短时间，不可能返回 -1。</p></blockquote><p><strong>思路脉络</strong>：经典<strong>多源 BFS</strong>。初始所有烂橘子同时入队（它们都是第 0 层），按层扩散，层数就是分钟数。过程中统计新鲜橘子数 <code>fresh</code>，全腐烂即 <code>fresh==0</code>。</p><blockquote><p>[!example] 多源 BFS 按层扩散（动图）<br />3×3 网格：初始烂橘子(0,0)同时入队为第0层，每分钟感染四邻、按层推进，层数即分钟数。本例 4 分钟全烂。若最后 <code>fresh&gt;0</code> 返回 -1。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0994-oranges.gif" alt="0994-oranges" /></p></blockquote><div class="mermaid">graph LR    A[&quot;所有烂橘子同时入队&quot;] --&gt; B[&quot;按层 BFS 扩散&quot;]    B --&gt; C[&quot;感染邻居, fresh--&quot;]    C --&gt; D[&quot;层数++&quot;]    D --&gt; E{&quot;fresh==0?&quot;}    E -- 是 --&gt; F[&quot;返回层数&quot;]    E -- 否 --&gt; B    F2[&quot;队列空仍 fresh&gt;0&quot;] --&gt; G[&quot;返回 -1&quot;]</div><pre><code class="language-java">class Solution {    public int orangesRotting(int[][] grid) {        int m = grid.length, n = grid[0].length, fresh = 0;        Queue&lt;int[]&gt; q = new ArrayDeque&lt;&gt;();        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++) {                if (grid[i][j] == 2) q.offer(new int[]{i, j});                else if (grid[i][j] == 1) fresh++;            }        int[][] d = {{1,0},{-1,0},{0,1},{0,-1}};        int min = 0;        while (!q.isEmpty() &amp;&amp; fresh &gt; 0) {       // fresh&gt;0 才继续, 否则多算一层            for (int k = q.size(); k &gt; 0; k--) {                int[] c = q.poll();                for (int[] dir : d) {                    int x = c[0]+dir[0], y = c[1]+dir[1];                    if (x&lt;0||x&gt;=m||y&lt;0||y&gt;=n||grid[x][y]!=1) continue;                    grid[x][y] = 2; fresh--;                    q.offer(new int[]{x, y});                }            }            min++;        }        return fresh == 0 ? min : -1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(mn)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>循环条件带 <code>fresh &gt; 0</code>：避免最后一层已无新鲜橘子时多加一分钟。</li><li>初始就 <code>fresh==0</code>（没有新鲜橘子）应返回 0，上面写法天然满足。</li></ul><h2 id="207-课程表">207. 课程表</h2><blockquote><p><code>n</code> 门课，<code>prerequisites[i]=[a,b]</code> 表示先修 b 再修 a。判断能否修完。</p></blockquote><p><strong>思路脉络</strong>：建图后就是<strong>拓扑排序</strong>——能否把所有节点排成无冲突顺序，等价于图无环。</p><p>BFS 做法：建入度表，入度为 0 的先入队，每次出队并把其后继入度减一，新的 0 入队。最后看处理的节点数是否等于 <code>n</code>。</p><div class="mermaid">graph LR    A[&quot;建图 + 入度表&quot;] --&gt; B[&quot;入度 0 的入队&quot;]    B --&gt; C[&quot;出队, 后继入度--&quot;]    C --&gt; D{&quot;新的入度 0?&quot;}    D -- 是 --&gt; E[&quot;入队&quot;]    D -- 否 --&gt; C    E --&gt; C    C --&gt; F{&quot;处理数 == n?&quot;}    F -- 是 --&gt; G[&quot;可完成(无环)&quot;]    F -- 否 --&gt; H[&quot;有环, 不可&quot;]</div><pre><code class="language-java">class Solution {    public boolean canFinish(int n, int[][] prerequisites) {        List&lt;List&lt;Integer&gt;&gt; g = new ArrayList&lt;&gt;();        int[] indeg = new int[n];        for (int i = 0; i &lt; n; i++) g.add(new ArrayList&lt;&gt;());        for (int[] p : prerequisites) {           // p[1] -&gt; p[0]            g.get(p[1]).add(p[0]);            indeg[p[0]]++;        }        Queue&lt;Integer&gt; q = new ArrayDeque&lt;&gt;();        for (int i = 0; i &lt; n; i++) if (indeg[i] == 0) q.offer(i);        int cnt = 0;        while (!q.isEmpty()) {            int c = q.poll(); cnt++;            for (int nx : g.get(c)) if (--indeg[nx] == 0) q.offer(nx);        }        return cnt == n;                          // 全部排完则无环    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(V+E)，空间 O(V+E)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>边方向别建反：<code>[a,b]</code> 表示&quot;先 b 后 a&quot;，所以是 <code>b→a</code>（b 是 a 的前驱，a 依赖 b，b 完成后 a 入度才减）。建反了入度语义就错。</li></ul><h2 id="208-实现-trie-前缀树">208. 实现 Trie (前缀树)</h2><blockquote><p>实现 <code>insert</code>、<code>search</code>、<code>startsWith</code>。</p></blockquote><p><strong>思路脉络</strong>：Trie 是一棵 26 叉树，每条边代表一个字母。共享前缀的词共用路径，省空间。每个节点有 <code>children[26]</code> 和 <code>isEnd</code> 标记&quot;是否有词在此结束&quot;。</p><ul><li><code>search</code> 要求整条路径存在且终点 <code>isEnd</code>。</li><li><code>startsWith</code> 只要求路径存在，不要求 <code>isEnd</code>。</li></ul><div class="mermaid">graph LR    A[&quot;root&quot;] --&gt; B[&quot;按字符逐层下走&quot;]    B --&gt; C{&quot;children 存在?&quot;}    C -- 是 --&gt; B    C -- 否 --&gt; D[&quot;新建节点&quot;]    D --&gt; B    B --&gt; E[&quot;末尾置 isEnd (insert)&quot;]</div><pre><code class="language-java">class Trie {    private Trie[] children = new Trie[26];    private boolean isEnd;    public Trie() {}    public void insert(String word) {        Trie node = this;        for (char c : word.toCharArray()) {            int i = c - 'a';            if (node.children[i] == null) node.children[i] = new Trie();            node = node.children[i];        }        node.isEnd = true;    }    public boolean search(String word) {        Trie node = walk(word);        return node != null &amp;&amp; node.isEnd;    }    public boolean startsWith(String prefix) {        return walk(prefix) != null;    }    private Trie walk(String s) {        Trie node = this;        for (char c : s.toCharArray()) {            int i = c - 'a';            if (node.children[i] == null) return null;            node = node.children[i];        }        return node;    }}</code></pre><blockquote><p>[!info] 复杂度<br />insert/search/startsWith 均 O(L)（L 为串长），空间 O(总字符数)。</p></blockquote><blockquote><p>[!tip] 价值</p></blockquote><ul><li>Trie 把&quot;前缀匹配&quot;从 O(总词数 × 词长) 降到 O(前缀长)。自动补全、词典检索、IP 路由都靠它。<code>walk</code> 抽出来复用是这题的整洁写法。</li></ul><hr /><h1 id="10-回溯">10. 回溯</h1><p>回溯是&quot;带撤销的 DFS&quot;。所有&quot;枚举所有方案&quot;的题——排列、组合、子集、分割、棋盘——都能套一个框架：</p><pre><code>void backtrack(路径, 选择列表):    if 满足结束条件: 收集结果; return    for 选择 in 选择列表:        做选择          // 加入路径、标记        backtrack(...)        撤销选择         // 移除路径、取消标记</code></pre><blockquote><p>[!tip] 三要素</p><ol><li><strong>结束条件</strong>：路径满了/达到约束。</li><li><strong>选择列表</strong>：当前位置能选什么。排列用 <code>used</code> 数组，组合/子集用 <code>start</code> 去重。</li><li><strong>撤销</strong>：和&quot;做选择&quot;严格对称。<code>add</code> 配 <code>remove</code>、<code>true</code> 配 <code>false</code>、<code>append</code> 配 <code>deleteCharAt</code>。</li></ol></blockquote><p>区分排列 vs 组合的关键：<strong>排列要顺序（[1,2]≠[2,1]），组合不要顺序</strong>。所以排列每层从头扫（用 <code>used</code> 跳过已选），组合用 <code>start</code> 保证只往后选（避免同集合不同序）。</p><h2 id="46-全排列">46. 全排列</h2><blockquote><p>返回无重复数组的所有全排列。</p></blockquote><p><strong>思路脉络</strong>：排列的经典。用 <code>used</code> 数组标记已选，每层从头扫跳过已用的。结束条件是路径长度等于数组长度。</p><blockquote><p>[!example] 回溯搜索树（动图）<br /><code>[1,2,3]</code>：每层枚举未用数，做选择→递归→撤销。路径满(长度=n)收集。撤销让同一路径变量被复用。n=3 共 3!=6 个全排列。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0046-permutations.gif" alt="0046-permutations" /></p></blockquote><div class="mermaid">graph LR    A[&quot;选一个未用数&quot;] --&gt; B[&quot;加入路径, used=true&quot;]    B --&gt; C[&quot;递归&quot;]    C --&gt; D{&quot;路径满?&quot;}    D -- 是 --&gt; E[&quot;收集结果&quot;]    D -- 否 --&gt; A    E --&gt; F[&quot;撤销: remove, used=false&quot;]    F --&gt; A</div><pre><code class="language-java">class Solution {    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; permute(int[] nums) {        backtrack(nums, new ArrayList&lt;&gt;(), new boolean[nums.length]);        return res;    }    void backtrack(int[] nums, List&lt;Integer&gt; path, boolean[] used) {        if (path.size() == nums.length) { res.add(new ArrayList&lt;&gt;(path)); return; }        for (int i = 0; i &lt; nums.length; i++) {            if (used[i]) continue;            path.add(nums[i]); used[i] = true;            backtrack(nums, path, used);            path.remove(path.size() - 1); used[i] = false; // 撤销        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n·n!)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>收集时要 <code>new ArrayList&lt;&gt;(path)</code> 复制，不能直接 <code>add(path)</code>——<code>path</code> 是同一个引用，回溯后会被清空，最终结果全空。</li></ul><h2 id="78-子集">78. 子集</h2><blockquote><p>返回无重复数组的所有子集。</p></blockquote><p><strong>思路脉络</strong>：子集的特别之处：<strong>每个递归节点都是一个合法子集</strong>（不只是叶子）。所以收集放在进入函数时，而非结束条件里。用 <code>start</code> 控制只往后选，保证不重复。</p><pre><code class="language-java">class Solution {    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; subsets(int[] nums) {        backtrack(nums, 0, new ArrayList&lt;&gt;());        return res;    }    void backtrack(int[] nums, int start, List&lt;Integer&gt; path) {        res.add(new ArrayList&lt;&gt;(path));           // 每个节点都是子集        for (int i = start; i &lt; nums.length; i++) {            path.add(nums[i]);            backtrack(nums, i + 1, path);            path.remove(path.size() - 1);        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n·2ⁿ)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;收集在每个节点&quot; vs &quot;收集在叶子&quot;：子集类前者，排列/组合类后者。这个区别决定 <code>res.add</code> 放在哪。</li></ul><h2 id="17-电话号码的字母组合">17. 电话号码的字母组合</h2><blockquote><p>数字串（如 &quot;23&quot;）映射到字母组合。</p></blockquote><p><strong>思路脉络</strong>：每个数字对应一组字母，求笛卡尔积。回溯按数字位置 <code>idx</code> 推进，每层选当前数字对应的一个字母。<code>StringBuilder</code> 的撤销是 <code>deleteCharAt</code>。</p><pre><code class="language-java">class Solution {    String[] map = {&quot;&quot;, &quot;&quot;, &quot;abc&quot;, &quot;def&quot;, &quot;ghi&quot;, &quot;jkl&quot;, &quot;mno&quot;, &quot;pqrs&quot;, &quot;tuv&quot;, &quot;wxyz&quot;};    List&lt;String&gt; res = new ArrayList&lt;&gt;();    public List&lt;String&gt; letterCombinations(String digits) {        if (digits.isEmpty()) return res;        backtrack(digits, 0, new StringBuilder());        return res;    }    void backtrack(String digits, int idx, StringBuilder sb) {        if (idx == digits.length()) { res.add(sb.toString()); return; }        for (char c : map[digits.charAt(idx) - '0'].toCharArray()) {            sb.append(c);            backtrack(digits, idx + 1, sb);            sb.deleteCharAt(sb.length() - 1);     // 撤销        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(4ⁿ)（最坏每个数字 4 字母），空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>map[0]</code>、<code>map[1]</code> 是空串（数字 0/1 无对应字母）。空输入直接返回空列表，别误返回含空串的列表。</li></ul><h2 id="39-组合总和">39. 组合总和</h2><blockquote><p>无重复正整数 <code>candidates</code>，找所有和为 <code>target</code> 的组合，元素可无限次使用。</p></blockquote><p><strong>思路脉络</strong>：回溯，<code>remain</code> 跟踪剩余目标。元素可重复用，所以递归时 <code>start</code> 传 <code>i</code>（不是 <code>i+1</code>）。排序后可剪枝：当前候选 <code>&gt; remain</code> 就 <code>break</code>（后面的更大）。</p><pre><code class="language-java">class Solution {    List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;Integer&gt;&gt; combinationSum(int[] c, int target) {        Arrays.sort(c);                            // 剪枝前提        backtrack(c, target, 0, new ArrayList&lt;&gt;());        return res;    }    void backtrack(int[] c, int remain, int start, List&lt;Integer&gt; path) {        if (remain == 0) { res.add(new ArrayList&lt;&gt;(path)); return; }        for (int i = start; i &lt; c.length; i++) {            if (c[i] &gt; remain) break;               // 排序后剪枝            path.add(c[i]);            backtrack(c, remain - c[i], i, path);   // i 不+1, 可重复用            path.remove(path.size() - 1);        }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间取决于组合数，最坏 O(2ⁿ) 级别，空间 O(target/min)。</p></blockquote><blockquote><p>[!tip] 辨析</p></blockquote><ul><li>&quot;元素可重复用&quot;→递归传 <code>i</code>；&quot;每个用一次&quot;→传 <code>i+1</code>（[[#40. 组合总和 II]]）；&quot;含重复元素去重&quot;→排序后跳过相邻重复（[[#90. 子集 II]] 同理）。</li></ul><h2 id="22-括号生成">22. 括号生成</h2><blockquote><p>生成 <code>n</code> 对括号的所有合法组合。</p></blockquote><p><strong>思路脉络</strong>：回溯时跟踪两个计数：已用左括号 <code>open</code>、已用右括号 <code>close</code>。两条规则保证合法：</p><ul><li><code>open &lt; n</code>：还能放左括号。</li><li><code>close &lt; open</code>：只有左括号比右括号多时才能放右括号（否则会先闭后开，非法）。</li></ul><div class="mermaid">graph LR    A[&quot;sb, open, close&quot;] --&gt; B{&quot;open &lt; n?&quot;}    B -- 是 --&gt; C[&quot;放 (, open++&quot;]    C --&gt; A    B -- 否 --&gt; D{&quot;close &lt; open?&quot;}    D -- 是 --&gt; E[&quot;放 ), close++&quot;]    E --&gt; A    D -- 否 --&gt; F{&quot;len == 2n?&quot;}    F -- 是 --&gt; G[&quot;收集&quot;]</div><pre><code class="language-java">class Solution {    List&lt;String&gt; res = new ArrayList&lt;&gt;();    public List&lt;String&gt; generateParenthesis(int n) {        backtrack(new StringBuilder(), 0, 0, n);        return res;    }    void backtrack(StringBuilder sb, int open, int close, int n) {        if (sb.length() == 2 * n) { res.add(sb.toString()); return; }        if (open &lt; n)  { sb.append('('); backtrack(sb, open + 1, close, n); sb.deleteCharAt(sb.length() - 1); }        if (close &lt; open) { sb.append(')'); backtrack(sb, open, close + 1, n); sb.deleteCharAt(sb.length() - 1); }    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(4ⁿ/√n)（卡特兰数），空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>close &lt; open</code> 是合法性的核心：右括号必须匹配一个已放的左括号。写成 <code>close &lt; n</code> 会生成 <code>)(</code> 这种非法串。</li></ul><h2 id="79-单词搜索">79. 单词搜索</h2><blockquote><p>在二维字符网格中搜索单词（上下右左连通），字母不可重复用。</p></blockquote><p><strong>思路脉络</strong>：每个格子都可能作起点，DFS 四向扩展。关键在<strong>回溯标记</strong>：进入格子先改成 <code>#</code>（或 visited）防止重复用，退出时<strong>恢复</strong>——这是回溯的&quot;撤销&quot;在网格题的体现。</p><div class="mermaid">graph LR    A[&quot;每个格子作起点&quot;] --&gt; B[&quot;DFS 匹配 word[k]&quot;]    B --&gt; C{&quot;匹配?&quot;}    C -- 是 --&gt; D[&quot;标记 # &quot;]    D --&gt; E[&quot;四向 DFS k+1&quot;]    E --&gt; F[&quot;恢复原字符 撤销&quot;]    F --&gt; G{&quot;任一方向成功?&quot;}    C -- 否 --&gt; H[&quot;回退 false&quot;]    G -- 是 --&gt; I[&quot;返回 true&quot;]    G -- 否 --&gt; H</div><pre><code class="language-java">class Solution {    public boolean exist(char[][] board, String word) {        int m = board.length, n = board[0].length;        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++)                if (dfs(board, word, i, j, 0)) return true;        return false;    }    boolean dfs(char[][] b, String w, int i, int j, int k) {        if (k == w.length()) return true;                       // 匹配完        if (i&lt;0||i&gt;=b.length||j&lt;0||j&gt;=b[0].length||b[i][j]!=w.charAt(k)) return false;        char t = b[i][j]; b[i][j] = '#';                        // 标记        boolean f = dfs(b,w,i+1,j,k+1)||dfs(b,w,i-1,j,k+1)                  ||dfs(b,w,i,j+1,k+1)||dfs(b,w,i,j-1,k+1);        b[i][j] = t;                                            // 撤销        return f;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn·3ᴸ)（每步最多 3 个新方向，L 是词长），空间 O(L)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>标记和恢复必须配对：用 <code>char t</code> 存原值，DFS 后 <code>b[i][j]=t</code> 还原。漏恢复会让其他起点的搜索踩到 <code>#</code> 而误判。</li></ul><h2 id="131-分割回文串">131. 分割回文串</h2><blockquote><p>把字符串切成若干子串，每个子串都是回文，返回所有切法。</p></blockquote><p><strong>思路脉络</strong>：回溯切分位置。从 <code>start</code> 起，枚举分割点 <code>end</code>，若 <code>[start,end]</code> 是回文就切下来递归处理 <code>end+1</code> 起。<code>start == n</code> 时收集一组完整分割。</p><pre><code class="language-java">class Solution {    List&lt;List&lt;String&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;String&gt;&gt; partition(String s) {        backtrack(s, 0, new ArrayList&lt;&gt;());        return res;    }    void backtrack(String s, int start, List&lt;String&gt; path) {        if (start == s.length()) { res.add(new ArrayList&lt;&gt;(path)); return; }        for (int end = start; end &lt; s.length(); end++) {            if (!isP(s, start, end)) continue;       // 非回文就跳过            path.add(s.substring(start, end + 1));            backtrack(s, end + 1, path);            path.remove(path.size() - 1);        }    }    boolean isP(String s, int l, int r) {        while (l &lt; r) if (s.charAt(l++) != s.charAt(r--)) return false;        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n·2ⁿ)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 优化</p></blockquote><ul><li>预处理 <code>dp[i][j]</code> 回文表（O(n²)），回溯时 O(1) 查询，避免每次双指针判回文。对长串显著提速。</li></ul><h2 id="51-n-皇后">51. N 皇后</h2><blockquote><p>在 <code>n×n</code> 棋盘放 <code>n</code> 个皇后，互不攻击（同行同列同对角线），返回所有摆法。</p></blockquote><p><strong>思路脉络</strong>：逐行回溯，每行选一列放皇后。冲突检查只需看<strong>上方</strong>（同行只放一个、下方还没放）：同列、左上对角、右上对角。</p><div class="mermaid">graph LR    A[&quot;逐行 row&quot;] --&gt; B[&quot;枚举列 col&quot;]    B --&gt; C{&quot;不冲突?&quot;}    C -- 是 --&gt; D[&quot;放 Q&quot;]    D --&gt; E[&quot;递归 row+1&quot;]    E --&gt; F[&quot;撤销, 放 .&quot;]    F --&gt; B    C -- 否 --&gt; B    E --&gt; G{&quot;row == n?&quot;}    G -- 是 --&gt; H[&quot;收集棋盘&quot;]</div><pre><code class="language-java">class Solution {    List&lt;List&lt;String&gt;&gt; res = new ArrayList&lt;&gt;();    public List&lt;List&lt;String&gt;&gt; solveNQueens(int n) {        char[][] b = new char[n][n];        for (char[] r : b) Arrays.fill(r, '.');        backtrack(b, 0);        return res;    }    void backtrack(char[][] b, int row) {        if (row == b.length) { res.add(build(b)); return; }        for (int col = 0; col &lt; b.length; col++) {            if (!valid(b, row, col)) continue;            b[row][col] = 'Q';            backtrack(b, row + 1);            b[row][col] = '.';                    // 撤销        }    }    boolean valid(char[][] b, int r, int c) {        for (int i = 0; i &lt; r; i++) if (b[i][c] == 'Q') return false;        // 同列        for (int i=r-1,j=c-1; i&gt;=0&amp;&amp;j&gt;=0; i--,j--) if (b[i][j]=='Q') return false; // 左上对角        for (int i=r-1,j=c+1; i&gt;=0&amp;&amp;j&lt;b.length; i--,j++) if (b[i][j]=='Q') return false; // 右上对角        return true;    }    List&lt;String&gt; build(char[][] b) {        List&lt;String&gt; l = new ArrayList&lt;&gt;();        for (char[] r : b) l.add(new String(r));        return l;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n!)（每行可选列递减），空间 O(n)。</p></blockquote><blockquote><p>[!tip] 优化</p></blockquote><ul><li>用三个 <code>Set</code> 记录&quot;已占列、主对角(row−col)、副对角(row+col)&quot;，<code>valid</code> 降到 O(1)。对角线用 <code>row−col</code>、<code>row+col</code> 作 key 是经典技巧。</li></ul><blockquote><p>[!tip] 套路</p></blockquote><ul><li>N 皇后是回溯的&quot;集大成者&quot;：选位置→递归→撤销，加上约束剪枝。掌握它，[[#37. 解数独]]、[[#52. N 皇后 II]] 都是同模板。</li></ul><hr /><h1 id="11-二分查找">11. 二分查找</h1><p>二分的本质是<strong>在单调性上收缩边界</strong>：每次比较排除一半。难点不在二分本身，在<strong>边界处理</strong>——找精确值、找左界、找右界，三种目标的循环条件和收缩方式不同。</p><blockquote><p>[!tip] 左闭右闭模板三态<br />统一用 <code>[l, r]</code> 左闭右闭，<code>while (l &lt;= r)</code>：</p><ul><li><strong>精确值</strong>：<code>nums[m]==t</code> 直接返回 <code>m</code>。</li><li><strong>左界</strong>（第一个 <code>==t</code>）：命中也 <code>r = m - 1</code> 继续往左找，记录候选 <code>ans</code>，最后返回 <code>l</code>。</li><li><strong>右界</strong>（最后一个 <code>==t</code>）：命中也 <code>l = m + 1</code> 继续往右找，最后返回 <code>r</code>。</li></ul></blockquote><p>记一句口诀：<strong>左界命中往左压，右界命中往右压</strong>。</p><h2 id="35-搜索插入位置">35. 搜索插入位置</h2><blockquote><p>给有序数组找 <code>target</code>，找到返回下标，找不到返回应插入位置。</p></blockquote><p><strong>思路脉络</strong>：标准精确二分。找不到时，<code>l</code> 正好停在该插入的位置——因为循环结束时 <code>l == r + 1</code>，<code>l</code> 指向第一个大于 <code>target</code> 的位置。</p><pre><code class="language-java">class Solution {    public int searchInsert(int[] nums, int target) {        int l = 0, r = nums.length - 1;        while (l &lt;= r) {            int m = (l + r) / 2;            if (nums[m] == target) return m;            else if (nums[m] &lt; target) l = m + 1;            else r = m - 1;        }        return l;                        // 插入位置    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>这题是&quot;左界&quot;二分的简化版。把&quot;找到返回&quot;去掉，<code>return l</code> 就是&quot;<code>&gt;= target</code> 的第一个位置&quot;，正是插入位置。</li></ul><h2 id="74-搜索二维矩阵">74. 搜索二维矩阵</h2><blockquote><p>每行升序，且每行首大于上一行尾。找 <code>target</code>。</p></blockquote><p><strong>思路脉络</strong>：这种&quot;行间也升序&quot;的矩阵<strong>等价于一维升序数组</strong>——把 <code>mid</code> 映射到 <code>matrix[mid/n][mid%n]</code> 即可，一次二分搞定。别被二维外表骗了。</p><pre><code class="language-java">class Solution {    public boolean searchMatrix(int[][] matrix, int target) {        int m = matrix.length, n = matrix[0].length;        int l = 0, r = m * n - 1;        while (l &lt;= r) {            int mid = (l + r) / 2;            int v = matrix[mid / n][mid % n];    // 一维下标转二维            if (v == target) return true;            else if (v &lt; target) l = mid + 1;            else r = mid - 1;        }        return false;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log mn)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 辨析</p></blockquote><ul><li>注意和 [[#240. 搜索二维矩阵 II]] 区别：那题每行升、每列升但行间未必衔接，不能压成一维，得用右上角 O(m+n)。本题行间严格衔接，才能一维二分。</li></ul><h2 id="34-在排序数组中查找元素的第一个和最后一个位置">34. 在排序数组中查找元素的第一个和最后一个位置</h2><blockquote><p>找 <code>target</code> 在有序数组里的起止下标，没有返回 <code>[-1,-1]</code>。</p></blockquote><p><strong>思路脉络</strong>：两次二分，一次找左界、一次找右界。命中时不停，继续往对应方向压。</p><div class="mermaid">graph LR    A[&quot;二分找左界&quot;] --&gt; B{&quot;nums[m] == t?&quot;}    B -- 是 --&gt; C[&quot;ans=m, r=m-1 往左压&quot;]    B -- &quot;&lt;t&quot; --&gt; D[&quot;l=m+1&quot;]    B -- &quot;&gt;t&quot; --&gt; E[&quot;r=m-1&quot;]    C --&gt; A    D --&gt; A    E --&gt; A</div><pre><code class="language-java">class Solution {    public int[] searchRange(int[] nums, int target) {        return new int[]{findLeft(nums, target), findRight(nums, target)};    }    int findLeft(int[] a, int t) {        int l = 0, r = a.length - 1, ans = -1;        while (l &lt;= r) {            int m = (l + r) / 2;            if (a[m] == t) { ans = m; r = m - 1; }  // 命中往左压            else if (a[m] &lt; t) l = m + 1;            else r = m - 1;        }        return ans;    }    int findRight(int[] a, int t) {        int l = 0, r = a.length - 1, ans = -1;        while (l &lt;= r) {            int m = (l + r) / 2;            if (a[m] == t) { ans = m; l = m + 1; }  // 命中往右压            else if (a[m] &lt; t) l = m + 1;            else r = m - 1;        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>找左界命中时 <code>r = m - 1</code>，找右界命中时 <code>l = m + 1</code>——方向反了会找错界。</li></ul><h2 id="33-搜索旋转排序数组">33. 搜索旋转排序数组</h2><blockquote><p>升序数组在某个点旋转过（如 <code>[4,5,6,7,0,1,2]</code>），找 <code>target</code>，O(log n)。</p></blockquote><p><strong>思路脉络</strong>：旋转数组虽整体无序，但<strong>对 mid 切一刀，必有一半是有序的</strong>。判断哪半有序，再看 <code>target</code> 在不在那半的范围内，决定往哪边缩。</p><ul><li><code>nums[l] &lt;= nums[m]</code>：左半有序。若 <code>target</code> 在 <code>[nums[l], nums[m])</code> 内，搜左半；否则搜右半。</li><li>否则右半有序。若 <code>target</code> 在 <code>(nums[m], nums[r]]</code> 内，搜右半；否则搜左半。</li></ul><div class="mermaid">graph LR    A[&quot;取 mid&quot;] --&gt; B{&quot;nums[l] ≤ nums[m]?&quot;}    B -- 是, 左半有序 --&gt; C{&quot;target 在左半?&quot;}    C -- 是 --&gt; D[&quot;r=mid-1&quot;]    C -- 否 --&gt; E[&quot;l=mid+1&quot;]    B -- 否, 右半有序 --&gt; F{&quot;target 在右半?&quot;}    F -- 是 --&gt; E    F -- 否 --&gt; D</div><pre><code class="language-java">class Solution {    public int search(int[] nums, int target) {        int l = 0, r = nums.length - 1;        while (l &lt;= r) {            int m = (l + r) / 2;            if (nums[m] == target) return m;            if (nums[l] &lt;= nums[m]) {                        // 左半有序                if (nums[l] &lt;= target &amp;&amp; target &lt; nums[m]) r = m - 1;                else l = m + 1;            } else {                                         // 右半有序                if (nums[m] &lt; target &amp;&amp; target &lt;= nums[r]) l = m + 1;                else r = m - 1;            }        }        return -1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>判断&quot;左半有序&quot;用 <code>nums[l] &lt;= nums[m]</code>（含等号，处理 <code>l==m</code> 的两元素情况）。范围判断用闭开区间 <code>[nums[l], nums[m])</code>，边界别漏。</li></ul><h2 id="153-寻找旋转排序数组中的最小值">153. 寻找旋转排序数组中的最小值</h2><blockquote><p>旋转过的升序数组（无重复）找最小值，O(log n)。</p></blockquote><p><strong>思路脉络</strong>：和 <code>right</code> 比。<code>nums[m] &gt; nums[r]</code> 说明最小值在 mid 右侧（左半都比 <code>nums[r]</code> 大，不是最小）；否则最小值在 mid 或左侧。注意循环用 <code>l &lt; r</code>，收缩 <code>r = m</code>（不是 <code>m - 1</code>），因为 mid 本身可能是最小值。</p><pre><code class="language-java">class Solution {    public int findMin(int[] nums) {        int l = 0, r = nums.length - 1;        while (l &lt; r) {            int m = (l + r) / 2;            if (nums[m] &gt; nums[r]) l = m + 1;   // 最小在右半            else r = m;                          // 最小在 m 或左半        }        return nums[l];                          // l == r 即最小    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>和 <code>right</code> 比而不是 <code>left</code>：和 <code>left</code> 比无法区分&quot;未旋转&quot;和&quot;旋转后最小在右&quot;。<code>r = m</code> 不减一，因为 mid 可能就是答案。</li></ul><h2 id="4-寻找两个正序数组的中位数">4. 寻找两个正序数组的中位数</h2><blockquote><p>两个升序数组找中位数，O(log (m+n))。</p></blockquote><p><strong>思路脉络</strong>：中位数是&quot;把合并数组平分两半的分界点&quot;。不合并，而是<strong>二分较短数组的划分数</strong>，让左半元素总数等于 <code>(m+n+1)/2</code>。</p><p>设 <code>a</code> 短、<code>b</code> 长。在 <code>a</code> 上二分切 <code>i</code> 个到左半，则 <code>b</code> 切 <code>j = half - i</code> 个。合法划分要满足：<code>a</code> 左最大 <code>aL &lt;= bR</code> 且 <code>b</code> 左最大 <code>bL &lt;= aR</code>。</p><ul><li>满足：找到划分。奇数取 <code>max(aL, bL)</code>，偶数取 <code>(max(aL,bL) + min(aR,bR))/2</code>。</li><li><code>aL &gt; bR</code>：<code>a</code> 切多了，<code>hi = i - 1</code>。</li><li>否则 <code>a</code> 切少了，<code>lo = i + 1</code>。</li></ul><p>边界用 <code>±∞</code> 处理一侧切空的情况。</p><div class="mermaid">graph LR    A[&quot;二分 a 的切点 i&quot;] --&gt; B[&quot;j = half - i&quot;]    B --&gt; C[&quot;aL,aR,bL,bR&quot;]    C --&gt; D{&quot;aL ≤ bR 且 bL ≤ aR?&quot;}    D -- 是 --&gt; E[&quot;合法划分, 算中位数&quot;]    D -- &quot;aL&gt;bR&quot; --&gt; F[&quot;hi=i-1&quot;]    D -- &quot;bL&gt;aR&quot; --&gt; G[&quot;lo=i+1&quot;]    F --&gt; A    G --&gt; A</div><pre><code class="language-java">class Solution {    public double findMedianSortedArrays(int[] a, int[] b) {        if (a.length &gt; b.length) return findMedianSortedArrays(b, a); // 保证 a 短        int m = a.length, n = b.length, half = (m + n + 1) / 2;        int lo = 0, hi = m;        while (lo &lt;= hi) {            int i = (lo + hi) / 2, j = half - i;            int aL = i == 0 ? Integer.MIN_VALUE : a[i - 1];            int aR = i == m ? Integer.MAX_VALUE : a[i];            int bL = j == 0 ? Integer.MIN_VALUE : b[j - 1];            int bR = j == n ? Integer.MAX_VALUE : b[j];            if (aL &lt;= bR &amp;&amp; bL &lt;= aR) {                if ((m + n) % 2 == 1) return Math.max(aL, bL);                return (Math.max(aL, bL) + Math.min(aR, bR)) / 2.0;            } else if (aL &gt; bR) hi = i - 1;            else lo = i + 1;        }        return 0;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(log(min(m,n)))，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>必须<strong>在短数组上二分</strong>：否则 <code>j = half - i</code> 可能越界（长数组切太多导致 j 为负）。开头的 <code>swap</code> 保证 <code>a</code> 短。</li><li>边界用 <code>±∞</code>：<code>i==0</code> 时 <code>aL</code> 取 <code>MIN_VALUE</code>，保证 <code>max(aL,bL)</code> 永远是另一边的真实值。漏了会数组越界。</li></ul><hr /><h1 id="12-栈">12. 栈</h1><p>栈的两类典型：</p><ol><li><strong>匹配/消除</strong>：括号配对、字符串解码，遇同类闭合就弹出。</li><li><strong>单调栈</strong>：求&quot;下一个更大/更小&quot;，维护单调性，弹栈时计算答案。</li></ol><p>单调栈的精髓：<strong>栈里存下标（不是值）</strong>，弹栈时由&quot;被弹元素&quot;和&quot;触发弹出的元素&quot;配对出答案。</p><h2 id="20-有效的括号">20. 有效的括号</h2><blockquote><p>判断括号串是否合法（<code>()[]{}</code> 闭合且顺序正确）。</p></blockquote><p><strong>思路脉络</strong>：遇左括号入栈，遇右括号检查栈顶是否是对应左括号——是则弹，否则非法。最后栈空才合法。用 <code>Map</code> 存右→左映射，简化匹配。</p><pre><code class="language-java">class Solution {    public boolean isValid(String s) {        Map&lt;Character, Character&gt; m = Map.of(')', '(', ']', '[', '}', '{');        Deque&lt;Character&gt; st = new ArrayDeque&lt;&gt;();        for (char c : s.toCharArray()) {            if (!m.containsKey(c)) st.push(c);          // 左括号入栈            else if (st.isEmpty() || st.pop() != m.get(c)) return false; // 右括号配对        }        return st.isEmpty();                            // 栈空才合法    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>右括号配对前先判栈空：形如 <code>]</code> 开头，栈空直接 <code>pop</code> 会异常。<code>st.pop() != m.get(c)</code> 复用短路，但前面要先查 <code>isEmpty</code>。</li></ul><h2 id="155-最小栈">155. 最小栈</h2><blockquote><p>栈支持 <code>push</code>/<code>pop</code>/<code>top</code>/<code>getMin</code> 全 O(1)。</p></blockquote><p><strong>思路脉络</strong>：单栈无法 O(1) 取最小。辅助栈 <code>minSt</code> 同步压栈，每步记录&quot;当前栈内最小值&quot;。<code>pop</code> 时两栈同步弹。空间 O(n)。</p><pre><code class="language-java">class MinStack {    Deque&lt;Integer&gt; st = new ArrayDeque&lt;&gt;(), minSt = new ArrayDeque&lt;&gt;();    public MinStack() { minSt.push(Integer.MAX_VALUE); }    public void push(int v) { st.push(v); minSt.push(Math.min(minSt.peek(), v)); }    public void pop() { st.pop(); minSt.pop(); }    public int top() { return st.peek(); }    public int getMin() { return minSt.peek(); }}</code></pre><blockquote><p>[!info] 复杂度<br />各操作 O(1)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 优化</p></blockquote><ul><li>O(1) 空间做法：存差值 <code>v - min</code>。但辅助栈法最直观、不易错，面试首选。</li></ul><h2 id="394-字符串解码">394. 字符串解码</h2><blockquote><p><code>3[a2[c]]</code> → <code>accaccacc</code>，数字表示重复次数。</p></blockquote><p><strong>思路脉络</strong>：遇数字攒次数、遇 <code>[</code> 把当前串和次数压栈并重置、遇 <code>]</code> 弹栈拼接。两个栈分别存&quot;外层串&quot;和&quot;重复次数&quot;，<code>[</code> 时入栈、<code>]</code> 时出栈。</p><div class="mermaid">graph LR    A[&quot;遍历字符&quot;] --&gt; B{&quot;类型?&quot;}    B -- 数字 --&gt; C[&quot;攒成完整数&quot;]    B -- 字母 --&gt; D[&quot;拼到 cur&quot;]    B -- &quot;[ &quot; --&gt; E[&quot;压栈 cur, num; 重置&quot;]    B -- &quot;]&quot; --&gt; F[&quot;弹出 prev, k; cur=prev+cur×k&quot;]</div><pre><code class="language-java">class Solution {    public String decodeString(String s) {        Deque&lt;String&gt; strSt = new ArrayDeque&lt;&gt;();        Deque&lt;Integer&gt; numSt = new ArrayDeque&lt;&gt;();        StringBuilder cur = new StringBuilder();        int num = 0;        for (char c : s.toCharArray()) {            if (Character.isDigit(c)) num = num * 10 + (c - '0');            else if (c == '[') { strSt.push(cur.toString()); numSt.push(num); cur = new StringBuilder(); num = 0; }            else if (c == ']') {                int k = numSt.pop();                StringBuilder prev = new StringBuilder(strSt.pop());                for (int i = 0; i &lt; k; i++) prev.append(cur);                cur = prev;            } else cur.append(c);        }        return cur.toString();    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(输出长度)，空间 O(嵌套深度)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>数字可能多位（如 <code>12[a]</code>），必须 <code>num*10 + c-'0'</code> 攒完整数。<code>[</code> 时重置 <code>num=0</code> 和 <code>cur</code>，<code>]</code> 时用 prev 包裹 cur 重复 k 次。</li></ul><h2 id="739-每日温度">739. 每日温度</h2><blockquote><p>每天温度，问要等几天才出现更高温度，没有填 0。</p></blockquote><p><strong>思路脉络</strong>：单调栈经典。维护<strong>递减栈</strong>（存下标）：当天比栈顶高，栈顶出栈，答案 = 当前下标 − 栈顶下标。栈里剩下的没人更高，答案 0。</p><blockquote><p>[!example] 单调递减栈弹栈结算（动图）<br /><code>[73,74,75,71,69,72,76,73]</code>：遇更高温弹栈，<code>ans=当前下标−栈顶下标</code>。递减段连续入栈不结算，升温时连续弹栈结算。末栈未结算填 0。结果 <code>[1,1,4,2,1,1,0,0]</code>。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0739-daily-temp.gif" alt="0739-daily-temp" /></p></blockquote><div class="mermaid">graph LR    A[&quot;遍历 i, t&quot;] --&gt; B{&quot;栈非空且 t &gt; 栈顶温度?&quot;}    B -- 是 --&gt; C[&quot;弹 top, ans=top = i - top&quot;]    C --&gt; B    B -- 否 --&gt; D[&quot;i 入栈&quot;]    D --&gt; A</div><pre><code class="language-java">class Solution {    public int[] dailyTemperatures(int[] T) {        int n = T.length, res[] = new int[n];        Deque&lt;Integer&gt; st = new ArrayDeque&lt;&gt;();        for (int i = 0; i &lt; n; i++) {            while (!st.isEmpty() &amp;&amp; T[i] &gt; T[st.peek()]) {                int top = st.pop();                res[top] = i - top;            }            st.push(i);        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)（每元素入栈出栈各一次），空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;下一个更大&quot;问题万能模板：单调栈存下标，弹栈时由&quot;被弹者&quot;和&quot;触发者&quot;算答案。[[#503. 下一个更大元素 II]] 同模板。</li></ul><h2 id="84-柱状图中最大的矩形">84. 柱状图中最大的矩形</h2><blockquote><p>柱状图里能画出的最大矩形面积。</p></blockquote><p><strong>思路脉络</strong>：固定每根柱子作&quot;高&quot;，矩形最大宽 = 左边第一个更矮的位置到右边第一个更矮的位置之间。所以对每根柱子找&quot;左右第一个更矮&quot;——正是单调栈（递增栈）。</p><blockquote><p>[!example] 单调栈弹栈结算（动图）<br />加左右哨兵 0，递增栈存下标，遇更矮柱子弹栈结算：高=弹出柱，宽=当前 i − 新栈顶 − 1。本例最大矩形面积 10（高 5 × 宽 2）。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0084-largest-rect.gif" alt="0084-largest-rect" /></p></blockquote><p>加左右两个 <code>0</code> 哨兵柱简化边界：左哨兵让栈不空，右哨兵确保最后所有柱子被弹出结算。</p><div class="mermaid">graph LR    A[&quot;遍历 i, h&quot;] --&gt; B{&quot;栈非空且 h &lt; 栈顶高?&quot;}    B -- 是 --&gt; C[&quot;弹 top 作高&quot;]    C --&gt; D[&quot;宽 = i - 新栈顶 - 1&quot;]    D --&gt; E[&quot;算面积更新答案&quot;]    E --&gt; B    B -- 否 --&gt; F[&quot;i 入栈&quot;]    F --&gt; A</div><pre><code class="language-java">class Solution {    public int largestRectangleArea(int[] heights) {        int n = heights.length, ans = 0;        int[] h = new int[n + 2];                    // 左右加 0 哨兵        System.arraycopy(heights, 0, h, 1, n);        Deque&lt;Integer&gt; st = new ArrayDeque&lt;&gt;();        for (int i = 0; i &lt; h.length; i++) {            while (!st.isEmpty() &amp;&amp; h[i] &lt; h[st.peek()]) {                int height = h[st.pop()];                int width = i - st.peek() - 1;       // 新栈顶是左边界                ans = Math.max(ans, height * width);            }            st.push(i);        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>宽度计算 <code>i - st.peek() - 1</code>：弹栈后，新栈顶是&quot;左边第一个更矮&quot;的位置，右边界是当前 <code>i</code>（第一个让当前更矮的位置），宽 = 两者间距减一。哨兵 <code>0</code> 保证首元素和尾元素都能被结算。</li></ul><blockquote><p>[!tip] 关联</p></blockquote><ul><li>这是单调栈的巅峰题。和 [[#42. 接雨水]] 看似都在柱状图上，思路相反：接雨水求&quot;凹槽&quot;（用递减栈算水量），最大矩形求&quot;凸起&quot;（用递增栈算面积）。[[#85. 最大矩形]] 把它扩展到二维矩阵。</li></ul><hr /><h1 id="13-堆">13. 堆</h1><p>堆用于维护&quot;半个有序集&quot;：快速取最大/最小。Java 用 <code>PriorityQueue</code>，默认小顶堆，<code>Collections.reverseOrder()</code> 变大顶堆。</p><p>两类核心场景：</p><ul><li><strong>Top-K</strong>：维护大小为 K 的堆，比堆顶差就丢弃，堆顶就是第 K 大/小。</li><li><strong>动态中位数</strong>：大顶堆装左半（较小的一半）、小顶堆装右半（较大的一半），两堆规模平衡。</li></ul><h2 id="215-数组中的第-k-个最大元素">215. 数组中的第 K 个最大元素</h2><blockquote><p>找第 K 大，要求 O(n) 期望。</p></blockquote><p><strong>思路脉络</strong>：三种。</p><p><strong>小顶堆 O(n log K)</strong>：维护大小 K 的小顶堆，遍历完堆顶就是第 K 大。空间 O(K)。</p><p><strong>大顶堆 O(n + K log n)</strong>：全部入大顶堆，弹 K 次。</p><p><strong>快速选择 O(n) 期望</strong>：本题正解。基于快排的 partition：每次选 pivot 划分，看 pivot 位置和 K 的关系，只递归一侧。期望 O(n)，最坏 O(n²)（随机化 pivot 几乎不会退化）。</p><pre><code class="language-java">class Solution {    public int findKthLargest(int[] nums, int k) {        k = nums.length - k;                  // 第 K 大 = 升序第 (n-k) 个        int l = 0, r = nums.length - 1;        while (l &lt; r) {            int p = partition(nums, l, r);            if (p == k) return nums[p];            else if (p &lt; k) l = p + 1;            else r = p - 1;        }        return nums[l];    }    int partition(int[] a, int l, int r) {        int pivot = a[r], i = l;        for (int j = l; j &lt; r; j++) if (a[j] &lt;= pivot) swap(a, i++, j);        swap(a, i, r);        return i;    }    void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n) 期望，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 辨析</p></blockquote><ul><li>&quot;第 K 大&quot;转成&quot;升序第 (n−K) 个&quot;是关键转换，让快选的索引语义清晰。面试求 O(n) 必用快选，求简单可用小顶堆。</li></ul><h2 id="347-前-k-个高频元素">347. 前 K 个高频元素</h2><blockquote><p>返回出现频率前 K 高的元素。</p></blockquote><p><strong>思路脉络</strong>：先统计频率（哈希），再取前 K。取前 K 用<strong>小顶堆</strong>：堆大小 K，按频率建堆，频率比堆顶高才入。最后堆里就是前 K 高。</p><pre><code class="language-java">class Solution {    public int[] topKFrequent(int[] nums, int k) {        Map&lt;Integer, Integer&gt; freq = new HashMap&lt;&gt;();        for (int n : nums) freq.merge(n, 1, Integer::sum);        PriorityQueue&lt;Integer&gt; pq = new PriorityQueue&lt;&gt;((a, b) -&gt; freq.get(a) - freq.get(b)); // 小顶堆按频率        for (int key : freq.keySet()) {            pq.offer(key);            if (pq.size() &gt; k) pq.poll();            // 超过 K 弹最小的        }        int[] res = new int[k];        for (int i = 0; i &lt; k; i++) res[i] = pq.poll();        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log K)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;Top-K 频率&quot;标准两步：哈希计数 + 小顶堆截断。堆大小恒为 K 是精髓——超出就弹最小，最终堆里是前 K 大。桶排序能做 O(n)，但堆更通用。</li></ul><h2 id="295-数据流的中位数">295. 数据流的中位数</h2><blockquote><p>数据流动态加入数字，随时能取中位数。<code>addNum</code>/<code>findMedian</code> 平均 O(log n)。</p></blockquote><p><strong>思路脉络</strong>：单有序结构取中位数 O(1) 但插入 O(n)。用<strong>两个堆</strong>：大顶堆 <code>lo</code> 装较小的一半（堆顶是这半的最大）、小顶堆 <code>hi</code> 装较大的一半（堆顶是这半的最小）。平衡两堆规模差 ≤1，中位数由两堆顶决定。</p><p>加入逻辑：先入 <code>lo</code>，把 <code>lo</code> 最大推到 <code>hi</code>，再若 <code>lo</code> 比 <code>hi</code> 少则从 <code>hi</code> 回补一个。保证 <code>lo.size &gt;= hi.size</code>。</p><div class="mermaid">graph LR    A[&quot;加入 num&quot;] --&gt; B[&quot;先入大顶堆 lo&quot;]    B --&gt; C[&quot;lo 最大 → 小顶堆 hi&quot;]    C --&gt; D{&quot;lo 比 hi 少?&quot;}    D -- 是 --&gt; E[&quot;hi 最小 → lo&quot;]    D -- 否 --&gt; F[&quot;平衡完成&quot;]    E --&gt; F    F --&gt; G[&quot;中位数 = lo 顶 或 两顶平均&quot;]</div><pre><code class="language-java">class MedianFinder {    PriorityQueue&lt;Integer&gt; lo = new PriorityQueue&lt;&gt;(Collections.reverseOrder()); // 大顶堆    PriorityQueue&lt;Integer&gt; hi = new PriorityQueue&lt;&gt;();                            // 小顶堆    public void addNum(int num) {        lo.offer(num);        hi.offer(lo.poll());      // lo 最大给 hi        if (lo.size() &lt; hi.size()) lo.offer(hi.poll()); // 平衡: hi 回补 lo    }    public double findMedian() {        return lo.size() &gt; hi.size() ? lo.peek() : (lo.peek() + hi.peek()) / 2.0;    }}</code></pre><blockquote><p>[!info] 复杂度<br /><code>addNum</code> O(log n)，<code>findMedian</code> O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>平衡方向：保证 <code>lo.size &gt;= hi.size</code>，奇数时中位数取 <code>lo.peek</code>。若反过来保证 <code>hi</code> 多，奇数取 <code>hi.peek</code>，要和 <code>findMedian</code> 一致。<code>lo.poll()</code> 给 <code>hi</code> 这步保证&quot;小半的最大 ≤ 大半的最小&quot;的不变量。</li></ul><hr /><h1 id="14-贪心算法">14. 贪心算法</h1><p>贪心每步选&quot;当前最优&quot;，赌局部最优能拼出全局最优。关键是<strong>证明&quot;换一种选法不会更好&quot;</strong>，否则贪心可能错。不像 DP 有通用框架，贪心要靠对具体问题的洞察。</p><h2 id="121-买卖股票的最佳时机">121. 买卖股票的最佳时机</h2><blockquote><p>一次买卖，求最大利润。</p></blockquote><p><strong>思路脉络</strong>：遍历时维护&quot;到当前为止的最低价&quot; <code>minPrice</code>，每天算&quot;今天卖能赚多少&quot; = <code>price - minPrice</code>，取最大。一次遍历，O(n)。</p><pre><code class="language-java">class Solution {    public int maxProfit(int[] prices) {        int minPrice = Integer.MAX_VALUE, ans = 0;        for (int p : prices) {            minPrice = Math.min(minPrice, p);            ans = Math.max(ans, p - minPrice);        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 关联</p></blockquote><ul><li>这题是&quot;一次买卖&quot;特例。[[#122. 买卖股票的最佳时机 II]]（无限次买卖）贪心更直接：只要今天比昨天高就累加差值。[[#309. 最佳买卖股票含冷冻期]]、[[#188. 买卖股票的最佳时机 IV]] 才需 DP。</li></ul><h2 id="55-跳跃游戏">55. 跳跃游戏</h2><blockquote><p>数组每格表示最大跳跃步数，判断能否跳到末尾。</p></blockquote><p><strong>思路脉络</strong>：维护&quot;当前能到达的最远位置&quot; <code>maxReach</code>。遍历时若 <code>i &gt; maxReach</code> 说明走不到这里，返回 false；否则更新 <code>maxReach = max(maxReach, i + nums[i])</code>，能覆盖末尾即 true。</p><div class="mermaid">graph LR    A[&quot;遍历 i&quot;] --&gt; B{&quot;i &gt; maxReach?&quot;}    B -- 是 --&gt; C[&quot;走不到, false&quot;]    B -- 否 --&gt; D[&quot;maxReach = max(maxReach, i+nums[i])&quot;]    D --&gt; E{&quot;maxReach ≥ 末尾?&quot;}    E -- 是 --&gt; F[&quot;true&quot;]    E -- 否 --&gt; A</div><pre><code class="language-java">class Solution {    public boolean canJump(int[] nums) {        int maxReach = 0;        for (int i = 0; i &lt; nums.length; i++) {            if (i &gt; maxReach) return false;            maxReach = Math.max(maxReach, i + nums[i]);        }        return true;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>判断 <code>i &gt; maxReach</code> 要在更新之前：先确认当前位置可达，才能从这里继续跳。漏判会误把不可达当可达。</li></ul><h2 id="45-跳跃游戏-ii">45. 跳跃游戏 II</h2><blockquote><p>保证能到末尾，求最少跳跃次数。</p></blockquote><p><strong>思路脉络</strong>：BFS 思想的贪心。维护当前跳跃能覆盖的范围 <code>[l, r]</code>，下一步能到的最远是这范围内所有格子的 <code>i+nums[i]</code> 最大值。每跳一步，<code>l = r+1</code>、<code>r = nextMax</code>，步数加一，直到 <code>r</code> 覆盖末尾。</p><div class="mermaid">graph LR    A[&quot;l=0,r=0,步数=0&quot;] --&gt; B[&quot;扫 l..r 算 nextMax&quot;]    B --&gt; C[&quot;步数++, l=r+1, r=nextMax&quot;]    C --&gt; D{&quot;r ≥ 末尾?&quot;}    D -- 是 --&gt; E[&quot;返回步数&quot;]    D -- 否 --&gt; B</div><pre><code class="language-java">class Solution {    public int jump(int[] nums) {        int steps = 0, l = 0, r = 0;        while (r &lt; nums.length - 1) {            int nextMax = 0;            for (int i = l; i &lt;= r; i++) nextMax = Math.max(nextMax, i + nums[i]);            l = r + 1;            r = nextMax;            steps++;        }        return steps;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 直觉</p></blockquote><ul><li>把每一步能到的范围当成 BFS 的一层，步数就是层数。这视角让&quot;最少跳几步&quot;自然映射成&quot;BFS 几层到终点&quot;。</li></ul><h2 id="763-划分字母区间">763. 划分字母区间</h2><blockquote><p>把字符串切成尽量多的段，使每段内每个字母只出现在该段。</p></blockquote><p><strong>思路脉络</strong>：每个字母最后一次出现的位置是该段必须延伸到的下限。遍历时维护&quot;当前段的最远边界&quot; <code>end = max(end, last[c])</code>，当 <code>i == end</code> 时说明当前段所有字母都已在段内，可切。</p><pre><code class="language-java">class Solution {    public List&lt;Integer&gt; partitionLabels(String s) {        int[] last = new int[26];        for (int i = 0; i &lt; s.length(); i++) last[s.charAt(i) - 'a'] = i; // 记录每个字母最后位置        List&lt;Integer&gt; res = new ArrayList&lt;&gt;();        int start = 0, end = 0;        for (int i = 0; i &lt; s.length(); i++) {            end = Math.max(end, last[s.charAt(i) - 'a']);            if (i == end) { res.add(i - start + 1); start = i + 1; } // 可切        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;区间合并/分割&quot;用&quot;最远边界&quot;统一处理：[[#56. 合并区间]] 按 end 排序后取 max，本题按 last 取 max。核心都是&quot;用一个边界变量吸收所有约束&quot;。</li></ul><hr /><h1 id="15-动态规划">15. 动态规划</h1><p>DP 的核心三问，每题先想清楚再写代码：</p><ol><li><strong>状态定义</strong>：<code>dp[i]</code>（或 <code>dp[i][j]</code>）代表什么？定义错了全盘错。</li><li><strong>转移方程</strong>：<code>dp[i]</code> 由哪些更小的子问题推出？</li><li><strong>边界与遍历顺序</strong>：初始值填什么？按什么顺序填保证依赖已就绪？</li></ol><blockquote><p>[!tip] 空间优化<br />一维 DP 只依赖前几项 → 压成两三个变量。二维 DP 只依赖上一行 → 压成一维数组。先写对再优化。</p></blockquote><h2 id="70-爬楼梯">70. 爬楼梯</h2><blockquote><p>每次 1 或 2 阶，爬到第 n 阶有几种方法。</p></blockquote><p><strong>思路脉络</strong>：到第 n 阶只能从 n−1 阶爬一步或 n−2 阶爬两步，所以 <code>dp[n] = dp[n-1] + dp[n-2]</code>——斐波那契。边界 <code>dp[0]=1</code>（不动算 1 种）、<code>dp[1]=1</code>。</p><pre><code class="language-java">class Solution {    public int climbStairs(int n) {        if (n &lt;= 2) return n;        int a = 1, b = 2;        for (int i = 3; i &lt;= n; i++) { int c = a + b; a = b; b = c; }        return b;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;几种方法&quot;类计数 DP，转移是&quot;方案数相加&quot;。和&quot;最优值&quot;类 DP（转移取 max/min）区分。</li></ul><h2 id="118-杨辉三角">118. 杨辉三角</h2><blockquote><p>生成前 <code>n</code> 行杨辉三角。</p></blockquote><p><strong>思路脉络</strong>：每行首尾是 1，中间 <code>dp[i][j] = dp[i-1][j-1] + dp[i-1][j]</code>。逐行生成。</p><pre><code class="language-java">class Solution {    public List&lt;List&lt;Integer&gt;&gt; generate(int n) {        List&lt;List&lt;Integer&gt;&gt; res = new ArrayList&lt;&gt;();        for (int i = 0; i &lt; n; i++) {            List&lt;Integer&gt; row = new ArrayList&lt;&gt;();            for (int j = 0; j &lt;= i; j++) {                if (j == 0 || j == i) row.add(1);                else row.add(res.get(i - 1).get(j - 1) + res.get(i - 1).get(j));            }            res.add(row);        }        return res;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(n²)。</p></blockquote><h2 id="198-打家劫舍">198. 打家劫舍</h2><blockquote><p>偷一排房子，相邻不能同偷，求最大金额。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i]</code> = 前 i 间能偷的最大金额。偷第 i 间则不能偷 i−1：<code>dp[i] = dp[i-2] + nums[i]</code>；不偷则 <code>dp[i] = dp[i-1]</code>。取大者。</p><pre><code class="language-java">class Solution {    public int rob(int[] nums) {        int prev2 = 0, prev1 = 0;        for (int n : nums) {            int cur = Math.max(prev1, prev2 + n);            prev2 = prev1; prev1 = cur;        }        return prev1;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 关联</p></blockquote><ul><li>[[#213. 打家劫舍 II]]（环状）：分两种情况偷不偷首，各跑一次线性版本取 max。[[#337. 打家劫舍 III]]（树形）：树形 DP，每个节点返回&quot;偷/不偷&quot;两个值。</li></ul><h2 id="279-完全平方数">279. 完全平方数</h2><blockquote><p>把 n 表示成最少几个完全平方数之和。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i]</code> = 和为 i 的最少平方数个数。转移：枚举某个平方数 <code>j*j</code>，<code>dp[i] = min(dp[i], dp[i-j*j] + 1)</code>。这本质是<strong>完全背包</strong>——每个平方数可无限用，求装满 i 的最少件数。</p><pre><code class="language-java">class Solution {    public int numSquares(int n) {        int[] dp = new int[n + 1];        Arrays.fill(dp, Integer.MAX_VALUE);        dp[0] = 0;        for (int i = 1; i &lt;= n; i++)            for (int j = 1; j * j &lt;= i; j++)                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);        return dp[n];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n√n)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;用若干物品凑成目标和/容量&quot;是背包问题。完全背包（物品无限）内层正序遍历容量，0-1 背包（每物一件）内层逆序。[[#322. 零钱兑换]] 同模板。</li></ul><h2 id="322-零钱兑换">322. 零钱兑换</h2><blockquote><p>用若干面额硬币凑成 amount 的最少枚数。</p></blockquote><p><strong>思路脉络</strong>：完全背包。<code>dp[i]</code> = 凑成金额 i 的最少硬币数。<code>dp[i] = min(dp[i], dp[i-coin] + 1)</code>。初始化 <code>dp[0]=0</code>，其余 <code>MAX_VALUE</code>。</p><pre><code class="language-java">class Solution {    public int coinChange(int[] coins, int amount) {        int[] dp = new int[amount + 1];        Arrays.fill(dp, amount + 1);  // 用不可达大值代替 MAX, 避免+1溢出        dp[0] = 0;        for (int i = 1; i &lt;= amount; i++)            for (int c : coins)                if (c &lt;= i) dp[i] = Math.min(dp[i], dp[i - c] + 1);        return dp[amount] &gt; amount ? -1 : dp[amount];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(amount × 种类数)，空间 O(amount)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>初始化用 <code>amount+1</code> 而非 <code>Integer.MAX_VALUE</code>：因为 <code>MAX+1</code> 会溢出成负数。<code>amount+1</code> 是&quot;硬币最多 amount 枚（全用 1 元）&quot;的上界，超过即不可达。</li></ul><h2 id="139-单词拆分">139. 单词拆分</h2><blockquote><p>字符串能否拆成字典里的若干词。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i]</code> = 前 i 个字符能否拆分。转移：枚举 j，若 <code>dp[j]</code> 为真且 <code>s[j..i)</code> 在字典，则 <code>dp[i] = true</code>。这题 DP 而非贪心。</p><pre><code class="language-java">class Solution {    public boolean wordBreak(String s, List&lt;String&gt; wordDict) {        Set&lt;String&gt; dict = new HashSet&lt;&gt;(wordDict);        boolean[] dp = new boolean[s.length() + 1];        dp[0] = true;        for (int i = 1; i &lt;= s.length(); i++)            for (int j = 0; j &lt; i; j++)                if (dp[j] &amp;&amp; dict.contains(s.substring(j, i))) { dp[i] = true; break; }        return dp[s.length()];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n² × 子串哈希)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>内层 <code>break</code> 找到一个就停，避免无谓循环。子串 <code>substring(j, i)</code> 是 <code>[j, i)</code> 左闭右开。</li></ul><h2 id="300-最长递增子序列">300. 最长递增子序列</h2><blockquote><p>找最长严格递增子序列长度。</p></blockquote><p><strong>思路脉络</strong>：两种。</p><p><strong>DP O(n²)</strong>：<code>dp[i]</code> = 以 <code>nums[i]</code> 结尾的 LIS 长度。对所有 <code>j &lt; i</code> 且 <code>nums[j] &lt; nums[i]</code>，<code>dp[i] = max(dp[i], dp[j]+1)</code>。</p><p><strong>二分 O(n log n)</strong>：维护 <code>tails</code> 数组，<code>tails[k]</code> = 长度为 k+1 的递增子序列的最小末尾。每来一个数，二分找第一个 ≥ 它的位置替换。<code>tails</code> 长度即 LIS。</p><div class="mermaid">graph LR    A[&quot;遍历 num&quot;] --&gt; B[&quot;二分找 tails 中第一个 ≥ num 的位置 p&quot;]    B --&gt; C[&quot;tails[p] = num&quot;]    C --&gt; D[&quot;LIS 长度 = tails.length&quot;]    D --&gt; A</div><pre><code class="language-java">class Solution {    public int lengthOfLIS(int[] nums) {        int[] tails = new int[nums.length];        int len = 0;        for (int num : nums) {            int l = 0, r = len;            while (l &lt; r) {                            // 找第一个 ≥ num                int m = (l + r) / 2;                if (tails[m] &lt; num) l = m + 1; else r = m;            }            tails[l] = num;            if (l == len) len++;        }        return len;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n log n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>tails</code> 存的不是真实子序列，而是&quot;各长度最小末尾&quot;。替换保证后续更容易接上更长的，是贪心+二分的精妙。求具体序列要回溯，不能直接读 <code>tails</code>。</li></ul><h2 id="152-乘积最大子数组">152. 乘积最大子数组</h2><blockquote><p>找乘积最大的连续子数组，含负数。</p></blockquote><p><strong>思路脉络</strong>：[[#53. 最大子数组和]] 的和变积，但负数让&quot;最大&quot;和&quot;最小&quot;会翻转——一个负数乘&quot;最小负积&quot;反而变最大。所以同时维护 <code>maxProd</code> 和 <code>minProd</code>，遇负数两者交换。</p><pre><code class="language-java">class Solution {    public int maxProduct(int[] nums) {        int maxP = nums[0], minP = nums[0], ans = nums[0];        for (int i = 1; i &lt; nums.length; i++) {            int n = nums[i];            if (n &lt; 0) { int t = maxP; maxP = minP; minP = t; } // 负数翻转            maxP = Math.max(n, maxP * n);            minP = Math.min(n, minP * n);            ans = Math.max(ans, maxP);        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>遇负数先交换 <code>maxP</code>/<code>minP</code> 再算，否则用旧的 <code>maxP*负数</code> 得到的是更小值，逻辑错。<code>max(n, maxP*n)</code> 处理&quot;从头开始新子数组&quot;的情况。</li></ul><h2 id="416-分割等和子集">416. 分割等和子集</h2><blockquote><p>能否把数组分成两份，使两份和相等。</p></blockquote><p><strong>思路脉络</strong>：总和 <code>sum</code> 奇数则不行。偶数则问&quot;能否挑若干元素凑成 <code>sum/2</code>&quot;——0-1 背包。<code>dp[i]</code> = 能否凑出和 i。每来一个数，从大到小更新（0-1 背包逆序，保证每个用一次）。</p><pre><code class="language-java">class Solution {    public boolean canPartition(int[] nums) {        int sum = 0;        for (int n : nums) sum += n;        if (sum % 2 != 0) return false;        int target = sum / 2;        boolean[] dp = new boolean[target + 1];        dp[0] = true;        for (int n : nums)            for (int i = target; i &gt;= n; i--)     // 逆序, 0-1 背包                dp[i] = dp[i] || dp[i - n];        return dp[target];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n × target)，空间 O(target)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>0-1 背包内层<strong>逆序</strong>：若正序，一个数会被用多次（变成完全背包）。逆序保证 <code>dp[i-n]</code> 还是上一轮的值，每物只用一次。</li></ul><h2 id="32-最长有效括号">32. 最长有效括号</h2><blockquote><p>最长合法括号子串长度。</p></blockquote><p><strong>思路脉络</strong>：DP。<code>dp[i]</code> = 以 <code>i</code> 结尾的最长有效长度。<code>s[i]==')'</code> 时：</p><ul><li><code>s[i-1]=='('</code>：配对，<code>dp[i] = dp[i-2] + 2</code>。</li><li><code>s[i-1]==')'</code> 且 <code>s[i - dp[i-1] - 1]=='('</code>：与前面一个 <code>)</code> 对应的 <code>(</code> 配对，<code>dp[i] = dp[i-1] + dp[i - dp[i-1] - 2] + 2</code>。</li></ul><pre><code class="language-java">class Solution {    public int longestValidParentheses(String s) {        int[] dp = new int[s.length()];        int ans = 0;        for (int i = 1; i &lt; s.length(); i++) {            if (s.charAt(i) == ')') {                if (s.charAt(i - 1) == '(') {                    dp[i] = (i &gt;= 2 ? dp[i - 2] : 0) + 2;                } else if (dp[i - 1] &gt; 0) {                    int j = i - dp[i - 1] - 1;             // 跨过内部有效段找配对 (                    if (j &gt;= 0 &amp;&amp; s.charAt(j) == '(') {                        dp[i] = dp[i - 1] + 2 + (j &gt;= 2 ? dp[j - 1] : 0);                    }                }                ans = Math.max(ans, dp[i]);            }        }        return ans;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(n)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>第二种情况要&quot;跨过 <code>dp[i-1]</code> 这段有效子串&quot;找前面的 <code>(</code>，索引 <code>i - dp[i-1] - 1</code>。再加 <code>dp[j-1]</code> 把前面相连的有效段也并进来。栈法（存下标）更直观但 DP 法空间一致。</li></ul><hr /><h1 id="16-多维动态规划">16. 多维动态规划</h1><p>状态升到二维：<code>dp[i][j]</code> 通常表示&quot;从起点到 <code>(i,j)</code>&quot;或&quot;两个序列的前缀&quot;的解。难点在<strong>状态含义</strong>和<strong>遍历顺序</strong>——依赖关系跨行跨列，要确保填表时依赖项已就绪。</p><h2 id="62-不同路径">62. 不同路径</h2><blockquote><p><code>m×n</code> 网格左上到右下，每次只往右或下，几种走法。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i][j]</code> = 到 <code>(i,j)</code> 的走法数。只能从左或上来，<code>dp[i][j] = dp[i-1][j] + dp[i][j-1]</code>。首行首列全 1。可压缩成一维。</p><pre><code class="language-java">class Solution {    public int uniquePaths(int m, int n) {        int[] dp = new int[n];        Arrays.fill(dp, 1);        for (int i = 1; i &lt; m; i++)            for (int j = 1; j &lt; n; j++)                dp[j] += dp[j - 1];        // dp[j]=上, dp[j-1]=左        return dp[n - 1];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(n)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>网格 DP 一律&quot;从左上来&quot;。压缩成一维时，<code>dp[j]</code> 在更新前是&quot;上一行同列&quot;（上），<code>dp[j-1]</code> 是&quot;本行左列&quot;（左），正好凑齐。</li></ul><h2 id="64-最小路径和">64. 最小路径和</h2><blockquote><p>网格每格有代价，左上到右下路径最小代价和。</p></blockquote><p><strong>思路脉络</strong>：和 [[#62. 不同路径]] 同框架，把&quot;方案数相加&quot;换成&quot;代价取 min&quot;。<code>dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]</code>。原地修改 grid 可省空间。</p><pre><code class="language-java">class Solution {    public int minPathSum(int[][] grid) {        int m = grid.length, n = grid[0].length;        for (int i = 0; i &lt; m; i++)            for (int j = 0; j &lt; n; j++) {                if (i == 0 &amp;&amp; j == 0) continue;                else if (i == 0) grid[i][j] += grid[i][j - 1];                else if (j == 0) grid[i][j] += grid[i - 1][j];                else grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);            }        return grid[m - 1][n - 1];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(1)（原地）。</p></blockquote><h2 id="5-最长回文子串">5. 最长回文子串</h2><blockquote><p>找最长回文子串。</p></blockquote><p><strong>思路脉络</strong>：<strong>中心扩展法</strong>最直观。回文以中心对称，中心可以是单字符（奇数长）或两字符间隙（偶数长）。对每个中心向两侧扩展，记录最长。</p><p>DP 法：<code>dp[i][j]</code> 表示 <code>s[i..j]</code> 是否回文，<code>dp[i][j] = s[i]==s[j] &amp;&amp; dp[i+1][j-1]</code>。O(n²) 但常数大、空间 O(n²)，不如中心扩展。</p><div class="mermaid">graph LR    A[&quot;每个中心 i&quot;] --&gt; B[&quot;奇数: 从 i 向两侧扩&quot;]    A --&gt; C[&quot;偶数: 从 i,i+1 向两侧扩&quot;]    B --&gt; D[&quot;记录最长起止&quot;]    C --&gt; D    D --&gt; E[&quot;返回最长子串&quot;]</div><pre><code class="language-java">class Solution {    public String longestPalindrome(String s) {        int st = 0, max = 1;        for (int i = 0; i &lt; s.length(); i++) {            int l1 = expand(s, i, i), l2 = expand(s, i, i + 1);            int len = Math.max(l1, l2);            if (len &gt; max) { max = len; st = i - (len - 1) / 2; }        }        return s.substring(st, st + max);    }    int expand(String s, int l, int r) {        while (l &gt;= 0 &amp;&amp; r &lt; s.length() &amp;&amp; s.charAt(l) == s.charAt(r)) { l--; r++; }        return r - l - 1;                  // 扩过头了, 回退一步的真实长度    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n²)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li><code>expand</code> 返回 <code>r - l - 1</code>：循环退出时 <code>l</code>、<code>r</code> 已是不匹配的位置，真实回文是 <code>(l+1, r-1)</code>，长度 <code>r-1 - (l+1) + 1 = r - l - 1</code>。起点 <code>i - (len-1)/2</code> 对奇偶都成立。</li></ul><h2 id="1143-最长公共子序列">1143. 最长公共子序列</h2><blockquote><p>两字符串的最长公共子序列长度。</p></blockquote><p><strong>思路脉络</strong>：<code>dp[i][j]</code> = <code>s1[0..i]</code> 与 <code>s2[0..j]</code> 的 LCS。字符相同则 <code>dp[i][j] = dp[i-1][j-1] + 1</code>；不同则 <code>dp[i][j] = max(dp[i-1][j], dp[i][j-1])</code>。</p><div class="mermaid">graph LR    A[&quot;dp[i][j]&quot;] --&gt; B{&quot;s1[i]==s2[j]?&quot;}    B -- 是 --&gt; C[&quot;dp[i-1][j-1] + 1&quot;]    B -- 否 --&gt; D[&quot;max(dp[i-1][j], dp[i][j-1])&quot;]</div><pre><code class="language-java">class Solution {    public int longestCommonSubsequence(String t1, String t2) {        int m = t1.length(), n = t2.length();        int[][] dp = new int[m + 1][n + 1];        for (int i = 1; i &lt;= m; i++)            for (int j = 1; j &lt;= n; j++) {                if (t1.charAt(i - 1) == t2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;                else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);            }        return dp[m][n];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(mn)，可压缩到 O(min(m,n))。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>&quot;两序列&quot;DP 通用模板：相同加 1、不同取两边 max。[[#72. 编辑距离]]、[[#583. 两个字符串的删除操作]] 都在这框架上加权。下标用 <code>i-1</code>、<code>j-1</code> 对应字符，留出 <code>dp[0][*]=0</code> 边界。</li></ul><h2 id="72-编辑距离">72. 编辑距离</h2><blockquote><p>把 <code>word1</code> 变成 <code>word2</code> 的最少操作（增/删/改）次数。</p></blockquote><p><strong>思路脉络</strong>：[[#1143. 最长公共子序列]] 的进阶。<code>dp[i][j]</code> = <code>w1[0..i]</code> 变成 <code>w2[0..j]</code> 的最少操作。</p><ul><li>字符相同：<code>dp[i][j] = dp[i-1][j-1]</code>（不用操作）。</li><li>不同：取三选一取 min：<ul><li><code>dp[i-1][j] + 1</code>：删 <code>w1[i]</code>。</li><li><code>dp[i][j-1] + 1</code>：增一个 <code>w2[j]</code>。</li><li><code>dp[i-1][j-1] + 1</code>：把 <code>w1[i]</code> 改成 <code>w2[j]</code>。</li></ul></li></ul><p>边界：<code>dp[i][0] = i</code>（全删）、<code>dp[0][j] = j</code>（全增）。</p><blockquote><p>[!example] 二维 DP 表逐格填充（动图）<br /><code>ab</code>→<code>abc</code>：先填边界(全删/全增)，再逐格推。字符相同取左上 <code>dp[i-1][j-1]</code>；不同取左/上/左上三者 min+1。<code>dp[2][3]=1</code>：末尾增一个 c 即可。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0072-edit-distance.gif" alt="0072-edit-distance" /></p></blockquote><div class="mermaid">graph LR    A[&quot;dp[i][j]&quot;] --&gt; B{&quot;w1[i-1]==w2[j-1]?&quot;}    B -- 是 --&gt; C[&quot;dp[i-1][j-1]&quot;]    B -- 否 --&gt; D[&quot;删 dp[i-1][j]+1&quot;]    B -- 否 --&gt; E[&quot;增 dp[i][j-1]+1&quot;]    B -- 否 --&gt; F[&quot;改 dp[i-1][j-1]+1&quot;]    D --&gt; G[&quot;三者取 min&quot;]    E --&gt; G    F --&gt; G</div><pre><code class="language-java">class Solution {    public int minDistance(String w1, String w2) {        int m = w1.length(), n = w2.length();        int[][] dp = new int[m + 1][n + 1];        for (int i = 0; i &lt;= m; i++) dp[i][0] = i;        for (int j = 0; j &lt;= n; j++) dp[0][j] = j;        for (int i = 1; i &lt;= m; i++)            for (int j = 1; j &lt;= n; j++) {                if (w1.charAt(i - 1) == w2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];                else dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;            }        return dp[m][n];    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(mn)，空间 O(mn)，可压缩到 O(n)（需保留上一行和左上角）。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>压缩成一维时，<code>dp[i-1][j-1]</code>（左上角）会被 <code>dp[j-1]</code> 的更新覆盖，要用 <code>prev</code> 变量暂存上一轮的 <code>dp[j-1]</code>。这是二维 DP 压缩的经典坑。</li></ul><blockquote><p>[!tip] 关联</p></blockquote><ul><li>编辑距离是&quot;两序列 DP&quot;的集大成者，三种操作对应三个方向。理解了它，[[#583. 两个字符串的删除操作]]（只删不改）、[[#712. 两个字符串的最小 ASCII 删除和]]（带权删）都是降维版本。</li></ul><hr /><h1 id="17-技巧">17. 技巧</h1><p>这类题靠特定 trick：位运算、摩尔投票、下标原地哈希、下一个排列的&quot;魔法步骤&quot;。trick 背后都有数学或不变量支撑，记住推导而非死记步骤。</p><h2 id="136-只出现一次的数字">136. 只出现一次的数字</h2><blockquote><p>数组里除一个数外每个出现两次，找那个出现一次的。O(n) 时间 O(1) 空间。</p></blockquote><p><strong>思路脉络</strong>：异或 <code>^</code> 的性质：<code>a ^ a = 0</code>、<code>a ^ 0 = a</code>、交换结合。把所有数异或起来，成对的抵消成 0，剩下就是那个只出现一次的。</p><pre><code class="language-java">class Solution {    public int singleNumber(int[] nums) {        int x = 0;        for (int n : nums) x ^= n;        return x;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!tip] 套路</p></blockquote><ul><li>异或处理&quot;成对抵消&quot;。[[#137. 只出现一次的数字 II]]（其他出现三次）用位统计：每位对 3 取模。位运算是这类题的统一工具。</li></ul><h2 id="169-多数元素">169. 多数元素</h2><blockquote><p>找出现超过 <code>⌊n/2⌋</code> 次的元素，保证存在。O(n) 时间 O(1) 空间。</p></blockquote><p><strong>思路脉络</strong>：<strong>摩尔投票</strong>。想象两军对战，同阵营人数 +1、异阵营人数 −1，人数归零就换候选。多数派超过半数，最终剩下的候选一定是它。正确性来自&quot;多数派抵消掉所有少数派后还有盈余&quot;。</p><div class="mermaid">graph LR    A[&quot;遍历 n&quot;] --&gt; B{&quot;n == 候选?&quot;}    B -- 是 --&gt; C[&quot;count++&quot;]    B -- 否 --&gt; D[&quot;count--&quot;]    D --&gt; E{&quot;count==0?&quot;}    E -- 是 --&gt; F[&quot;换候选 = n, count=1&quot;]    E -- 否 --&gt; A    C --&gt; A    F --&gt; A</div><pre><code class="language-java">class Solution {    public int majorityElement(int[] nums) {        int cand = 0, cnt = 0;        for (int n : nums) {            if (cnt == 0) { cand = n; cnt = 1; }            else if (n == cand) cnt++;            else cnt--;        }        return cand;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>摩尔投票只在&quot;保证有多数元素（超过 n/2）&quot;时直接返回候选。不保证时需二次遍历验证计数。本题保证存在，免验证。</li></ul><h2 id="75-颜色分类">75. 颜色分类</h2><blockquote><p>数组只有 0/1/2，原地把它们排成升序，不能排序库。</p></blockquote><p><strong>思路脉络</strong>：<strong>荷兰国旗问题</strong>，三指针一遍扫描。<code>l</code> 指向 0 的右边界、<code>r</code> 指向 2 的左边界、<code>i</code> 扫描。遇 0 换到 <code>l</code>、遇 2 换到 <code>r</code>、遇 1 跳过。换 2 后 <code>i</code> 不动（因为换回来的还没看）。</p><blockquote><p>[!example] 荷兰国旗三指针（动图）<br />三区随扫描成型：<code>[0,l)</code> 全 0、<code>[l,i)</code> 全 1、<code>(r,n)</code> 全 2。遇 2 换到 r 后 <code>i</code> 不动是关键——换回的值还没判断；遇 0 换到 l 后 <code>i++</code> 安全。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2026/07/0075-sort-colors.gif" alt="0075-sort-colors" /></p></blockquote><div class="mermaid">graph LR    A[&quot;i 扫描&quot;] --&gt; B{&quot;nums[i]?&quot;}    B -- 0 --&gt; C[&quot;与 l 交换, l++, i++&quot;]    B -- 1 --&gt; D[&quot;i++&quot;]    B -- 2 --&gt; E[&quot;与 r 交换, r--&quot;]    E --&gt; F[&quot;i 不动, 看换回来的&quot;]    F --&gt; B</div><pre><code class="language-java">class Solution {    public void sortColors(int[] nums) {        int l = 0, r = nums.length - 1, i = 0;        while (i &lt;= r) {            if (nums[i] == 0) { swap(nums, i++, l++); }            else if (nums[i] == 2) { swap(nums, i, r--); } // i 不动            else i++;        }    }    void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>换 2 后 <code>i</code> 不自增：从 <code>r</code> 换回来的可能是 0/1/2 任意值，得重新判断。换 0 后 <code>i++</code> 安全，因为换到 <code>i</code> 的是 <code>l</code> 位置（<code>l &lt;= i</code>，那里已是 1 或就是 <code>i</code> 自己）。</li></ul><h2 id="31-下一个排列">31. 下一个排列</h2><blockquote><p>把数组重排成字典序的下一个排列，没有更大排列就升序。</p></blockquote><p><strong>思路脉络</strong>：四步法，每步都有依据：</p><ol><li>从右找第一个&quot;降序对&quot; <code>i</code>（<code>nums[i] &lt; nums[i+1]</code>）——这是能变大的最低位。</li><li>从右找第一个比 <code>nums[i]</code> 大的 <code>j</code>——这是比原数大的最小替换。</li><li>交换 <code>i</code>、<code>j</code>。</li><li>反转 <code>i+1</code> 到末尾——降序变升序，让后缀最小。</li></ol><div class="mermaid">graph LR    A[&quot;从右找降序对 i&quot;] --&gt; B{&quot;找到?&quot;}    B -- 否 --&gt; C[&quot;整体反转(最小排列)&quot;]    B -- 是 --&gt; D[&quot;从右找 &gt;nums[i] 的 j&quot;]    D --&gt; E[&quot;交换 i,j&quot;]    E --&gt; F[&quot;反转 i+1..末尾&quot;]</div><pre><code class="language-java">class Solution {    public void nextPermutation(int[] nums) {        int n = nums.length, i = n - 2;        while (i &gt;= 0 &amp;&amp; nums[i] &gt;= nums[i + 1]) i--;      // 找降序对        if (i &gt;= 0) {            int j = n - 1;            while (nums[j] &lt;= nums[i]) j--;                 // 找比 nums[i] 大的            swap(nums, i, j);        }        reverse(nums, i + 1, n - 1);                        // 反转后缀    }    void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }    void reverse(int[] a, int l, int r) { while (l &lt; r) swap(a, l++, r--); }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>找 <code>i</code> 用 <code>&gt;=</code>（要严格降序对的左端，跳过相等的）；找 <code>j</code> 用 <code>&lt;=</code>（要严格大于 <code>nums[i]</code>）。两个比较符号方向别搞反。</li></ul><h2 id="287-寻找重复数">287. 寻找重复数</h2><blockquote><p><code>n+1</code> 个数取值 <code>1..n</code>，有一个重复，找出来。不能改数组，O(1) 空间。</p></blockquote><p><strong>思路脉络</strong>：三种。</p><p><strong>二分</strong>：对值域 <code>1..n</code> 二分，统计 <code>≤mid</code> 的个数，超过 <code>mid</code> 个说明重复在左半。</p><p><strong>原地哈希</strong>：把 <code>i</code> 放到 <code>nums[i]</code>，冲突即重复。但会改数组，违规。</p><p><strong>快慢指针（推荐）</strong>：把数组看成&quot;i → nums[i]&quot;的链表，重复数意味着有环。复用 [[#142. 环形链表 II]] 的找环入口算法，环入口就是重复数。</p><pre><code class="language-java">class Solution {    public int findDuplicate(int[] nums) {        int slow = nums[0], fast = nums[nums[0]];        while (slow != fast) { slow = nums[slow]; fast = nums[nums[fast]]; } // 相遇        slow = 0;        while (slow != fast) { slow = nums[slow]; fast = nums[fast]; }      // 入口        return slow;    }}</code></pre><blockquote><p>[!info] 复杂度<br />时间 O(n)，空间 O(1)。</p></blockquote><blockquote><p>[!warning] 易错点</p></blockquote><ul><li>快慢指针能用的前提是&quot;值域 <code>1..n</code> 且索引 <code>0..n</code>&quot;——<code>nums[i]</code> 永远是合法索引，构成链表。若有 0 值就崩。快指针起步 <code>nums[nums[0]]</code> 而非 <code>nums[0]</code>，让快慢真正不同步。</li></ul><blockquote><p>[!tip] 关联</p></blockquote><ul><li>这是把&quot;链表找环&quot;迁移到&quot;数组下标链&quot;的妙用，和 [[#142. 环形链表 II]]、[[#202. 快乐数]] 同源：只要能定义&quot;i → f(i)&quot;的映射且存在环，快慢指针就能找入口。</li></ul><hr /><h2 id="写在最后">写在最后</h2><p>100 题刷完不是终点。建议二刷时关注三件事：</p><ol><li><strong>横向归类</strong>：把同套路的题放一起刷（如所有单调栈、所有两序列 DP），体会模板的复用。</li><li><strong>纵向追问</strong>：每题问&quot;还有别的解法吗&quot;&quot;数据范围变大怎么办&quot;，逼自己想优化。</li><li><strong>手写默码</strong>：挑高频题（两数之和、反转链表、LRU、全排列、二分模板）闭着眼能写出无 bug 代码，面试就稳了。</li></ol><!-- END -->]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Java工具框架收集]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/java-gong-ju-kuang-jia-shou-ji" />
                <id>tag:https://lymboy.com,2024-06-10:java-gong-ju-kuang-jia-shou-ji</id>
                <published>2024-06-10T13:11:03+08:00</published>
                <updated>2024-07-07T22:19:05+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=20744782&auto=1&height=66"></iframe><h1 id="%E6%95%8F%E6%84%9F%E8%AF%8D%E6%A3%80%E6%B5%8B" tabindex="-1">敏感词检测</h1><p><a href="https://github.com/houbb/sensitive-word.git" target="_blank">https://github.com/houbb/sensitive-word.git</a></p><h1 id="%E6%B1%89%E5%AD%97%E8%BD%AC%E6%8B%BC%E9%9F%B3" tabindex="-1">汉字转拼音</h1><p><a href="https://github.com/houbb/pinyin" target="_blank">https://github.com/houbb/pinyin</a></p><h1 id="%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D" tabindex="-1">中文分词</h1><p><a href="https://github.com/houbb/segment" target="_blank">https://github.com/houbb/segment</a></p><h1 id="%E7%AE%80%E7%B9%81%E8%BD%AC%E6%8D%A2" tabindex="-1">简繁转换</h1><p><a href="https://github.com/houbb/opencc4j" target="_blank">https://github.com/houbb/opencc4j</a></p><h1 id="%E6%B1%89%E5%AD%97%E7%9B%B8%E4%BC%BC%E6%80%A7" tabindex="-1">汉字相似性</h1><p><a href="https://github.com/houbb/nlp-hanzi-similar" target="_blank">https://github.com/houbb/nlp-hanzi-similar</a></p><h1 id="%E5%8D%95%E8%AF%8D%E6%8B%BC%E5%86%99%E6%A3%80%E6%9F%A5" tabindex="-1">单词拼写检查</h1><p><a href="https://github.com/houbb/word-checker" target="_blank">https://github.com/houbb/word-checker</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[垃圾收集算法]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/la-ji-shou-ji-suan-fa" />
                <id>tag:https://lymboy.com,2022-11-15:la-ji-shou-ji-suan-fa</id>
                <published>2022-11-15T16:43:37+08:00</published>
                <updated>2022-11-15T16:43:37+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E7%AE%97%E6%B3%95" tabindex="-1">垃圾收集算法</h3><p><strong>标记-清除</strong></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2022/11/15/a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg" alt="" /></p><p>将存活的对象进行标记，然后清理掉未被标记的对象。</p><p>不足:</p><ul><li>标记和清除过程效率都不高；</li><li>会产生大量不连续的内存碎片，导致无法给大对象分配内存。</li></ul><p><strong>标记-整理</strong></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2022/11/15/902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg" alt="" /></p><p>让所有存活的对象都向一端移动，然后直接清理掉端边界以外的内存。</p><p><strong>复制</strong></p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2022/11/15/e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg" alt="" /></p><p>将内存划分为大小相等的两块，每次只使用其中一块，当这一块内存用完了就将还存活的对象复制到另一块上面，然后再把使用过的内存空间进行一次清理。</p><p>主要不足是只使用了内存的一半。</p><p><strong>分代收集策略</strong></p><p>JDK1.8采用分代收集策略</p><pre><code class="language-shell">$ java -XX:+PrintCommandLineFlags -version...UseParallelGC = Parallel Scavenge + Parallel Old</code></pre><p><strong>分代收集</strong>是根据对象的存活时间把内存分为新生代和老年代，根据个代对象的存活特点，每个代采用不同的垃圾回收算法。新生代采用标记—复制算法，老年代采用标记—整理算法。</p><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2022/11/15/image-20221115151554366.png" alt="" /></p><pre><code class="language-java">import java.util.UUID;/** * @author 小怪兽 * @version 1.0 * @since 2022-11-15 */public class Jmap {    private byte[] buffer = new byte[500*1024];    private String h = UUID.randomUUID().toString();    public static void main(String[] args) throws Exception {        int n = 100000;        for (int i = 0; i &lt; n; i++) {            Jmap jmap = new Jmap();            Thread.sleep(10);        }    }}</code></pre><p>可以使用如下脚本监控jvm的内存使用情况</p><pre><code class="language-shell">pid=&#96;jps | grep Jmap | awk &#39;{print $1}&#39;&#96;echo PID: $pidwatch -n 2 -d &quot;jmap -heap $pid&quot;</code></pre><pre><code class="language-">Every 2.0s: jmap -heap 25834                                                                                                         Tue Nov 15 23:31:43 2022Attaching to process ID 25834, please wait...Debugger attached successfully.Server compiler detected.JVM version is 25.202-b08using thread-local object allocation.Parallel GC with 13 thread(s)Heap Configuration:   MinHeapFreeRatio         = 0   MaxHeapFreeRatio         = 100   MaxHeapSize              = 16846422016 (16066.0MB)   NewSize                  = 351272960 (335.0MB)   MaxNewSize               = 5615124480 (5355.0MB)   OldSize                  = 703594496 (671.0MB)   NewRatio                 = 2   SurvivorRatio            = 8   MetaspaceSize            = 21807104 (20.796875MB)   CompressedClassSpaceSize = 1073741824 (1024.0MB)   MaxMetaspaceSize         = 17592186044415 MB   G1HeapRegionSize         = 0 (0.0MB)Heap Usage:PS Young GenerationEden Space:   capacity = 264241152 (252.0MB)   used     = 95229184 (90.817626953125MB)     free     = 169011968 (161.182373046875MB)     36.038740854414684% usedFrom Space:   capacity = 43515904 (41.5MB)   used     = 0 (0.0MB)   free     = 43515904 (41.5MB)   0.0% usedTo Space:   capacity = 43515904 (41.5MB)   used     = 0 (0.0MB)   free     = 43515904 (41.5MB)   0.0% usedPS Old Generation   capacity = 703594496 (671.0MB)   used     = 0 (0.0MB)   free     = 703594496 (671.0MB)   0.0% used1070 interned Strings occupying 78624 bytes.</code></pre><pre><code class="language-">Heap Usage:PS Young GenerationEden Space:   capacity = 264241152 (252.0MB)   used     = 206210320 (196.65748596191406MB)   free     = 58030832 (55.34251403808594MB)   78.03868490552145% usedFrom Space:   capacity = 43515904 (41.5MB)   used     = 0 (0.0MB)   free     = 43515904 (41.5MB)   0.0% usedTo Space:   capacity = 43515904 (41.5MB)   used     = 0 (0.0MB)   free     = 43515904 (41.5MB)   0.0% usedPS Old Generation   capacity = 703594496 (671.0MB)   used     = 0 (0.0MB)   free     = 703594496 (671.0MB)   0.0% used1070 interned Strings occupying 78624 bytes.</code></pre><pre><code class="language-">Heap Usage:PS Young GenerationEden Space:   capacity = 264241152 (252.0MB)   used     = 264241152 (252.0MB)                free     = 0 (0.0MB)                        100.0% used            From Space:   capacity = 43515904 (41.5MB)   used     = 0 (0.0MB)   free     = 43515904 (41.5MB)   0.0% usedTo Space:   capacity = 43515904 (41.5MB)   used     = 0 (0.0MB)   free     = 43515904 (41.5MB)   0.0% usedPS Old Generation   capacity = 703594496 (671.0MB)   used     = 0 (0.0MB)   free     = 703594496 (671.0MB)   0.0% used1073 interned Strings occupying 78792 bytes.</code></pre><pre><code class="language-">Heap Usage:PS Young GenerationEden Space:   capacity = 264241152 (252.0MB)   used     = 10466912 (9.982025146484375MB)   free     = 253774240 (242.01797485351562MB)   3.961121089874752% usedFrom Space:   capacity = 43515904 (41.5MB)   used     = 524320 (0.500030517578125MB)   free     = 42991584 (40.999969482421875MB)   1.204892813441265% usedTo Space:   capacity = 43515904 (41.5MB)   used     = 0 (0.0MB)   free     = 43515904 (41.5MB)   0.0% usedPS Old Generation   capacity = 703594496 (671.0MB)   used     = 8192 (0.0078125MB)   free     = 703586304 (670.9921875MB)   0.0011643070044709389% used1058 interned Strings occupying 77752 bytes.</code></pre><pre><code class="language-">Heap Usage:PS Young GenerationEden Space:   capacity = 264241152 (252.0MB)   used     = 15956744 (15.217536926269531MB)   free     = 248284408 (236.78246307373047MB)   6.038705129472036% used From Space:   capacity = 43515904 (41.5MB)   used     = 425984 (0.40625MB)   free     = 43089920 (41.09375MB)   0.9789156626506024% usedTo Space:   capacity = 43515904 (41.5MB)   used     = 0 (0.0MB)   free     = 43515904 (41.5MB)   0.0% usedPS Old Generation   capacity = 703594496 (671.0MB)   used     = 8192 (0.0078125MB)   free     = 703586304 (670.9921875MB)   0.0011643070044709389% used1058 interned Strings occupying 77752 bytes.</code></pre><pre><code class="language-">Heap Usage:PS Young GenerationEden Space:   capacity = 257949696 (246.0MB)   used     = 20690464 (19.731964111328125MB)   free     = 237259232 (226.26803588867188MB)   8.021123622491107% usedFrom Space:   capacity = 524288 (0.5MB)   used     = 442384 (0.4218902587890625MB)   free     = 81904 (0.0781097412109375MB)   84.3780517578125% usedTo Space:   capacity = 1048576 (1.0MB)   used     = 0 (0.0MB)   free     = 1048576 (1.0MB)   0.0% usedPS Old Generation   capacity = 703594496 (671.0MB)   used     = 16384 (0.015625MB)   free     = 703578112 (670.984375MB)   0.0023286140089418777% used1058 interned Strings occupying 77752 bytes.</code></pre><pre><code class="language-">Heap Usage:PS Young GenerationEden Space:   capacity = 257949696 (246.0MB)   used     = 58366424 (55.662559509277344MB)   free     = 199583272 (190.33744049072266MB)   22.62705671108835% usedFrom Space:   capacity = 43515904 (41.5MB)   used     = 442384 (0.4218902587890625MB)   free     = 43073520 (41.07810974121094MB)   1.0166030332266567% usedTo Space:   capacity = 524288 (0.5MB)      used     = 0 (0.0MB)   free     = 524288 (0.5MB)      0.0% usedPS Old Generation   capacity = 703594496 (671.0MB)   used     = 24576 (0.0234375MB)   free     = 703569920 (670.9765625MB)   0.003492921013412817% used </code></pre><p>可以看出，创建新对象是总是在<code>Eden</code>区操作，随着不断<code>new</code>对象，<code>Eden</code>区内存使用不断上升，当<code>Eden</code>区空间使用达<code>100%</code>时会执行复制算法将对象复制到<code>From</code>区, 虽然<code>From</code>区的容量远小于<code>Eden</code>区但仍然使Eden的内存使用降到了<code>0%</code>，这是因为当Eden区满了之后会触发<code>YoungGC（Minor GC）</code>清理掉垃圾对象。</p><p>当再次触发<code>YoungGC（Minor GC）</code>并且<code>From</code>区满了之后会将存活对象复制到<code>To</code>区，就这样循环往复一定次数（默认15次）之后，存活的数据会复制到老年代（<code>Old Generation</code>）</p><p>当老年代的内存不足时会触发<code>Old GC(Major GC)</code></p><blockquote><p>从概念上讲Old GC(Major GC)指的是针对老年代的GC，Full GC指的是对整个堆空间的GC,不过也有说法是Major GC其实等价于Full GC，因为Major GC其实就是老年代空间不足，新生代的对象没法转移到老年代所以需要先对老年代GC，然后再对新生代GC，可以说Major GC的目的就是为了Minor GC，这其实就是Full GC了。</p></blockquote><blockquote><p>这里老年代空间不足指的探讨，不足指的是老年代空间使用100%还是超过了一定阈值？这个笔者尚没有看到相关定论，后续可以再深究。</p></blockquote><h4 id="%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99" tabindex="-1">参考资料</h4><ul><li><a href="https://blog.csdn.net/weixin_40739833/article/details/80717638" target="_blank">https://blog.csdn.net/weixin_40739833/article/details/80717638</a></li><li><a href="https://blog.csdn.net/yanghenan19870513/article/details/115669223" target="_blank">https://blog.csdn.net/yanghenan19870513/article/details/115669223</a></li><li><a href="https://pdai.tech/md/java/jvm/java-jvm-gc.html#full-gc-%E7%9A%84%E8%A7%A6%E5%8F%91%E6%9D%A1%E4%BB%B6" target="_blank">https://pdai.tech/md/java/jvm/java-jvm-gc.html#full-gc-的触发条件</a></li><li><a href="https://cloud.tencent.com/developer/article/1830320" target="_blank">https://cloud.tencent.com/developer/article/1830320</a></li><li><a href="https://blog.csdn.net/mccand1234/article/details/52078645" target="_blank">https://blog.csdn.net/mccand1234/article/details/52078645</a></li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[LEETCODE 817. 链表组件]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/leetcode817-lian-biao-zu-jian" />
                <id>tag:https://lymboy.com,2022-10-12:leetcode817-lian-biao-zu-jian</id>
                <published>2022-10-12T13:34:01+08:00</published>
                <updated>2022-10-12T13:34:01+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="ac%E4%BB%A3%E7%A0%81" tabindex="-1">AC代码</h2><pre><code class="language-java">class Solution {    /**     * 因为题目说链表节点值是不重复的，所以可以用HashSet存储数组中元素，便于查询     * flag 变量表示当前元素是否属于某个集合，true: 当前元素属于某个集合（组件）， false: 不属于某个集合（组件）     * @param head     * @param nums     * @return     */    public int numComponents(ListNode head, int[] nums) {        Set&lt;Integer&gt; set = new HashSet&lt;&gt;();        for (int num : nums) {            set.add(num);        }        boolean flag = false;        int ans = 0;        while (head != null) {            if (!set.contains(head.val)) {                flag = false;            }            if (set.contains(head.val) &amp;&amp; !flag) {                ans++;                flag = true;            }            head = head.next;        }        return ans;    }}</code></pre><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2022/10/12/image-20221012114530231.png" alt="" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[LeetCode 200. 岛屿数量]]></title>
                <link rel="alternate" type="text/html" href="https://lymboy.com/archives/leetcode200-dao-yu-shu-liang" />
                <id>tag:https://lymboy.com,2022-09-11:leetcode200-dao-yu-shu-liang</id>
                <published>2022-09-11T14:59:15+08:00</published>
                <updated>2022-09-11T14:59:15+08:00</updated>
                <author>
                    <name>sairo</name>
                    <uri>https://lymboy.com</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>链接：<a href="https://leetcode.cn/problems/number-of-islands/" target="_blank">200. 岛屿数量</a></p><p>深度优先搜索</p><h2 id="%E7%89%88%E6%9C%AC1" tabindex="-1">版本1</h2><pre><code class="language-java">class Solution {    // 上下左右四个方向遍历    int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};    // 记录行，列数    int m, n;    // 标记数组，判断是否已经访问过    boolean[][] visited = null;    public int numIslands(char[][] grid) {        if (grid == null || grid[0].length == 0) {            return 0;        }        int res = 0;        m = grid.length;        n = grid[0].length;        visited = new boolean[m][n];        for (int i = 0; i &lt; m; i++) {            for (int j = 0; j &lt; n; j++) {                if (grid[i][j] == &#39;1&#39; &amp;&amp; !visited[i][j]) {                    res++;                    dfs(grid, i, j);                }            }        }        return res;    }    /**     * @param grid     * @param x     * @param y     * @return     */    private boolean dfs(char[][] grid, int x, int y) {        // 如果当前遍历的字符是陆地，则继续访问后续位置        if (grid[x][y] == &#39;1&#39;) {            visited[x][y] = true;            // 依次从四个方向分别计算            for (int i = 0; i &lt; 4; i++) {                int newX = x + direction[i][0];                int newy = y + direction[i][1];                if (inArea(newX, newy) &amp;&amp; !visited[newX][newy]) {                    if (dfs(grid, newX, newy)) {                        return true;                    }                }            }        }        return false;    }    /**     * 判断当前坐标点是否越界     *     * @param x     * @param y     * @return     */    private boolean inArea(int x, int y) {        return x &gt;= 0 &amp;&amp; x &lt; m &amp;&amp; y &gt;= 0 &amp;&amp; y &lt; n;    }}</code></pre><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2022/09/11/image-20220911125042945.png" alt="" /></p><h2 id="%E7%89%88%E6%9C%AC2" tabindex="-1">版本2</h2><pre><code class="language-java">class Solution {    // 上下左右四个方向遍历    int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};    // 记录行，列数    int m, n;    public int numIslands(char[][] grid) {        if (grid == null || grid[0].length == 0) {            return 0;        }        int res = 0;        m = grid.length;        n = grid[0].length;        visited = new boolean[m][n];        for (int i = 0; i &lt; m; i++) {            for (int j = 0; j &lt; n; j++) {                if (grid[i][j] == &#39;1&#39;) {                    res++;                    dfs(grid, i, j);                }            }        }        return res;    }    /**     * @param grid     * @param x     * @param y     * @return     */    private boolean dfs(char[][] grid, int x, int y) {        // 如果当前遍历的字符是陆地，则继续访问后续位置        if (grid[x][y] == &#39;1&#39;) {            // 一点小改进，直接将当前字符改为0，变相的设置为已访问，节省内存消耗，避免开辟visited内存            //visited[x][y] = true;            grid[x][y] = &#39;0&#39;;            // 依次从四个方向分别计算            for (int i = 0; i &lt; 4; i++) {                int newX = x + direction[i][0];                int newy = y + direction[i][1];                if (inArea(newX, newy) &amp;&amp; grid[newX][newy] == &#39;1&#39;) {                    if (dfs(grid, newX, newy)) {                        return true;                    }                }            }        }        return false;    }    /**     * 判断当前坐标点是否越界     *     * @param x     * @param y     * @return     */    private boolean inArea(int x, int y) {        return x &gt;= 0 &amp;&amp; x &lt; m &amp;&amp; y &gt;= 0 &amp;&amp; y &lt; n;    }}</code></pre><p><img src="https://itbird.oss-cn-beijing.aliyuncs.com/img/2022/09/11/image-20220911124632782.png" alt="" /></p>]]>
                </content>
            </entry>
</feed>
